嵌入 vs 继承在 Go 语言中的应用

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

Embedding vs. Inheritance in Go

问题

我正在尝试学习Go语言,但是我在一些概念上遇到了困难,因为它们与其他语言的应用方式不同。

假设我有一个结构体:

type Vehicle struct {
    Seats int
}

现在我想要另一个结构体,它嵌入了Vehicle

type Car struct {
    Vehicle
    Color string
}

据我理解,Car结构体现在嵌入Vehicle

现在我想要一个函数,它可以接受任何车辆:

func getSeats(v Vehicle){
    return v.Seats
}

但是每当我尝试传递一个Car时:

    getSeats(myCar)

我会得到以下错误:
cannot use myCar (value of type Car) as Vehicle value in argument to getSeats
但是我的IDE告诉我myCar有一个Seats属性!

从这个错误中,我理解到嵌入不同于继承。

我的问题是:在Go语言中是否有类似于C++的结构体继承的等价方式,即一个函数可以接受一个基本结构体,或者Go语言处理这个问题的方式完全不同?在"Go way"中,我应该如何实现类似的功能?

英文:

I'm trying to learn Go, but I keep bashing my head against some of its concepts that apply differently compared to other languages.

Suppose I have a struct

type Vehicle struct {
    Seats int
}

I now want another struct, which embeds Vehicle:

type Car struct {
    Vehicle
    Color string
}

As I understand it, the Car struct now embeds Vehicle.

Now I want to have a function that takes any vehicle

func getSeats(v Vehicle){
    return v.Seats
}

But whenever I try to pass a Car:

    getSeats(myCar)

I get the following error:
cannot use myCar (value of type Car) as Vehicle value in argument to getSeats
But my IDE tells me myCar has a Seats property!

From this I understand that embedding is not the same as inheriting.

My question is; Is there an equivalent to struct inheritance like c++, where a function can take a base struct, or is this something Go handles completely different? How would I implement something like this in the "Go way"?

答案1

得分: 5

正如你提到的,Go语言在传统意义上没有继承。嵌入实际上只是一种语法糖。

在嵌入时,你在结构体中添加一个与嵌入类型完全相同名称的字段。嵌入结构体的任何方法都可以在嵌入它们的结构体上调用,这只是简单地将它们转发。

一个要注意的地方是,如果嵌入另一个结构体的结构体已经声明了一个方法,那么它将优先于转发,这样你就可以在某种程度上覆盖函数。

正如你注意到的,即使Car嵌入了Vehicle,我们也不能将Car用作Vehicle,因为它们严格来说不是相同的类型。但是,任何嵌入Vehicle的结构体都将拥有由Vehicle定义的所有方法,因此如果我们定义了一个由Vehicle实现的接口,所有嵌入Vehicle的类型也应该实现该接口。

例如:

package main

import (
	"fmt"
)

type Seater interface {
	Seats() int
}

type Vehicle struct {
	seats int
}

func (v *Vehicle) Seats() int {
	return v.seats
}

type Car struct {
	Vehicle
	Color string
}

type Bike struct {
	Vehicle
	Flag bool
}

// 自行车始终有1个座位
func (b *Bike) Seats() int {
	return 1
}

type Motorcycle struct {
	Vehicle
	Sidecar bool
}

// 摩托车有基本座位数,如果有边车则再加1
func (m *Motorcycle) Seats() int {
	return m.Vehicle.seats + 1
}

func getSeats(v Seater) int {
	return v.Seats()
}

func main() {
	fmt.Println(getSeats(&Bike{
		Vehicle: Vehicle{
			seats: 2, // 在Vehicle中设置为2
		},
		Flag: true,
	}))

	fmt.Println(getSeats(&Motorcycle{
		Vehicle: Vehicle{
			seats: 1,
		},
		Sidecar: true,
	}))

	fmt.Println(getSeats(&Car{
		Vehicle: Vehicle{
			seats: 4,
		},
		Color: "blue",
	}))
}

这将输出:

1
2
4

Bike的情况下,调用的是Bike.Seats方法,这就是为什么即使其Vehicleseats值为2,它仍然返回1的原因。

Motorcycle的情况下,也调用了Motorcycle.Seats方法,但在这里我们可以访问嵌入类型并仍然使用它来获得结果。

Car的情况下,调用的是Vehicle.Seats方法,因为Car没有“覆盖”Seats方法。

英文:

Like you mentioned, Go has no inheritance in the typical sense. Embedding is really just syntactic sugar.

When embedding, you add a field to you struct with the exact same name as the type you are embedding. Any methods of the embedded struct can be called on the struct which embeds them, this does nothing more than forwarding them.

One tick is that, if the struct which embeds another already declares a method, it will be preferred over forwarding it, which allows you to sort of overwrite functions if you want to think of it like that.

As you have noticed, we can't use Car as a Vehicle even if Car embeds Vehicle since they are strictly not the same type. But any struct which embeds Vehicle will have all methods which were defined by Vehicle, thus if we define an interface which Vehicle implements, all types that embed Vehicle should also implement that interface.

For example:

package main

import (
	"fmt"
)

type Seater interface {
	Seats() int
}

type Vehicle struct {
	seats int
}

func (v *Vehicle) Seats() int {
	return v.seats
}

type Car struct {
	Vehicle
	Color string
}

type Bike struct {
	Vehicle
	Flag bool
}

// A bike always has 1 seat
func (b *Bike) Seats() int {
	return 1
}

type Motorcycle struct {
	Vehicle
	Sidecar bool
}

// A motorcycle has the base amounts of seats, +1 if it has a side car
func (m *Motorcycle) Seats() int {
	return m.Vehicle.seats + 1
}

func getSeats(v Seater) int {
	return v.Seats()
}

func main() {
	fmt.Println(getSeats(&Bike{
		Vehicle: Vehicle{
			seats: 2, // Set to 2 in the Vehicle
		},
		Flag: true,
	}))

	fmt.Println(getSeats(&Motorcycle{
		Vehicle: Vehicle{
			seats: 1,
		},
		Sidecar: true,
	}))

	fmt.Println(getSeats(&Car{
		Vehicle: Vehicle{
			seats: 4,
		},
		Color: "blue",
	}))
}

This prints:

1
2
4

In the case of Bike the Bike.Seats method is called which is why it returns 1 even when the seats value of its Vehicle is 2.

In the case of Motorcycle the Motorcycle.Seats method is also called, but here we can access the embedded type and still use it to get a result.

In the case of Car, the Vehicle.Seats method is called since Car doesn't "overwrite" Seats.

答案2

得分: 2

这是我在开始使用Go时关心的事情。我认为使用像Java这样的面向对象编程语言的概念是一种自然的做事方式。然而事实并非如此,Go不是面向对象的,它不通过继承来实现多态性。最终,Go支持的组合方式继承了继承所提供的好处,并消除了增加更多开销而不是有效帮助的东西。Go对多态性的回答是接口。Go试图保持简单,通常你只能以一种明显的方式实现事情。假设你有这样的Java类:

class Base {
    private int attribute;

    public int getAttribute() {
        return this.attribute;
    }
}

class Something extends Base {}

在Java中,每个对象都在堆上,并且你有指向它的指针。它可以是null,并且这也意味着当你传递它时,它的大小是相同的。这是你可以更自由地传递对象的原因之一(无论你传递Base还是Something,只要参数类型是超类,它就会编译)。Go被设计为更有效地管理内存并更多地使用堆栈,甚至堆也不太分散。当你声明一个struct时,该结构的大小由它所持有的数据决定,因此你不能将它传递到嵌入结构所属的位置。让我们举个例子:

type Base struct {
   attribute int32 
}

func (b *Base) Attribute() int32 {
    return b.attribute
}

type Something struct {
    garbage int32
    Base
}

虽然Base有4个字节,Something有8个字节,并且attribute有不同的偏移量。如果编译器允许你将Something替换为Base,你将访问garbage而不是attribute。如果你想要获得这种行为,你必须使用接口。幸运的是,你只需要声明这个:

type BaseInterface interface {
    Attribute() int32
}

现在你有了这个接口,你可以对所有嵌入了Base的结构体进行通用函数操作(除非它还嵌入了其他具有相同级别的方法名Attribute的东西),像这样:

func DoubleOfBaseAttribute(base BaseInterface) int32 {
    return base.Attribute() * 2
}

这也被称为动态分派,这是C++或Java等语言隐式使用的方式。

英文:

This is something I cared about when I started with Go. I thought using concepts from OOP languages like Java is natural way of doing things. Well its not, Go is not object oriented and it does not implement polymorphism trough inheritance. In the end, composition that Go supports takes good things from what inheritance offers and removes things that adds more overhead then effective help. Go's answer to polymorphism are interfaces. Go tries to be simple and you can usually achieve things only one obvious way. Lets say you have a Java classes like these:

class Base {
    private int attribute;

    public int getAttribute() {
        return this.attribute;
    }
}

class Something extends Base {}

In java every object is on the heap and you have pointer to it. It can be null and it also means it has same size when you pass it around. This is one of the reasons why you can pass objects more freely (Whether you pass Base or Something, as long as argument type is super class, it will compile). Go is designed to manage memory more efficiently and use stack more then heap, and even heap is less fragmented. When you declare a struct, the size of that struct is determined by data it holds, so you cannot pass it to places where embedded structure belongs. Let's give an example:

type Base struct {
   attribute int32 
}

func (b *Base) Attribute() int32 {
    return b.attribute
}

type Something struct {
    garbage int32
    Base
}

While Base has 4 bytes, Something has 8, and attribute has different offset. If the compiler lets you pass Something in place of Base, you would access garbage instead of attribute. If you want to get this behavior you have to use interface. Luckily, all you have to declare is this:

type BaseInterface interface {
    Attribute() int32
}

Now that you have this interface, you can make general functions over all structs that embed Base (unless it also embeds something else that has method names Attribute on the same level) like this:

func DoubleOfBaseAttribute(base BaseInterface) int32 {
    return base.Attribute() * 2
}

This is also called Dynamic Dispatch and its what languages like C++ or Java use implicitly.

huangapple
  • 本文由 发表于 2022年1月15日 19:16:27
  • 转载请务必保留本文链接:https://go.coder-hub.com/70720911.html
匿名

发表评论

匿名网友

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

确定