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<out T>
等效于 Java 中的 List<? extends T>
。
Kotlin 中的List<in T>
等价于 Java 中的 List<? super T>
例如,在 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<out T>
的例子中,很明显我们可以这样做:
val n1: Number = numberList[0]
val n2: Number = doubleList[0]
因此,当需要List<Number>
时提供List<Double>
是安全的,因此List<Number>
是List<Double>
的超类型,但反之则不然。
在Comparable<in T>
的示例中:
val double: Double = 1.0
doubleComparable.compareTo(double)
numberComparable.compareTo(double)
所以当需要Comparable<Double>
时提供Comparable<Number>
是安全的,因此Comparable<Double>
是Comparable<Number>
的超类型,但反之则不然。
【讨论】:
我认为对于看到List<out T>
声明的人来说最重要的一点是out
使其不可变(与可变集合相比,后者没有输出)。在答案中提及和强调这一点可能会有所帮助。隐式转换是这一点的结果,而不是要点(因为不能写入 Listout
部分并不是使 List
不可变的原因。您可以轻松创建自己的具有 clear()
方法的 List<out T>
接口,因为它不需要任何参数。
这太令人困惑了。我想记住的是规则 PECS - 生产者扩展,消费者超级。在 Kotlin 中,out
映射到生产者,in
映射到消费者。
这个例子非常清楚正确。不过,我认为 Kotlin 库中定义的 Collection<out E>
非常令人困惑。事实上,如果Collection
也接受E
作为输入参数,那么为什么Collection
可以成为E
的生产者(参见::contains(E)
)。诀窍是@UnsafeVariance
注释。但是没有地方提到这一点,你必须通过源代码来获取它并说......“oooohhhh”!!!【参考方案3】:
方差修饰符 out
和 in
允许我们通过允许子类型来减少泛型类型的限制和更可重用。
让我们借助对比示例来理解这一点。我们将使用案例作为各种武器的容器。假设我们有以下类型层次结构:
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
修饰符,子类型保留,因此SniperRifle
是Rifle
的子类型时Case<SniperRifle>
是Case<Rifle>
的子类型。因此,useProducer()
函数也可以用Case<SniperRifle>
调用:
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<Weapon>
是Case<Rifle>
的子类型,而Rifle
是Weapon
的子类型。因此,useConsumer()
函数也可以用Case<Weapon>
调用:
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
没有in
或out
修饰符声明的Case
产生并使用T
及其子类型:
fun useProducerConsumer(case: Case<Rifle>)
// Produces Rifle and its subtypes
case.produce()
// Consumes Rifle and its subtypes
case.consume(SniperRifle())
没有in
或out
修饰符,子类型不允许,所以现在Case<Weapon>
和Case<SniperRifle>
都不是Case<Rifle>
的子类型。结果useProducerConsumer()
函数只能用Case<Rifle>
调用:
useProducerConsumer(Case<SniperRifle>()) // Error
useProducerConsumer(Case<Rifle>()) // OK
useProducerConsumer(Case<Weapon>()) // Error
这在生产和消费时限制更多并且可重用性较低,但我们可以读写。
结论
Kotlin 中的 List
只是生产者。因为它是使用out
修饰符声明的:List<out T>
。这意味着您不能向其中添加元素,因为 add(element: T)
是一个消费者函数。每当您希望能够get()
和add()
元素时,请使用不变版本MutableList<T>
。
就是这样!希望这有助于理解in
s 和out
s 的差异!
【讨论】:
很好的解释,用一个很好的例子直奔主题。我正在阅读其他答案,但仍然无法真正理解这些概念。 很好的答案,我也从这个答案中理解得更好。 解释得很清楚。比公认的答案好得多。【参考方案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")
由于Dog
是Animal
的子类型,我们希望使用List<Dog>
作为List<Animal>
的子类型,这意味着我们希望能够做到这一点:
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<Animal>
可以包含Dog
和Cat
,我们可以将其中任何一个添加到MutableList<Animal>
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<Dog>
视为MutableList<Animal>
的子类型,因为您可以对后者做一些您不能对前者做的事情(插入一只猫)。
举个更极端的例子:
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<Dog>
用作List<Animal>
是安全的,因为您不能附加到List
。这就是out
告诉我们的。 out
说“这是我输出的一种类型,但我不把它当作我消费的新输入”
【讨论】:
@NaveenRao 如果您没有out
,那么尝试调用allSpeak(mixed)
将无法编译。您只能使用 List<Animal>
调用它
@NaveenRao 其实很抱歉,我想我误解了你的问题。 List
被声明为 List<out T>
。您是否想知道in
为您提供什么安全性?
是的,我的意思是你举例说明为什么需要out
和in
。你能用out
和in
给出同样的例子吗?
@NaveenRao 在现实世界中,List
确实使用out
,所以allSpeak
有效且安全
这是一个很好的解释,但在我看来,In 和 Out 似乎是为了纠正程序中的设计问题。良好的类和继承设计,你不应该面对任何需要进出的情况。例如,打字稿中不存在这些概念,或者我没有抓住重点?【参考方案5】:
这样记住:
in
是“for input”——你想在里面放(写)一些东西(所以它是一个“消费者”)
out
是“for output” - 你想从中获取(读取)一些东西(所以它是一个“生产者”)
如果你来自 Java,
<in T>
用于输入,所以它就像<? super T>
(消费者)
<out T>
用于输出,所以就像<? extends T>
(生产者)
【讨论】:
【参考方案6】:请参考manual of kotlin
Kotlin
List<out T>
类型是一个提供只读的接口 大小,获取等操作。就像在 Java 中一样,它继承自Collection<T>
又继承自Iterable<T>
。方法 更改列表由MutableList<T>
接口添加。这 模式也适用于Set<out T>/MutableSet<T>
和Map<K, out
V>/MutableMap<K, V>
还有这个,
在 Kotlin 中,有一种方法可以向 编译器。这称为声明站点差异:我们可以注释 Source 的类型参数 T 以确保它只被返回 (制作)来自
Source<T>
的成员,从未消费过。去做这个 我们提供了 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
的成员中,但在 returnC<Base>
可以安全地成为C<Derived>
的超类型。用“聪明的话”他们说
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 检查泛型参数类型 )