Go не предоставляет типичное, управляемое типом понятие подкласса, но у него есть возможность "одолжить" части реализации с помощью вложения типов в структуру или интерфейс.
Интерфейс вложения очень прост. Мы уже упоминали интерфейсы io.Reader
и io.Writer
; Вот их определения.
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
Пакет io
также экспортирует несколько других интерфейсов, которые указывают объекты, которые могут реализовать несколько таких методов. Например, есть io.ReadWriter
, интерфейс содержит как Read
, так и Write
. Мы могли бы указать io.ReadWriter
, перечислив два метода явно, но это проще и лучше запоминается - встроить два интерфейса для формирования нового, например так:
// ReadWriter - это интерфейс,
// который объединяет Reader и Writer интерфейсы.
type ReadWriter interface {
Reader
Writer
}
Это означает: ReadWriter
может сделать, что Reader
делает и что Writer
делает; это объединение вложенных интерфейсов (которые должны быть непересекающимися наборами методов). Только интерфейсы могут быть вложены в интерфейсы.
Та же самая идея относится и к структурам, но с более далеко идущими последствиями. Пакет bufio
имеет два типа структуры: bufio.Reader
и bufio.Writer
, каждая из
которых, конечно, реализует аналогичные интерфейсы из пакета io
. Кроме того bufio
реализует буферизованное устройство чтения/записи, что он делает, объединяя читателя и писателя в одну структуру, используя вложения: перечисляет типы в структуре, но не дает им имена полей.
// ReadWriter сохраняет указатели на Reader и Writer.
// Он реализует io.ReadWriter.
type ReadWriter struct {
*Reader // *bufio.Reader
*Writer // *bufio.Writer
}
Вложенные элементы являются указателями на структуры и, конечно, должны быть инициализированы, чтобы указывать на действительные структуры, прежде чем они могут быть использованы. Структура ReadWriter
может быть записана как
type ReadWriter struct {
reader *Reader
writer *Writer
}
но затем, чтобы продвигать методы полей и удовлетворять интерфейсам io
, нам также потребуется предоставить методы пересылки, например:
func (rw *ReadWriter) Read(p []byte) (n int, err error) {
return rw.reader.Read(p)
}
Вкладывая структуры напрямую, мы избегаем этого. Методы встроенных типов предоставляются бесплатно, что означает, что bufio.ReadWriter
не только имеет методы bufio.Reader
и bufio.Writer
, но он также удовлетворяет всем трем интерфейсам: io.Reader
, io.Writer
и io.ReadWriter
.
Есть важная деталь, которой вложение отличается от подкласса. Когда мы встраиваем тип, методы этого типа становятся методами внешнего типа, но когда они вызываются, получатель метода является внутренним типом, а не внешним. В нашем примере, когда метод Read
для bufio.ReadWriter
вызывается, он имеет тот же эффект, что и метод пересылки, описанный выше; получатель - это поле reader
в ReadWriter
, а не сам ReadWriter
.
Вложение также может быть просто удобством. В следующем примере показано вложенное поле рядом с обычным именованным полем.
type Job struct {
Command string
*log.Logger
}
Тип Job
теперь имеет Log
, Logf
и другие методы *log.Logger
. Мы могли бы дать Logger
имя поля, конечно, но это не обязательно. А теперь однажды инициализировав, мы можем логировать в Job
:
job.Log("starting now...")
Logger
является обычным полем структуры Job
, поэтому мы можем инициализировать его обычным способом внутри конструктора для Job
, например:
func NewJob(command string, logger *log.Logger) *Job {
return &Job{command, logger}
}
или с составным литералом,
job := &Job{command, log.New(os.Stderr, "Job: ", log.Ldate)}
Если нам нужно обратиться к вложенному полю напрямую, имя типа поля, игнорируя спецификатор пакета, служит именем поля, как это было в методе Read
нашей структуры ReadWriter
. Здесь, если нам нужно было получить доступ к *log.Logger
Job
переменной job
, мы бы написали job.Logger
, что было бы полезно, если бы мы хотели усовершенствовать методы Logger
.
func (job *Job) Logf(format string, args ...interface{}) {
job.Logger.Logf("%q: %s",
job.Command,
fmt.Sprintf(format, args...))
}
Вложение типов представляет проблему конфликтов имен, но правила, которые необходимо учитывать, просты. Во-первых, поле или метод X
скрывает любой другой элемент X
более глубоко вложенной части типа. Если log.Logger
содержал поле или метод с именем Command
, поле Command
Job
будет доминировать над ним.
Во-вторых, если одно и то же имя появляется на том же уровне вложенности - это обычно ошибка; было бы неправильно вкладывать log.Logger
, если структура Job
содержит другое поле или метод с именем Logger
. Однако, если дублированное имя никогда не упоминается в программе вне определения типа, это нормально. Эта ограничение обеспечивает некоторую защиту от изменений, внесенных в типы, внедренные извне; не проблема, если добавлено поле, конфликтующее с другим полем другого подтипа, если ни одно из полей никогда не используется.
Читайте также:
Комментариев нет:
Отправить комментарий