【问题标题】:Interfaces and pointer receivers接口和指针接收器
【发布时间】:2018-01-20 23:54:24
【问题描述】:

我是新手 gopher,并试图了解指针接收器和接口。

type Foo interface {
    foo()
}
type Bar struct {}
func (b *Bar) foo() {}

基于上述定义..

--- Allowed ---------
b := Bar{}
b.foo()

--- Not allowed ----- 

var foo Foo = Bar{}

获取编译器错误: 不能在赋值中使用 Bar 字面量(类型 Bar)作为 Foo 类型: Bar 没有实现 Foo(foo 方法有指针接收器)

我了解编译器在第一个场景中代表我们进行了一些指针转换和取消引用。为什么它在第二种情况下不做同样的事情?

【问题讨论】:

  • 所以您不是在问如何让它工作,而是在问为什么它不起作用? (顺便说一句var foo Foo = &Bar{} 有效)

标签: go go-interface


【解决方案1】:

解释在于,在处理具体结构本身时,它有适当的信息来自动处理。你可以阅读in the tour here 那个:

Go 自动处理值和指针之间的转换 方法调用。

但是当您处理interface{} 类型时,它对变量中实际包含的内容的信息较少。它只知道有一个foo() 方法。但是这里有一个微妙之处需要额外解释,所以这里是一个例子。

https://play.golang.org/p/Y0fJcAISw1

type Foo interface {
    foo()
}
type Bar struct {}
func (b *Bar) foo() {}

type Baz struct {}
func (b Baz) foo() {}

func main() {
    b := Bar{}
    b.foo()

    var v Foo = &Bar{}
    // v = Bar{} // fails
    v.foo()

    v = Baz{}
    v.foo()
    v = &Baz{} // works too
    v.foo()
}

请注意,&Baz{} 可以工作,即使它有一个值接收器,但反之则不行。原因是*Baz 正好指向一个 Baz,两者都存在(指针和值),所以值很容易获得。当您尝试执行 v = Bar{} 时,该值存在,但指针不存在,Go 不会自动为 interface{} 值创建一个。

这一切都在指针和接口标题in this blog post

下详细解释

【讨论】:

  • 这一切都没有失败。我已经测试了代码,它就像一个魅力。
  • @Sebi2020 取消注释第 18 行,然后重试。它无法编译。
  • 对不起,我的错。
  • @Sebi2020 不用担心。我可能应该将示例保留为未注释,以便更清楚它会在哪里失败。百感交集,但给出了一个破碎的例子,哈哈。不过,我猜这是本次 Q/A 的重点。
【解决方案2】:

简答 var foo Foo = Bar{} 不起作用,因为存储在接口中的具体值不可寻址。

加长版

请阅读https://github.com/golang/go/wiki/MethodSets

在任何对象上调用指针值方法是合法的 已经是一个指针或可以获取其地址。打电话是合法的 value 方法对任何值或值可以是 取消引用。

关于上面的解释,你的代码

b := Bar{}
b.foo()

有效,因为b 是可寻址的。

存储在接口中的具体值是不可寻址的。 因此,当您在接口上调用方法时,它必须要么 相同的接收器类型,或者它必须可以直接从 具体类型:指针和值接收器方法可以调用 正如您所期望的那样,分别指向指针和值。价值接收者 方法可以用指针值调用,因为它们可以 首先取消引用。指针接收器方法不能被调用 但是,因为存储在接口中的值没有 地址。为接口赋值时,编译器确保 实际上可以调用所有可能的接口方法 值,因此尝试进行不正确的分配将失败 编译。

根据上面的解释,存储在接口中的具体值是不可寻址的,因此代码,

var foo Foo = Bar{}

将不起作用,因为存储在接口中的具体值(在本例中为 Bar{})是不可寻址的。

【讨论】:

  • 您的答案比我的答案使用了更多的权威来源。希望你的最终被接受。干杯!
  • 这绝对应该是公认的答案!
【解决方案3】:

您的问题有一半取决于您的价值是否可寻址:

对于T 类型的操作数x,地址操作&x 生成一个 *T 类型的指针指向 x。操作数必须是可寻址的,即 要么:

  • 一个变量,
  • 指针间接,或
  • 切片索引操作;或
  • 可寻址结构操作数的字段选择器;或
  • 可寻址数组的数组索引操作。

作为可寻址性要求的一个例外,x 也可以是 (可能用括号括起来)复合文字。

——Address operators

Bar{} 是复合文字,因此不可寻址。您可以键入&Bar{} 来创建*Bar 类型的对象,但这被列为“可寻址要求的例外”,强化了Bar{} 本身不可寻址的想法。

Bar 类型的变量 b 可以调用 b.foo(),尽管 Bar.foo() 需要一个指针接收器是有充分理由的:

如果x的(类型)方法集,则方法调用x.m()是有效的 包含m 并且可以将参数列表分配给参数 m 的列表。 如果x 是可寻址的并且&x 的方法集包含mx.m()(&x).m() 的简写

——Calls

但是,这并不意味着Bar.foo()b 的方法集中。这是因为b 具有Bar 类型,而Bar.foo() 接收*Bar 类型的值:

一个类型可能有一个与之关联的方法集。方法集 接口类型是它的接口。任何其他类型的方法集T 包含声明为接收器类型T 的所有方法。方法 对应指针类型的集合*T是所有方法的集合 用接收者*TT 声明(也就是说,它还包含 T) 的方法集。

——来自Method sets

由于bFoo接口的方法集不同,尽管b.foo()被编译器转换为(&b).foo(),你还是不能使用var foo Foo = b。否则,var foo Foo = Bar{} 会起作用。但是,由于Bar.foo() 收到*Bar,因此您可以使用以下任一方法:

var foo Foo = &b
var foo Foo = &Bar{}

【讨论】:

  • 我并没有贬低你的回答,也没有建议改变,但这可能远高于将自己称为新手地鼠的人。我喜欢我们现在有 3 个不同深度的解释答案。
猜你喜欢
  • 1970-01-01
  • 2018-10-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-03-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多