【问题标题】:Why it is forbidden to use 'out' keyword in generics if a method excepts the type parameter as a parameter?如果方法接受类型参数作为参数,为什么禁止在泛型中使用“out”关键字?
【发布时间】:2021-04-22 20:25:20
【问题描述】:

我正在寻找一个在类声明中使用 out 时可能导致问题的示例,并且该类有一个将参数类型作为参数的方法。

另外我正在寻找一个在类声明中使用 in 并且参数类型是类的 var 成员时可能导致问题的示例? 我想我只能通过例子来理解规则

【问题讨论】:

    标签: kotlin generics covariance contravariance


    【解决方案1】:

    假设这些是我们正在使用的类:

    open class Animal
    
    class Cat: Animal() {
        fun meow() = println("meow")
    }
    

    如果我们用协变 out 类型创建这样的类,并且编译器允许我们使用该类型作为函数参数:

    class Foo<out T: Animal> {
        private var animal: T? = null
    
        fun consumeValue(x: T) { // NOT ALLOWED
            animal = x
        }
    
        fun produceValue(): T? {
            return animal
        }
    }
    

    那么,如果你这样做,将导致我们试图在没有 meow 函数的 Animal 上调用 meow 的不可能的情况:

    val catConsumer = Foo<Cat>()
    val animalConsumer: Foo<Animal> = catConsumer // upcasting is valid for covariant type
    animalConsumer.consumeValue(Animal())
    catConsumer.produceValue()?.meow() // can't call `meow` on plain Animal
    

    如果我们用逆变in 类型创建一个这样的类并且编译器允许我们使用该类型作为返回值:

    class Bar<in T: Animal>(private val library: List<T>) {
        fun produceValue(): T  { // NOT ALLOWED
            return library.random()
        }
    }
    

    那么如果你这样做,它会导致编译器不可能将返回类型转换为子类型。

    val animalProducer: Bar<Animal> = Bar(List(5) { Animal() })
    val catProducer: Bar<Cat> = animalProducer // downcasting is valid for contravariant type
    catProducer.produceValue().meow() // can't call `meow` on plain Animal
    

    属性有一个getter,就像一个返回值的函数,var 属性还有一个setter,就像一个带参数的函数。所以val 属性与逆变(in)不兼容,var 属性与逆变或协方差(out)不兼容。私有属性不受这些限制的影响,因为在类的内部工作中,类型是不变的。所有类可以知道的关于它自己的类型就是它的边界。差异只会影响外部世界如何投射(查看)类。

    因此,val 的示例足以说明为什么任何属性都与逆变不兼容。您可以将val 替换为下面的var,也没有什么不同。

    class Bar<in T: Animal>(
        val animal: T // NOT ALLOWED
    )
    
    val animalProducer: Bar<Animal> = Bar(Animal())
    val catProducer: Bar<Cat> = animalProducer // downcasting is valid for contravariant type
    catProducer.animal.meow() // can't call `meow` on plain Animal
    

    【讨论】:

    • 我猜AnimalCatChildParent 做出了更好的例子:D
    • @Tenfour04,非常感谢。但是能否请您参考 in 关键字的 var 问题?
    • class Foo&lt;out T: Cat&gt; { fun consumeValue(x: T) { // NOT ALLOWED x.meow() } } 不应该是 而不是 T:cat?否则你怎么能创建一个 Foo?
    • 我理解你所有的解释(因为它非常详尽)......只有第一个例子(我们现在正在谈论的那个)有点模糊......有没有有效的方式来说明使用out 并且该类具有获取参数类型的方法时的问题?很抱歉打扰,但对我来说,永远理解这个问题很重要
    • Foo 的例子是我能想到的最简单的例子。如果类型必须是 Cat 的子类型,那么消费函数可以假设传递给它的 T 是 Cat,并在其上调用 Cat 唯一函数。如果我们随后将T 设为out 参数,则此假设不再有效,因为out 类型可以向上转换为超类型。
    【解决方案2】:

    关于方差的小提示

    当您有一个泛型类G&lt;T&gt;(参数化类型)时,差异在于定义不同Ts 的类型G&lt;T&gt; 的层次结构与不同Ts 的层次结构之间的关系自己。

    例如,如果子类 C 扩展了父类 P 那么:

    • List&lt;C&gt; 是否扩展List&lt;P&gt;? (List&lt;T&gt;T 中是协变的)
    • 还是相反? (逆变)
    • 或者List&lt;C&gt;List&lt;P&gt;之间没有关系? (不变量)。

    示例

    现在,考虑List&lt;out T&gt;,这意味着ListT 中的协变。 正如我们刚刚看到的,这样声明列表意味着以下内容成立:“如果C 扩展P,则List&lt;C&gt; 扩展List&lt;P&gt;”。

    让我们在这里假设以下类声明:

    open class Parent {
        fun doParentStuff()
    }
    
    class Child : Parent() {
        fun doChildStuff()
    }
    

    List&lt;out T&gt; 的协方差意味着这是可能的:

    val listOfChild: List<Child> = listOf<Child>(Child(), Child())
    // this is ok because List is covariant in T (out T)
    // so List<Child> is a subtype of List<Parent>, and can be assigned to listOfParent
    val listOfParent: List<Parent> = listOfChild 
    

    那么如果我们可以在List 类中声明一个接受参数T 的方法会发生什么?

    class List<out T> {
        fun add(element: T) {
            // I can guarantee here that I have an instance of T, right?
        }
    }
    

    大多数语言(包括 Kotlin)的规则规定,如果方法接受 T 类型的参数,从技术上讲,您可以获得 T 或 T 的任何子类的实例(此是子类化的重点),但您至少拥有 T 的所有 API。

    但请记住,我们声明了List&lt;out T&gt;,这意味着我可以做到:

    val listOfChild: List<Child> = listOf<Child>(Child(), Child())
    // this is ok because List is covariant in T (out T)
    val listOfParent: List<Parent> = listOfChild
    
    // listOfChild and listOfParent point to the same list instance
    // so here we are effectively adding a Parent instance to the listOfChild
    listOfParent.add(Parent()) 
    
    // oops, the last one is not an instance of Child, bad things will happen here
    // we could fail right here at runtime because Parent cannot be cast to Child
    val child: Child = listOfChild.last
    
    // even worse, look at what looks possible, but is not:
    child.doChildThing()
    

    在这里您可以看到,在List&lt;Child&gt; 实例中,我们实际上可以在声明了Child 类型参数的方法中接收到Parent 的实例,它不是Child 的子类。

    【讨论】:

    • 我认为你的帖子可以为我提供一个缺少我的例子
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-09-28
    相关资源
    最近更新 更多