日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

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

發布時間:2024/6/21 编程问答 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 不变(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,依次列出下表:

輸入類型輸出類型是否可用
A1B1no
A1B2yes
A1B3yes
A2B1no
A2B2yes
A2B3yes
A3B1no
A3B2no
A3B3no

測試代碼:

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) : 一个程序猿进化的故事的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。