英文:
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
,它做了一些不同的事情,但是具有与接口A
和B
相似的函数:
// 接口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
的值也会同时实现 InterfaceA
和 InterfaceB
,因为 InterfaceA
和 InterfaceB
的方法集都是 InterfaceC
的方法集的子集。
如果你想要能够“区分” InterfaceC
的实现,你必须“修改”方法集,使得上述关系不再成立(InterfaceC
的方法集不再是 InterfaceA
和 InterfaceB
的方法集的超集)。如果你希望 StructC
不是 InterfaceA
的实现,你必须改变 ActionA()
的方法签名(在 InterfaceA
或 InterfaceC
中),类似地,ActionB()
不再是 InterfaceB
的实现。
你还可以向 InterfaceA
(和 InterfaceB
)添加一个在 InterfaceC
中缺失的方法:
type InterfaceA interface {
ActionA() string
implementsA()
}
type InterfaceB interface {
ActionB() string
implementsB()
}
当然,你还必须将它们添加到 StructA
和 StructB
中:
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论