[comment]: # 不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事

阿袁工作的第1天: 不变(Invariant), 协变(Covarinat), 逆变(Contravariant)的初次约

阿袁,早!开始工作吧。

阿袁在笔记上写下今天工作清单:

实现一个scala类ObjectHelper,带一个功能:

  • 函数1:将一个对象转换成另一种类型的对象。

这个似乎是小菜一碟。

虽然不知道如何转换对象,那就定义一个函数参数,让外部把转换逻辑传进来。我真聪明啊!

这样,阿袁实现了第一个函数convert.

class ObjectHelper[TInput, TOutput] {
def convert(x: TInput, f: TInput => TOutput): TOutput = {
f(x)
}
}

本文是用Scala语言写的示例。(最近开始学Scala)

Scala语言中的 expression-oriented 编程风格中,不写return, 最后一个语句的结果会被当成函数结果返回。

f(x) 等价于 return f(x)。

完成了。

哦,对了!昨天在和阿静交流后,猿进化了 - 知道要写单元测试。

单元测试

阿袁想考虑一下类的继承关系,在调用convert时,对函数参数f的赋值有没有什么限制。

先定义这几个类:

class A1 {}
class A2 extends A1 {}
class A3 extends A2 {} class B1 {}
class B2 extends B1 {}
class B3 extends B2 {}

A系列的类,将会被用于输入的泛型参数类型。其关系为 A3 继承 A2 继承 A1。

B系列的类,将会被用于输出的泛型参数类型。其关系为 B3 继承 B2 继承 B1。

它们的笛卡尔乘积是9,就是说有9种组合情况。定义一个测试类:

object ObjectHelperTest {
def convertA1ToB1(x: A1) : B1 = {new B1()}
def convertA1ToB2(x: A1) : B2 = {new B2()}
def convertA1ToB3(x: A1) : B3 = {new B3()} def convertA2ToB1(x: A2) : B1 = {new B1()}
def convertA2ToB2(x: A2) : B2 = {new B2()}
def convertA2ToB3(x: A2) : B3 = {new B3()} def convertA3ToB1(x: A3) : B1 = {new B1()}
def convertA3ToB2(x: A3) : B2 = {new B2()}
def convertA3ToB3(x: A3) : B3 = {new B3()} def test () = {
var helper = new ObjectHelper[A2, B2]()
var result : B2 = null
result = helper.convert(, ???)
}
}
  • 问题:对于一个ObjectHelper[A2, B2]对象,上面的9个自定义的convertXtoY函数中,哪些可以用到convert的第二个参数上?
// 对于函数参数的输入参数的数据类型TInput,看看是否可以转换成传入函数的输入参数的数据类型?
TInput ---> f(x: TInputSuperType) // 逆变在输入中是允许的
TInput ---> f(x: TInput) // 不变在输入中是允许的
TInput -->X f(x: TInputSubType) // 协变在输入中是不允许的 // 对于传入函数的返回值,看看是否可以转换为调用函数的返回值类型TOutput?
f(): TOutputSuperType -->X TOutput // 逆变在输出中是不允许的
f(): TOutput ---> TOutput // 不变在输出中是允许的
f(): TOutputSubType ---> TOutput // 协变在输出中是允许的

注: 因为不能把一个子类对象转换成父类对象。

逆变(contravariant),可以理解为: 将一个对象转换成它的父类对象。

协变(coavariant),可以理解为: 将一个对象转换成它的子类对象。

应用场景:给一个函数参数(或变量)赋一个函数值。

输入参数类型 - 不变规则:给一个函数参数赋一个函数值时,传入函数的输入参数类型,可以是函数参数对应的泛型参数类型。

输入参数类型 - 逆变规则:给一个函数参数赋一个函数值时,传入函数的输入参数类型,可以是函数参数对应的泛型参数类型的父类。

输入参数类型 - 协变不能规则:给一个函数参数赋一个函数值时,传入函数的输入参数类型,不能是函数参数对应的泛型参数类型的子类。

输出参数类型 - 不变规则:给一个函数参数赋一个函数值时,传入函数的返回值类型,可以是函数参数对应的泛型参数类型。

输出参数类型 - 协变规则:给一个函数参数赋一个函数值时,传入函数的返回值类型,可以是函数参数对应的泛型参数类型的子类。

输出参数类型 - 逆变不能规则:给一个函数参数赋一个函数值时,传入函数的返回值类型,不能是函数参数对应的泛型参数类型的父类。

根据上面的发现,传入函数的输入类型不能是A3,输出类型不能是B1,依次列出下表:

输入类型 输出类型 是否可用
A1 B1 no
A1 B2 yes
A1 B3 yes
A2 B1 no
A2 B2 yes
A2 B3 yes
A3 B1 no
A3 B2 no
A3 B3 no

测试代码:

class A1 {}
class A2 extends A1 {}
class A3 extends A2 {} class B1 {}
class B2 extends B1 {}
class B3 extends B2 {} object ObjectHelperTest {
def convertA1ToB1(x: A1) : B1 = {new B1()}
def convertA1ToB2(x: A1) : B2 = {new B2()}
def convertA1ToB3(x: A1) : B3 = {new B3()} def convertA2ToB1(x: A2) : B1 = {new B1()}
def convertA2ToB2(x: A2) : B2 = {new B2()}
def convertA2ToB3(x: A2) : B3 = {new B3()} def convertA3ToB1(x: A3) : B1 = {new B1()}
def convertA3ToB2(x: A3) : B2 = {new B2()}
def convertA3ToB3(x: A3) : B3 = {new B3()} def testConvert() = {
var helper = new ObjectHelper[A2, B2]()
var result : B2 = null
result = helper.convert(new A2(), convertA1ToB2)
println(result)
result = helper.convert(new A2(), convertA1ToB3)
println(result)
result = helper.convert(new A2(), convertA2ToB2)
println(result)
result = helper.convert(new A2(), convertA2ToB3)
println(result)
}
} ObjectHelperTest.testConvert()

跑了一遍,都正常输出。在提交了写好的代码之后,阿袁开启了他的美好的学习时间。

阿袁工作的第2天: 协变(Covariant)用途的再次理解

第二天,阿静看到了阿袁的代码,准备在自己的工作中使用一下。

不久,阿袁看到阿静面带一种奇怪的微笑,走了过来,而目的地明显是他。让人兴奋,又有种不妙的感觉。

“阿袁,你写的ObjectHelper有点小问题哦!”

“有什么问题吗?我这次可是写了测试用例的。”

“我看了你的测试用例,我需要可以这样调用convert。”

阿静写出了代码:

helper.convert(new A2(), convertA3ToB2)

阿袁看到一个在阿静面前显摆的机会,立刻,毫不保留地向阿静讲解了自己的规则。

并说明这个用例违反了输入参数类型 - 协变不能规则

“好吧,这样写code,总该可以吧?”,阿静继续问道。

helper.convert(new A3(), convertA3ToB2)

阿静把代码中的new A2()改成new A3()

阿静继续说:

“调用者传入子类A3的实例,后台程序只要负责把这个实例传给处理函数convertA3ToB2不就行了。”

阿袁也看出了可能性。

“你说的有些道理。调用者可以维护输入参数和输入函数之间的一致性,这样就可以跳过输入参数类型 - 协变不能规则的约束。”

“我们发现了一个新的规则。”

输入参数类型 - 调用者的协变规则:调用者可以维护这样一种一致性:输入值 匹配 输入函数的输入参数类型,这样可以使用协变。

阿袁画出下面的说明草图:

// 对于函数参数的输入参数的数据类型TInput,看看是否可以转换成传入函数的输入参数的数据类型?
TInput -->X f(x: TInputSubType) // 协变在输入中是不允许的 // 然而, 如果调用者输入一个TInputSubType实例,
// 并且使用一个支持TInputSubType的函数f,造成了前后一致。
// 输入中的协变就变得允许了。
TInputSubType ---> convert(x: TInput, f(x: TInputSubType))

“谢谢!我把这个实现一下,我的代码可以进化了。”

阿袁使用了协变语法,代码变成了:

class ObjectHelper[TInput, TOutput] {
def convert[T1 <: TInput](x: T1, f: T1 => TOutput): TOutput = {
f(x)
}
}

使用了[T1 <: TInput],表示T1可以是TInput的子类。

增加了测试代码:

    def testConvert() = {
//... // covariant
result = helper.convert(new A3(), convertA3ToB2)
println(result)
result = helper.convert(new A3(), convertA3ToB3)
println(result)
}

阿袁工作的第3天: 逆变(Contravariant)用途的再次理解

阿袁昨晚并没有睡好,一直在考虑昨天的问题,既然,输入可以允许协变,那么是否有输出需要逆变的例子呢?

早上,找到了阿静,和她商量商量这个问题。

“关于昨天那个问题,你的例子证明了对于输入,有需要协变的情况。你觉得有没有对于输出,需要逆变的例子呢?”

“我想,我们可以从你的草图继续看下去。”

昨天,输出逆变的草图是这样:

// 对于传入函数的返回值,看看是否可以转换为调用函数的返回值类型TOutput?
f(): TOutputSuperType -->X TOutput // 逆变在输出中是不允许的

"怎么能变成这样呢?"

f(): TOutputSuperType ---> TOutput

“我觉得还是需要调用者,来参与。” 阿静说。

阿袁突然间醍醐灌顶的说道,“我明白了。调用者可以只接受父类类型。像这样子。”

// 对于传入函数的返回值,看看是否可以转换为调用函数的返回值类型TOutput?
f(): TOutputSuperType -->X TOutput // 逆变在输出中是不允许的 // 然而, 如果调用者使用一个返回值为TOutputSubType的函数f,
// 并且把调用函数的返回值赋给一个TOutputSubType对象。
// 输出中的逆变就变得允许了。
y: TOutputSubType = convert(x, f(): TOutputSubType): TOutput ---> TOutputSubType

“太好了,阿袁。今天又进化了。”

“好,我去把它改好。”

阿袁回去后,使用了逆变的语法,把ObjectHelper代码改成了:

class ObjectHelper[TInput, TOutput] {
def convert[T1 <: TInput, T2 >: TOutput](x: T1, f: T1 => T2): T2 = {
f(x)
}
}

测试用例也补全了:

    def testConvert() = {
var helper = new ObjectHelper[A2, B2]()
var result : B2 = null
result = helper.convert(new A2(), convertA1ToB2)
println(result)
result = helper.convert(new A2(), convertA1ToB3)
println(result)
result = helper.convert(new A2(), convertA2ToB2)
println(result)
result = helper.convert(new A2(), convertA2ToB3)
println(result) // covariant
result = helper.convert(new A3(), convertA3ToB2)
println(result)
result = helper.convert(new A3(), convertA3ToB3)
println(result) // contrvariant
var resultB1 : B1 = null
resultB1 = helper.convert(new A2(), convertA1ToB1)
println(resultB1)
resultB1 = helper.convert(new A2(), convertA2ToB1)
println(resultB1) // covariant & contrvariant
resultB1 = helper.convert(new A3(), convertA3ToB1)
println(resultB1)
}

阿袁工作的第4天:一个更简洁的实现

一个更简洁的实现

今天,阿袁在做了大量尝试后,发现一个简洁的实现方案。

似乎scala编译器,已经很好的考虑了这个问题。不用协变和逆变的语法也能支持想要的功能,

所有的9个函数都可以合理的使用。

    def convert[TInput, TOutput](x: TInput, f: TInput => TOutput): TOutput = {
f(x)
}

也发现了C#中等价的实现方式:

        public TOutput Convert<TInput, TOutput>(TInput x, Func<TInput, TOutput> f) {
return f(x);
}

对一个函数变量,会怎么样呢?

由于函数变量不能设定协变和逆变约束,因此只有最基本的四种函数可以设置。

    def testConvertVariable() = {
var convertFun : A2 => B2 = null;
val convertFunA1ToB2 : A1 => B2 = convertA1ToB2
// set a function value
convertFun = convertFunA1ToB2
println(convertFun) // set a function
convertFun = convertA1ToB2
println(convertFun)
convertFun = convertA1ToB3
println(convertFun)
convertFun = convertA2ToB2
println(convertFun)
convertFun = convertA2ToB3
println(convertFun)
}

C#中等价的实现方式:

        delegate T2 ConvertFunc<in T1, out T2>(T1 x);
public static void TestDelegateGood() {
ConvertFunc<A2, B2> helper = null; // set a function, ok
helper = ConvertA1ToB2; // set a function variable, ok
ConvertFunc<A1, B3> helperA1ToB3 = ConvertA1ToB3;
helper = helperA1ToB3;

注意: delege中,使用了in/out。C#的逆变,协变语法。

不带关键字in/out的实现,有个小问题:

        delegate T2 BadConvertFunc<T1, T2>(T1 x);
public static void TestDelegateBad() {
BadConvertFunc<A2, B2> helper = null; // set a function, ok
helper = ConvertA1ToB2; // set a function variable, error
ConvertFunc<A1, B3> helperA1ToB3 = ConvertA1ToB3;
// helper = helperA1ToB3; // complie error
}

可以看出关键字in/out在赋函数变量赋值的时候,会起到作用。但是不影响直接赋函数。

总觉得这个限制,可以绕过去似的。

阿袁工作的第5天:协变、逆变的一个真正用途。

昨天的简洁方案,让阿袁认识到了自己还没有明白协变、逆变的真正用途。

它们到底有什么用呢?难道只是编译器自己玩的把戏吗?

阿袁设计了这样一个用例:

这是一个新的ObjectHelper,提供了一个比较函数compare,

这个函数可以把比较两个对象,并返回一个比较结果。

class ObjectHelper[TInput, TOutput] (a: TInput) {
def x: TInput = a def compare(y: TInput, f: (TInput, TInput) => TOutput): TOutput = {
f(x, y)
}
}

测试用例是这样,还是使用了A系列作为输入类型,B系列作为输出类型。

class A1 {}
class A2 extends A1 {}
class A3 extends A2 {} class B1 {}
class B2 extends B1 {}
class B3 extends B2 {}

测试用例,考虑了这样一个case:

期望可以比较两个A3类型的数据,返回一个B1的比较结果。

可是我们只有一个A1对象的比较器,这个比较器可以返回一个B3的比较结果。

object ObjectHelperTest{

    // 一个A1对象的比较器,可以返回一个B3的比较结果
def compareA1ToB3(x: A1, y: A1) : B3 = {new B3()} def test(): Unit = {
// helper的类型是ObjectHelper[A2, B2]
var helper: ObjectHelper[A2, B2] = null // 我们期望可以比较A3类型的数据,返回B1的比较结果。
helper = new ObjectHelper[A3, B1](new A3()) // 可是我们只有一个A1对象的比较器,可以返回一个B3的比较结果。
println(helper.compare(new A3(), compareA1ToB3))
}
} ObjectHelperTest.test()

第一次测试

  • 失败:
Line: helper = new ObjectHelper[A3, B1](new A3(), new A3())
error: type mismatch;
found : this.ObjectHelper[this.A3,this.B1]
required: this.ObjectHelper[this.A2,this.B2]
Note: this.A3 <: this.A2, but class ObjectHelper is invariant in type TInput.
You may wish to define TInput as +TInput instead. (SLS 4.5)
Note: this.B1 >: this.B2, but class ObjectHelper is invariant in type TOutput.
You may wish to define TOutput as -TOutput instead. (SLS 4.5)
helper = new ObjectHelper[A3, B1](new A3())
^
  • 失败原因

    类型匹配不上,错误信息提示要使用+TInput和-TOutput.

第二次测试

  • 根据提示,修改代码为:
class ObjectHelper[+TInput, -TOutput] (a: TInput) {
def x: TInput = a def compare(y: TInput, f: (TInput, TInput) => TOutput): TOutput = {
f(x, y)
}
}
  • 再次运行,再次失败:
Line: def compare(y: TInput, f: (TInput, TInput) => TOutput): TOutput = {
error: contravariant type TOutput occurs in covariant position in type (y: TInput, f: (TInput, TInput) => TOutput)TOutput of method compare
def compare(y: TInput, f: (TInput, TInput) => TOutput): TOutput = {
^
error: covariant type TInput occurs in contravariant position in type TInput of value y
def compare(y: TInput, f: (TInput, TInput) => TOutput): TOutput = {
^
  • 失败原因:

    -TOutput为逆变,却要使用到协变的返回值位置上。+TInput为协变,却要使用到逆变的位置上。

第三次测试

根据提示,修改代码为:

class ObjectHelper[+TInput, -TOutput] (a: TInput) {
def x: TInput = a def compare[T1 >: TInput, T2 <: TOutput](y: T1, f: (T1, T1) => T2): T2 = {
f(x, y)
}
}

再次运行,成功!

总结:

这个用例的一个特点是:在实际场合下,不能找到一个类型完全匹配的外部帮助函数。

一个糟糕的情况是,外部帮助函数的输入参数类型比较弱(就是说,是父类型),

可以使用逆变的方法,调用这个弱的外部帮助函数。

阿袁的日记

2016年9月X日 星期六

这几天,有了一些协变和逆变的经验。根据认识的高低,分为下面的几个Level。

  • Level 0:知道

    • 其实,编译器和类库已经做好了一切,这些概念只是它们的内部把戏。我根本不用考虑它。
  • Level 1:知道

    • 协变和逆变发生的场景

      • 给一个泛型对象赋值
      • 给一个函数变量赋值
      • 给一个泛型函数传入一个函数参数
    • 协变是将对象从父类型转换成子类型
    • 逆变是将对象从子类型转换成父类型
  • Level 2:了解协变和逆变的语法

    • Scala: +T : class的协变
    • Scala: -T :class的逆变
    • Scala: T <: S :function的协变
    • Scala: T >: S : function的逆变
    • C#: out :协变
    • C#: in : 逆变
  • Level 3:理解协变和逆变发生的场景和用例

    • 调用者对输入参数的协变用例
    • 调用者对输出参数的逆变用例
    • 调用者只有一个不平配的比较函数用例
// 对于函数参数的输入参数的数据类型TInput,看看是否可以转换成传入函数的输入参数的数据类型?
TInput ---> f(x: TInputSuperType) // 逆变在输入中是允许的
TInput ---> f(x: TInput) // 不变在输入中是允许的
TInput -->X f(x: TInputSubType) // 协变在输入中是不允许的 // 然而, 如果调用者输入一个TInputSubType实例,
// 并且使用一个支持TInputSubType的函数f,造成了前后一致。
// 输入中的协变就变得允许了。
TInputSubType ---> convert(x: TInput, f(x: TInputSubType)) // 对于传入函数的返回值,看看是否可以转换为调用函数的返回值类型TOutput?
f(): TOutputSuperType -->X TOutput // 逆变在输出中是不允许的
f(): TOutput ---> TOutput // 不变在输出中是允许的
f(): TOutputSubType ---> TOutput // 协变在输出中是允许的 // 然而, 如果调用者使用一个返回值为TOutputSubType的函数f,
// 并且把调用函数的返回值赋给一个TOutputSubType对象。
// 输出中的逆变就变得允许了。
y: TOutputSubType = convert(x, f(): TOutputSubType): TOutput ---> TOutputSubType
  • Level 4:能够写出协变、逆变的代码和测试用例

    • 针对类的测试用例
    • 针对函数的测试用例
    • 针对函数变量的测试用例

最后,阿静真美!

不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事的更多相关文章

  1. 蒙特卡洛树搜索算法(UCT): 一个程序猿进化的故事

    前言: 本文是根据的文章Introduction to Monte Carlo Tree Search by Jeff Bradberry所写. Jeff Bradberry还提供了一整套的例子,用p ...

  2. 从底层实现剖析Kotlin协变与逆变的原理

    继续还是探究协变与逆变,在正式开始之前,先来对Kotlin和Java的协变与逆变进行一个对比: 1.Kotlin是声明处协变:而在Java中是在使用处协变: 如何理解,我们先来回顾一下在Java使用协 ...

  3. 协变(covariant)和逆变(contravariant)

    我们知道子类转换到父类,在C#中是能够隐式转换的.这种子类到父类的转换就是协变. 而另外一种类似于父类转向子类的变换,可以简单的理解为“逆变”. 上面对逆变的简单理解有些牵强,因为协变和逆变只能针对接 ...

  4. 逆变(contravariant)与协变(covariant):只能用在接口和委托上面

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...

  5. Java的协变、逆变与不可变

    package javase; import java.util.ArrayList; import java.util.List; class Animal{ } class Cat extends ...

  6. C#协变与逆变

    http://zh.wikipedia.org/wiki/%E5%8D%8F%E5%8F%98%E4%B8%8E%E9%80%86%E5%8F%98 协变与逆变是程序设计语言中的类型系统的一对概念.类 ...

  7. Scala 基础(十六):泛型、类型约束-上界(Upper Bounds)/下界(lower bounds)、视图界定(View bounds)、上下文界定(Context bounds)、协变、逆变和不变

    1 泛型 1)如果我们要求函数的参数可以接受任意类型.可以使用泛型,这个类型可以代表任意的数据类型. 2)例如 List,在创建 List 时,可以传入整型.字符串.浮点数等等任意类型.那是因为 Li ...

  8. 深入理解 C# 协变和逆变

    msdn 解释如下: “协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型. “逆变”则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样,不过不够直白. 直白的理解: “协变” ...

  9. Scala中的协变,逆变,上界,下界等

    Scala中的协变,逆变,上界,下界等 目录 [−] Java中的协变和逆变 Scala的协变 Scala的逆变 下界lower bounds 上界upper bounds 综合协变,逆变,上界,下界 ...

随机推荐

  1. 魔兽争霸3 replay 格式

    ******************************************************************************* * WarCraft III Repla ...

  2. iOS 7定制UIPageControl遇到的问题

    转自snorlax's blog 先说下ios7之前 那些点点的实现非常简单 就是UIPageControl.subviews 就是一个个点的UIImageView 所以只需简单的替换掉就好了代码如下 ...

  3. 安卓开发笔记——重识Activity

    Activity并不是什么新鲜的东西,老生常谈,这里只是随笔记录一些笔记. 每当说起Activity,感觉最关注的还是它的生命周期,因为要使我们的应用程序更加健壮,客户体验更加良好,如果对生命周期不熟 ...

  4. WebKit内核分析之FrameLoader

    参考地址:http://blog.csdn.net/dlmu2001/article/details/6168545 FrameLoader类负责一个Frame的加载,在Frame的流程中起到非常重要 ...

  5. raphael绘制矢量图2

    最近重新再次考虑了下raphael,也没办法把公司的项目给换成raphael的渲染了.大体上的效果稍微考了下其实并不难实现,难点大多集中在对路线以及子路线和方案的转换,以及位置的确定,几乎每操作一步都 ...

  6. Xenia and Bit Operations(线段树单点更新)

    Xenia and Bit Operations time limit per test 2 seconds memory limit per test 256 megabytes input sta ...

  7. Intel HAXM安装错误处理:(TV-x) is not turned on

    Android x86模拟器Intel Atom x86 System Image时提示Intel execute disable bit(xd) is not turned on 运行Elicpse ...

  8. STL中stack小结

    (1)为了运用stack,你必须包含头文件<stack>:#include<stack> (2)在头文件中stack定义如下: namespace std{ template ...

  9. win7激活

    应亲戚要求,装了次win7系统,重新删除分区,格盘,重新划分好分区.完毕.发现系统分区全自动变成动态磁盘.使用win7激活工具时,注意选择使用 小马通用版激活工具. 动态磁盘 稍后解释 小马工具

  10. IOS开发UI基础UISegment属性

    UISegment属性 1.segmentedControlStyle设置segment的显示样式.typedef NS_ENUM(NSInteger, UISegmentedControlStyle ...