Go接收器方法调用语法混淆

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

Go receiver methods calling syntax confusion

问题

我刚刚阅读了Effective Go,在指针 vs. 值部分的末尾,它说:

> 关于接收器的指针 vs. 值的规则是,值方法可以在指针和值上调用,但指针方法只能在指针上调用。这是因为指针方法可以修改接收器;在值的副本上调用它们会导致这些修改被丢弃。

为了测试它,我写了这个:

<!-- language: lang-go -->

package main

import (
  "fmt"
  "reflect"
)

type age int

func (a age) String() string {
  return fmt.Sprintf("%d yeasr(s) old", int(a))
}

func (a *age) Set(newAge int) {
  if newAge >= 0 {
    *a = age(newAge)
  }
}

func main() {
  var vAge age = 5
  pAge := new(age)

  fmt.Printf("TypeOf =>\n\tvAge: %v\n\tpAge: %v\n", reflect.TypeOf(vAge),
    reflect.TypeOf(pAge))

  fmt.Printf("vAge.String(): %v\n", vAge.String())
  fmt.Printf("vAge.Set(10)\n")
  vAge.Set(10)
  fmt.Printf("vAge.String(): %v\n", vAge.String())

  fmt.Printf("pAge.String(): %v\n", pAge.String())
  fmt.Printf("pAge.Set(10)\n")
  pAge.Set(10)
  fmt.Printf("pAge.String(): %v\n", pAge.String())
}

尽管文档说不应该这样做,但它仍然可以编译,因为指针方法Set()应该不能通过值变量vAge调用。我在这里做错了什么吗?

英文:

I was just reading through Effective Go and in the Pointers vs. Values section, near the end it says:

> The rule about pointers vs. values for receivers is that value methods can be invoked on pointers and values, but pointer methods can only be invoked on pointers. This is because pointer methods can modify the receiver; invoking them on a copy of the value would cause those modifications to be discarded.

To test it, I wrote this:

<!-- language: lang-go -->

package main

import (
  &quot;fmt&quot;
  &quot;reflect&quot;
)

type age int

func (a age) String() string {
  return fmt.Sprintf(&quot;%d yeasr(s) old&quot;, int(a))
}

func (a *age) Set(newAge int) {
  if newAge &gt;= 0 {
    *a = age(newAge)
  }
}

func main() {
  var vAge age = 5
  pAge := new(age)

  fmt.Printf(&quot;TypeOf =&gt;\n\tvAge: %v\n\tpAge: %v\n&quot;, reflect.TypeOf(vAge),
    reflect.TypeOf(pAge))

  fmt.Printf(&quot;vAge.String(): %v\n&quot;, vAge.String())
  fmt.Printf(&quot;vAge.Set(10)\n&quot;)
  vAge.Set(10)
  fmt.Printf(&quot;vAge.String(): %v\n&quot;, vAge.String())

  fmt.Printf(&quot;pAge.String(): %v\n&quot;, pAge.String())
  fmt.Printf(&quot;pAge.Set(10)\n&quot;)
  pAge.Set(10)
  fmt.Printf(&quot;pAge.String(): %v\n&quot;, pAge.String())
}

And it compiles, even though the document says it shouldn't since the pointer method Set() should not be invocable through the value var vAge. Am I doing something wrong here?

答案1

得分: 9

这是有效的,因为vAge是可寻址的。请参考语言规范中Calls的最后一段:

如果方法集(the type of)x 包含 m,并且参数列表可以赋值给 m 的参数列表,则方法调用 x.m() 是有效的。如果 x 是可寻址的并且 &x 的方法集包含 m,则 x.m() 是 (&x).m() 的简写。

英文:

That's valid because vAge is addressable. See the last paragraph in Calls under the language spec:

> A method call x.m() is valid if the method set of (the type of) x
> contains m and the argument list can be assigned to the parameter list
> of m. If x is addressable and &x's method set contains m, x.m() is
> shorthand for (&x).m().

1: http://golang.org/ref/spec#Calls "Calls"

答案2

得分: 3

vAge不仅仅被视为一个“值变量”,因为它是一个已知的内存位置,用于存储age类型的值。仅仅看vAge的值,vAge.Set(10)本身是无效的表达式,但是因为vAge是可寻址的,规范声明在编译时将该表达式视为“获取vAge的地址,并在其上调用Set”的简写是可以的。在编译时,我们将能够验证Setage*age的方法集的一部分。如果编译器确定有必要且可能,你基本上允许编译器对原始表达式进行文本扩展。

与此同时,编译器允许你调用age(23).String(),但不允许调用age(23).Set(10)。在这种情况下,我们正在处理一个非可寻址的age类型的值。由于不能说&age(23),所以也不能说(&age(23)).Set(10);编译器不会进行这种扩展。

看看Effective Go的例子,你并没有在我们知道b的完整类型的范围内直接调用b.Write()。相反,你正在创建b的临时副本,并试图将其作为类型为interface io.Writer()的值传递。问题在于Printf的实现对传入的对象一无所知,除了它承诺它知道如何接收Write(),因此它不知道在调用函数之前将byteSlice转换为*ByteSlice。是否对b进行取址的决定必须在编译时发生,而PrintF是在其第一个参数将不被引用的前提下编译的。

你可能会认为,如果系统知道如何将age指针转换为age值,那么它应该能够做相反的操作;但实际上这是没有意义的。在Effective Go的例子中,如果你传递b而不是&b,你将修改一个在Printf返回后将不再存在的切片,这几乎没有用处。在我上面的age示例中,将值23覆盖为值10根本没有意义。在前一种情况下,当将b传递给函数时,编译器停下来询问程序员她实际上想要做什么。在后一种情况下,编译器当然会拒绝修改一个常量值。

此外,我不认为系统在动态扩展age的方法集以适应*age;我猜测指针类型静态地为基本类型的每个方法分配一个方法,该方法只是解引用指针并调用基本类型的方法。自动执行此操作是安全的,因为按值接收的方法中的任何内容都无法更改指针。在另一个方向上,将要求修改数据的一组方法包装在稍后数据将消失的方式中并不总是有意义的。肯定有一些情况下这样做是有意义的,但这需要由程序员明确决定,并且编译器停下来询问这样做是有意义的。

简而言之,我认为Effective Go中的段落可能需要重新措辞(尽管我可能太啰嗦了,不能接受这份工作),但它是正确的。类型为*X的指针实际上可以访问X的所有方法,但X不能访问*X的方法。因此,在确定一个对象是否可以满足给定接口时,*X可以满足X可以满足的任何接口,但反过来则不成立。此外,即使在编译时已知作用域中的类型为X的变量是可寻址的,编译器也会拒绝将其转换为*X以满足接口,因为这样做可能没有意义。

英文:

vAge is not considered as only a "value variable", because it's a known location in memory that stores a value of type age. Looking at vAge only as its value, vAge.Set(10) is not valid as an expression on its own, but because vAge is addressable, the spec declares that it's okay to treat the expression as shorthand for "get the address of vAge, and call Set on that" at compile-time, when we will be able to verify that Set is part of the method set for either age or *age. You're basically allowing the compiler to do a textual expansion on the original expression if it determines that it's necessary and possible.

Meanwhile, the compiler will allow you to call age(23).String() but not age(23).Set(10). In this case, we're working with a non-addressable value of type age. Since it's not valid to say &amp;age(23), it can't be valid to say (&amp;age(23)).Set(10); the compiler won't do that expansion.

Looking at the Effective Go example, you're not directly calling b.Write() at the scope where we know b's full type. You're instead making a temporary copy of b and trying to pass it off as a value of type interface io.Writer(). The problem is that the implementation of Printf doesn't know anything about the object being passed in except that it has promised it knows how to receive Write(), so it doesn't know to take a byteSlice and turn it into a *ByteSlice before calling the function. The decision of whether to address b has to happen at compile time, and PrintF was compiled with the precondition that its first argument would know how to receive Write() without being referenced.

You may think that if the system knows how to take an age pointer and convert it to an age value, that it should be able to do the reverse; t doesn't really make sense to be able to, though. In the Effective Go example, if you were to pass b instead of &amp;b, you'd modify a slice that would no longer exist after PrintF returns, which is hardly useful. In my age example above, it literally makes no sense to take the value 23 and overwrite it with the value 10. In the first case, it makes sense for the compiler to stop and ask the programmer what she really meant to do when handing b off. In the latter case, it of course makes sense for the compiler to refuse to modify a constant value.

Furthermore, I don't think the system is dynamically extending age's method set to *age; my wild guess is that pointer types are statically given a method for each of the base type's methods, which just dereferences the pointer and calls the base's method. It's safe to do this automatically, as nothing in a receive-by-value method can change the pointer anyway. In the other direction, it doesn't always make sense to extend a set of methods that are asking to modify data by wrapping them in a way that the data they modify disappears shortly thereafter. There are definitely cases where it makes sense to do this, but this needs to be decided explicitly by the programmer, and it makes sense for the compiler to stop and ask for such.

tl;dr I think that the paragraph in Effective Go could use a bit of rewording (although I'm probably too long-winded to take the job), but it's correct. A pointer of type *X effectively has access to all of X's methods, but 'X' does not have access to *X's. Therefore, when determining whether an object can fulfill a given interface, *X is allowed to fulfill any interface X can, but the converse is not true. Furthermore, even though a variable of type X in scope is known to be addressable at compile-time--so the compiler can convert it to a *X--it will refuse to do so for the purposes of interface fulfillment because doing so may not make sense.

huangapple
  • 本文由 发表于 2012年11月9日 14:35:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/13303254.html
匿名

发表评论

匿名网友

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

确定