kotlin 中的 out 关键字是啥

Posted

技术标签:

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

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

您可以在这里查看示例:

List<out T>

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

【问题讨论】:

【参考方案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 等等,完全符合我们的要求。

【讨论】:

以上是关于kotlin 中的 out 关键字是啥的主要内容,如果未能解决你的问题,请参考以下文章

这个“if”分支中的“out”关键字的目的是啥(用于 WebMatrix 的 OAuth“Starter Site”模板)? [复制]

Kotlin中接口抽象类泛型out(协变)in(逆变)reified关键字的详解

Kotlin泛型 ③ ( 泛型 out 协变 | 泛型 in 逆变 | 泛型 invariant 不变 | 泛型逆变协变代码示例 | 使用 reified 关键字检查泛型参数类型 )

Kotlin泛型总结 ★ ( 泛型类 | 泛型参数 | 泛型函数 | 多泛型参数 | 泛型类型约束 | 可变参数结合泛型 | out 协变 | in 逆变 | reified 检查泛型参数类型 )

ref、val 和 out 对方法参数的含义是啥?

Kotlin 中的 reified 关键字是如何工作的?