英文:
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 (
"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())
}
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”的简写是可以的。在编译时,我们将能够验证Set
是age
或*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 &age(23)
, it can't be valid to say (&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 &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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论