代码行为取决于 switch 操作符中的类型顺序,如何消除这种依赖?

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

Code behaviour depends on type order in switch operator, how to get rid of this?

问题

最近我开始学习Go语言。
我试图理解Go语言中的接口原则,但有一件事情完全困惑了我。
鸭子原则说:如果某物像鸭子一样嘎嘎叫,像鸭子一样走路,那它就是鸭子。
但我想知道如果我们有三个如下所示的接口,Go语言会如何行为:

// 接口A
type InterfaceA interface {
ActionA() string
}

// 接口B
type InterfaceB interface {
ActionB() string
}

还有一个接口C,它做了一些不同的事情,但是具有与接口AB相似的函数:

// 接口C包含接口A和接口B的方法
type InterfaceC interface {
ActionA() string
ActionB() string
}

然后我们有三个结构体来实现上述接口:

type StructA struct{}

// 如果它执行ActionA,那么它是接口A
func (a StructA) ActionA() string {
return "Interface A implementation"
}

type StructB struct{}

// 如果它执行ActionB,那么它是接口B
func (b StructB) ActionB() string {
return "Interface B implementation"
}

type StructC struct{}

// 如果它执行ActionA和ActionB,那么它是接口C
func (c StructC) ActionA() string {
return "Interface C implementation"
}

func (c StructC) ActionB() string {
return "Interface C implementation"
}

还有一个函数用于识别它接收到的类型:

func getType(data interface{}) string {
switch data.(type) {
default:
return "Unknown"
case InterfaceA:
return "Interface A"
case InterfaceB:
return "Interface B"
case InterfaceC:
return "Interface C"
}
}

main函数中的代码:

func main() {
a := StructA{}
fmt.Println(a.ActionA())
fmt.Println(getType(a)) // 应该返回InterfaceA
fmt.Println("")
b := StructB{}
fmt.Println(b.ActionB())
fmt.Println(getType(b)) // 应该返回InterfaceB
fmt.Println("")
c := StructC{}
fmt.Println(c.ActionA())
fmt.Println(c.ActionB())
fmt.Println(getType(c)) // 应该返回InterfaceC
}

输出结果:

Interface A implementation
Interface A

Interface B implementation
Interface B

Interface C implementation
Interface C implementation
Interface A

经过一些实验,我发现如果我们在switch语句中改变case的顺序,那么函数会正确识别类型:

func getType(data interface{}) string {
switch data.(type) {
default:
return "Unknown"
case InterfaceC:
return "Interface C"
case InterfaceB:
return "Interface B"
case InterfaceA:
return "Interface A"
}
}

输出结果:

Interface A implementation
Interface A

Interface B implementation
Interface B

Interface C implementation
Interface C implementation
Interface C

play.golang.org上查看完整代码。

我的问题:这是一个bug还是一个特性?如果这是一个特性,我应该如何修改getType函数,使其不依赖于case的顺序?

英文:

Recently I started to learn the Go language.
I am trying to understand interface principles in Go and was completely puzzled by one thing.
The duck principle says: if something quacks like a duck and walks like a duck, then it's a duck.
But I wondered how Go will behave if we have three interfaces like this:

// Interface A
type InterfaceA interface {
	ActionA() string
}

// Interface B
type InterfaceB interface {
	ActionB() string
}

And interface C, which does something different but has functions which are similar to interfaces A and B functions:

// Interface C with methods A and B interfaces
type InterfaceC interface {
	ActionA() string
	ActionB() string
}

Then we have three structures which implement the interfaces above:

type StructA struct{}

// If it does ActionA then it's interface A
func (a StructA) ActionA() string {
	return "Interface A implementation"
}

type StructB struct{}

// If it does ActionB then it's interface B
func (b StructB) ActionB() string {
	return "Interface B implementation"
}

type StructC struct{}

// If it does ActionA and ActionB, it's an Interface C
func (c StructC) ActionA() string {
	return "Interface C implementation"
}

func (c StructC) ActionB() string {
	return "Interface C implementation"
}

And a function that identifies which type it gets:

func getType(data interface{}) string {
	switch data.(type) {
	default:
		return "Unknown"
	case InterfaceA:
		return "Interface A"
	case InterfaceB:
		return "Interface B"
	case InterfaceC:
		return "Interface C"
	}
}

Code inside main function:

func main() {
	a := StructA{}
	fmt.Println(a.ActionA())
	fmt.Println(getType(a)) // should return InterfaceA
	fmt.Println("")
	b := StructB{}
	fmt.Println(b.ActionB())
	fmt.Println(getType(b)) // should return InterfaceB
	fmt.Println("")
	c := StructC{}
	fmt.Println(c.ActionA())
	fmt.Println(c.ActionB())
	fmt.Println(getType(c)) // should return InterfaceC
}

Output:

Interface A implementation
Interface A

Interface B implementation
Interface B

Interface C implementation
Interface C implementation
Interface A

After some experiments I found out if we change the case order inside switch then the function identifies the type correctly:

func getType(data interface{}) string {
	switch data.(type) {
	default:
		return "Unknown"
	case InterfaceC:
		return "Interface C"
	case InterfaceB:
		return "Interface B"
	case InterfaceA:
		return "Interface A"
	}
}

Output:

Interface A implementation
Interface A

Interface B implementation
Interface B

Interface C implementation
Interface C implementation
Interface C

See also full code on play.golang.org

My question: Is it a bug or a feature? And if it's a feature, how should I change getType so that the function doesn't depend on case order?

答案1

得分: 3

这是根据语言规范定义的预期工作方式。

switch 语句有两种类型,表达式 switch类型 switch,这种行为在表达式 switch 中有文档记录:

> 在表达式 switch 中,会先计算 switch 表达式,然后按从左到右、从上到下的顺序计算 case 表达式;第一个与 switch 表达式相等的 case 会触发执行相关 case 的语句;其他的 case 会被跳过。如果没有匹配的 case,并且有一个 "default" case,那么会执行它的语句。一个 switch 语句中最多只能有一个 default case,并且它可以出现在任何位置。
>
> [...]
>
> 类型 switch 比较的是类型而不是值。除此之外,它与表达式 switch 类似

在 Go 中,如果一个类型的 方法集 是一个接口的超集,那么它会隐式地实现该接口。不需要声明意图。因此,在 Go 中,不管哪个接口定义了这些方法,唯一重要的是方法的签名:如果一个类型拥有一个接口所要求的所有方法,那么该类型隐式地实现了该接口。

问题在于你想要使用类型 switch 来做一些它设计之外的事情。你想要找到一个“最宽泛”的类型(具有最多方法),该类型仍然被该值实现。只有在你按照预期的顺序枚举 case(不同的类型)时,它才会这样做。

话虽如此,在你的情况下,并不存在一个值仅仅是 InterfaceC 的实现。你的代码没有说谎:所有实现 InterfaceC 的值也会同时实现 InterfaceAInterfaceB,因为 InterfaceAInterfaceB 的方法集都是 InterfaceC 的方法集的子集。

如果你想要能够“区分” InterfaceC 的实现,你必须“修改”方法集,使得上述关系不再成立(InterfaceC 的方法集不再是 InterfaceAInterfaceB 的方法集的超集)。如果你希望 StructC 不是 InterfaceA 的实现,你必须改变 ActionA() 的方法签名(在 InterfaceAInterfaceC 中),类似地,ActionB() 不再是 InterfaceB 的实现。

你还可以向 InterfaceA(和 InterfaceB)添加一个在 InterfaceC 中缺失的方法:

type InterfaceA interface {
    ActionA() string
    implementsA()
}

type InterfaceB interface {
    ActionB() string
    implementsB()
}

当然,你还必须将它们添加到 StructAStructB 中:

func (a StructA) implementsA() {}

func (b StructB) implementsB() {}

这样你就可以得到期望的输出。在 Go Playground 上试一试。

如果你不能或者不想这样做,你唯一的选择就是按正确的顺序枚举 case。或者不要使用类型 switch 来做这个。

英文:

This is the intended working, as defined by the language spec.

There are 2 types of switch statement, Expression switches and Type switches, and this behavior is documented at the Expression switches:

> In an expression switch, the switch expression is evaluated and the case expressions, which need not be constants, are evaluated left-to-right and top-to-bottom; the first one that equals the switch expression triggers execution of the statements of the associated case; the other cases are skipped. If no case matches and there is a "default" case, its statements are executed. There can be at most one default case and it may appear anywhere in the "switch" statement.
>
> [...]
>
> A type switch compares types rather than values. It is otherwise similar to an expression switch.

In Go a type implicitly implements an interface if its method set is a superset of the interface. There is no declaration of the intent. So in Go it doesn't matter which interface defines the methods, only thing that matters is the method signatures: if a type has all the methods an interface "prescribes", then that type implicitly implements the said interface.

The problem is that you want to use the Type switch to something it was not designed for. You want to find the "widest" type (with the most methods) that is still implemented by the value. It will only do this if you enumerate the cases (the different types) in this intended order.

That being said, in your case there's no such thing that a value is only InterfaceC implementation. Your code doesn't lie: all values that implement InterfaceC will also implement InterfaceA and InterfaceB too, because the method sets of both InterfaceA and InterfaceB are subsets of the method set of InterfaceC.

If you want to be able to "differentiate" InterfaceC implementations, you have to "alter" the method sets so that the above mentioned relation will not hold (method set of InterfaceC will not be a superset of the method set of InterfaceA and InterfaceB). If you want StructC to not be an InterfaceA implementation, you must change the method signature of ActionA() (either in InterfaceA or in InterfaceC), and similarly ActionB() to not be an InterfaceB implementation.

You could also add a method to InterfaceA (and to InterfaceB) which is missing from InterfaceC:

type InterfaceA interface {
    ActionA() string
    implementsA()
}

type InteraceB interface {
    ActionB() string
    implementsB()
}

And you have to add them to StructA and StructB of course:

func (a StructA) implementsA() {}

func (b StructB) implementsB() {}

This way you get the desired output. Try it on the Go Playground.

If you can't or don't want to do this, your only option is to enumerate the cases in the right order. Or don't use type switch for this.

huangapple
  • 本文由 发表于 2017年3月31日 17:14:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/43136897.html
匿名

发表评论

匿名网友

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

确定