分享一个关于协变、逆变、不变的优秀回答

2020-11-12 12:00:29 +08:00
 JasonLaw

之前对于这几个概念没有一个深入的理解,这个回答真的解释得太好了。

关于数组是协变的,Jon Skeet 在Is List<Dog> a subclass of List<Animal>? Why are Java generics not implicitly polymorphic? - Stack Overflow中有这么一个评论

1952 次点击
所在节点    程序员
6 条回复
sunjourney
2020-11-12 13:25:03 +08:00
这不算解释吧,原因在于数据是否有可变性
no1xsyzy
2020-11-12 13:51:40 +08:00
只有将类型作为参量(包括泛型)才有协变逆变之说
假定一个函数签名为 f: A -> B
A 对于 f 是逆变的,因为对于任何 A 的父类 C,g: C -> B 可以和 f 一样被使用
B 对于 f 是协变的,因为对于任何 B 的子类 D,h: A -> D 可以和 f 一样被使用
而如果是 List 这个具有泛型的类,因为它的 “范畴”(记得面向对象是范畴论的近似)既包含了列表取元素的 ref: A[] -> A,又包含了元素构造列表的 list: A -> A[],甚至包含了尾部添加元素的 append: A[] -> A -> A[] 和头部添加元素的 prepend: A -> A[] -> A[],那 A 对于 A[] 既不总是协变的,也不总是逆变的,那就是不变的。

而即使底层实现了类似的结构,如果是只出不进的队列,那 A 对于 QueueOut 是协变的,只进不出的队列,A 对于 QueueIn 是逆变的。
(这似乎和 traits 有点类似?接口的某些子集达成协变,某些子集达成逆变,合在一起就成了不变)

整套 OO 体系是混乱的。
滥用继承。
希望来点只能进行包裹、静态鸭子语言的 OO 语言。
顺便这需要语法糖来快速实现 this.some_method = this.wrapped_object.method
`convey wrapped_object.method as some_method`
JasonLaw
2020-11-12 16:29:55 +08:00
@sunjourney #1 数据是否有可变性?
JasonLaw
2020-11-12 16:31:07 +08:00
@no1xsyzy #2 完全看不懂你所说的😅,可能是我很多不懂吧。
aguesuka
2020-11-12 17:32:48 +08:00
既然选择了 java 就不要纠结这种东西,java 不是依赖类型的语言。泛型边界是对代码可读性的毁灭打击
xiuy
2020-11-16 02:21:24 +08:00
这个回答不能称得上是对协变、逆变的解释,只是针对这个问题来说,这个答案还是讲得挺明白的。

要理解 covariant 和 contravariant 的话,首先要明白的是 subtype 的基础概念。

> Type S is a subtype of a type T, written S <: T, if an expression of type S can be used in any context that expects an element of type T.

举个例子的话,就是任何需要一个 Animal 的地方,都可以放进去一个 Dog,那么 Dog <: Animal 。(看着很像 Inheritance 是因为大多数的程序语言把 Inheritance 和 Subtyping 混在一起了)

而协变与逆变其实与 Function Subtyping 有关。(也就是 #2 中的「只有将类型作为参量(包括泛型)才有协变逆变之说」)

Function Subtyping 的定义是这样婶儿滴:(S' -> T') <: (S -> T) if S <: S' and T' <: T.

协变和逆变就是从这里来的,这里的 S <: S' 就是逆变,而 T' <: T 是协变。

说直白一点就是 T' <: T 是顺着 (S' -> T') <: (S -> T) 来的,而 S <: S' 这个关系要求逆过来。

再叨叨多一点,这个定义其实挺反直觉的,如果要 (S' -> T') <: (S -> T) 的话,我们可能以为条件应该是 S' <: S 和 T' <: T 。这时候需要回头在看一下 subtypes 的定义:*S <: T, if an expression of type S can be used in any context that expects an element of type T*. **当 T 变成了 f, 问题就转为寻找一个能代替 f 的 f'。**

这个图示非常有助于理解:

![Untitled.png]( https://i.loli.net/2020/11/16/Hy3fKGed5gYI6uv.png)

S 需要是 S' 的 subtype ( S <: S'),任何原来的输入才能放得进去;而 T' 必须是 T 的 subtype,任何出来的 T' 才能被视作 T 。

对于有范型的类来说,我觉得 [Scala 这个文档]( https://docs.scala-lang.org/zh-cn/tour/variances.html) 讲得挺好的,不会 Scala 也能看懂。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/724371

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX