不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事
阿袁工作的第1天: 不變(Invariant), 協變(Covarinat), 逆變(Contravariant)的初次約
阿袁,早!開始工作吧。
阿袁在筆記上寫下今天工作清單:
實現一個scala類ObjectHelper,帶一個功能:
- 函數1:將一個對象轉換成另一種類型的對象。
這個似乎是小菜一碟。
雖然不知道如何轉換對象,那就定義一個函數參數,讓外部把轉換邏輯傳進來。我真聰明啊!
這樣,阿袁實現了第一個函數convert.
本文是用Scala語言寫的示例。(最近開始學Scala)
Scala語言中的 expression-oriented 編程風格中,不寫return, 最后一個語句的結果會被當成函數結果返回。
f(x) 等價于 return f(x)。
完成了。
哦,對了!昨天在和阿靜交流后,猿進化了 - 知道要寫單元測試。
單元測試
阿袁想考慮一下類的繼承關系,在調用convert時,對函數參數f的賦值有沒有什么限制。
先定義這幾個類:
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的第二個參數上?
注: 因為不能把一個子類對象轉換成父類對象。
逆變(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。”
阿靜寫出了代碼:
阿袁看到一個在阿靜面前顯擺的機會,立刻,毫不保留地向阿靜講解了自己的規則。
并說明這個用例違反了輸入參數類型 - 協變不能規則。
“好吧,這樣寫code,總該可以吧?”,阿靜繼續問道。
阿靜把代碼中的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“我覺得還是需要調用者,來參與。” 阿靜說。
阿袁突然間醍醐灌頂的說道,“我明白了。調用者可以只接受父類類型。像這樣子。”
“太好了,阿袁。今天又進化了。”
“好,我去把它改好。”
阿袁回去后,使用了逆變的語法,把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個函數都可以合理的使用。
也發現了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,
這個函數可以把比較兩個對象,并返回一個比較結果。
測試用例是這樣,還是使用了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的比較結果。
第一次測試
- 失敗:
- 失敗原因
類型匹配不上,錯誤信息提示要使用+TInput和-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:理解協變和逆變發生的場景和用例
- 調用者對輸入參數的協變用例
- 調用者對輸出參數的逆變用例
- 調用者只有一個不平配的比較函數用例
- Level 4:能夠寫出協變、逆變的代碼和測試用例
- 針對類的測試用例
- 針對函數的測試用例
- 針對函數變量的測試用例
最后,阿靜真美!
轉載于:https://www.cnblogs.com/steven-yang/p/5877647.html
總結
以上是生活随笔為你收集整理的不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 广播(broadcast)、电视与电视网
- 下一篇: 垂死挣扎-4