在将子类型多态性(面向对象)与参数多态性(泛型)相结合的任何编程语言中,都会出现方差问题。 假设我有一个字符串列表,键入List<String> 。 我可以将其传递给接受List<Object>的函数吗? 让我们从这个定义开始:

interface List<T> {void add(T element);Iterator<T> iterator();...
}

破碎的协方差

凭直觉,我们可能首先认为应该允许这样做。 看起来不错:

void iterate(List<Object> list) {Iterator<Object> it = list.iterator();...
}
iterate(ArrayList<String>());

确实,包括Eiffel和Dart在内的某些语言确实接受此代码。 可悲的是,它是不完善的,如以下示例所示:

//Eiffel/Dart-like language with
//broken covariance:
void put(List<Object> list) {list.add(10);
}
put(ArrayList<String>());

在这里,我们将List<String>传递给接受List<Object>的函数,该函数尝试将Integer添加到列表中。

Java使用数组也会犯同样的错误。 以下代码编译:

//Java:
void put(Object[] list) {list[0]=10;
}
put(new String[1]);

它在运行时失败,并带有ArrayStoreException

使用地点差异

Java对于通用类和接口类型采用了不同的方法。 默认情况下,类或接口类型为invariant ,即:

  • 当且仅当UV完全相同类型时,才可将L<V>分配给L<V>

由于在很多时候这非常不方便,因此Java支持一种称为“ 使用站点差异”的方法 ,其中:

  • L<U>可分配给L<? extends V> 如果UV的子类型,则L<? extends V> ,并且
  • L<U>可分配给L<? super V> L<? super V>如果U是的超类型V

丑陋的语法? extends V ? extends V? super V ? super V称为通配符 。 我们还说:

  • L<? extends V> L<? extends V>V协变的,并且
  • L<? super V> L<? super V>V反变的。

由于Java的通配符表示法很丑陋,因此在本讨论中我们将不再使用它。 取而代之的是,我们将分别使用关键字inout来表示通变量和协方差。 从而:

  • L<out V>V协变的,并且
  • L<in V>是在逆变 V

给定的V称为通配符的边界

  • out V是一个上限通配符, V是其上限,并且
  • in V下界通配符, V是其下界。

从理论上讲,我们可以有一个具有上限和下限的通配符,例如L<out X in Y>
我们可以使用交集类型表示多个上限或多个下限,例如L<out U&V>L<in U&V>
请注意,类型表达式L<out Anything>L<in Nothing>指的是完全相同的类型,并且此类型是L的所有实例的超类型。 您会经常看到人们将通配符类型称为存在性类型 。 他们的意思是,如果我知道该list的类型为List<out Object>

List<out Object> list;

然后我知道存在一个未知的类型T ,这是Object的子类型,因此list的类型为List<T>
或者,我们可以从更宽泛的角度出发,说List<out Object>是所有List<T>类型的并集,其中TObject的子类型。
在具有使用地点差异的系统中,以下代码无法编译:

void iterate(List<Object> list) {Iterator<Object> it = list.iterator();...
}
iterate(ArrayList<String>()); //error: List<String> not a List<Object>

但是这段代码可以做到:

void iterate(List<out Object> list) {Iterator<out Object> it = list.iterator();...
}
iterate(ArrayList<String>());

正确地,此代码无法编译:

void put(List<out Object> list) {list.add(10); //error: Integer is not a Nothing
}
put(ArrayList<String>());

现在我们在兔子洞的入口。 为了将通配符类型集成到类型系统中,同时拒绝如上例所示的错误代码,我们需要一种更为复杂的类型参数替换算法。

会员输入使用地点差异

也就是说,当我们有一个泛型类型类似List<T>有一种方法void add(T element) ,而不是仅仅直截了当代ObjectT ,就像我们做普通不变的类型,我们需要考虑的方差类型参数出现的位置。 在这种情况下, T出现在List类型的反位置 ,即作为方法参数的类型。 我不会在这里写下的复杂算法告诉我们,在此位置我们应该用Nothing (底部类型)代替。
现在想象一下我们的List接口有一个带有以下签名的partition()方法:

interface List<T> {List<List<T>> partition(Integer length);...
}

List<out Y>partition()的返回类型是什么? 好吧,在不损失精度的情况下,它是:

List<in List<in Y out Nothing> out List<in Nothing out Y>>

哎哟。
由于没有人在他们的头脑中想去考虑这样的类型,因此明智的语言会抛弃其中的某些界限,留下这样的东西:

List<out List<out Y>>

这是可以接受的。 不幸的是,即使在这种非常简单的情况下,我们也已经远远超出了程序员可以轻松跟随类型检查器所做的工作的地步。
因此,这就是我不信任使用地点差异的原因所在:

  • Ceylon设计的一个重要原则是,程序员应始终能够重现编译器的推理。 这是原因的一些与使用现场方差出现的复杂类型的非常困难。
  • 它具有病毒性作用:一旦这些通配符类型在代码中立足,它们便开始传播,很难回到我的普通不变式类型。

申报地点差异

使用场所方差的一个更合理的选择是声明场所方差 ,在声明时我们指定泛型类型的方差。 这是我们在锡兰使用的系统。 在此系统下,我们需要将List分为三个接口:

interface List<out T> {Iterator<T> iterator();List<List<T>> partition(Integer length);...
}interface ListMutator<in T> {void add(T element);
}interface MutableList<T>satisfies List<T>&ListMutator<T> {}

List声明为协变类型, ListMutator为逆变类型, MutableList为两者的不变子类型。
似乎对多个接口的需求似乎是声明站点差异的一个很大的缺点,但事实证明,将变异与读取操作分开是很有用的,并且:

  • 变异运算通常是不变的,而
  • 读取操作通常是协变的。

现在我们可以这样编写函数:

void iterate(List<Object> list) {Iterator<Object> it = list.iterator();...
}
iterate(ArrayList<String>());void put(ListMutator<Integer> list) {list.add(10);
}
put(ArrayList<String>()); //error: List<String> is not a ListMutator<Integer>

您可以在此处阅读有关声明位置差异的更多信息。

为什么我们在锡兰需要使用场所差异

可悲的是,Java没有声明站点差异,并且与Java的干净互操作对我们来说很重要。 我不喜欢纯粹为了与Java互操作而在语言的类型系统中添加主要功能,因此多年来,我一直拒绝向Ceylon添加通配符。 最后,现实和实用性获胜,我的顽固失去了。 因此,锡兰1.1现在具有带有单界通配符的使用站点差异。
我试图尽可能严格地限制此功能,而仅提供体面的Java互操作所需的最低限度。 这意味着,就像在Java中一样:

  • 没有形式为List<in X out Y>双界通配符,并且
  • 通配符类型不能出现在类或接口定义的extendssatisfies子句中。

此外,与Java不同:

  • 没有隐含界的通配符,上限必须始终以显式形式编写,并且
  • 不支持通配符捕获

通配符捕获是Java的一个非常聪明的功能,它利用了通配符类型的“现有”解释。 给定这样的通用函数:

List<T> unmodifiableList<T>(List<T> list) => ... :

Java让我调用unmodifiableList() ,传递一个通配符类型,如List<out Object> ,返回另一个通配符List<out Object> ,理由是存在一些未知的X ,这是Object的子类型,对其进行调用是正确的。 也就是说,即使无法为任何TList<out Object>类型分配给List<T> ,此代码也被认为是类型正确的代码:

List<out Object> objects = .... ;
List<out Object> unmodifiable = unmodifiableList(objects);

在Java中,涉及通配符捕获的键入错误几乎是无法理解的,因为它们涉及未知且难以理解的类型。 我没有计划向锡兰添加对通配符捕获的支持。

试试看

使用站点差异已经实现,并且已经在Ceylon 1.1中起作用,如果您非常有动力,可以从GitHub获得。
即使此功能的主要动机是强大的Java互操作性,但在通配符很有用的其他场合(可能很少见)。 但是,这并不表示我们的方法有任何重大变化。 除极端情况外,我们将继续在Ceylon SDK中使用声明站点差异。 更新: 我只是意识到我忘了感谢Ross Tate,感谢他为我提供了有关使用站点差异的成员键入算法的详细知识。 罗斯知道这些非常棘手的东西!

翻译自: https://www.javacodegeeks.com/2014/08/why-i-distrust-wildcards-and-why-we-need-them-anyway.html

为什么我不信任通配符,以及为什么我们仍然需要通配符相关推荐

  1. 通配符SSL证书知识 怎样获取通配符域名证书

    什么是通配符证书 通配符证书又可以称作:通配符ssl证书,通配符域名证书,泛域名证书,Wildcard certificate 申请免费的通配符证书:来此加密. 在计算机网络中,通配符证书是一个可以被 ...

  2. 限定通配符和非限定通配符_为什么我不信任通配符以及为什么我们仍然需要通配符...

    限定通配符和非限定通配符 在将子类型多态性(面向对象)与参数多态性(泛型)相结合的任何编程语言中,都会出现方差问题. 假设我有一个字符串列表,键入List<String> . 我可以将其传 ...

  3. JAVA中的通配符的符号_Linux下的通配符和特殊符号用法详解

    在Linu系统中我们会遇到一些特殊符号 ,下面让我给大家大致说一下 * 代表0个或者多个特殊字符 例子 yum.* 代表的可以使yum.也可以是yum.a.yum.ab.yum.abc 当然小数点后面 ...

  4. 【Java 泛型】泛型用法 ( 泛型编译期擦除 | 上界通配符 <? extends T> | 下界通配符 <? super T> )

    文章目录 一.泛型擦除 二.泛型的上界通配符 <? extends T> 三.泛型的下界通配符 <? super T> 一.泛型擦除 泛型只保留到 编译期 , 在 编译完毕后 ...

  5. mysql正则通配符全解_mysql正则表达式与通配符

    扩展正则表达式的一些字符是:  "."匹配任何单个的字符.  一个字符类"[...]"匹配在方括号内的任何字符.例如,"[abc]"匹配&q ...

  6. java 类型通配符_java中泛型之类型通配符(?)

    实体类 package cn.xy.model; /** * 生物类 * @author xy * */ public class Living { private String name; publ ...

  7. oracle数字通配符,oracle sql语言模糊查询--通配符like的使用

     oracle在Where子句中,可以对datetime.char.varchar字段类型的列用Like子句配合通配符选取那些"很像..."的数据记录,以下是可使用的通配符: ...

  8. linux 通配符 正则表达式 区别,linux 正则表达式和通配符

    linux 正则表达式和通配符 通配符用于查找文件 包含三种:  * ? [] * 代表任意个任意字符 ? 代表任意一个字符 [] 代表中括号中的一个字符 正则表达式(正则是包含匹配,只要包含就可以匹 ...

  9. MySQL like 通配符是_MySql模糊查询like通配符使用详细介绍

    MySQL提供标准的SQL模式匹配,以及一种基于象Unix实用程序如vi.grep和sed的扩展正则表达式模式匹配的格式. 一.SQL模式 SQL的模式匹配允许你使用"_"匹配任何 ...

  10. linux 星号 通配符,如何在bash中转义通配符/星号字符?

    简短的回答 像其他人所说的那样 - 你应该总是引用变量来防止奇怪的行为.所以使用echo"$ foo"代替echo $ foo. 长期回答 我确实认为这个例子值得进一步解释,因为它 ...

最新文章

  1. 用ruby的net/ssh链接远程的服务器
  2. CodeForces - 1353E K-periodic Garland(思维+dp)
  3. 数据结构之二叉搜索树
  4. 第 2 章 索引优化分析
  5. 统计自然语言处理基础_聚类
  6. Windows电脑桌面云便签快捷键怎么查看?
  7. 云计算服务三层架构-IaaS-PaaS-SaaS解析
  8. 苹果App Store商店中国区如何改为美国区
  9. ASO优化续:详解appstore的排名规则
  10. 鼠标计算机英语怎么说,鼠标英语
  11. Vue移动端H5手势缩放滚动拖拽插件Easyscroller
  12. 在VS code中运行matlab
  13. 闲聊机器人实例四:python实现小姜机器人(检索式chatbot_sentence_vec_by_bert_bert句向量)
  14. QQ返利当当特惠活动(10.26~11.4)
  15. Isaac SDK Sim 环境
  16. 网络安全自学入门:(超详细)从入门到精通学习路线规划,学完即可就业
  17. 【转载】知识普及:天煞的HTML5到底是个什么东西
  18. i3-10110U和i5 10210u 哪个好
  19. Lang.NET 2008 相关Session
  20. 金字塔原理学习笔记1

热门文章

  1. mybatisPlus的分页查询
  2. JS中闭包的应用自定义JS模块
  3. SpringCloud Config 分布式配置
  4. Android10创建文件Permission denied 失败
  5. 583. 两个字符串的删除操作用时6ms的另类解法
  6. 二叉树:HDU1754
  7. 云服务器的优点和缺点_为什么要使用云计算? 的优点和缺点
  8. openjdk 编译_使用OpenJDK 11运行JAXB xjc编译器
  9. java关闭窗口函数_2016年将是Java终于拥有窗口函数的那一年!
  10. java实现ldap服务器_Java到LDAP教程(包括如何安装LDAP服务器/客户端)