вторник, 29 января 2019 г.

Эффективный Go: вложение (embedding)

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. Однако, если дублированное имя никогда не упоминается в программе вне определения типа, это нормально. Эта ограничение обеспечивает некоторую защиту от изменений, внесенных в типы, внедренные извне; не проблема, если добавлено поле, конфликтующее с другим полем другого подтипа, если ни одно из полей никогда не используется.


Читайте также:


Комментариев нет:

Отправить комментарий