如何处理Go接口中的重复方法?

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

How to deal with duplicate methods in Go interface?

问题

如何处理Go接口中的重复方法?

在这种情况下,如果运行这段代码,会出现以下错误:

$ go run foo.go
# command-line-arguments
./foo.go:24: duplicate method Hello

如何处理这种情况,以及如何避免在这种情况下出现重复的方法?

在Go语言中,接口是一种约定,用于指定类型应该具有的方法。接口中的方法是唯一的,不允许重复定义相同的方法。

在你的代码中,Entertainer 接口嵌入了 JokerJumper 接口,而这两个接口都嵌入了 Person 接口。由于 Person 接口中定义了 Hello() 方法,而 JokerJumper 接口又分别嵌入了 Person 接口,导致了重复定义 Hello() 方法的错误。

要解决这个问题,你可以通过以下两种方式之一来避免重复方法的定义:

  1. 使用接口嵌入的方式来组合接口,但确保每个嵌入的接口中没有重复的方法。例如,你可以在 JokerJumper 接口中移除 Person 接口的嵌入,因为 Entertainer 接口已经嵌入了 JokerJumper 接口,所以 Person 接口的方法会被间接地包含在 Entertainer 接口中。

    type Joker interface {
        Joke()
    }
    
    type Jumper interface {
        Jump()
    }
    
    type Entertainer interface {
        Joker
        Jumper
    }
    
  2. 如果你需要在多个接口中都使用相同的方法,可以考虑将这个方法定义为一个单独的接口,并让其他接口嵌入这个单独的接口。这样可以避免重复定义方法。

    type Person interface {
        Hello()
    }
    
    type Joker interface {
        Person
        Joke()
    }
    
    type Jumper interface {
        Person
        Jump()
    }
    
    type Greeting interface {
        Person
    }
    
    type Entertainer interface {
        Greeting
        Joker
        Jumper
    }
    

通过以上两种方式,你可以避免在接口中出现重复的方法定义,并且能够正确地组合接口。

英文:

How to deal with duplicate methods in Go interface?

package main

import (
	"fmt"
)

type Person interface {
	Hello()
}

type Joker interface {
	Person
	Joke()
}

type Jumper interface {
	Person
	Jump()
}

type Entertainer interface {
	Joker
	Jumper
}

func main() {
	fmt.Println("hello, world")
}

The following error occurs if I run this code.

$ go run foo.go
# command-line-arguments
./foo.go:24: duplicate method Hello

How to deal with situations like this and how can we avoid duplicate
methods in such a scenario?

答案1

得分: 12

这样做的方法是明确提供所需的方法,而不是使用简写语法:

type Entertainer interface {
    Hello()
    Joke()
    Jump()
}

这可能看起来像是代码重复,但请注意,在Go语言中,重复的代码并不是一件不寻常的事情,特别是当它能使代码更清晰时。

还要注意:如果你从其他语言的典型继承的角度来思考,这样做可能会让你觉得你正在丢失一些信息,因为你没有记录EntertainerPerson继承的事实。但是Go语言的接口是纯粹结构化的,没有继承的概念。因为Entertainer有一个Hello()方法,所以每个Entertainer自动成为一个Person,无论你是否在Entertainer声明中明确提到Person

所有这些代码都可以编译通过(除了一个"declared and not used"的错误),即使你没有使用任何接口的简写语法:

var e Entertainer
var ju Jumper
var jo Joker
var p Person

p = e    // 每个Entertainer也是一个Person
p = ju   // 每个Jumper也是一个Person
p = jo   // 每个Joker也是一个Person

ju = e   // 每个Entertainer也是一个Jumper

jo = e   // 每个Entertainer也是一个Joker

下面是一个完整的程序,它可以编译并正常运行。给定以下声明:

package main

import (
    "fmt"
)

type Person interface {
    Hello()
}

type Joker interface {
    Hello()
    Joke()
}

type Jumper interface {
    Hello()
    Jump()
}

type Entertainer interface {
    Hello()
    Joke()
    Jump()
}

我们创建一个Clown类型:

type Clown struct {}

func (c Clown) Hello() {
    fmt.Println("Hello everybody")
}

func (c Clown) Joke() {
    fmt.Println("I'm funny")
}

func (c Clown) Jump() {
    fmt.Println("And up I go")
}

Clown可以打招呼、跳跃和开玩笑,因此它实现了我们所有的接口。给定以下四个函数:

func PersonSayHello(p Person) {
    p.Hello()
}

func JumperJump(j Jumper) {
    j.Jump()
}

func JokerJoke(j Joker) {
    j.Joke()
}

func EntertainerEntertain(e Entertainer) {
    e.Joke()
    e.Jump()
}

你可以将Clown传递给其中任何一个函数:

func main() {
    c := Clown{}

    PersonSayHello(c)
    JokerJoke(c)
    JumperJump(c)
    EntertainerEntertain(c)
}

这是一个带有上述代码的Go Playground链接

最后一件事——你可能会争辩说:“但是如果我稍后对Person进行更改,它不会反映在其他接口中。”这是真的,你必须手动进行这样的调整,但编译器会提醒你。

如果你有这个函数:

func JumperSayHello(j Jumper) {
    PersonSayHello(j)
}

你的代码将正常工作,没有任何问题。但是如果你给Person添加另一个方法,依赖于JumperPerson的事实的代码将不再编译通过。例如:

type Person interface {
    Hello()
    Think()
}

你会得到以下错误:

.\main.go:18: cannot use j (type Jumper) as type Person in argument to PersonSayHello:
        Jumper does not implement Person (missing Think method)

只要你的代码任何地方依赖于Jumper始终是Person的事实,这种情况就会发生。如果你没有,甚至没有在你的测试中,那么——嗯,也许Jumper不思考实际上并不重要?

但是,如果出于任何原因,你确实需要确保Jumper始终是Person,无论你对这些接口进行了什么更改,但这个事实实际上在任何地方都没有被使用,你总是可以为此目的创建代码:

package main

type Person interface {
    Hello()
}

type Jumper interface {
    Hello()
    Jump()
}

// 这个函数从未被使用,它只是为了在编译时确保接口兼容性
func ensureJumperIsPerson(j Jumper) {
    var p Person = j
    _ = p
}

func main() {
}
英文:

The way to do this is to explicitly provide the required methods instead of using the shorthand syntax:

type Entertainer interface {
	Hello()
	Joke()
	Jump()
}

This may seem like code duplication, but note that duplicate code isn't an untypical thing in Go, especially when it leads to clearer code.

Also note this: If you think in terms of typical inheritance in other languages, it may seem like you're losing some information by doing this, because you're not recording the fact that Entertainer inherits from, say, Person. But Go interfaces are purely structural, there is no inheritance. Because an Entertainer has a Hello() method, every Entertainer is automatically a Person, whether or not you explicitly mention Person insided the Entertainer declaration.

All of this compiles without problems (except for a "declared and not used" error) even when you don't use the shorthand syntax for any of the interfaces:

var e Entertainer
var ju Jumper
var jo Joker
var p Person

p = e    // every Entertainer is also a Person
p = ju   // every Jumper is also a Person
p = jo   // every Joker is also a Person

ju = e   // every Entertainer is also a Jumper

jo = e   // every Entertainer is also a Joker

Here's a complete program that compiles and runs just fine. Given these declarations:

package main

import (
	"fmt"
)

type Person interface {
	Hello()
}

type Joker interface {
	Hello()
	Joke()
}

type Jumper interface {
	Hello()
	Jump()
}

type Entertainer interface {
	Hello()
	Joke()
	Jump()
}

let's create a Clown type:

type Clown struct {}

func (c Clown) Hello() {
	fmt.Println("Hello everybody")
}

func (c Clown) Joke() {
	fmt.Println("I'm funny")
}

func (c Clown) Jump() {
	fmt.Println("And up I go")
}

A Clown can greet, jump, and joke, and so it implements all of our interfaces. Given these four functions:

func PersonSayHello(p Person) {
	p.Hello()
}

func JumperJump(j Jumper) {
	j.Jump()
}

func JokerJoke(j Joker) {
	j.Joke()
}

func EntertainerEntertain(e Entertainer) {
	e.Joke()
	e.Jump()
}

you can pass a Clown to any of them:

func main() {
    c := Clown{}

	PersonSayHello(c)
	JokerJoke(c)
	JumperJump(c)
	EntertainerEntertain(c)
}

Here's a link to a Go Playground with the above code.

One final thing – you could argue something like this: "But if I later make a change to Person, it won't be reflected in the other interfaces." It's true, you have to make such an adjustment manually, but the compiler will let you know about it.

If you have this function:

func JumperSayHello(j Jumper) {
    PersonSayHello(j)
}

your code will work without any issues. But if you add another method to Person, code that relies on the fact that a Jumper is a Person will no longer compile. With

type Person interface {
    Hello()
	Think()
}

you get

<pre>.\main.go:18: cannot use j (type Jumper) as type Person in argument to PersonSayHello:
Jumper does not implement Person (missing Think method)</pre>

This will be the case as long as you have code anywhere that relies on the fact that a Jumper is always a Person. And if you don't, not even in your tests, then &ndash; well, maybe it doesn't actually matter that the jumper doesn't think?

But if for whatever reason you actually need to ensure that a Jumper is always a Person, no matter what changes you make to these interfaces, but this fact isn't actually used anywhere, you can always create code just for this purpose:

package main

type Person interface {
    Hello()
}

type Jumper interface {
    Hello()
    Jump()
}

// this function is never used, it just exists to ensure
// interface compatibility at compile time
func ensureJumperIsPerson(j Jumper) {
	var p Person = j
	_ = p
}

func main() {
}

答案2

得分: 2

我认为这是不可能的。在我看来,接口嵌入只是一种简写方式,可以直接在那里使用这些函数。所以这等同于有两个Hello()函数。因此编译器会报错。

英文:

I don't think it is possible to do this. IMO, interface embedding is just a shorthand for having those functions directly there. So it is equivalent as having two Hello() functions. Hence the error from compiler.

答案3

得分: 2

这是接着"问题 6997,提案:规范:允许嵌入重叠接口"的内容。

> 如果你将接口视为对实现类型的约束集合,那么组合两个接口(它们不是互斥的)如下所示:

> type I interface { f(); String() string }
type J interface { g(); String() string }

> 有一个自然的解释,相当于包含这些约束的接口的并集。例如,下面的两个接口应该是等价的:

> type IJ interface { I; J }
type IJ interface { f(); g(); String() string }

> 但实际上第一个是一个错误:"重复的方法:String"。

Go 1.14(2020年第一季度,五年后)可能包含一些改进:

CL 190258: 允许嵌入重叠接口,被 CL 191257 替代

> 如果重复的方法与现有方法具有相同的签名,则静默删除嵌入接口中的重复方法。

> 这个改变完全重写了接口方法集的计算方式,不再仅基于语法的方法集计算,而是采用了 cmd/compiler 的实现方法。

> 在第一次通过类型检查接口时,会收集显式方法和嵌入接口,但接口不会被“展开”,也就是最终的方法集计算是惰性进行的,只有在需要进行方法查找时或者在类型检查结束时才会进行。

规范已更新(CL 190378

在这里可以看到一些示例

英文:

This is followed by "issue 6997, proposal: spec: allow embedding overlapping interfaces".

> If you view an interface as a set of constraints on the implementing type, then
combining two interfaces (that are not mutually incompatible) such as:

> type I interface { f(); String() string }
type J interface { g(); String() string }

> has a natural interpretation that is equivalent to an interface containing the union of
such constraints. e.g. these should be equivalent:

> type IJ interface { I; J }
type IJ interface { f(); g(); String() string }

> but in fact the first is an error: "duplicate method: String".

Go 1.14 (Q1 2020, five years later) might include some improvments:

CL 190258: allow embedding overlapping interfaces , replaced by CL 191257

> Quietly drop duplicate methods from embedded interfaces if they have an identical signature to existing methods.

> Instead of adjusting the prior syntax-based only method set computation where methods don't have signature information (and thus where de-duplication according to the new rules would have been somewhat tricky to get right), this change completely rewrites interface method set computation, taking a page from the cmd/compiler's implementation.

> In a first pass, when type-checking interfaces, explicit methods and embedded interfaces are collected, but the interfaces are not "expanded", that is the final method set computation is done lazily, either when needed for method lookup, or
at the end of type-checking.

The spec is updated (CL 190378)

See some examples here.

huangapple
  • 本文由 发表于 2017年5月2日 12:46:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/43730255.html
匿名

发表评论

匿名网友

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

确定