今天為了解釋某個問題而提到協變和逆變,發現每次解釋這兩個概念都會忘掉它們的本質,然后要重新看看定義,重新消化一下才能說明白。所以我決定把自己對協變和逆變的理解寫下來,以免將來再次忘掉。
我知道 .NET 的用戶喜歡用 delegate TResult Func<in T, out TResult>(T arg);
來解釋協變逆變,我則喜歡把 Func
的簽名簡寫為 Haskell 簽名形式。也就是說,把 Func<T, TResult>
寫成 f :: a -> b
的形式;把 Func<T1, T2, Result>
寫成 f :: a -> b -> c
的形式。
其實無論是協變還是逆變,本質都是一樣的:對於簽名為 f :: A -> B
的函數,實際可接受的參數范圍為 ASub
,實際可返回的參數范圍為 BSub
。這個很容易理解吧?任何時候子類的實例都可以當做超類實例來使用,無論是接受還是返回。
協變和逆變用於描述高階函數簽名,如 f :: (X -> Y) -> Z
。那上面的 f :: A -> B
做模版,我們可以把 (X -> Y)
看做 A
,把 Z
看做 B
。應用同樣的邏輯,函數實際可接受的參數范圍是 (X -> Y)
的子類,實際可返回的參數范圍是 Z
的子類。對於后者我們沒什么疑問,但 (X -> Y)
的子類到底是什么呢?它的所謂「子類」應該是 (XSuper -> YSub)
。
為什么說 (X -> Y)
的「子類」應該是 (XSuper -> YSub)
呢?因為子類在能力上應該完整覆蓋超類的能力,因此如果對方要求你提供一個函數,這個函數接受 X
類型返回 Y
類型,你提供的函數至少要能接受 X
的超類而返回必須是 Y
的子類。這時候 X
是逆變參數(類型可以更寬松),而 Y
是協變參數(類型可以更嚴格)。
一般來說,如果把「類型可以更嚴格」看做協變的話,函數的返回類型一定可以協變,非高階函數的參數也可以協變,高階函數的非函數參數同樣可以協變。把「類型可以更寬松」看做逆變的話,只有高階函數中的函數參數中會出現逆變,也就是作為參數的參數出現。那么參數的參數的參數呢?也就是說高階函數的參數仍然是高階函數,那會怎么樣呢?這個大家可以嘗試自行分析,盯住 f :: ((X -> Y) -> Z) -> W
看一會兒,再不停類比上文的 f :: A -> B
,或許你就明白了。