为什么我不能在 Go 中用一种类型的切片替换另一种类型?

Why can't I substitute a slice of one type for another in Go?

我正在尝试了解 Go 的类型转换规则。假设我们有这些接口:

type woofer interface {
  woof()
}

type runner interface {
  run()
}

type woofRunner interface {
  woofer
  runner
}

为了满足接口,我们有一个 dog 类型:

type dog struct{}

func (*dog) run()  {}
func (*dog) woof() {}

这两个函数正在使用接口:

func allWoof(ws []woofer) {}

func oneWoof(w woofer) {}

要使用这些方法,我可以编写以下内容:

dogs := make([]woofRunner, 10)
oneWoof(dogs[0])
allWoof(dogs)

第一个函数 oneWoof() 按预期运行; *dog 实现所有 oneWoof 需求,这是一个 woof 功能。

但是对于第二个函数 allWoof,Go 不会编译尝试的调用,报告如下:

cannot use dogs (type []woofRunner) as type []woofer in argument to allWoof

使用类型转换也是不可能的;写入 []woofer(dogs) 也会失败:

cannot convert dogs (type []woofRunner) to type []woofer

[]woofRunner的每个成员都有满足[]woofer的所有必要功能,为什么要禁止这种转换?

(我不确定这是否与 Go FAQ 和 Stack Overflow 上的各种问题中解释的情况相同,人们在这些问题中询问有关将类型 T 转换为 interface{}。slice/array 中的每个指针都指向可直接转换为另一种类型的类型。使用这些指针应该是可能的,原因与将 dog[0] 传递给 'oneWoof` 是可能的。)

注1:我知道一种解决方案是循环并逐项转换。我的问题是为什么这是必要的以及是否有更好的解决方案。

注2:关于Assignability的规则:

A value x is assignable to a variable of type T [when] T is an interface type and x implements T.

我们不能说如果 slice/array 的类型可以赋值给另一个类型,那么这些类型的数组也可以赋值吗?

您必须循环转换接口数组:

var woofers []woofer
for _, w := range dogs {
    woofers = append(woofers, w)
}

来自Go reference

Two slice types are identical if they have identical element types.

Two interface types are identical if they have the same set of methods with the same names and identical function types(...).

所以 woofRunnerwoofer 不同,这导致我们 []woofRunner[]woofer 不同。

除了 Go 拒绝沿此处其他答案中提到的这些方差关系转换切片之外,思考 为什么 Go 拒绝这样做是很有用的,即使在 -两种类型之间的内存表示是相同的。

在您的示例中,提供 woofRunners 的切片作为类型 []woofer 的参数要求切片的元素类型为 covariant treatment。当 从切片中读取 时,确实,因为 woofRunner woofer,你知道每个元素出现在[]woofRunner 将满足 reader 寻找 []woofer

然而,在 Go 中,切片是一种引用类型。当将一个切片作为参数传递给一个函数时,该切片被复制,但被调用的函数主体中使用的副本继续引用相同的支持数组(在 append 超出其容量之前不需要重新分配)。数组的可变视图——更一般地说,将一个项目插入集合中——需要对元素类型进行逆变处理。也就是说,当涉及到要求函数参数以 插入 覆盖 类型 woofRunner 的元素时,它是可以提供 []woofer.

问题是函数是否要求

的切片参数
  • 从中读取(对于读取 woofers,[]woofRunner[]woofer 一样好),
  • 写入(对于写入 woofRunners,[]woofer[]woofRunner 一样好),
  • 或两者兼而有之(两者都不是可接受的替代品)。

考虑一下如果 Go 确实以协变方式接受切片参数会发生什么,并且有人出现并更改 allWoof 如下:

// Another type satisfying `woofRunner`:
type wolf struct{}
func (*wolf) run()  {}
func (*wolf) woof() {}

func allWoof(ws []woofer) {
  if len(ws) > 0 {
    ws[0] = &wolf{}
  }
}

dogs := []*dog{&dog{}, &dog{}}
allWoof(dogs)  // Doesn't compile, but what if it did?

即使 Go 愿意将 []*dog 视为 []woofer,我们也会在此处的 *dog 数组中得到 *wolf。一些语言通过对尝试的数组插入或覆盖进行 运行 次类型检查来防止此类事故,但是因为 Go 阻止我们做到这一点,所以它不需要这些额外的检查。