Наследование является одним из основных принципов ООП и описывает отношение между классами по принципу того, кто/что представляет собой. Например, утка является птицей. Птица — это животное, то есть строится иерархия наследования к максимально абстрактному объекту.
Можно сказать, наследование должно подразумевать, что все утки — это птицы. При этом обратное неверно, ведь не все птицы утки.
Однако в Go вместо наследования используется композиция. Структуры могут в себя включать другие структуры и типы.
Композиция — это отношение, которое что-либо включает в себя. Например, утка состоит из клюва, туловища и лап. Клюв даёт ей возможность крякать, значит, утка может крякать. Не всё, что имеет клюв — это утка. Но все утки имеют клюв.
Эмбеддинг, то есть встраивание — это реализация композиции в Go.
В общем случае встраивание выглядит так:
Tip
Все поля и методы
EmbeddedType
будут переданы в структуруOuterStruct
, как если бы она сама их содержала. Это позволяет переиспользовать код сложных структур, встраивая одни в другие.
Рассмотрим на примере. Создадим две структуры Person
и Student
. При этом Student
будет включать в себя Person
. Это похоже на наследование, но есть существенные отличия. Структура Student
не является Person
, можно сказать, что она включает её в себя.
Объект Student
не может быть приведён к типу Person
с помощью приведения типов. Здесь имеется в виду классическое приведение типов в ООП языках, где экземпляр производного класса может выступать в качестве экземпляра базового класса.
Условно говоря такой конструкции, как person := Student(person)
для встроенных типов в Go нет.
Имя: John Doe, Год рождения: 1980
Имя: John Doe, Год рождения: 1980, Группа: 701
John Doe 1980 701
В примере тип Student
наследует все поля и методы объекта Person
. Так как метод Print
определён только для типа Person
, то он выводит только имя и год рождения. При вызове функции fmt.Println
используется метод String()
, который определён для типа Student
, поэтому выводится вся информация о студенте.
Доступ к полям вложенных структур
Если вложенный тип описан в другом пакете, то использующий его тип имеет доступ только к экспортируемым (публичным) методам и полям. Есть несколько способов обеспечить доступ к полям вложенных структур. Покажем их, добавив Debug()
-метод для типа Student
:
Метод структуры, в которую встроен другой тип, не переопределяется, а затеняется. То есть при вызове метода с таким же именем Go сначала попытается найти метод среди методов структуры Student
, затем ищет его среди вложенных типов. Если несколько вложенных типов имеют одинаковые методы, происходит конфликт.
Обращаться к полям (или методам) вложенной структуры можно как с указанием типа объекта (s.Person.Print()
), так и без (s.Print()
). Для String
-метода объекта Person
требуется явно указывать тип, так как метод переопределён типом Student
.
Фактически структура имеет поле с именем, совпадающим с именем типа. Однако при вызове метода вложенной структуры в неё передаётся не переменная, которая её вызвала, а это поле. При необходимости можно явно обратиться к методу вложенной структуры.
Есть два правила разрешения конфликтов имён полей и методов:
- Именованное поле (метод) структуры скрывает поле (метод) с тем же именем для вложенных структур. Имя верхнего уровня доминирует над именами более низких уровней. Например, вызов метода
student.String()
вызывает метод структурыStudent
, а не структурыPerson
. - Если имя поля (метода) встречается на том же уровне вложенности (дупликация имён), и оно использовано в коде, это ошибка. Если дупликация имён существует, но это имя не используется в коде, компилятор не выдаст ошибку. Например, если тип
Student
имел бы ещё одну вложенную структуруFaculty
с методомPrint()
, то тогда при вызове метода необходимо указывать имя вложенного типаs.Person.Print()
илиs.Faculty.Print()
. Например:
Если в примере выше расскомментировать строку 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-интерфейсов. Он воспринимается как синтаксический сахар, но становится полезным, когда нужно, чтобы объект соответствовал определённым интерфейсам. Если тип имеет вложенный тип, то он реализует все интерфейсы этого типа.
Встраивание указателей на тип
В структуру может быть встроен указатель на тип. Рассмотрим на примере:
Внешние отличия небольшие, но встраивание указателя может быть удобно, если вы встраиваете в вашу структуру какую-то большую структуру и затем передаёте её по значению. Это может повысить производительность за счёт того, что копируется не вся структура, а только указатель на неё. Важно отметить, что в таком случае встроенная структура может быть изменена:
Эта функция изменит имя студента, если поле Person
встроено как указатель. Если встроить просто Person
, то имя не изменится:
Ограничения на встроенные типы
Согласно спецификации Go, не все типы данных могут быть встроены в структуры. Могут быть встроены типы или указатели на типы. При этом, если встраивается указатель на тип, то сам тип не может быть указателем. То есть можно встроить, например, тип Person
и тип *Person
, но нельзя встроить тип **Person
.
Применение
Ещё одно применение встраивания — это расширение возможностей внешних типов. Хочется иметь все возможности, которые представляет тип, находящийся во внешнем пакете, но не можем изменить сам пакет. В таком случае можно создать свой тип, встроить в него внешний и добавить необходимые методы и поля. Полученный тип будет содержать методы встроенного типа.
Часто разработчики библиотек предполагают, что предоставляемый библиотекой тип будет встраиваться в структуру пользователя, а не использоваться самостоятельно.
Возьмём из предыдущего урока тип CircularBuffer
и добавим метод, который может добавлять в наш буфер сразу несколько значений. Представим, что нельзя изменить его код (такое очень часто бывает в промышленной разработке):
[5]: [1 2 3 4 5]
📂 Go | Последнее изменение: 26.08.2024 19:32