今天为了解释某个问题而提到协变和逆变,发现每次解释这两个概念都会忘掉它们的本质,然后要重新看看定义,重新消化一下才能说明白。所以我决定把自己对协变和逆变的理解写下来,以免将来再次忘掉。
我知道 .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
,或许你就明白了。