Why a Class/Interface can't be prefixed with out if the class/Interface has val property or function with generic type?

huangapple go评论75阅读模式
英文:

Why a Class/Interface can't be prefixed with out if the class/Interface has val property or function with generic type?

问题

在学习 Kotlin 中的泛型时,我在一本书中看到以下内容:

> 一般来说,类或接口的泛型类型如果在类中被用作返回类型的函数,或者类中拥有该类型的 val 属性,可以在其前面加上 out。然而,如果类具有使用泛型类型作为函数参数或 var 属性的情况,则不能使用 out。

我理解这条规则的含义,但我很乐意通过示例来理解什么情况下可以不遵循这个规则(即在声明泛型类/接口时不受约束地使用 out),以及为什么将返回类型设置为 T 类型可能不“危险”,尽管类/接口仍然可以包含 out T。

以下是一个例子,其中我不理解类属性为何会表现为协变:

class Pet {....}
class Dog : Pet {...}

class PetSomething<T : Pet> {
    val t: T
    fun petDoSomething(t: T) {
        ....   // 这里可能存在什么问题?
    }
}

class DogSomething {
    fun dogDoSomething() {
        val d: Dog = Dog()
        petDoSomething(d)
        // 这里可能有什么问题?
    }
}

此外,该书还展示了以下代码:

> abstract class E&lt;out T&gt; (t:T) { val x = t }

尽管泛型类型是构造函数的输入,但该代码仍然可以被编译。这难道不违反了规则吗?

英文:

When learning generics in Kotlin, I read in a book the following :

> In general, a class or interface generic type may be prefixed with out if the class has functions that use it as a return type, or if the class has val properties of that type. You can’t, however, use out if the class has function parameters or var properties of that generic type.

I understand what the rule says, but i will be happy to understand (by examples) what may be without this rule (i.e there weren't constraint when using out when declaring a generic Class/Interface), and also why it isn't "dangerous" that the return type can be from type T and still class/Interface can contain out T.

Example where can't understand what is the problem that class property will behave as covariant:

   class Pet{....}
class Dog:Pet{...}

class PetSomething &lt;T : Pet&gt;  
{
	T t;
	public fun petDoSomething(T t)
	{
		....   // what can be the problem here?
	}
}


class DogSomething
{
    dogDoSomething()
	{
		d : Dog = Dog()
		petDoSomething(d)
		//what is the problem here???
	}
}

In addition the book display the following code:

> abstract class E&lt;out T&gt; (t:T) { val x = t }

and the code is being compiled although generic type is an input of constructor. Doesn't it break the rule?

答案1

得分: 1

你引用了:"You can’t, however, use out if the class has function parameters or var properties of that generic type."

构造函数不是成员函数或属性,因此不受此规则限制。在构造函数的位置使用参数的类型是安全的,因为在构造时已知类型。

考虑以下类:

abstract class Pet

class Cat: Pet()
class Dog: Pet()

class PetOwner<out T: Pet>(val pet: T)

当你调用PetOwner构造函数并传入一个Cat时,编译器知道你正在构造一个PetOwner,因为它知道传递给构造函数的值满足的类型。在对象构造之前,它不必将Cat向上转型为Pet。然后构造的对象可以安全地向上转型为PetOwner,因为不会再次向实例传递任何T。不会发生不安全的情况,因为没有对参数进行转换。

对于函数参数和var属性,对于out类型来说是不安全的,因为对象已经构造出来,可能已经传递给某个变量,该变量已经将其向上转型为其他类型。

想象一下,如果编译器允许您为var属性定义out T,就像这样:

class PetOwner<out T: Pet>(var pet: T)

那么你可以这样做:

val catOwner: PetOwner<out Cat> = PetOwner(Cat())
val petOwner: PetOwner<out Pet> = catOwner
petOwner.pet = Dog()
val cat: Cat = catOwner.pet // ClassCastException!

类型安全规则阻止了这种情况的发生。但是对于val构造函数参数来说,这是不可能的。在将参数传递给构造函数并获得可以传递的实例之间,无法将对象传递给其他变量并向上转型其类型。

英文:

You quoted: "You can’t, however, use out if the class has function parameters or var properties of that generic type."

A constructor is not a member function or property, so it is not subject to this rule. It is safe to use the type for a parameter at the site of the constructor, because the type is known when you are constructing it.

Consider these classes:

abstract class Pet

class Cat: Pet()
class Dog: Pet()

class PetOwner&lt;out T: Pet&gt;(val pet: T)

When you call the PetOwner constructor and pass in a Cat, the compiler knows you are constructing a PetOwner&lt;out Cat&gt; because it knows the value passed to the constructor satisfies the type of &lt;out Cat&gt;. It doesn't have to upcast Cat to Pet before the object is constructed. Then the constructed object can be safely upcast to PetOwner&lt;Pet&gt; because no T is ever going to be passed to the instance again. There is nothing unsafe that can happen, because no casting is done to the parameter.

Function parameters and var properties would be unsafe for an out type because the object is already constructed and might have been passed to some variable that has already upcast it to something else.

Imagine that the compiler let you define out T for a var property like this:

class PetOwner&lt;out T: Pet&gt;(var pet: T)

Then you could do this:

val catOwner: PetOwner&lt;out Cat&gt; = PetOwner(Cat())
val petOwner: PetOwner&lt;out Pet&gt; = catOwner
petOwner.pet = Dog()
val cat: Cat = catOwner.pet // ClassCastException!

The type safety rules prevent this scenario from being possible. But this isn't possible for a val constructor parameter. There is no way to pass the object to other variables and upcast its type in between passing the parameter to the constructor and having an instance that you can pass around.

答案2

得分: 0

问题如下:

val x = DogSomething()
val y: PetSomething<Pet> = x // out 可以允许这样
y.petDoSomething(Cat())

请注意,DogSomething 上的 petDoSomething 只需要处理 Dog

而且尽管泛型类型是构造函数的输入,代码仍在被编译。这不违反规则吗?

不会,因为构造函数不是相关意义上的成员;它无法在上面的 y 上调用。

英文:

The problem is this:

val x = DogSomething() 
val y: PetSomething&lt;Pet&gt; = x // would be allowed by out
y.petDoSomething(Cat())

Note that petDoSomething on DogSomething only has to handle Dogs.

> and the code is being compiled although generic type is an input of constructor. Doesn't it break the rule?

It doesn't, because the constructor isn't a member in the relevant sense; it couldn't be called on y above.

答案3

得分: 0

首先,让我们澄清一下,通过在type parameter前加上out关键字,我们可以获得什么。考虑以下的class

class MyList<out T: Number>{
    private val list: MutableList<T> = mutableListOf()
    operator fun get(index: Int) : T = list[index]
}

在这里,out关键字使得MyList对于T是协变的,这实际上意味着你可以执行以下操作:

// 注意等号左边的类型与右边的类型不同
val numberList: MyList<Number> = MyList<Int>()

如果你移除out关键字并尝试重新编译,你将会得到类型不匹配的错误。

通过在type parameter前加上out,你基本上声明了这个类型是T生产者。在上面的例子中,MyListNumbers的生产者。这意味着无论你将T实例化为IntDouble或者Number的某个子类型,你总是能从MyList中获得一个Number(因为Number的每个子类型都是Number)。这也使得你可以执行以下操作:

fun process(list: MyList<Number>) { // 对每个数字做些处理 }
fun main(){
  val ints = MyList<Int>()
  val doubles = MyList<Double>()
  process(ints)     // Int 是 Number,可以将其作为 Number 处理
  process(doubles)  // Double 也是 Number,没问题
}
// 如果移除 out,只能将 MyList<Number> 传递给 process

现在来回答一下,在有out关键字的情况下,为什么T只能在返回位置上?以及如果没有这个限制会发生什么?那就是如果MyList有一个以T作为参数的函数。

fun add(value: T) { list.add(T) }  // MyList 有这个函数

fun main() {
    val numbers = getMyList()   // numbers 可能是 MyList<Int>、MyList<Double> 或其他类型
    numbers.add(someInt)        // 不能存储 Int,如果它是 MyList<Double> 怎么办(Int 不等于 Double)
    numbers.add(someDouble)     // 不能这样做,如果它是 MyList<Int> 怎么办
}

// 我们不知道我们会得到什么类型的 MyList
fun getMyList(): MyList<Number>(){
  return if(feelingGood) { MyList<Int>() }
         else if(feelingOk) { MyList<Double>() }
         else { MyList<SomeOtherSubType>() }
}

这就是为什么需要这个限制,基本上是为了保证类型安全性。

至于abstract class E<out T> (t:T) { val x = t }能否被编译通过,《Kotlin In Action》中有以下解释:

注意构造函数参数既不属于in位置也不属于out位置。即使类型参数被声明为out,你仍然可以在构造函数参数中使用它。这种变化保护了类实例免受滥用,如果你将其作为更通用类型的实例来处理:你只是不能调用潜在危险的方法。构造函数不是可以稍后调用的方法(在实例创建之后),因此它不能潜在地危险。

英文:

First lets clearfy what do we get by prefixing a type parameter with out keyword. consider the following class:

class MyList&lt;out T: Number&gt;{
    private val list: MutableList&lt;T&gt; = mutableListOf()
    operator fun get(index: Int) : T = list[index]
}

out keyword here makes MyList covariant in T, which essentially means you can do the following :

// note that type on left side of = is different than the one on right

val numberList: MyList&lt;Number&gt; = MyList&lt;Int&gt;()

if you remove the out keyword and try to compile again, you will get type mismatch error.

by prefixing the type parameter with out, you are basically declaring the type to be a producer of T's, in the example above MyList is a producer of Numbers.
which means no matter if you instantiate T as Int or Double or some other subtype of Number, you will always be able to get a Number from MyList (Because every subtype of Number is a Number). which also allows you to to do the following:

fun process(list: MyList&lt;Number&gt;) { // do something with every number }
fun main(){
  val ints = MyList&lt;Int&gt;()
  val doubles = MyList&lt;Double&gt;()
  process(ints)     // Int is a Number, go ahead and process them as Numbers
  process(doubles)  // Double is also a Number, no prob here
}
// if you remove out, you can only pass MyList&lt;Number&gt; to process

Now lets answer With out keyword why T should only be in return position? and what can happen without this constraint?, that is if MyList had a function taking T as parameter.

fun add(value: T) { list.add(T) }  // MyList has this function

fun main() {
    val numbers = getMyList()   // numbers can be MyList&lt;Int&gt;, MyList&lt;Double&gt; or something else
    numbers.add(someInt)        // cant store Int, what if its MyList&lt;Double&gt; ( Int != Double) 
    numbers.add(someDouble)     // cant do this, what if its MyList&lt;Int&gt;
}

// We dont know what type of MyList we going to get
fun getMyList(): MyList&lt;Number&gt;(){
  return if(feelingGood) { MyList&lt;Int&gt; () }
         else if(feelingOk&gt; { MyList&lt;Double&gt; () }
         else { MyList&lt;SomeOtherSubType&gt;() }
}

that is why constraint is required, its basically there to guarantee type safety.

as for abstract class E&lt;out T&gt; (t:T) { val x = t } being compiled, Kotlin In Action has following to say

> Note that constructor parameters are in neither the in nor the out
> position. Even if a type parameter is declared as out, you can still
> use it in a constructor parameter. The variance protects the class
> instance from misuse if you’re working with it as an instance of a
> more generic type: you just can’t call the potentially dangerous
> methods. The constructor isn’t a method that can be called later
> (after an instance creation), and therefore it can’t be potentially
> dangerous.

huangapple
  • 本文由 发表于 2020年10月18日 21:04:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/64413654.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定