实现混入和编译器行为的不一致性

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

Implementing Mixins and an Inconsistency in Compiler Behavior

问题

混入(Mixins)可以在Go(1.4.1)中使用嵌入来实现,由于struct{}不占用内存(据我所知),它适用于我们想要向类型添加一些功能或仅添加一个方法的情况,即使该方法实际上与其状态无关,但我们喜欢避免使用ParseThing(...),而是使用thing.Parse(...)

所以有:

type X struct{}

func (x X) F() {
    fmt.Println("functionality in X.F()")
}

type Y struct{ X }
type Z struct{ Y }

然后如果我们执行:

var z Z
z.F()

将会得到:

functionality in X.F()

到目前为止还不错。

现在让我们添加另一个类型OX,其中包含方法F(),并将其嵌入到Z中:

type Z struct {
    Y
    OX
}

type OX struct{} // 覆盖 X

func (x OX) F() {
    fmt.Println("functionality in OX.F()")
}

有趣!现在我们得到了functionality in OX.F(),这表明Go编译器从类型本身开始搜索方法,然后搜索最后一个嵌入的类型。我们可以通过将F()添加到Z中来验证:

func (x Z) F() {
    fmt.Println("functionality in Z.F()")
}

输出是functionality in Z.F()。现在,如果我们删除Z.F()方法并将F()添加到Y中:

//func (x Z) F() {
//    fmt.Println("functionality in Z.F()")
//}

func (x Y) F() {
    fmt.Println("functionality in Y.F()")
}

然后我们会看到这个错误ambiguous selector z.F;通过指针进行重定向没有任何区别。

**问题1:**为什么会这样?

额外的间接层Y本来是为了其他用途,但却导致了这个问题。而且我猜测func (t T) String() string{}是一个例外。这段代码:

type X struct{}

func (x X) String() string {
    return "in X.String()"
}

type Y struct{ X }
type Z struct {
    Y
    OX
}

type OX struct{} // 覆盖 X

func (x OX) String() string {
    return "in OX.String()"
}

func (x Y) String() string {
    return "in Y.String()"
}

然后执行:

var z Z
fmt.Println(z)

会得到:

{in Y.String() in OX.String()}

这是合乎逻辑的。但如果我们使用指针接收器:

import (
    "fmt"
    "testing"
)

func TestIt(t *testing.T) {
    var z Z
    fmt.Println(z)
}

type X struct{}

func (x *X) String() string {
    return "in X.String()"
}

type Y struct{ X }
type Z struct {
    Y
    OX
}

type OX struct{} // 覆盖 X

func (x *OX) String() string {
    return "in OX.String()"
}

func (x *Y) String() string {
    return "in Y.String()"
}

将会打印出:

{{{}} {}}

**问题2:**为什么会这样?

英文:

Mixins can be implemented in Go (1.4.1) using embedding and since struct{} occupies no memory (as I understand) it fits for the situations that we want to add some functionality or just add a method to a type that may actually has nothing to do with it's state, but we like to avoid ParseThing(...) and instead write thing.Parse(...).

So having:

type X struct{}

func (x X) F() {
    fmt.Println("functionality in X.F()")
}

type Y struct{ X }
type Z struct{ Y }

Then if we do:

var z Z
z.F()

Will give us:

functionality in X.F()

So far so good.

Now let's add another type OX with method F() and embed it in Z:

type Z struct {
    Y
    OX
}

type OX struct{} // overriding X

func (x OX) F() {
    fmt.Println("functionality in OX.F()")
}

Interesting! Now we get functionality in OX.F() which shows us that Go compiler searches for the method, starting from type it self and then the last embedded type. We can check that by adding F() to Z:

func (x Z) F() {
    fmt.Println("functionality in Z.F()")
}

The output is functionality in Z.F(). Now if we remove the Z.F() method and add F() to Y:

//func (x Z) F() {
//    fmt.Println("functionality in Z.F()")
//}

func (x Y) F() {
    fmt.Println("functionality in Y.F()")
}

Then we see this error ambiguous selector z.F; redirecting via pointers makes no difference.

Question 1: Why that's so?

The extra level of indirection Y meant for something else, but brought me to this. And as I've guessed func (t T) String() string{} is an exception. This code:

type X struct{}

func (x X) String() string {
    return "in X.String()"
}

type Y struct{ X }
type Z struct {
    Y
    OX
}

type OX struct{} // overriding X

func (x OX) String() string {
    return "in OX.String()"
}

func (x Y) String() string {
    return "in Y.String()"
}

And then this:

var z Z
fmt.Println(z)

Gives us:

{in Y.String() in OX.String()}

Which is logical. But if we use pointer receivers:

import (
    "fmt"
    "testing"
)

func TestIt(t *testing.T) {
    var z Z
    fmt.Println(z)
}

type X struct{}

func (x *X) String() string {
    return "in X.String()"
}

type Y struct{ X }
type Z struct {
    Y
    OX
}

type OX struct{} // overriding X

func (x *OX) String() string {
    return "in OX.String()"
}

func (x *Y) String() string {
    return "in Y.String()"
}

Will print out:

{{{}} {}}

Question 2: Why is that so?

答案1

得分: 8

问题1

编译器是正确的。它应该如何决定使用 OX.FY.F 中的哪一个?它无法决定。所以你需要直接调用所需的方法:要么使用

z.Y.F()

要么使用

z.OX.F()

**编辑:**至于为什么在你定义了 Y 上的 F 之前,这个例子是可以工作的,这在规范中有提到:

>对于类型为 T*T 的值 x,其中 T 不是指针类型或接口类型,x.f 表示在 T 中最浅层的存在 f 的字段或方法。如果最浅层存在的 f 不止一个,则选择表达式是非法的。

(强调添加。)

在你定义方法之前,最浅层的实现是 OX.F。在你定义了 Y.F 之后,就有了两个位于同一层级的 F,这是非法的。

问题2

同样,编译器是正确的。你将类型 YOX 嵌入到了 Z 中,而不是 *Y*OX。根据规范的描述:

>相应指针类型 *T 的方法集是所有以接收者 *TT 声明的方法的集合(也就是说,它还包含了 T 的方法集)。

*T 拥有 T 的所有方法,但反过来则不成立。OXY 的方法集为空,所以显然,fmt.Println 只会将它们打印为没有定义 String() 方法的任何其他类型的结构体。

英文:

Question 1

The compiler is correct. How should it decide, which of OX.F and Y.F should it use? It can't. So it's up to you to call the desired method directly: either with

z.Y.F()

or

z.OX.F()

Edit: As for why your example worked until you've defined F on Y, this is mentioned in the Spec:

>For a value x of type T or *T where T is not a pointer or interface type, x.f denotes the field or method at the shallowest depth in T where there is such an f. If there is not exactly one f with shallowest depth, the selector expression is illegal.

(Emphasis added.)

Before you defined the method, the shallowest implementation was OX.F. After you've defined Y.F, there became two Fs on the same level, which is illegal.

Question 2

Again, the compiler is correct. You have embedded types Y and OX into Z, not *Y and *OX. As written in the Spec,

>The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T).

*T has all methods of T, but not the other way around. Methods sets of OX and Y are empty, so obviously, fmt.Println just prints them as if they were any other kind of struct with no String() method defined.

答案2

得分: 2

Ainar-G写了一个整洁的答案

规范:
> 对于类型为T或*T的值x,其中T不是指针或接口类型,x.f表示在T中最浅层次的位置上具有f的字段或方法。如果最浅层次上没有恰好一个f,则选择表达式是非法的。

我想再补充一点

规范:
> 如果S包含一个匿名字段T,则S和*S的方法集都包括接收器为T的提升方法。S的方法集还包括接收器为T的提升方法。

所以,如果你只是使用引用来提升方法,事情就会正常工作,比如

fmt.Println(&z)

但是这会导致选择时的歧义,因为String方法有几种可能性,所以选择器String是非法的。编译器应该报错,但它没有。这种行为看起来是未指定的,只能解释为对于常见的打印操作的特殊情况。
这将按预期工作

var y Y
fmt.Println(&y)

这是一个可行的示例
Playground

英文:

Ainar-G write neat answer

Spec:
> For a value x of type T or *T where T is not a pointer or interface
> type, x.f denotes the field or method at the shallowest depth in T
> where there is such an f. If there is not exactly one f with
> shallowest depth, the selector expression is illegal.

I'd like to add a bit

Spec:
> If S contains an anonymous field T, the method sets of S and *S both include promoted methods with receiver T. The method set of *S also includes promoted methods with receiver *T.

So things would work if you just use referencing to promote methods like

fmt.Println(&z)

But this will cause ambiguity in selection cause of there are few possibilities for String method and so selector String is illegal due to spec. Compiler must complain, but it doesn't. This behaviour looks unspecified and can be only explained as special case for common printing operation to my mind.
This will work as expected

var y Y
fmt.Println(&y)

Here is working example
Playground

huangapple
  • 本文由 发表于 2015年2月2日 23:54:13
  • 转载请务必保留本文链接:https://go.coder-hub.com/28281564.html
匿名

发表评论

匿名网友

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

确定