当前位置: 首页 > 知识库问答 >
问题:

如果类/接口具有泛型类型的val属性或函数,为什么类/接口不能以out作为前缀?

艾泰
2023-03-14

在学习静态编程语言中的泛型时,我在一本书中读到了以下内容:

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

我理解规则所说的,但我很乐意(通过示例)理解没有此规则可能会有什么(即在声明泛型类/接口时使用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???
    }
}

此外,本书显示以下代码:

抽象类E

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

共有3个答案

公冶谦
2023-03-14

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

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

这里的关键字使MyList在T中协变,这意味着您可以执行以下操作:

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

val numberList: MyList<Number> = MyList<Int>()

如果删除out关键字并再次尝试编译,则会出现类型不匹配错误。

通过在type参数前面加上out,您基本上是在声明typeT的生产者,在上面的示例中MyList是Numbers的生产者。这意味着无论您将实例化T作为IntDoubleNumber的其他子类型,您都可以从MyList中获取Number(因为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

现在让我们回答without关键字为什么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>() }
}

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

对于抽象类E

请注意,构造函数参数既不在in位置,也不在out位置。即使类型参数声明为out,您仍然可以在构造函数参数中使用它。如果您将类实例作为更泛型类型的实例来使用,那么这种差异可以防止类实例被滥用:您只是不能调用潜在危险的方法。构造函数不是稍后(在创建实例之后)可以调用的方法,因此它不会有潜在的危险。

封锐藻
2023-03-14

问题是:

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

请注意,狗狗身上的宠物物品只需处理狗狗。

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

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

鞠修雅
2023-03-14

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

构造函数不是成员函数或属性,因此不受此规则的约束。在构造函数的站点将该类型用于参数是安全的,因为在构造它时该类型是已知的。

考虑这些类:

abstract class Pet

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

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

当您调用PetOwner构造函数并传入Cat时,编译器知道您正在构造PetOwner

函数参数和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!

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

 类似资料:
  • 我正在尝试返回一个对象,它应该是IClass的一个实现,具有一个通用类型,是IType的一个实现。 我要返回的实际类扩展了Class (abstract ),其泛型类型为ActualType: 抽象类对象实现了IClass接口,可以有任何扩展IType的类型 ActualType只是实现了IType接口 我在编译时得到一个“类型不匹配:无法从ActualClass转换为IClass”错误。我不明白

  • 7.2. 接口类型 接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。 io.Writer类型是用的最广泛的接口之一,因为它提供了所有的类型写入bytes的抽象,包括文件类型,内存缓冲区,网络链接,HTTP客户端,压缩工具,哈希等等。io包中定义了很多其它有用的接口类型。Reader可以代表任意可以读取bytes的类型,Closer可以是任意可以关闭的值,例如一

  • 在阅读了这个问题或这篇文章后,我仍然对和之间的细微差异感到有点困惑。 在本例中,我的目标是将一个简单对象分配给更广泛的

  • 问题内容: 我正处于起步阶段,想全神贯注于Go。目前,我正在模拟一个API请求,该请求返回一个包含对象数组的JSON格式的字符串。我正在尝试找出最合适的方法来遍历每个记录并访问每个字段。最终,每个字段都将被写入Excel电子表格,但是现在我只想打印每个字段的键和值。 这就是我所拥有的(我会在Go Playground中提供它,但不支持HTTP请求): 一切工作正常,直到尝试循环访问包含每个记录的属

  • 考虑一个具有的API,如下所示: 很简单,只有页面大小和跳过计数属性。 此外,现在我还有一些类,它们也包含但未分页。 在我的测试中,我希望他们都能实现一个接口,这样我就可以用一些更基本的测试来生成一个通用的基本测试类。为此,我添加了我认为会起作用的内容: 我将PagedResults更改为: 错误 但现在编译器抱怨PagedResultBase继承的所有地方的使用情况(?)从。 但是,如果我将接口