导出仅嵌入结构实现的子集方法。

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

Export only subset of methods implemented by embedded struct

问题

可以通过嵌入结构体的方式来实现只导出部分方法。在Go语言中,嵌入结构体会将嵌入结构体的方法也一同继承过来。如果你只想导出部分方法,可以使用接口来实现。

以下是修改后的代码:

package main

import "fmt"

type A struct {
}

func (a *A) Hello() {
	fmt.Println("Hello!")
}

func (a *A) World() {
	fmt.Println("World!")
}

type Helloer interface {
	Hello()
}

type B struct {
	A
}

func (b *B) Hello() {
	b.A.Hello()
}

type C struct {
	A
}

func main() {
	b := B{}
	c := C{}

	// B只导出Hello方法
	var bHello Helloer = &b
	bHello.Hello()

	// C导出Hello和World方法
	c.Hello()
	c.World()
}

在修改后的代码中,我们定义了一个名为Helloer的接口,该接口只包含了Hello方法。然后,我们在结构体B中实现了Hello方法,并在该方法中调用了嵌入结构体AHello方法。这样,通过将B类型转换为Helloer接口类型,我们就只能访问到Hello方法了。

希望对你有帮助!如果还有其他问题,请随时提问。

英文:

Is it possible to export only a subset of methods implemented by an embedded struct?
Is this a very go-unlike way to reduce copy - and - pasting of code and there is a more idiomatic way to do this?

type A struct {
}

func (a *A) Hello() {
    fmt.Println("Hello!")
}

func (a *A) World() {
    fmt.Println("World!")
}

type B struct {
    A
}

type C struct {
    A
}

func main() {
    b := B{}
    c := C{}

    // B should only export the Hello - function
    b.Hello()

    // C should export both Hello - and World - function
    c.Hello()
    c.World()
}

答案1

得分: 6

这是嵌入的工作原理,你无法改变它。(实际上是可以的,见最后的“dirty trick”部分。)

通过接口可以实现你想要的效果。将结构体设为非导出(B 变为 bC 变为 c),并创建类似“构造函数”的函数,返回只包含你想要公开的方法的接口类型:

type b struct {
    A
}

type c struct {
    A
}

type Helloer interface {
    Hello()
}

type HelloWorlder interface {
    Helloer
    World()
}

func NewB() Helloer {
    return &b{}
}

func NewC() HelloWorlder {
    return &c{}
}

你可以根据需要给接口和函数起不同的名字,这只是为了演示。

还要注意,虽然返回的 Helloer 接口不包含 World() 方法,但仍然可以使用类型断言“访问”它,例如:

h := NewB() // h 的类型是 Helloer
if hw, ok := h.(HelloWorlder); ok {
    hw.World() // 使用上述实现将成功调用此方法
}

Go Playground上尝试一下。

“Dirty trick”

如果一个类型嵌入了类型 A,那么被“提升”的 A 的字段和方法将成为嵌入类型的方法集的一部分(从而成为类型 A 的方法)。这在规范:结构体类型中有详细说明。

如果结构体 x 中的匿名字段 f(字段和方法)是合法的选择器,那么字段或方法 f 就被称为“提升”。

重点在于“提升”,选择器必须是合法的规范:选择器描述了 x.f 的解析过程:

以下规则适用于选择器:

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

[...]

这意味着什么?通过嵌入,B.World 将表示 B.A.World 方法,因为它是最浅层的。但是,如果我们能够实现 B.A.World 不是最浅层,那么类型 B 就不会有这个 World() 方法,因为 B.A.World 不会被提升。

我们如何实现呢?可以添加一个名为 World 的字段:

type B struct {
    A
    World int
}

这个 B 类型(或者更确切地说是 *B)将没有 World() 方法,因为 B.World 表示的是字段,而不是 B.A.World,前者是最浅层的。在Go Playground上尝试一下。

同样地,这并不能阻止任何人显式地引用 B.A.World(),所以该方法仍然可以被“访问”和调用,我们只是实现了类型 B*B 没有 World() 方法的效果。

这个“dirty trick”的另一个变体是利用第一条规则的结尾:“如果最浅层不止一个 f”。这可以通过嵌入另一个类型来实现,另一个也有 World 字段或方法的结构体,例如:

type hideWorld struct{ World int }

type B struct {
    A
    hideWorld
}

Go Playground上尝试这个变体。

英文:

This is how embedding works, there's nothing you can do about it. (Actually there is, see dirty trick at the end.)

What you want may be achieved with interfaces though. Make your structs unexported, (B => b and C => c), and create "constructor" like functions, which return interface types, containing only the methods you wish to publish:

type b struct {
    A
}

type c struct {
    A
}

type Helloer interface {
    Hello()
}

type HelloWorlder interface {
    Helloer
    World()
}

func NewB() Helloer {
    return &b{}
}

func NewC() HelloWorlder {
    return &c{}
}

You might want to call the interfaces and functions different, this is just for demonstration.

Also note that while the returned Helloer interface does not include the World() method, it is still possible to "reach" it using type assertion, e.g.:

h := NewB() // h is of type Helloer
if hw, ok := h.(HelloWorlder); ok {
    hw.World() // This will succeed with the above implementations
}

Try this on the Go Playground.

Dirty trick

If a type embeds the type A, (fields and) methods of A that get promoted will become part of the method set of the embedder type (and thus become methods of type A). This is detailed in Spec: Struct types:

> A field or method f of an anonymous field in a struct x is called promoted if x.f is a legal selector that denotes that field or method f.

The focus is on the promotion, for which the selector must be legal. Spec: Selectors describes how x.f is resolved:

> The following rules apply to selectors:
>
> 1. 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.
>
> [...]

What does this mean? Simply by embedding, B.World will denote the B.A.World method as that is at the shallowest depth. But if we can achieve so that B.A.World won't be the shallowest, the type B won't have this World() method, because B.A.World won't get promoted.

How can we achieve that? We may add a field with name World:

type B struct {
    A
    World int
}

This B type (or rather *B) will not have a World() method, as B.World denotes the field and not B.A.World as the former is at the shallowest depth. Try this on the Go Playground.

Again, this does not prevent anyone to explicitly refer to B.A.World(), so that method can be "reached" and called, all we achieved is that the type B or *B does not have a World() method.

Another variant of this "dirty trick" is to exploit the end of the first rule: "If there is not exactly one f with shallowest depth". This can be achieved to also embed another type, another struct which also has a World field or method, e.g.:

type hideWorld struct{ World int }

type B struct {
    A
    hideWorld
}

Try this variant on the Go Playground.

huangapple
  • 本文由 发表于 2017年6月8日 22:53:39
  • 转载请务必保留本文链接:https://go.coder-hub.com/44439087.html
匿名

发表评论

匿名网友

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

确定