首先我想说的是,我知道这个话题已经在 Stack Overflow 上进行了深入讨论。但我不认为给出的解释提供了有助于保留示例和知识的强有力的故事。
有人可以提供一个比喻或故事来帮助我理解与 Kotlin 中的以下示例相关的协变和逆变的概念吗?
以下是结构:
open class Animal
class Dog : Animal()
fun doSomething(fn: (Dog) -> Number) {}
我试图理解为什么以下代码有效:
animalToIntMethod: (Animal) -> Int = {TODO()}
doSomething(animalToIntMethod)
在Kotlin中,
(Dog) -> Number
的内部表示是Function<Dog, Number>
,这涉及到逆变和协变的使用,具体来说是Function<in T, out R>
。但是,我很难理解这些概念的实际工作原理。有人可以解释一下这种情况下逆变和协变的原理吗?
很难用几句话解释整个概念,正如你所说,你已经阅读了一些解释,所以我想我会重复其他地方所说的内容。相反,我将重点关注您的示例。
您的函数
doSomething()
需要另一个将狗“转换”为数字的函数:(Dog) -> Number
。假设 doSomething()
是一位记录员,他创建了某种狗的目录。这个人必须知道每只狗的身高才能将其放入目录中,但他们不知道如何测量狗的身高,因此他们需要一位合格的兽医。兽医是我们的(Dog) -> Number
职能 - 他们知道如何为给定的狗提供身高。
我们找到了一位超级合格的兽医
animalToIntMethod
,他们知道如何测量世界上任何动物 - 因此他们的类型是(Animal) -> Int
。
两个人关注的领域不同。记录员需要一个能够专门测量狗的人。兽医不关注狗,他们可以测量一般动物,他们的专业领域更广泛。但当然,这并不意味着他们不能合作。兽医可以测量任何动物,包括狗。
这是逆变的一个例子,在
in
中表示为 Function<in T, out R>
。纪录片导演不需要专门与狗一起工作的人(这将是不变性)。如果所提供的兽医可以与哺乳动物或一般动物一起工作就可以 - 只要他们可以与狗一起工作即可。
Dog 是
doSomething()
所请求函数的输入,因此它是逆变的。随着输入(逆变)类型变得更加具体,函数类型变得更加具体,反之亦然。
可以接受 Any 作为输入的函数也是可以接受 Dogs 作为输入的函数。因此
(Any)->____
也符合 (Dog)->_____
(并且是其子类型)。
或者另一种说法...
doSomething()
希望能够通过向其传递 Dogs 来使用此函数,因此传入的函数必须能够接受 Dogs 作为输入。接受 Any
作为输入的函数可以接受任何输入,因此它肯定可以接受 Dogs。
Number 是
doSomething()
所请求函数的输出,因此它是协变的。随着输出(协变)类型变得更加具体,函数类型也变得更加具体,反之亦然。
可以生成整数的函数也是生成数字的函数。因此
(___)->Int
也符合 (___)->Number
(并且是其子类型)。
另一种说法...
doSomething()
期望能够使用此函数来生成数字。如果它得到一个产生整数的函数,那么它得到的是数字,所以请求得到满足。
函数类型参数不支持此功能。由于函数是第一类类型,因此强制输入或输出保持不变会违反里氏替换原则。
我花了一段时间才找到可以帮助我牢固掌握这些概念的资源。我知道有时发布链接并继续前进似乎有点逃避,但这篇文章(全文阅读)确实证明是一个极好的资源,可以帮助我理解这些概念(特别是对于理解更深奥的逆变) ):https://proandroiddev.com/understanding-type-variance-in-kotlin-d12ad566241b