Golang区分T和*T的方法集的原因是什么?

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

What is the reason golang discriminates method sets on T and *T?

问题

这是我在学习Go语言时最困惑的地方。我们都知道,在类型T上定义的方法只会影响到T的副本,而在类型*T上定义的方法会影响到T上的实际数据。

为什么在类型T上定义的方法也可以被*T使用,但反过来却不允许呢?你能给我一个例子(或原因)来解释为什么不允许在*T上定义的方法被T使用吗?

这种设计的优缺点是什么?

英文:

This is where confuses me the most while learning go. We all know that methods on T only affect the copy of T, and methods on *T will affect the actual data on T.

Why does methods on T can also be used by *T, but the opposite is not allowed? So,can you give me an example(or reason) on why they do not allow method on *T be used by T?

What is the pros and cons of this design?

答案1

得分: 9

这里有很多答案,但没有一个回答为什么会出现这种情况。

首先,让我们来看看当你有一个 *T 并且想调用一个接受 T 的方法时会发生什么。要做到这一点,你只需要将 *yourT(其中 * 用于解引用指针)传递给函数。这是可以保证的,因为你只是在复制一个已知位置的内存块。

现在假设你有一个 T 并且想要一个 *T。你可能会认为你可以只需使用 &yourT 来获取它的地址。但是生活并不总是那么简单。并不总是有一个静态地址可供获取。

规范中可以看到:

对于类型为 T 的操作数 x,地址操作 &x 会生成一个类型为 *T 的指针,指向 x。操作数必须是可寻址的,也就是说,要么是一个变量、指针间接引用或切片索引操作;或者是可寻址结构操作数的字段选择器;或者是可寻址数组的数组索引操作。作为对可寻址要求的例外,x 还可以是一个(可能带括号的)复合字面量。

你可能会问为什么他们会对获取内存地址设置这些任意的限制。每个变量都必须有一个内存地址,对吗?虽然这是正确的,但是优化可能会使这些地址相当短暂。

例如,假设变量在一个映射中:

res := TMap["key"].pointerMethod()

在这种情况下,你实际上是在说你想要一个指向映射内存的指针。这将迫使 Go 的每个实现以使内存地址保持静态的方式来实现映射。这将严重限制运行时的内部结构,并使实现者在构建高效映射时的自由度大大减少。

还有其他一些例子,比如函数返回值或接口,但你只需要一个例子来证明这个操作不能保证一定可行。

总之,计算机内存并不简单,虽然你可能想说“只需获取地址”,但并不总是那么简单。获取一个保证为静态的地址并不总是可能的。因此,你不能保证任何 T 的实例都可以转换为指针并传递给指针方法。

英文:

There are many answers here, but non of them answer why this is the case.

First lets take the case of you having a *T and wanting to call a method which takes T. To do this, all you need to do is pass *yourT (where * is being used to dereference the pointer) to the function. This is guaranteed to be possible because you are just copying blob of memory at a known location.

Now lets say you have a T and want a *T. You may be thinking that you could just do &yourT and get its address. But life isn't always so simple. There isn't always a static address to take.


From the spec:

> For an operand x of type T, the address operation &x generates a pointer of type *T to x. The operand must be addressable, that is, either a variable, pointer indirection, or slice indexing operation; or a field selector of an addressable struct operand; or an array indexing operation of an addressable array. As an exception to the addressability requirement, x may also be a (possibly parenthesized) composite literal.

You may be asking yourself why they would place these arbitrary restrictions on getting a memory address. Every variable must have some memory address, right? While this is true, optimizations can make those addresses rather ephemeral.

For example, lets say the variable was inside a map:

res := TMap["key"].pointerMethod()

In this case, you are effectively saying you want a pointer to memory being held inside a map. This would force every implementation of Go to implement map in such a way that memory addresses remain static. This would severely limit the internal structures of the runtime and give the implementers much less freedom in building an efficient map.

There are other examples such as function returns or interfaces, but you only need one example to prove that the operation is not guaranteed to be possible.


The bottom line is that computer memory isn't simple and while you may want to say "just take the address", it isn't always that simple. Taking an address that is guaranteed to be static isn't always possible. Therefore, you can't guarantee that any instance of T may be turned into a pointer and passed to a pointer method.

答案2

得分: 3

关于接口的一篇最佳文章是由Jordan OREILLI撰写的《如何在Go中使用接口》。

其中包括以下示例:

type Animal interface {
    Speak() string
}

type Dog struct {
}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct {
}

func (c *Cat) Speak() string {
    return "Meow!"
}

并且解释了以下内容:

指针类型可以访问其关联值类型的方法,但反过来不行。
也就是说,*Dog类型的值可以使用在Dog上定义的Speak方法,但正如我们之前所看到的,Cat类型的值无法访问在*Cat上定义的Speak方法。

(这反映了你的问题)

这听起来可能有些晦涩,但当你记住以下内容时就会明白:在Go中,一切都是按值传递的
每次调用函数时,传递给它的数据都会被复制
对于具有值接收器的方法,调用该方法时会复制该值

当你理解以下方法签名时,这就更加明显了:

func (t T) MyMethod(s string) {
    // ...
}

它是类型为func(T, string)的函数;方法接收器像任何其他参数一样按值传递给函数。

在值类型上定义的方法(例如func (d Dog) Speak() { ... })对接收器所做的任何更改都不会被调用者看到,因为调用者作用域中有一个完全独立的Dog值。

(这是“按值复制”的部分)

由于一切都是按值传递的,很明显为什么*Cat方法不能被Cat值使用;一个Cat值可能有任意数量指向它的*Cat指针。如果我们尝试使用Cat值调用*Cat方法,我们根本没有*Cat指针。

相反,如果我们在Dog类型上有一个方法,并且有一个*Dog指针,我们知道在调用此方法时要使用哪个Dog因为*Dog指针指向一个确切的Dog;Go运行时将在必要时将指针解引用为其关联的Dog值。也就是说,给定一个*DogdDog类型上的方法Speak,我们只需写d.Speak();我们不需要像在其他语言中那样写d->Speak()

英文:

One of the best article on interface is "How to use interfaces in Go" by Jordan OREILLI.

It includes the example:

<!-- language-all: go -->

type Animal interface {
	Speak() string
}

type Dog struct {
}

func (d Dog) Speak() string {
	return &quot;Woof!&quot;
}

type Cat struct {
}

func (c *Cat) Speak() string {
	return &quot;Meow!&quot;
}

And it explains:

> a pointer type can access the methods of its associated value type, but not vice versa.
That is, a *Dog value can utilize the Speak method defined on Dog, but as we saw earlier, a Cat value cannot access the Speak method defined on *Cat.

(which reflects your question)

> That may sound cryptic, but it makes sense when you remember the following: everything in Go is passed by value.
Every time you call a function, the data you’re passing into it is copied.
In the case of a method with a value receiver, the value is copied when calling the method.

> This is slightly more obvious when you understand that a method of the following signature:

func (t T) MyMethod(s string) {
    // ...
}

> is a function of type func(T, string); method receivers are passed into the function by value just like any other parameter.

> Any changes to the receiver made inside of a method defined on a value type (e.g., func (d Dog) Speak() { ... }) will not be seen by the caller because the caller is scoping a completely separate Dog value.

(That is the "copy by value" part)

> Since everything is passed by value, it should be obvious why a *Cat method is not usable by a Cat value; any one Cat value may have any number of *Cat pointers that point to it. If we try to call a *Cat method by using a Cat value, we never had a *Cat pointer to begin with.

> Conversely, if we have a method on the Dog type, and we have a *Dog pointer, we know exactly which Dog value to use when calling this method, because the *Dog pointer points to exactly one Dog value; the Go runtime will dereference the pointer to its associated Dog value any time it is necessary.
That is, given a *Dog value d and a method Speak on the Dog type, we can just say d.Speak(); we don’t need to say something like d-&gt;Speak() as we might do in other languages.

答案3

得分: 2

你在问题中已经提到了答案的关键:对T的方法只会影响T的副本。所以根据这个信息,以下摘录来自Go的常见问题解答应该能够帮助你解决剩下的困惑:

来自Go规范:

"任何其他命名类型T的方法集由所有接收器类型为T的方法组成。相应指针类型T的方法集是所有接收器类型为T或T的方法集(也就是说,它还包含T的方法集)。"

如果接口值包含指针*T,方法调用可以通过解引用指针来获取值,但如果接口值包含值T,则没有有效的方法调用可以获取指针。

即使在编译器可以获取值的地址并将其传递给方法的情况下,如果方法修改了该值,更改将在调用者中丢失。作为一个常见的例子,以下代码:

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

将会将标准输入复制到buf的副本中,而不是buf本身。这几乎永远不是期望的行为。

英文:

You have the key to your answer in your question: methods on T only affect the copy of T. So with this information, the following excerpt from Go's FAQ should help clear the rest of the confusion:

> From the Go Spec:
>
>> "The method set of any other named type T consists of all methods with
>> receiver type T. The method set of the corresponding pointer type *T
>> is the set of all methods with receiver *T or T (that is, it also
>> contains the method set of T)."
>
> If an interface value contains a
> pointer *T, a method call can obtain a value by dereferencing the
> pointer, but if an interface value contains a value T, there is no
> useful way for a method call to obtain a pointer.
>
> Even in cases where the compiler could take the address of a value to
> pass to the method, if the method modifies the value the changes will
> be lost in the caller. As a common example, this code:
>
> var buf bytes.Buffer
> io.Copy(buf, os.Stdin)
>
> would copy standard input
> into a copy of buf, not into buf itself. This is almost never the
> desired behavior.

huangapple
  • 本文由 发表于 2014年4月13日 23:56:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/23044855.html
匿名

发表评论

匿名网友

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

确定