美文网首页
Covariance, Contravariance以及Gene

Covariance, Contravariance以及Gene

作者: 跷脚啖牛肉 | 来源:发表于2018-05-16 22:39 被阅读0次

    初次看到这两个单词 Covariance, Contravariance也许很茫然, 先解释一下这两个单词的由来, variance是"型变"的意思, 表示两个源类型的关系是如何影响它们派生出来的复杂类型的关系.

    con + variance 表示"共同变化", 即"协变"; contra + variance 表示"相反变化", 即"逆变"; in + variance 表示"不变", 无论这两个源类型关系如何都不影响派生出来的复杂类型的关系.

    Covariance, Contravariance到底是什么东西, 怎么使用呢? 先来看看跟它们关系最为紧密的subtype, supertype.

    subtype, supertype

    class

    subtype, supertype是面向对象开发中最常见的类型关系, 即子类型和父类型. 通常情况下父类型出现的地方都可以用子类型的替换, 举个最常见的例子: Animal 和 Dog, 为了简化语法我用 Swift 写, 使用 OC 的效果一致.

    class Animal {
        // animal class
    }
    
    class Dog: Animal {
        // dog class
    }
    

    Dog 类是 Animal 类的子类, 所有的 dog 都是 animal, 但不是所有的 animal 都是 dog.

    子类型替换父类型, 显然下面第一句正确, 第二句错误.

    var dog: Animal = Dog()
    var animal: Dog = Animal()
    

    Swift 在声明变量时可以不写类型, 编译时可以通过类型推断出来, 如果工程里面非常多的地方用到类型推断, 势必会加重编译时的负担, 你会发现在Xcode9.0以下编译时, SourceKitService占用 CPU 极高, 提示也会挂掉, 这里是为了更加鲜明地说明问题.

    function / closure / block

    function, closure, block 其实都是函数指针类型, 都具备输入输出的能力, 源类型的关系影响函数指针类型的关系主要是通过输入参数和返回值决定的, 在函数指针类型中, 有两个源类型: 输入参数类型, 返回值类型, 因为这两种类型的源类型对函数指针类型的关系影响各不相同, 所以函数指针类型的关系受两种源类型的共同影响.

    先看没有输入参数, 只有返回值的方法:

    /// function
    func getAnimal() -> Animal {
        return Animal()
    }
    
    func getDog() -> Dog {
        return Dog()
    }
    
    /// closure
    var animalGet: () -> Animal = {
        return Animal()
    }
    
    var dogGet: () -> Dog = {
        return Dog()
    }
    
    var animalGetter: () -> Animal = getDog
    animalGetter = dogGet
    
    // error
    var dogGetter: () -> Dog = getAnimal 
    dogGet = animalGet
    

    上面的代码在 OC 的 block 同样适用, functionclosure 的使用在工厂方法和抽象工厂方法中很常见, 当没有输入参数时, 方法的返回值类型如果是父子关系, 那么这两个方法类型(函数指针类型)也是父子关系, 同样遵守 subtypesupertype 的规则, 可以用子类型替换父类型, 也就是子类能赋值给父类.

    如果 function 有输入参数, 有返回值的情况也是一样吗?

    /// function
    func getAnimal(_ animal: Animal) -> Animal {
        return animal
    }
    
    func getDog(_ dog: Dog) -> Dog {
        return dog
    }
    
    /// closure
    var animalGet: (Animal) -> Animal = { (animal) in
        return animal
    }
    
    var dogGet: (Dog) -> Dog = { (dog) in
        return dog
    }
    
    /// error
    var animalGetter: (Animal) -> Animal = getDog
    var dogGetter: (Dog) -> Dog = getAnimal
    
    /// error
    animalGetter = dogGet
    dogGet = animalGet
    

    加了输入参数以后, 输入参数类型是父子关系, 返回值也是父子关系, 但是它们派生出来的函数指针类型就不是父子关系了. 在实际开发中给 closure / block 赋值是很普遍的做法, UIKitFoundation 框架中也有很多类似的场景, 比如集合类型在标准库的方法: map, filter, flatMap, compact, forEach等, 都是通过传入闭包来实现可定制化的任务.

    subtype, supertype的关系是建立在子类型完全可以替换父类型的基础上, 是根据Liskov substitution principle准则来判断的

    It says, in short, that an instance of a subclass can always be substituted for an instance of its superclass.

    简单来讲这个规则就是子类对象总是能替换父类对象, 即子类对象能赋值给父类对象.

    再看下面的例子, 稍微改一下输入参数的类型:

    var animalGet: (Animal) -> Animal = { (animal) in
        return animal
    }
    
    var dogGet: (Animal) -> Dog = { (_) in
        return Dog()
    }
    
    /// success
    var animalGetter: (Dog) -> Animal = animalGet
    var dogGetter: (Dog) -> Animal = getAnimal
    

    从上面可以看出, 输入参数类型的父子关系和函数指针类型的父子关系是相反的, 也就是逆向的, 为什么会这样呢? 其实利用函数式的思维不难理解, 如果你使用过响应式的一些框架 RAC/RxSwift 就更能想明白, 可以把方法(函数指针类型)想象成"流式"结构, 有输入输出, 通过许多方法的输入输出就构成了的程序, 数据就在一张由方法铺成的网状结构中流动.

    f: (A) -> B

    上图 f: (A) -> B 表示方法的输入输出, 如果能有一个方法无论怎样的输入和输出都能完全替换它, 那说明这个方法就是 f: (A) -> B 方法类型的子类, 把 f 方法想象成水管, 需要另外一个方法 sub f 方法替换 f 方法后"水流"能正常"流动"而不"阻塞"(理解为不能输入该类型的参数). 那说明 sub f 的输入口应该比 A 大, 输出口可比 B 小, 这样才能确保"水流"的正常流动, 用 sub f 替换 f 没有任何影响, 如下:

    f: (super A) -> sub B

    这就说明了上面的例子, 输入参数类型是父子关系, 派生出的函数指针类型确是子父关系, 这就是"逆变".

    override function / properties

    重载在开发中非常常见, 在明确类型的父子关系的情况下, 重载父类的属性或者方法. 那在重载的方法和属性的时候是如何确保父子关系不破裂的呢?

    来看下面列子, 增加一个 Cat 类同样是 Animal 的子类

    class Cat: Animal {
        
    }
    
    class Person {
        func getAnimal() -> Animal {
            return Cat()
        }
        
        func feed(animal: Animal) {
            
        }
    }
    
    class Man: Person {
        override func getAnimal() -> Dog {
            return Dog()
        }
        
        override func feed(animal: Dog) {
            
        }
    }
    

    Man 类是 Person 类的子类, 重载 Person 的方法, 方法的返回值和参数均改成 Dog, 会出现什么问题吗? 我们会发现 getAnimal() 方法重载返回值是成功的, feed(animal: Dog)重载参数失败, 此时编译器会报错, 但是你知道错误的原因吗?

    再看下面例子:

    let person: Person = Man()
    let animal: Animal = Cat()
            
    person.feed(animal: animal)
    

    这个例子充分说明了为什么方法参数不能重载成子类, Man实例和 Animal实例均没有问题, 但是下面的 feed(animal:) Man 类的实例就不能完全替换 Person 类了, 如上 Person 实例调用 feed(animal:) 传入 Cat(), 因为 Person 的 feed(animal:) 方法需要 Animal 类型的参数, 所以传入 Cat() 是合理的, 此时如果用 Man 类实例去替换 Person 类实例了, 由于 Man 类实例方法需要一个 Dog(), 它不接收 Cat() 参数, 此时 Man 类就不能完全替换 Person 类, 所以不符合Liskov substitution principle准则, 即 Man类不是 Person 类的子类, 但是 Man 类却又继承自 Person 类, 就自相矛盾了, 所以这种做法是错误的, 编译器也会有错误提示. 这里也证明了上面说的, 方法的输入参数类型的关系和函数指针类型关系是相反的.

    那上面有办法解决吗?

    class Biology {
        
    }
    
    class Animal: Biology {
        
    }
    
    class Person {
        func getAnimal() -> Animal {
            return Cat()
        }
        
        func feed(animal: Animal) {
            
        }
    }
    
    class Man: Person {
        override func getAnimal() -> Dog {
            return Dog()
        }
        
        override func feed(animal: Biology) {
            
        }
    }
    

    让 Animal 类继承自 Biology 类, Man 类重载 feed(animal:) 方法输入参数重载为 Biology 类, 这样无论 Animal 类是 Cat, Dog, ...都能接住, 也就是说 Man 类实例可以在任何时候任何的地方替换(赋值) Person 类实例, 说明 Man 类是 Person 类的子类, 也符合继承规则.

    所以这也是为什么重载父类方法时, 参数不能是父类方法参数的子类, 而应该是父类参数类型或者父类参数的父类(超类). 一般我们重载时都默认是跟父类参数一致, 很少见到有超类的情况. 而方法返回值类型, 可以是父类方法返回值类型或者返回值类型的子类型.

    重载方法时, 返回值类型可以是顺着继承链向下, 输入参数类型是顺着继承链子向上

    Properties

    Swift 和 OC 的 Properties 按照读写属性分为 read-onlyread-write.
    read-only 属性其实就是调用 getter 方法, 所以 read-only 可以重载类型为子类型.
    read-write 属性其实是调用一对方法: 不带参数带返回值的 getter 方法, 带参数不带返回值的 setter 方法, 结合上面所说子类重载父类方法的结论: 返回值类型 <= 父类返回值类型 && 参数类型 >= 父类参数类型, 同时满足这两个条件才能构成父子关系, 所以重载 read-write 属性只能跟父类型属性类型一致.

    Generics

    Generics 泛型非常强大, 当工程里面有很多地方做相同的任务而且与类型无关时, 泛型就能发挥其强大的特性, 能够让代码更加的紧凑, 层次结构也更加清晰, 逻辑不会散落在工程各个地方.

    In Swift

    在 Swift 中简单演示一下泛型的使用:

    class Animal: NSObject {
    
    }
    
    class Dog: Animal {
        override var description: String {
            return "it's dog."
        }
    }
    
    class Cat: Animal {
        override var description: String {
            return "it's cat."
        }
    }
    
    class Person<T> {
        var pet: T?
    
        func feed(_ some: T) {
            print(some)
        }
    }
    
    let dog = Dog()
    let cat = Cat()
    
    let man: Person<Dog> = Person()
    let woman: Person<Cat> = Person()
    
    man.feed(dog)
    woman.feed(cat)
    

    打印结果

    it's dog.
    it's cat.
    

    Person 类接收泛型 T, feed 处理传入的泛型, 根据在初始化 person 时指定的泛型具体类型来处理, 还能给泛型增加限制, 比如遵守同一个 protocol, 继承自同一个 base class 基类, 还可以用 where 语句限制具体参数的详细条件, 这里不细说泛型的用法, 来看看泛型的 subtype, supertype 的关系如何?

    稍微修改一下上面的代码:

    let man: Person<Animal> = Person()
    let woman: Person<Cat> = Person()
    
    man = woman
    woman = man
    

    实例化 man 的时候指定泛型类型为 Animal, 那么 Person<Animal>Person<Cat> 的父子关系如何呢? Animal 和 Cat 虽然是父子关系, 他们派生出的泛型类型是什么关系呢? 测试上面的例子发现 man 和 woman 相互赋值均不成功, 说明 Person<Animal>Person<Cat> 没有任何关系, 不会因为指定泛型具体类型是父子关系就是父子关系. 所以 Swift 的泛型是 invariance(不变)的.

    真的是这样吗? 再看下面的列子:

    var animalArray: Array<Animal> = [Animal()]
    var dogArray: Array<Dog> = [Dog()]
    
    animalArray = dogArray
    dogArray = animalArray
    

    Swift 标准库中 collection集合类型中广泛地使用泛型, 比如数组的定义:

    public struct Array<Element> {
        ...
    }
    

    animalArray 数组指定 Animal 类型, dogArray 数组指定 Dog 类型, 按照上面的结论推测这两个赋值会失败, 但是实际上只有 dogArray = animalArray 失败, animalArray = dogArray 是成功的, 说明 Array<Animal>Array<Dog>的父类, WTF?

    因为标准库中的集合类型, 系统会做了很多处理让其具有协变性, 所以在开发的时候我们会认为是理所当然的.

    “Swift generics are normally invariant, but the Swift standard library collection types — even though those types appear to be regular generic types — use some sort of magic inaccessible to mere mortals that lets them be covariant.”

    In OC

    在 OC 使用的泛型其实是 Lightweight Generics 轻量级的泛型, 2015年的时候推出的新特性, 而且是编译器层面的特性, 其目的昭然若揭, 一是为了扩展 OC 这门"古老"的语言, 更是为了方便从 OC 迁到 Swift, 熟悉泛型特性. OC 里面泛型的写法用法与 Swift 类似, 但是有些地方需要注意, 泛型只能在 @interface 里使用, 在 @implementation 中只能用 id 类型, 显然泛型是接口类型, 通过接口暴露出去.

    /**
     Animal class
     */
    @interface Animal: NSObject
    
    @end
    
    @implementation Animal
    
    - (NSString *)description {
        return @"it's animal";
    }
    
    @end
    
    /**
     Dog class
     */
    @interface Dog: Animal
    
    @end
    
    @implementation Dog
    
    - (NSString *)description {
        return @"it's dog";
    }
    
    @end
    
    
    /**
     Person Class
     */
    @interface Person<T: Animal *>: NSObject
    
    @property (nonatomic, strong) T pet;
    
    - (void)feedAnimal: (T)animal;
    
    @end
    
    @implementation Person
    
    - (void)feedAnimal:(id)animal {
    
    }
    
    @end
    

    上 Person 类中简单演示一下泛型的使用方式, <T: Animal *> 表示限制泛型 T 的类型是 __kindof Animal 类型. 我们主要研究一下 OC 中泛型的类型关系:

    Person<Animal *> *man = [[Person alloc] init];
    Person<Dog *> *woman = [[Person alloc] init];
    
    man = woman;
    woman = man;
    

    Person<Animal *>Person<Dog *> 相互赋值编译器会报 warning, 而不像 Swift 中直接报错, 因为 Swift 是类型安全语言, 相对于 OC 语言更加安全, 把问题暴露在编译阶段, 但是也增加了编译的时间. 所以上面的赋值语句并不成功, Person<Animal *>Person<Dog *> 这两个类型并不是父子关系.

    Incompatible pointer types assigning to 'Person<Animal *> *' from 'Person<Dog *> *'
    Incompatible pointer types assigning to 'Person<Dog *> *' from 'Person<Animal *> *'

    但是在 OC 泛型里有两个 Swift 不具备的参数: __covariant, __contravariant. 用 __covariant 修饰泛型时, 派生出的类能够"协变", 也就是说 Animal 类与 Dog 类是父子关系, 那么用 __covariant 修饰 T 后, Person<Animal *>Person<Dog *> 是父子关系; __contravariant 修饰泛型时, 派生出的类能够"逆变", 即Person<Animal *>Person<Dog *> 是子父关系.

    @interface Person<__covariant T: Animal *>: NSObject
        ...
    @end
    
    Person<Animal *> *man = [[Person alloc] init];
    Person<Dog *> *woman = [[Person alloc] init];
    
    // success
    man = woman;
    // fail
    woman = man;
    

    上面用 __covariant 修饰泛型 T, 那么 Person<T> 具备协变性, 所以Person<Animal *> 是 Person<Dog *> 的父类; 用 __contravariant 修饰泛型 T,
    Person<T> 具备逆变性, 所以 Person<Animal *>Person<Dog *> 的子类, 这种场景还真没有遇见过.

    OC 中的集合类型又是如何呢? 是否跟 Swift 中一样具备协变性呢?
    看看 NSArray 的接口便知 Foundation 中的集合类型自带协变性.

    @interface NSArray<__covariant ObjectType> : NSObject <NSCopying, NSMutableCopying, NSSecureCoding, NSFastEnumeration>
     ...
    @end
    

    所以, 在 OC 里面可以通过 __covariant, __contravariant 来指定泛型类型是协变还是逆变, 但是在 Swift 中就不能, Swift 中自定义的泛型类型是 invariance(不变)性的, 如果想具备协变或者逆变性, 就得想法子绕过, 后面的文章会讲.

    Covariance, Contravariance, invariance

    上面一直都在讨论闭包, block, 方法, 属性, 泛型之间的类型关系, 能感知到 Covariance 协变, Contravariance 逆变, invariance 不变这三种属性并不难理解, 而且跟实际开发关系密切.

    Covariance

    Covariance is when subtypes are accepted.

    Covariance 协变是子类型被接受. 英语中很多地方是被动句, 换句话说就是: 一个类型能接收它的子类型, 那么此类型就具备协变性. 一言以蔽之: 子类型能赋值给父类型.

    通过我们上面的 subtype, supertype的演示, 有哪些类型具备协变性呢?

    • 系统提供的集合类型: Array/NSArray, Dictionary/NSDictionary...
    • function / closure / block 没有输入参数时, 返回值的类型关系是顺着继承链向下.
    • function / closure / block 有输入参数时, 输入参数的类型关系是顺着继承链向上的, 且返回值的类型关系是顺着继承链向下.
    • Overridden read-only 属性.
    • OC 中用 __covariant 修饰泛型时.

    协变在开发中应用广泛, 最常见的就是当你给 closure/block 赋值时, 有时参数不对也许会纠结半天.

    Contravariance

    Contravariance is when supertypes are accepted.

    Contravariance 协变是父类型被接受. 换句话说就是: 一个类型能接收它的父类型, 那么此类型就具备协变性. 一言以蔽之: 父类型能赋值给子类型.

    逆变

    • function / closure / block 没有输入参数时, 返回值的类型关系是顺着继承链向上.
    • function / closure / block 有输入参数时, 输入参数的类型关系是顺着继承链向上, 且返回值的类型关系是顺着继承链向下/向上.
    • Overridden read-write 属性顺着继承链向下/向上.
    • OC 中用 __contravariant 修饰泛型时.

    Contravariance 在实际开发中还真没见过, 在 Swift 中基本能立马报错, OC 里只会有警告, 但是知道它能方便快速定位问题.

    invariance

    Invariance is when neither supertypes nor subtypes are accepted.

    Invariance 协变是既不接受子类型也不接受父类型. 也就是没有子类型和父类型, 能称为子类型或父类型必然具备协变或逆变属性.

    • Swift, OC 自定义的泛型类型.

    协变/逆变能反映出源类型的关系如何影响到派生复杂类型的关系, 了解由源类型派生出的复杂类型的关系是很重要的, 即使在 Swift 强类型安全下也是很有必要的, 知道报错的原因, 在 OC 里面更是有必要, 不同类型之间赋值关系混乱会造成运行时各种崩溃, 而在编译阶段是没法检测出来的, 如果做静态检查可以检测出不同类型间赋值的问题的话, 成本也是比较高的, 所以最好是在代码编写阶段就杜绝此类问题的发生.

    欢迎大家斧正!

    相关文章

      网友评论

          本文标题:Covariance, Contravariance以及Gene

          本文链接:https://www.haomeiwen.com/subject/wppmdftx.html