V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
JasonLaw
V2EX  ›  程序员

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

  •  
  •   JasonLaw · 2020-11-12 12:00:29 +08:00 · 1954 次点击
    这是一个创建于 1479 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    6 条回复    2020-11-16 02:21:24 +08:00
    sunjourney
        1
    sunjourney  
       2020-11-12 13:25:03 +08:00
    这不算解释吧,原因在于数据是否有可变性
    no1xsyzy
        2
    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
        3
    JasonLaw  
    OP
       2020-11-12 16:29:55 +08:00
    @sunjourney #1 数据是否有可变性?
    JasonLaw
        4
    JasonLaw  
    OP
       2020-11-12 16:31:07 +08:00
    @no1xsyzy #2 完全看不懂你所说的😅,可能是我很多不懂吧。
    aguesuka
        5
    aguesuka  
       2020-11-12 17:32:48 +08:00 via Android
    既然选择了 java 就不要纠结这种东西,java 不是依赖类型的语言。泛型边界是对代码可读性的毁灭打击
    xiuy
        6
    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 也能看懂。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1047 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 32ms · UTC 19:32 · PVG 03:32 · LAX 11:32 · JFK 14:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.