【问题标题】:Why a Class/Interface can't be prefixed with out if the class/Interface has val property or function with generic type?如果类/接口具有 val 属性或具有泛型类型的函数,为什么类/接口不能以 out 作为前缀?
【发布时间】:2020-10-18 13:04:17
【问题描述】:

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

一般来说,如果类具有使用它作为返回类型的函数,或者如果类具有该类型的 val 属性,则类或接口泛型类型可以使用 out 作为前缀。但是,如果类具有该泛型类型的函数参数或 var 属性,则不能使用 out。

我理解规则所说的内容,但我很乐意(通过示例)理解没有此规则的情况(即在声明泛型 Class/ 时使用 out 时没有约束接口),以及为什么返回类型可以来自类型 T 并且仍然类/接口可以包含 out T 并不“危险”。

无法理解类属性将表现为协变的问题的示例:

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

class PetSomething <T : Pet>  
{
    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???
    }
}

另外书中显示如下代码:

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

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

【问题讨论】:

  • 您能否重新表述您问题的第一部分?我不明白。 “没有这条规则可能会发生什么(即在声明泛型类/接口时使用 out 时没有约束)”
  • @Tenfour04,我在问题中添加了一个示例。也许它可以帮助您理解我在问什么。如果没有,我会尝试另一种方式,因为这对我来说有点困难。

标签: java kotlin generics


【解决方案1】:

您引用了:“但是,如果类具有该泛型类型的函数参数或 var 属性,则不能使用 out。”

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

考虑这些类:

abstract class Pet

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

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

当您调用 PetOwner 构造函数并传入 Cat 时,编译器知道您正在构造 PetOwner&lt;out Cat&gt;,因为它知道传递给构造函数的值满足 &lt;out Cat&gt; 的类型。在构造对象之前,它不必将Cat 向上转换为Pet。然后构造的对象可以安全地向上转换为PetOwner&lt;Pet&gt;,因为不会再将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 构造函数参数是不可能的。在将参数传递给构造函数和拥有可以传递的实例之间,无法将对象传递给其他变量并向上转换其类型。

【讨论】:

  • "构造函数不是成员函数或属性,因此不受此规则约束。"但是,所表述的规则实际上并不完整(您也不能拥有var pets: List&lt;T&gt;)。
  • @Tenfour04,感谢您的解释和示例(示例在这种情况下真的很有帮助)。您能否也参考我在帖子中创建的示例并告诉我那里是否存在 ClassCastException 的情况?
  • 您的示例与我上面的示例没有什么不同,只是它没有使用在构造函数中定义属性的快捷方式。该属性仍然在实例化时分配。如果你对泛型做了一些不安全的事情,编译器会显示一个错误,或者如果你做了一个潜在的不安全的手动转换(使用as关键字),编译器会显示一个警告。我上面的示例无法编译,因为您不能将 out 与用于 var 属性的类型结合起来。
【解决方案2】:

问题是这样的:

val x = DogSomething() 
val y: PetSomething<Pet> = x // would be allowed by out
y.petDoSomething(Cat())

请注意,DogSomething 上的 petDoSomething 只需处理 Dogs。

虽然泛型类型是构造函数的输入,但代码正在编译。这不违反规则吗?

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

【讨论】:

    【解决方案3】:

    首先让我们通过在type parameter 前面加上out keyword 来明确我们得到了什么。考虑以下class

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

    out 关键字在这里使MyListT 中协变,这实际上意味着您可以执行以下操作:

    // note that type on left side of = is different than the one on right
    
    val numberList: MyList<Number> = MyList<Int>()
    

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

    通过在type parameter 前面加上out,您基本上将type 声明为T生产者,在上面的示例中MyList是数字的生产者。 这意味着无论您将instantiate T 设置为IntDouble 还是Number 的其他子类型,您都将始终能够从MyList 获得一个数字(因为Number 的每个子类型是Number)。这还允许您执行以下操作:

    fun process(list: MyList<Number>) { // do something with every number }
    fun main(){
      val ints = MyList<Int>()
      val doubles = MyList<Double>()
      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<Number> to process
    

    现在让我们回答out 关键字为什么T 应该只在返回位置?没有这个约束会发生什么? ,即如果MyList 有一个以T 为参数的函数。

    fun add(value: T) { list.add(T) }  // MyList has this function
    
    fun main() {
        val numbers = getMyList()   // numbers can be MyList<Int>, MyList<Double> or something else
        numbers.add(someInt)        // cant store Int, what if its MyList<Double> ( Int != Double) 
        numbers.add(someDouble)     // cant do this, what if its MyList<Int>
    }
    
    // We dont know what type of MyList we going to get
    fun getMyList(): MyList<Number>(){
      return if(feelingGood) { MyList<Int> () }
             else if(feelingOk> { MyList<Double> () }
             else { MyList<SomeOtherSubType>() }
    }
    

    这就是为什么需要约束,它基本上是为了保证类型安全。

    至于abstract class E&lt;out T&gt; (t:T) { val x = t }正在编译,Kotlin In Action有以下说法

    请注意,构造函数参数既不在 in 也不在 out 位置。即使一个类型参数被声明为 out,你仍然可以 在构造函数参数中使用它。方差保护类 如果您将其作为 更通用的类型:你不能调用潜在的危险 方法。构造函数不是以后可以调用的方法 (在实例创建之后),因此它不可能是潜在的 危险。

    【讨论】:

    • 很抱歉两周后才回复您。但是直到今天我还“努力”去理解泛型。所以只有今天我才有足够的(我希望)知识来回复你的帖子。是否可以创建/查找不适用于列表、堆栈等的示例?因为网上所有的例子都显示了如果我们“弄脏”一个列表可能会出现的问题。有没有办法找到一个不涉及那些数据结构的例子?
    猜你喜欢
    • 2021-01-25
    • 1970-01-01
    • 2023-03-30
    • 2018-02-10
    • 1970-01-01
    • 1970-01-01
    • 2014-05-07
    • 1970-01-01
    • 2021-10-11
    相关资源
    最近更新 更多