在Go语言中存在脆弱基类的问题吗?

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

Does fragile base class issue exist in Go?

问题

尽管使用组合而不是继承?如果是这样,是否有语言级别的解决方案?

英文:

Despite using composition over inheritance?

If so, is there any solution for it at the language level?

答案1

得分: 10

正如VonC所写,但我想指出一些事情。

脆弱基类问题通常归咎于虚方法(方法的动态分派——这意味着如果方法可以被重写,那么在运行时只能决定调用哪个实际实现)。为什么这是个问题呢?你有一个类,你给它添加了一些方法,如果MethodA()调用MethodB(),你不能保证调用的是你编写的MethodB(),而不是某个子类覆盖了你的MethodB()的其他方法。

在Go中,有嵌入,但没有多态。如果你在结构体中嵌入一个类型,嵌入类型的所有方法都会被“提升”,并且将成为包装结构体类型的方法集的一部分。但你不能“覆盖”被提升的方法。当然,你可以添加一个同名的自己的方法,在包装结构体上调用该方法的名称将调用你的方法,但如果该方法是从嵌入类型调用的,它将不会分派到你的方法,而是仍然调用被定义为嵌入类型的“原始”方法。

因此,由于这个原因,我认为脆弱基类问题在Go中只以相当缓解的形式存在。

示例

在Java中演示问题

让我们看一个例子。首先是Java,因为Java“遭受”这种问题。让我们创建一个简单的Counter类和一个MyCounter子类:

class Counter {
    int value;

    void inc() {
        value++;
    }

    void incBy(int n) {
        value += n;
    }
}

class MyCounter extends Counter {
    void inc() {
        incBy(1);
    }
}

实例化和使用MyCounter

MyCounter m = new MyCounter();
m.inc();
System.out.println(m.value);
m.incBy(2);
System.out.println(m.value);

输出如预期所示:

1
3

到目前为止一切正常。现在,如果基类Counter.incBy()被更改为:

void incBy(int n) {
    for (; n > 0; n--) {
        inc();
    }
}

基类Counter仍然完好无损和可操作。但是MyCounter变得失效:MyCounter.inc()调用Counter.incBy(),它调用inc(),但由于动态分派,它将调用MyCounter.inc()...是的...无限循环。堆栈溢出错误。

在Go中演示问题的缺失

现在让我们看看相同的例子,这次用Go编写:

type Counter struct {
    value int
}

func (c *Counter) Inc() {
    c.value++
}

func (c *Counter) IncBy(n int) {
    c.value += n
}

type MyCounter struct {
    Counter
}

func (m *MyCounter) Inc() {
    m.IncBy(1)
}

进行测试:

m := &MyCounter{}
m.Inc()
fmt.Println(m.value)
m.IncBy(2)
fmt.Println(m.value)

输出如预期(在Go Playground上尝试一下):

1
3

现在让我们像在Java示例中那样更改Counter.Inc()

func (c *Counter) IncBy(n int) {
    for ; n > 0; n-- {
        c.Inc()
    }
}

它运行得很完美,输出相同。在Go Playground上试一下。

这里发生的是MyCounter.Inc()将调用Counter.IncBy(),它将调用Inc(),但这个Inc()将是Counter.Inc(),所以这里没有无限循环。Counter甚至不知道MyCounter的存在,它没有任何对嵌入的MyCounter值的引用。

英文:

As VonC wrote, but I'd like to point out something.

The fragile base class problem is often blamed on virtual methods (dynamic dispatch of methods – this means if methods can be overridden, the actual implementation that has to be called in case of such an overridden method can only be decided at runtime).

Why is this a problem? You have a class, you add some methods to it, and if MethodA() calls MethodB(), you can't have any guarantee that the MethodB() you wrote will be called and not some other method of a subclass that overrides your MethodB().

In Go there is embedding, but there is no polymorphism. If you embed a type in a struct, all the methods of the embedded type get promoted and will be in the method set of the wrapper struct type. But you can't "override" the promoted methods. Sure, you can add your own method with the same name, and calling a method by that name on the wrapper struct will invoke your method, but if this method is called from the embedded type, that will not be dispatched to your method, it will still call the "original" method that was defined to the embedded type.

So because of this, I'd say the fragile base class problem is only present in a quite mitigated form in Go.

Example

Demonstrating the problem in Java

Let's see an example. First in Java, because Java "suffers" from this kind of problem. Let's create a simple Counter class and a MyCounter subclass:

class Counter {
    int value;

    void inc() {
        value++;
    }

    void incBy(int n) {
        value += n;
    }
}

class MyCounter extends Counter {
    void inc() {
        incBy(1);
    }
}

Instantiating and using MyCounter:

MyCounter m = new MyCounter();
m.inc();
System.out.println(m.value);
m.incBy(2);
System.out.println(m.value);

The output is as expected:

1
3

So far so good. Now if the base class, Counter.incBy() would be changed to this:

void incBy(int n) {
    for (; n > 0; n--) {
		inc();
	}
}

The base class Counter still remains flawless and operational. But the MyCounter becomes malfunctioning: MyCounter.inc() calls Counter.incBy(), which calls inc() but due to dynamic dispatch, it will call MyCounter.inc()... yes... endless loop. Stack overflow error.

Demonstrating the lack of the problem in Go

Now let's see the same example, this time written in Go:

type Counter struct {
	value int
}

func (c *Counter) Inc() {
	c.value++
}

func (c *Counter) IncBy(n int) {
	c.value += n
}

type MyCounter struct {
	Counter
}

func (m *MyCounter) Inc() {
	m.IncBy(1)
}

Testing it:

m := &MyCounter{}
m.Inc()
fmt.Println(m.value)
m.IncBy(2)
fmt.Println(m.value)

Output is as expected (try it on the Go Playground):

1
3

Now let's change Counter.Inc() the same way we did in the Java example:

func (c *Counter) IncBy(n int) {
	for ; n > 0; n-- {
		c.Inc()
	}
}

It runs perfectly, the output is the same. Try it on the Go Playground.

What happens here is that MyCounter.Inc() will call Counter.IncBy() which will call Inc(), but this Inc() will be Counter.Inc(), so no endless loop here. Counter doesn't even know about MyCounter, it does not have any reference to the embedder MyCounter value.

答案2

得分: 2

“脆弱基类”问题是指对基类进行看似安全的修改,但当派生类继承这些修改后,可能导致派生类出现故障。

正如这个教程中所提到的:

就实际效果而言,通过嵌入匿名类型进行组合与实现继承是等效的。嵌入的结构体与基类一样脆弱。

英文:

The Fragile base class problem is when a seemingly safe modifications to a base class, when inherited by the derived classes, may cause the derived classes to malfunction.

As mentioned in this tutorial:

> For all intents and purposes, composition by embedding an anonymous type is equivalent to implementation inheritance. An embedded struct is just as fragile as a base class.

huangapple
  • 本文由 发表于 2017年1月24日 10:28:00
  • 转载请务必保留本文链接:https://go.coder-hub.com/41819033.html
匿名

发表评论

匿名网友

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

确定