不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事
阿袁工作的第1天: 不變(Invariant), 協(xié)變(Covarinat), 逆變(Contravariant)的初次約
阿袁,早!開始工作吧。
阿袁在筆記上寫下今天工作清單:
實(shí)現(xiàn)一個(gè)scala類ObjectHelper,帶一個(gè)功能:
- 函數(shù)1:將一個(gè)對象轉(zhuǎn)換成另一種類型的對象。
這個(gè)似乎是小菜一碟。
雖然不知道如何轉(zhuǎn)換對象,那就定義一個(gè)函數(shù)參數(shù),讓外部把轉(zhuǎn)換邏輯傳進(jìn)來。我真聰明啊!
這樣,阿袁實(shí)現(xiàn)了第一個(gè)函數(shù)convert.
本文是用Scala語言寫的示例。(最近開始學(xué)Scala)
Scala語言中的 expression-oriented 編程風(fēng)格中,不寫return, 最后一個(gè)語句的結(jié)果會(huì)被當(dāng)成函數(shù)結(jié)果返回。
f(x) 等價(jià)于 return f(x)。
完成了。
哦,對了!昨天在和阿靜交流后,猿進(jìn)化了 - 知道要寫單元測試。
單元測試
阿袁想考慮一下類的繼承關(guān)系,在調(diào)用convert時(shí),對函數(shù)參數(shù)f的賦值有沒有什么限制。
先定義這幾個(gè)類:
A系列的類,將會(huì)被用于輸入的泛型參數(shù)類型。其關(guān)系為 A3 繼承 A2 繼承 A1。
B系列的類,將會(huì)被用于輸出的泛型參數(shù)類型。其關(guān)系為 B3 繼承 B2 繼承 B1。
它們的笛卡爾乘積是9,就是說有9種組合情況。定義一個(gè)測試類:
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(, ???)} }- 問題:對于一個(gè)ObjectHelper[A2, B2]對象,上面的9個(gè)自定義的convertXtoY函數(shù)中,哪些可以用到convert的第二個(gè)參數(shù)上?
注: 因?yàn)椴荒馨岩粋€(gè)子類對象轉(zhuǎn)換成父類對象。
逆變(contravariant),可以理解為: 將一個(gè)對象轉(zhuǎn)換成它的父類對象。
協(xié)變(coavariant),可以理解為: 將一個(gè)對象轉(zhuǎn)換成它的子類對象。
應(yīng)用場景:給一個(gè)函數(shù)參數(shù)(或變量)賦一個(gè)函數(shù)值。
輸入?yún)?shù)類型 - 不變規(guī)則:給一個(gè)函數(shù)參數(shù)賦一個(gè)函數(shù)值時(shí),傳入函數(shù)的輸入?yún)?shù)類型,可以是函數(shù)參數(shù)對應(yīng)的泛型參數(shù)類型。
輸入?yún)?shù)類型 - 逆變規(guī)則:給一個(gè)函數(shù)參數(shù)賦一個(gè)函數(shù)值時(shí),傳入函數(shù)的輸入?yún)?shù)類型,可以是函數(shù)參數(shù)對應(yīng)的泛型參數(shù)類型的父類。
輸入?yún)?shù)類型 - 協(xié)變不能規(guī)則:給一個(gè)函數(shù)參數(shù)賦一個(gè)函數(shù)值時(shí),傳入函數(shù)的輸入?yún)?shù)類型,不能是函數(shù)參數(shù)對應(yīng)的泛型參數(shù)類型的子類。
輸出參數(shù)類型 - 不變規(guī)則:給一個(gè)函數(shù)參數(shù)賦一個(gè)函數(shù)值時(shí),傳入函數(shù)的返回值類型,可以是函數(shù)參數(shù)對應(yīng)的泛型參數(shù)類型。
輸出參數(shù)類型 - 協(xié)變規(guī)則:給一個(gè)函數(shù)參數(shù)賦一個(gè)函數(shù)值時(shí),傳入函數(shù)的返回值類型,可以是函數(shù)參數(shù)對應(yīng)的泛型參數(shù)類型的子類。
輸出參數(shù)類型 - 逆變不能規(guī)則:給一個(gè)函數(shù)參數(shù)賦一個(gè)函數(shù)值時(shí),傳入函數(shù)的返回值類型,不能是函數(shù)參數(shù)對應(yīng)的泛型參數(shù)類型的父類。
根據(jù)上面的發(fā)現(xiàn),傳入函數(shù)的輸入類型不能是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()跑了一遍,都正常輸出。在提交了寫好的代碼之后,阿袁開啟了他的美好的學(xué)習(xí)時(shí)間。
阿袁工作的第2天: 協(xié)變(Covariant)用途的再次理解
第二天,阿靜看到了阿袁的代碼,準(zhǔn)備在自己的工作中使用一下。
不久,阿袁看到阿靜面帶一種奇怪的微笑,走了過來,而目的地明顯是他。讓人興奮,又有種不妙的感覺。
“阿袁,你寫的ObjectHelper有點(diǎn)小問題哦!”
“有什么問題嗎?我這次可是寫了測試用例的。”
“我看了你的測試用例,我需要可以這樣調(diào)用convert。”
阿靜寫出了代碼:
阿袁看到一個(gè)在阿靜面前顯擺的機(jī)會(huì),立刻,毫不保留地向阿靜講解了自己的規(guī)則。
并說明這個(gè)用例違反了輸入?yún)?shù)類型 - 協(xié)變不能規(guī)則。
“好吧,這樣寫code,總該可以吧?”,阿靜繼續(xù)問道。
阿靜把代碼中的new A2()改成new A3()。
阿靜繼續(xù)說:
“調(diào)用者傳入子類A3的實(shí)例,后臺(tái)程序只要負(fù)責(zé)把這個(gè)實(shí)例傳給處理函數(shù)convertA3ToB2不就行了。”
阿袁也看出了可能性。
“你說的有些道理。調(diào)用者可以維護(hù)輸入?yún)?shù)和輸入函數(shù)之間的一致性,這樣就可以跳過輸入?yún)?shù)類型 - 協(xié)變不能規(guī)則的約束。”
“我們發(fā)現(xiàn)了一個(gè)新的規(guī)則。”
輸入?yún)?shù)類型 - 調(diào)用者的協(xié)變規(guī)則:調(diào)用者可以維護(hù)這樣一種一致性:輸入值 匹配 輸入函數(shù)的輸入?yún)?shù)類型,這樣可以使用協(xié)變。
阿袁畫出下面的說明草圖:
// 對于函數(shù)參數(shù)的輸入?yún)?shù)的數(shù)據(jù)類型TInput,看看是否可以轉(zhuǎn)換成傳入函數(shù)的輸入?yún)?shù)的數(shù)據(jù)類型? TInput -->X f(x: TInputSubType) // 協(xié)變在輸入中是不允許的// 然而, 如果調(diào)用者輸入一個(gè)TInputSubType實(shí)例, // 并且使用一個(gè)支持TInputSubType的函數(shù)f,造成了前后一致。 // 輸入中的協(xié)變就變得允許了。 TInputSubType ---> convert(x: TInput, f(x: TInputSubType))“謝謝!我把這個(gè)實(shí)現(xiàn)一下,我的代碼可以進(jìn)化了。”
阿袁使用了協(xié)變語法,代碼變成了:
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)用途的再次理解
阿袁昨晚并沒有睡好,一直在考慮昨天的問題,既然,輸入可以允許協(xié)變,那么是否有輸出需要逆變的例子呢?
早上,找到了阿靜,和她商量商量這個(gè)問題。
“關(guān)于昨天那個(gè)問題,你的例子證明了對于輸入,有需要協(xié)變的情況。你覺得有沒有對于輸出,需要逆變的例子呢?”
“我想,我們可以從你的草圖繼續(xù)看下去。”
昨天,輸出逆變的草圖是這樣:
// 對于傳入函數(shù)的返回值,看看是否可以轉(zhuǎn)換為調(diào)用函數(shù)的返回值類型TOutput? f(): TOutputSuperType -->X TOutput // 逆變在輸出中是不允許的"怎么能變成這樣呢?"
f(): TOutputSuperType ---> TOutput“我覺得還是需要調(diào)用者,來參與。” 阿靜說。
阿袁突然間醍醐灌頂?shù)恼f道,“我明白了。調(diào)用者可以只接受父類類型。像這樣子。”
“太好了,阿袁。今天又進(jìn)化了。”
“好,我去把它改好。”
阿袁回去后,使用了逆變的語法,把ObjectHelper代碼改成了:
class ObjectHelper[TInput, TOutput] {def convert[T1 <: TInput, T2 >: TOutput](x: T1, f: T1 => T2): T2 = {f(x)} }測試用例也補(bǔ)全了:
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天:一個(gè)更簡潔的實(shí)現(xiàn)
一個(gè)更簡潔的實(shí)現(xiàn)
今天,阿袁在做了大量嘗試后,發(fā)現(xiàn)一個(gè)簡潔的實(shí)現(xiàn)方案。
似乎scala編譯器,已經(jīng)很好的考慮了這個(gè)問題。不用協(xié)變和逆變的語法也能支持想要的功能,
所有的9個(gè)函數(shù)都可以合理的使用。
也發(fā)現(xiàn)了C#中等價(jià)的實(shí)現(xiàn)方式:
public TOutput Convert<TInput, TOutput>(TInput x, Func<TInput, TOutput> f) {return f(x);}對一個(gè)函數(shù)變量,會(huì)怎么樣呢?
由于函數(shù)變量不能設(shè)定協(xié)變和逆變約束,因此只有最基本的四種函數(shù)可以設(shè)置。
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#中等價(jià)的實(shí)現(xiàn)方式:
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#的逆變,協(xié)變語法。
不帶關(guān)鍵字in/out的實(shí)現(xiàn),有個(gè)小問題:
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}可以看出關(guān)鍵字in/out在賦函數(shù)變量賦值的時(shí)候,會(huì)起到作用。但是不影響直接賦函數(shù)。
總覺得這個(gè)限制,可以繞過去似的。
阿袁工作的第5天:協(xié)變、逆變的一個(gè)真正用途。
昨天的簡潔方案,讓阿袁認(rèn)識到了自己還沒有明白協(xié)變、逆變的真正用途。
它們到底有什么用呢?難道只是編譯器自己玩的把戲嗎?
阿袁設(shè)計(jì)了這樣一個(gè)用例:
這是一個(gè)新的ObjectHelper,提供了一個(gè)比較函數(shù)compare,
這個(gè)函數(shù)可以把比較兩個(gè)對象,并返回一個(gè)比較結(jié)果。
測試用例是這樣,還是使用了A系列作為輸入類型,B系列作為輸出類型。
class A1 {} class A2 extends A1 {} class A3 extends A2 {}class B1 {} class B2 extends B1 {} class B3 extends B2 {}測試用例,考慮了這樣一個(gè)case:
期望可以比較兩個(gè)A3類型的數(shù)據(jù),返回一個(gè)B1的比較結(jié)果。
可是我們只有一個(gè)A1對象的比較器,這個(gè)比較器可以返回一個(gè)B3的比較結(jié)果。
第一次測試
- 失敗:
- 失敗原因
類型匹配不上,錯(cuò)誤信息提示要使用+TInput和-TOutput.
第二次測試
- 根據(jù)提示,修改代碼為:
- 再次運(yùn)行,再次失敗:
- 失敗原因:
-TOutput為逆變,卻要使用到協(xié)變的返回值位置上。+TInput為協(xié)變,卻要使用到逆變的位置上。
第三次測試
根據(jù)提示,修改代碼為:
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)} }再次運(yùn)行,成功!
總結(jié):
這個(gè)用例的一個(gè)特點(diǎn)是:在實(shí)際場合下,不能找到一個(gè)類型完全匹配的外部幫助函數(shù)。
一個(gè)糟糕的情況是,外部幫助函數(shù)的輸入?yún)?shù)類型比較弱(就是說,是父類型),
可以使用逆變的方法,調(diào)用這個(gè)弱的外部幫助函數(shù)。
阿袁的日記
2016年9月X日 星期六
這幾天,有了一些協(xié)變和逆變的經(jīng)驗(yàn)。根據(jù)認(rèn)識的高低,分為下面的幾個(gè)Level。
- Level 0:知道
- 其實(shí),編譯器和類庫已經(jīng)做好了一切,這些概念只是它們的內(nèi)部把戲。我根本不用考慮它。
- Level 1:知道
- 協(xié)變和逆變發(fā)生的場景
- 給一個(gè)泛型對象賦值
- 給一個(gè)函數(shù)變量賦值
- 給一個(gè)泛型函數(shù)傳入一個(gè)函數(shù)參數(shù)
- 協(xié)變是將對象從父類型轉(zhuǎn)換成子類型
- 逆變是將對象從子類型轉(zhuǎn)換成父類型
- 協(xié)變和逆變發(fā)生的場景
- Level 2:了解協(xié)變和逆變的語法
- Scala: +T : class的協(xié)變
- Scala: -T :class的逆變
- Scala: T <: S :function的協(xié)變
- Scala: T >: S : function的逆變
- C#: out :協(xié)變
- C#: in : 逆變
- Level 3:理解協(xié)變和逆變發(fā)生的場景和用例
- 調(diào)用者對輸入?yún)?shù)的協(xié)變用例
- 調(diào)用者對輸出參數(shù)的逆變用例
- 調(diào)用者只有一個(gè)不平配的比較函數(shù)用例
- Level 4:能夠?qū)懗鰠f(xié)變、逆變的代碼和測試用例
- 針對類的測試用例
- 針對函數(shù)的測試用例
- 針對函數(shù)變量的測試用例
最后,阿靜真美!
轉(zhuǎn)載于:https://www.cnblogs.com/steven-yang/p/5877647.html
總結(jié)
以上是生活随笔為你收集整理的不变(Invariant), 协变(Covarinat), 逆变(Contravariant) : 一个程序猿进化的故事的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 广播(broadcast)、电视与电视网
- 下一篇: 垂死挣扎-4