Наследование является одним из основных принципов ООП и описывает отношение между классами по принципу того, кто/что представляет собой. Например, утка является птицей. Птица — это животное, то есть строится иерархия наследования к максимально абстрактному объекту.
Можно сказать, наследование должно подразумевать, что все утки — это птицы. При этом обратное неверно, ведь не все птицы утки.
Однако в Go вместо наследования используется композиция. Структуры могут в себя включать другие структуры и типы.
Композиция — это отношение, которое что-либо включает в себя. Например, утка состоит из клюва, туловища и лап. Клюв даёт ей возможность крякать, значит, утка может крякать. Не всё, что имеет клюв — это утка. Но все утки имеют клюв.
Эмбеддинг, то есть встраивание — это реализация композиции в Go.
В общем случае встраивание выглядит так:
type OuterStruct struct {
EmbeddedType
A int
B int
}
Tip
Все поля и методы
EmbeddedType
будут переданы в структуруOuterStruct
, как если бы она сама их содержала. Это позволяет переиспользовать код сложных структур, встраивая одни в другие.
Рассмотрим на примере. Создадим две структуры Person
и Student
. При этом Student
будет включать в себя Person
. Это похоже на наследование, но есть существенные отличия. Структура Student
не является Person
, можно сказать, что она включает её в себя.
Объект Student
не может быть приведён к типу Person
с помощью приведения типов. Здесь имеется в виду классическое приведение типов в ООП языках, где экземпляр производного класса может выступать в качестве экземпляра базового класса.
Условно говоря такой конструкции, как person := Student(person)
для встроенных типов в Go нет.
package main
import (
"fmt"
)
// Person — структура, описывающая человека.
type Person struct {
Name string
Year int
}
// NewPerson возвращает новую структуру Person.
func NewPerson(name string, year int) Person {
return Person{
Name: name,
Year: year,
}
}
// String возвращает информацию о человеке.
func (p Person) String() string {
return fmt.Sprintf("Имя: %s, Год рождения: %d", p.Name, p.Year)
}
// Print выводит информацию о человеке.
func (p Person) Print() {
// вызовется метод String() для Person
fmt.Println(p)
}
// Student описывает студента с использованием вложенной структуры Person. То есть структура Student описывает.
type Student struct {
Person // вложенный объект Person
Group string
}
func NewStudent(name string, year int, group string) Student {
return Student{
Person: NewPerson(name, year), // Явно создаём структуру Person
Group: group,
}
}
// String возвращает информацию о студенте.
func (s Student) String() string {
return fmt.Sprintf("%s, Группа: %s", s.Person, s.Group)
}
func main() {
s := NewStudent("John Doe", 1980, "701")
s.Print()
// вызовется метод String() для Student
fmt.Println(s)
fmt.Println(s.Name, s.Year, s.Group)
}
Имя: John Doe, Год рождения: 1980
Имя: John Doe, Год рождения: 1980, Группа: 701
John Doe 1980 701
В примере тип Student
наследует все поля и методы объекта Person
. Так как метод Print
определён только для типа Person
, то он выводит только имя и год рождения. При вызове функции fmt.Println
используется метод String()
, который определён для типа Student
, поэтому выводится вся информация о студенте.
Доступ к полям вложенных структур
Если вложенный тип описан в другом пакете, то использующий его тип имеет доступ только к экспортируемым (публичным) методам и полям. Есть несколько способов обеспечить доступ к полям вложенных структур. Покажем их, добавив Debug()
-метод для типа Student
:
func (s *Student) Debug() {
// доступ к методам объекта Person
s.Print()
// или
s.Person.Print()
// доступ к полю 'Name' объекта Person
s.Name = "Mark Smith"
// или
s.Person.Name = "Mark Smith"
// вызовется метод String объекта Student
fmt.Println(s)
// вызовется метод String объекта Person
fmt.Println(s.Person)
}
Метод структуры, в которую встроен другой тип, не переопределяется, а затеняется. То есть при вызове метода с таким же именем Go сначала попытается найти метод среди методов структуры Student
, затем ищет его среди вложенных типов. Если несколько вложенных типов имеют одинаковые методы, происходит конфликт.
Обращаться к полям (или методам) вложенной структуры можно как с указанием типа объекта (s.Person.Print()
), так и без (s.Print()
). Для String
-метода объекта Person
требуется явно указывать тип, так как метод переопределён типом Student
.
Фактически структура имеет поле с именем, совпадающим с именем типа. Однако при вызове метода вложенной структуры в неё передаётся не переменная, которая её вызвала, а это поле. При необходимости можно явно обратиться к методу вложенной структуры.
Есть два правила разрешения конфликтов имён полей и методов:
- Именованное поле (метод) структуры скрывает поле (метод) с тем же именем для вложенных структур. Имя верхнего уровня доминирует над именами более низких уровней. Например, вызов метода
student.String()
вызывает метод структурыStudent
, а не структурыPerson
. - Если имя поля (метода) встречается на том же уровне вложенности (дупликация имён), и оно использовано в коде, это ошибка. Если дупликация имён существует, но это имя не используется в коде, компилятор не выдаст ошибку. Например, если тип
Student
имел бы ещё одну вложенную структуруFaculty
с методомPrint()
, то тогда при вызове метода необходимо указывать имя вложенного типаs.Person.Print()
илиs.Faculty.Print()
. Например:
type Faculty string
func (f Faculty) String() string {
return string(f)
}
func (f Faculty) Print() {
fmt.Println(f)
}
type Student struct {
Person
Faculty
Group string
}
func main {
// s.Print()
fmt.Println(s)
}
Если в примере выше расскомментировать строку s.Print()
, то получим ошибку компиляции. ambiguous selector s.Print
компилятор не может определить точно, пытается сообразить, какой же метод выбрать от Faculty
или от Person
. И выдает ошибку.
При этом метод String()
, который вызывается в Println()
, скрывает все методы String()
у вложенных типов. И ошибки не возникает.
Следует отменить, что эти же правила действуют не только для методов, но и для полей структур.
Можно переписать пример без использования вложенных структур и работать с нужными объектами, как с обычными полями:
type Student struct {
Person Person
Group string
}
В этом случае тип Person
уже не будет вложенным и необходимо явно указывать s.Person.Print()
, s.Person.Name
и т.д.
Механизм embedding находится в отрыве от Go-интерфейсов. Он воспринимается как синтаксический сахар, но становится полезным, когда нужно, чтобы объект соответствовал определённым интерфейсам. Если тип имеет вложенный тип, то он реализует все интерфейсы этого типа.
Встраивание указателей на тип
В структуру может быть встроен указатель на тип. Рассмотрим на примере:
type Student struct {
*Person
Group string
}
Внешние отличия небольшие, но встраивание указателя может быть удобно, если вы встраиваете в вашу структуру какую-то большую структуру и затем передаёте её по значению. Это может повысить производительность за счёт того, что копируется не вся структура, а только указатель на неё. Важно отметить, что в таком случае встроенная структура может быть изменена:
// принимает структуру по значению
func ChangeName( s Student, name string) {
s.Name = name
}
s := Student{&Person{Name: "alex"}, "021"}
ChangeName(s, "teodor")
fmt.Println(s.Name) // "teodor"
Эта функция изменит имя студента, если поле Person
встроено как указатель. Если встроить просто Person
, то имя не изменится:
type Student struct {
Person
Group string
}
func ChangeName( s Student, name string) {
s.Name = name
}
s := Student{Person{Name: "alex"}, "021"}
ChangeName(s, "teodor")
fmt.Println(s.Name) // "alex"
Ограничения на встроенные типы
Согласно спецификации Go, не все типы данных могут быть встроены в структуры. Могут быть встроены типы или указатели на типы. При этом, если встраивается указатель на тип, то сам тип не может быть указателем. То есть можно встроить, например, тип Person
и тип *Person
, но нельзя встроить тип **Person
.
Применение
Ещё одно применение встраивания — это расширение возможностей внешних типов. Хочется иметь все возможности, которые представляет тип, находящийся во внешнем пакете, но не можем изменить сам пакет. В таком случае можно создать свой тип, встроить в него внешний и добавить необходимые методы и поля. Полученный тип будет содержать методы встроенного типа.
Часто разработчики библиотек предполагают, что предоставляемый библиотекой тип будет встраиваться в структуру пользователя, а не использоваться самостоятельно.
Возьмём из предыдущего урока тип CircularBuffer
и добавим метод, который может добавлять в наш буфер сразу несколько значений. Представим, что нельзя изменить его код (такое очень часто бывает в промышленной разработке):
// ExtendedCircularBuffer — «наследник» типа CircularBuffer, реализующий добавление нескольких элементов
type ExtendedCircularBuffer struct {
CircularBuffer
}
func (cb * ExtendedCircularBuffer) AddValues(vals... float64) {
for _,val := range vals {
cb.Addvalues(val)
}
}
func NewExtendedCircularBuffer (size int) ExtendedCircularBuffer {
return ExtendedCircularBuffer{
CircularBuffer : NewCircularBuffer(size),
}
}
func main() {
buffer := NewExtendedCircularBuffer(5)
buffer.Addvalues(1,2,3,4,5)
fmt.Printf("[%d]: %v\n", buffer.GetCurrentSize(), buffer.GetValues())
}
[5]: [1 2 3 4 5]
📂 Go | Последнее изменение: 26.08.2024 19:32