不变(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 = nullresult = 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 = nullresult = 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() = {//...// covariantresult = 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 = nullresult = 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)// covariantresult = helper.convert(new A3(), convertA3ToB2)println(result)result = helper.convert(new A3(), convertA3ToB3)println(result)// contrvariantvar resultB1 : B1 = nullresultB1 = helper.convert(new A2(), convertA1ToB1)println(resultB1)resultB1 = helper.convert(new A2(), convertA2ToB1)println(resultB1)// covariant & contrvariantresultB1 = 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 valueconvertFun = convertFunA1ToB2println(convertFun)// set a functionconvertFun = convertA1ToB2println(convertFun)convertFun = convertA1ToB3println(convertFun)convertFun = convertA2ToB2println(convertFun)convertFun = convertA2ToB3println(convertFun)}
C#中等价的实现方式:
delegate T2 ConvertFunc<in T1, out T2>(T1 x);public static void TestDelegateGood() {ConvertFunc<A2, B2> helper = null;// set a function, okhelper = ConvertA1ToB2;// set a function variable, okConvertFunc<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, okhelper = ConvertA1ToB2;// set a function variable, errorConvertFunc<A1, B3> helperA1ToB3 = ConvertA1ToB3;// helper = helperA1ToB3; // complie error}
可以看出关键字in/out在赋函数变量赋值的时候,会起到作用。但是不影响直接赋函数。
总觉得这个限制,可以绕过去似的。
阿袁工作的第5天:协变、逆变的一个真正用途。
昨天的简洁方案,让阿袁认识到了自己还没有明白协变、逆变的真正用途。
它们到底有什么用呢?难道只是编译器自己玩的把戏吗?
阿袁设计了这样一个用例:
这是一个新的ObjectHelper,提供了一个比较函数compare,
这个函数可以把比较两个对象,并返回一个比较结果。
class ObjectHelper[TInput, TOutput] (a: TInput) {def x: TInput = adef 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 = adef 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 comparedef compare(y: TInput, f: (TInput, TInput) => TOutput): TOutput = {^
error: covariant type TInput occurs in contravariant position in type TInput of value ydef compare(y: TInput, f: (TInput, TInput) => TOutput): TOutput = {^
- 失败原因:
-TOutput为逆变,却要使用到协变的返回值位置上。+TInput为协变,却要使用到逆变的位置上。
第三次测试
根据提示,修改代码为:
class ObjectHelper[+TInput, -TOutput] (a: TInput) {def x: TInput = adef 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:能够写出协变、逆变的代码和测试用例
- 针对类的测试用例
- 针对函数的测试用例
- 针对函数变量的测试用例
最后,阿静真美!
转载于:https://www.cnblogs.com/steven-yang/p/5877647.html
不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事相关推荐
- 协变 (Covariant)、逆变 (Contravariant) 与不变 (Invariant)
协变(Covariant).逆变(Contravariant)与不变(Invariant) 1. 定义 2. 例子 3. 有什么用? 4. 应对 5. 其他问题 6. 特例(Dart 语言) 7. 写 ...
- 协变(covariant)和逆变(contravariant)
我们知道子类转换到父类,在C#中是能够隐式转换的.这种子类到父类的转换就是协变. 而另外一种类似于父类转向子类的变换,可以简单的理解为"逆变". 上面对逆变的简单理解有些牵强,因为 ...
- 秒懂Kotlin之协变(Covariance)逆变(Contravariance)与抗变(Invariant)
[版权申明] 非商业目的注明出处可自由转载 博文地址:https://blog.csdn.net/ShuSheng0007/article/details/108708218 出自:shusheng0 ...
- Scala入门到精通——第二十一节 类型参数(三)-协变与逆变
本节主要内容 协变 逆变 类型通匹符 1. 协变 协变定义形式如:trait List[+T] {} .当类型S是类型A的子类型时,则List[S]也可以认为是List[A}的子类型,即List[S] ...
- 泛型型协变逆变_Java泛型类型简介:协变和逆变
泛型型协变逆变 by Fabian Terh 由Fabian Terh Java泛型类型简介:协变和逆变 (An introduction to generic types in Java: cova ...
- scala 协变和逆变_Scala方差:协变,不变和逆变
scala 协变和逆变 In this post, we are going to discuss about Scala Variance and it's use cases. 在本文中,我们将讨 ...
- 协变与逆变的简单理解(C#)
一.基本概念 协变和逆变是在计算机科学中,描述具有父/子型别关系的多个型别,通过型别构造器.构造出的多个复杂型别之间是否有父/子型别有序或逆序的关系: 官方描述:协变和逆变都是术语,前者指能够使用比原 ...
- 深入理解 C# 协变和逆变【转】
msdn 解释如下: "协变"是指能够使用与原始指定的派生类型相比,派生程度更大的类型. "逆变"则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样 ...
- 深入理解 C# 协变和逆变
msdn 解释如下: "协变"是指能够使用与原始指定的派生类型相比,派生程度更大的类型. "逆变"则是指能够使用派生程度更小的类型. 解释的很正确,大致就是这样 ...
最新文章
- 世界半导体集成电路发展史(超细、超全)
- Dubbo服务暴露原理
- Python Web部署方式总结
- 三种CDN调度系统实现原理详解
- jsp中使用cookie时报错……
- mba案例分析_2020年(第八届)MBA企业案例分析实践课程暨大赛完美收官!
- 嵌入式Linux,4G模组驱动,移远EC20、EC25使用随笔-内核kernel
- 数字图像处理应用领域
- 阿里用的java编码规范积累
- 计算机病毒知识 360回答,计算机病毒与反病毒技术 课后习题答案
- android jni介绍
- 怎么用diskgenius扩大c盘,超简单。
- 域名CNAME记录不能同时适配根域名和www的解决方法
- 曙光服务器安装centOS8
- 手机上流行的各类谜语
- android 实现浮动窗口,轻松实现类VC界面浮动窗口(转载)
- Error: Can‘t find Python executable “python“, you can set the PYTHON env variable
- 物联网大数据平台TIZA STAR架构解析
- ERP、SCM、CRM的区别和联系?
- php x20有啥用,vivo X20全面屏手机的双MIMO天线有什么用?大部分人不知道