【问题标题】:What is out keyword in kotlinkotlin 中的 out 关键字是什么
【发布时间】:2017-06-01 04:44:13
【问题描述】:

我看不懂,在kotlin中找不到out关键字的意思。

您可以在这里查看示例:

List<out T>

如果有人能解释一下这个意思。将不胜感激。

【问题讨论】:

    标签: generics kotlin


    【解决方案1】:

    Kotlin 中的List&lt;out T&gt; 等效于 Java 中的 List&lt;? extends T&gt;

    Kotlin 中的List&lt;in T&gt; 等价于 Java 中的 List&lt;? super T&gt;

    例如,在 Kotlin 中,您可以执行以下操作

    val value : List<Any> = listOf(1,2,3)
    //since List signature is List<out T> in Kotlin
    

    【讨论】:

      【解决方案2】:

      有了这个签名:

      List<out T>
      

      你可以这样做:

      val doubleList: List<Double> = listOf(1.0, 2.0)
      val numberList: List<Number> = doubleList
      

      这意味着 T协变的

      C 类的类型参数 T 声明为 out 时,C 可以安全地是 C超类型

      这与 in 形成对比,例如

      Comparable<in T>
      

      你可以这样做:

      fun foo(numberComparable: Comparable<Number>) {
        val doubleComparable: Comparable<Double> = numberComparable
        // ...
      }
      

      这意味着T逆变的

      C类的类型参数T被声明in时,C可以安全地是 C超类型

      另一种记忆方式:

      消费者进入,生产者退出

      Kotlin Generics Variance

      --2019 年 1 月 4 日更新--

      对于“Consumer in, Producer out”,我们只从Producer中读取——调用方法获取T类型的结果;并且仅通过传入 T 类型的参数写入消费者 - 调用方法。

      List&lt;out T&gt;的例子中,很明显我们可以这样做:

      val n1: Number = numberList[0]
      val n2: Number = doubleList[0]
      

      因此,当需要List&lt;Number&gt; 时提供List&lt;Double&gt; 是安全的,因此List&lt;Number&gt;List&lt;Double&gt; 的超类型,但反之则不然。

      Comparable&lt;in T&gt; 的示例中:

      val double: Double = 1.0
      doubleComparable.compareTo(double)
      numberComparable.compareTo(double)
      

      所以当需要Comparable&lt;Double&gt; 时提供Comparable&lt;Number&gt; 是安全的,因此Comparable&lt;Double&gt;Comparable&lt;Number&gt; 的超类型,但反之则不然。

      【讨论】:

      • 我认为对于看到List&lt;out T&gt; 声明的人来说最重要的一点是out 使其不可变(与可变集合相比,后者没有输出)。在答案中提及和强调这一点可能会有所帮助。隐式转换是这一点的结果,而不是要点(因为不能写入 List,因此可以安全地将其作为对 List 的引用)。
      • 对不起,还是听不懂。
      • @minsk out 部分并不是使 List 不可变的原因。您可以轻松创建自己的具有 clear() 方法的 List&lt;out T&gt; 接口,因为它不需要任何参数。
      • 这太令人困惑了。我想记住的是规则 PECS - 生产者扩展,消费者超级。在 Kotlin 中,out 映射到生产者,in 映射到消费者。
      • 这个例子非常清楚正确。不过,我认为 Kotlin 库中定义的 Collection&lt;out E&gt; 非常令人困惑。事实上,如果Collection 也接受E 作为输入参数,那么为什么Collection 可以成为E 的生产者(参见::contains(E))。诀窍是@UnsafeVariance 注释。但是没有地方提到这一点,你必须通过源代码来获取它并说......“oooohhhh”!!!
      【解决方案3】:

      方差修饰符 outin 允许我们通过允许子类型来减少泛型类型的限制和更可重用。

      让我们借助对比示例来理解这一点。我们将使用案例作为各种武器的容器。假设我们有以下类型层次结构:

      open class Weapon
      open class Rifle : Weapon()
      class SniperRifle : Rifle()
      

      out 产生 T保留子类型

      当您使用out 修饰符声明泛型类型时,它称为协变。协变是T生产者,这意味着函数可以返回T,但不能将T 作为参数:

      class Case<out T> {
          private val contents = mutableListOf<T>()
          fun produce(): T = contents.last()         // Producer: OK
          fun consume(item: T) = contents.add(item)  // Consumer: Error
      }
      

      使用out 修饰符声明的Case 产生T 及其子类型:

      fun useProducer(case: Case<Rifle>) {
          // Produces Rifle and its subtypes
          val rifle = case.produce()
      }
      

      使用out 修饰符,子类型保留,因此SniperRifleRifle 的子类型时Case&lt;SniperRifle&gt;Case&lt;Rifle&gt; 的子类型。因此,useProducer() 函数也可以用Case&lt;SniperRifle&gt; 调用:

      useProducer(Case<SniperRifle>())               // OK
      useProducer(Case<Rifle>())                     // OK
      useProducer(Case<Weapon>())                    // Error
      

      这在生产时限制较少并且更可重用,但我们的类变成只读


      in 使用 T反转子类型

      当您使用in 修饰符声明泛型类型时,它称为contravariant。逆变器是T消费者,这意味着函数可以将T 作为参数,但不能返回T

      class Case<in T> {
          private val contents = mutableListOf<T>()
          fun produce(): T = contents.last()         // Producer: Error
          fun consume(item: T) = contents.add(item)  // Consumer: OK
      }
      

      使用in 修饰符声明的Case 消耗T 及其子类型:

      fun useConsumer(case: Case<Rifle>) {
          // Consumes Rifle and its subtypes
          case.consume(SniperRifle())
      }
      

      使用in 修饰符,子类型颠倒,所以现在Case&lt;Weapon&gt;Case&lt;Rifle&gt; 的子类型,而RifleWeapon 的子类型。因此,useConsumer() 函数也可以用Case&lt;Weapon&gt; 调用:

      useConsumer(Case<SniperRifle>())               // Error          
      useConsumer(Case<Rifle>())                     // OK
      useConsumer(Case<Weapon>())                    // OK
      

      限制较少并且在消费时更可重用,但是我们的类变成只写


      不变量产生和消费T不允许子类型化

      当你声明一个没有任何变体修饰符的泛型类型时,它被称为invariant。不变量是T 的生产者和消费者,这意味着函数可以将T 作为参数,也可以返回T

      class Case<T> {
          private val contents = mutableListOf<T>()
          fun produce(): T = contents.last()         // Producer: OK
          fun consume(item: T) = contents.add(item)  // Consumer: OK
      }
      

      没有inout 修饰符声明的Case 产生并使用T 及其子类型:

      fun useProducerConsumer(case: Case<Rifle>) {
          // Produces Rifle and its subtypes
          case.produce()
          // Consumes Rifle and its subtypes
          case.consume(SniperRifle())
      }
      

      没有inout 修饰符,子类型不允许,所以现在Case&lt;Weapon&gt;Case&lt;SniperRifle&gt; 都不是Case&lt;Rifle&gt; 的子类型。结果useProducerConsumer()函数只能用Case&lt;Rifle&gt;调用:

      useProducerConsumer(Case<SniperRifle>())       // Error
      useProducerConsumer(Case<Rifle>())             // OK
      useProducerConsumer(Case<Weapon>())            // Error
      

      这在生产和消费时限制更多并且可重用性较低,但我们可以读写


      结论

      Kotlin 中的 List 只是生产者。因为它是使用out 修饰符声明的:List&lt;out T&gt;。这意味着您不能向其中添加元素,因为 add(element: T) 是一个消费者函数。每当您希望能够get()add() 元素时,请使用不变版本MutableList&lt;T&gt;

      就是这样!希望这有助于理解ins 和outs 的差异!

      【讨论】:

      • 很好的解释,用一个很好的例子直奔主题。我正在阅读其他答案,但仍然无法真正理解这些概念。
      • 很好的答案,我也从这个答案中理解得更好。
      • 解释得很清楚。比公认的答案好得多。
      【解决方案4】:

      这些答案解释了out什么,但不是为什么你需要它,所以让我们假设我们根本没有out。想象三个类:Animal、Cat、Dog,以及一个获取Animal列表的函数

      abstract class Animal {
        abstract fun speak()
      }
      
      class Dog: Animal() {
        fun fetch() {}
        override fun speak() { println("woof") }
      }
      
      class Cat: Animal() {
        fun scratch() {}
        override fun speak() { println("meow") }
      }
      

      由于DogAnimal 的子类型,我们希望使用List&lt;Dog&gt; 作为List&lt;Animal&gt; 的子类型,这意味着我们希望能够做到这一点:

      fun allSpeak(animals: List<Animal>) {
          animals.forEach { it.speak() }
      }
      
      fun main() {
        val dogs: List<Dog> = listOf(Dog(), Dog())
        allSpeak(dogs)
      
        val mixed: List<Animal> = listOf(Dog(), Cat())
        allSpeak(mixed)
      }
      

      没关系,代码将为狗打印woof woof,为混合列表打印woof meow

      问题是当我们有一个可变容器时。由于List&lt;Animal&gt; 可以包含DogCat,我们可以将其中任何一个添加到MutableList&lt;Animal&gt;

      fun processAnimals(animals: MutableList<Animal>) {
         animals.add(Cat()) // uh oh, what if this is a list of Dogs?
      }
      
      fun main() {
        val dogs: MutableList<Dog> = mutableListOf(Dog(), Dog())
        processAnimals(dogs) // we just added a Cat to a list of Dogs!
        val d: Dog = dogs.last() // list of Dogs, so return type of .last() is Dog
                                 // but this is actually a Cat
        d.fetch() // a Cat can't fetch, so what should happen here?
      }
      

      您不能放心地将MutableList&lt;Dog&gt; 视为MutableList&lt;Animal&gt; 的子类型,因为您可以对后者做一些您不能对前者做的事情(插入一只猫)。

      举个更极端的例子:

      val dogs: MutableList<Dog> = mutableListOf(Dog())
      val anything: MutableList<Any> = dogs
      // now I can add any type I want to the dogs list through the anything list
      anything.add("hello world")
      

      问题仅在添加到列表时出现,而不是从列表中读取。将List&lt;Dog&gt; 用作List&lt;Animal&gt; 是安全的,因为您不能附加到List。这就是out 告诉我们的。 out 说“这是我输出的一种类型,但我不把它当作我消费的新输入”

      【讨论】:

      • @NaveenRao 如果您没有out,那么尝试调用allSpeak(mixed) 将无法编译。您只能使用 List&lt;Animal&gt; 调用它
      • @NaveenRao 其实很抱歉,我想我误解了你的问题。 List 被声明为 List&lt;out T&gt;。您是否想知道in 为您提供什么安全性?
      • 是的,我的意思是你举例说明为什么需要outin。你能用outin给出同样的例子吗?
      • @NaveenRao 在现实世界中,List 确实使用out,所以allSpeak 有效且安全
      • 这是一个很好的解释,但在我看来,In 和 Out 似乎是为了纠正程序中的设计问题。良好的类和继承设计,你不应该面对任何需要进出的情况。例如,打字稿中不存在这些概念,或者我没有抓住重点?
      【解决方案5】:

      这样记住:

      in 是“for input”——你想在里面放(写)一些东西(所以它是一个“消费者”)

      out 是“for output” - 你想从中获取(读取)一些东西(所以它是一个“生产者”)

      如果你来自 Java,

      &lt;in T&gt; 用于输入,所以它就像&lt;? super T&gt;(消费者)

      &lt;out T&gt; 用于输出,所以就像&lt;? extends T&gt;(生产者)

      【讨论】:

        【解决方案6】:

        请参考manual of kotlin

        Kotlin List&lt;out T&gt; 类型是一个提供只读的接口 大小,获取等操作。就像在 Java 中一样,它继承自 Collection&lt;T&gt; 又继承自 Iterable&lt;T&gt;。方法 更改列表由MutableList&lt;T&gt; 接口添加。这 模式也适用于 Set&lt;out T&gt;/MutableSet&lt;T&gt;Map&lt;K, out V&gt;/MutableMap&lt;K, V&gt;

        还有这个,

        在 Kotlin 中,有一种方法可以向 编译器。这称为声明站点差异:我们可以注释 Source 的类型参数 T 以确保它只被返回 (制作)来自Source&lt;T&gt; 的成员,从未消费过。去做这个 我们提供了 out 修饰符:

        > abstract class Source<out T> {
        >     abstract fun nextT(): T }
        > 
        > fun demo(strs: Source<String>) {
        >     val objects: Source<Any> = strs // This is OK, since T is an out-parameter
        >     // ... }
        

        一般规则是:当声明类C的类型参数T时 out,它可能只出现在C的成员中,但在 return C&lt;Base&gt; 可以安全地成为 C&lt;Derived&gt; 的超类型。

        用“聪明的话”他们说C 类在 参数T,或者T 是协变类型参数。你可以想到 C 是 T 的生产者,而不是 T 的消费者。 out 修饰符称为方差注释,因为它是 在类型参数声明站点提供,我们讲讲 声明站点差异。这与 Java 的使用站点相反 类型使用中的通配符使类型协变的差异。

        【讨论】:

          【解决方案7】:

          来自Functional programming in Kotlin的一个很好的解释:

          考虑以下代码:

          sealed class List<out A>
          
          object Nil : List<Nothing>()
          
          data class Cons<out A>(val head: A, val tail: List<A>) : List<A>()
          

          在声明类 List 中,类型参数 A 前面的 out 是方差注释,表明 A 是 List 的协变或“正”参数。例如,这意味着 List 被认为是 List 的子类型,假设 Dog 是 Animal 的子类型。 (更一般地说,对于所有类型 X 和 Y,如果 X 是 Y 的子类型,则 List 是 List 的子类型。)我们可以省略 A 前面的 out,这将使 List 在该类型参数中保持不变。 但请注意,现在 Nil 扩展了 List。 Nothing 是所有类型的子类型,这意味着结合方差注释,Nil 可以被视为 List、List 等等,完全符合我们的要求。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2016-06-03
            • 1970-01-01
            • 2018-08-11
            • 1970-01-01
            • 1970-01-01
            • 2010-09-28
            • 1970-01-01
            相关资源
            最近更新 更多