Skip to content

Latest commit

 

History

History

lesson3

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

7.3 Соответствие интерфейсу

Тип соответствует (удовлетворяет) интерфейсу, если он обладает всеми методами, которые требует интерфейс. Например, *os.File соответствует интерфейсам io.Reader, Writer, Closer, ReadWriter. *bytes.Buffer соответствует интерфейсам Reader, Writer, ReadWriter, но не Closer, потому что не имеет метода Close. Для краткости программисты Go часто говорят о том, что конкретный тип "является" типом некоторого интерфейса. Это означает, что он соответствует этому интерфейсу. Например, *bytes.Buffer является io.Writer, а *os.File является io.ReadWriter.

Правило присваиваемости (раздел 2.4.2) интерфейсов очень простое: выражение может быть присвоено интерфейсу, только если его тип соответствует этому интерфейсу. Таким образом:

var w io.Writer
w = os.Stdout           // OK: *os.File имеет метод Write
w = new(bytes.Buffer)   // OK: *bytes.Buffer имеет метод Write
w = time.Second         // Ошибка: у time.Duration нет метода Write

var rwc io.ReadWriteCloser
rwc = os.Stdout         // OK: *os.File имеет методы Read, Write, Close
rwc = new(bytes.Buffer) // Ошибка: у *bytes.Buffer нет метода Close

Это правило применимо даже тогда, когда правая сторона сама по себе является интерфейсом:

w = rwc // OK: io.ReadWriteCloser имеет метод Write
rwc = w // Ошибка: io.Writer не имеет метода Close

Поскольку ReadWriter и ReadWriteCloser включают все методы Writer, любой тип, соответствующий ReadWriter или ReadWriteCloser, обязательно соответствует Writer.

Прежде, чем мы пойдем дальше рассмотрим одно тонкое место, связанное со смыслом утверждения, что некоторый тип имеет некий метод. Вспомним из раздела 6.2, что для каждого именованного конкретного типа T одни из его методов имеют получатель типа T, в то время как другие требуют указатель *T. Вспомним также, что вполне законным является вызов метода *T с аргументом типа T при условии, что аргумент является переменной - компилятор неявно берет ее адрес. Но это лишь синтаксическое упрощение: значение типа T не обладает всеми методами, которыми обладает указатель *T, и в результате оно может удовлетворить меньшему количеству интерфейсов.

Чтобы было понятнее, приведем пример. Методу String типа IntSet из раздела 6.5 необходим получатель, являющийся указателем, так что мы не можем вызвать этот метод для неадресуемого значения IntSet:

type IntSet struct {
    /*...*/
}

func (*IntSet) String() string

var _ = IntSet{}.String() // Ошибка: String требует получатель `*IntSet`

Но можно вызвать его для переменной IntSet:

var s IntSet
var _ = s.String() // OK: s является переменной; &s имеет метод String

Однако, поскольку только *IntSet имеет метод String, только *IntSet соответствует интерфейсу fmt.Stringer:

var _ fmt.Stringer = &s // OK
var _ fmt.Stringer = s  // Ошибка: у IntSet нет метода String

В разделе 12.8 представлена программа, выводящая методы произвольных значений, а команда godoc -analysis=type (раздел 10.7.4) отображает методы каждого типа и взаимоотношения между интерфейсами и конкретными типами.

Подобно конверту, который является оболочкой, скрывающий письмо, которое в нем хранится, интерфейс обертывает и скрывает конкретный тип и значение, которое он хранит. При этом могут вызваться только методы интерфейса, даже если конкретный тип имеет и другие методы:

os.Stdout.Write([]byte("hello")) // OK: *os.File имеет метод Write
os.Stdout.Close()                // OK: *os.File имеет метод Close

var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer имеет метод Write
w.Close() // Ошибка: io.Writer не имеет метода Close

Интерфейс с бо́льшим количеством методов, такой как io.ReadWriter, больше говорит о значениях, которые он содержит, и устанавливает более высокие требования к типам, которые его реализуют, чем интерфейс с меньшим количеством методов, такой как io.Reader. Что до типа interface{}, который вообще не имеет методов, говорит нам о конкретных типах, которые ему соответствуют?

Именно так: ничего. Это может казаться бесполезным, но на самом деле тип interface{}, который называется пустым интерфейсом, является необходимым. Поскольку тип пустого интерфейса не накладывает никаких требований на типы, которые ему соответствуют, пустому интерфейсу можно присвоить любое значение.

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

Хотя это и не очевидно, мы использовали тип пустого интерфейса, начиная с первого примера это книги, потому что он позволяет таким функциям, как fmt.Println или errorf из раздела 5.7 принимать аргументы любого типа.

Конечно, создав значение interface{}, содержащие булев тип, тип с плавающей точкой, строку, карты, указатель или любой другой тип, мы ничего не можем сделать с ним непосредственно, поскольку интерфейс не имеет методов. Нам нужен способ вновь получить значение переданного типа. Как это сделать с помощью декларации типа, мы узнаем в разделе 7.10

Поскольку удовлетворение интерфейсу зависит только от методов двух типов, нет необходимости объявлять отношения между конкретным типом и интерфейсом, которому он соответствует. Тем не менее иногда полезно документировать и проверять взаимоотношения если они запланированы, но не могут быть обеспечены программой иным способом. Объявление, приведенное ниже, проверяет во время компиляции, что значение типа *bytes.Buffer соответствует интерфейсу io.Writer:

// *bytes.Buffer должен соответствовать io.Writer
var w io.Writer = new(bytes.Buffer)

Нам не нужно выделять новую переменную, поскольку любое значение типа *bytes.Buffer будет это делать, даже nil, который мы записываем с помощью явного преобразования как (*bytes.Bufer)(nil). А так как мы вообще не намерены обращаться к переменной w, ее можно заменить пустым идентификатором. Все эти изменения дают нам этот более скромный вариант:

// *bytes.Buffer должен соответствовать io.Writer
var _ io.Writer = (*bytes.Buffer)(nil)

Непустым типам интерфейсов, таким как io.Writer, чаще всегда соответствуют типы указателей, особенно когда один или несколько методов интерфейса подразумевают некоторые изменения получателя, как метод Write. Особенно часто используется указатель на структуру.

Однако типы указателей - это отнюдь не единственные типы, которые соответствуют интерфейсам, и даже интерфейсы с изменяющими получателя методами могут быть удовлетворены одним из других ссылочных типов Go. Мы видели примеры типов срезов с методами (geomety.Path, раздел 6.1), и типов карт с методами (url.Values, раздел 6.2.1), а позже мы увидим типы функций с методами (http.HandlerFunc, раздел 7.7). Даже фундаментальные типы могут соответствовать интерфейсам. Как мы видели в разделе 7.4, тип time.Duration соответствует интерфейсу fmt.Stringer.

Конкретный тип может соответствовать множеству не связанных интерфейсов. Рассмотрим программу, которая организует или продает оцифрованные произведения культуры, такие как музыка, фильмы и книги. Она может определить следующее множество конкретных типов:

Album
Book
Movie
Magazine
Podcast
TVEpisode
Track

Мы можем выразить каждую интересующую нас абстракцию как интерфейс. Одни свойства являются общими для всех произведений, такие как название, дата создания и список создателей (авторы и художники):

type Artifact interface {
  Title()    string
  Creators() []string
  Created()  time.Time
}

Другие свойства ограничены только определенными видами произведений. Свойства типографского издания актуальны только для книг и журналов, тогда как только фильмы и телевизионные эпизоды имеют разрешение экрана:

type Text interface {
  Pages()    int
  Words()    int
  PageSize() int
}

type Audio interface {
  Stream()      (io.ReadCloser, error)
  RunningTime() time.Duration
  Format()      string // Например, mp3, wav
}

type Video interface {
  Stream()      (io.ReadCloser, error)
  RunningTime   time.Duration
  Format()      string // Например mp4, wmv, mkv
  Resolution()  (x, y int)
}

Эти интерфейсы представляют собой лишь один из полезных способов группировки связанных конкретных типов и выражения их общих аспектов. Позже мы сможем обнаружить и другие способы группировки. Например, обнаружив, что нужно обрабатывать элементы Audio и Video одинаково, мы сможем определить интерфейс Streamer для представления их общих свойств без изменения каких-либо существующий объявлений типов:

type Steramer interface {
  Stream()      (io.ReadCloser, error)
  RunningTime() time.Duration
  Format()      string
}

Каждая группировка конкретных типов на основе их общего поведения может быть выражена как тип интерфейса. В отличие от языков на основе классов, в которых набор интерфейсов, которым соответствует класс, указывается явно, в Go мы можем определять новые абстракции или группы интересов, когда мы в них нуждаемся, не изменяя при этом объявления конкретных типов. Это особенно удобно, когда конкретный тип определен в пакете другого автора. Конечно, при этом должно удовлетворятся требование базовой общности конкретных типов.

Выводы:

  • Чтобы тип удовлетворял интерфейсу, он должен иметь все методы, которые требует интерфейс;
  • Чтобы выражение могло быть присвоено интерфейсу, его тип должен соответствовать этому интерфейсу. Это относится к обеим сторонам a = b, если они являются интерфейсами;
  • Для каждого типа T некоторые методы могут быть вызваны с помощью значения типа T, а другие требуют указателя на тип *T. Можно вызывать методы типа *T с помощью переменной типа T, но это не значит, что значение типа T обладает всеми методами, которые есть у указателя на *T. Поэтому, при использовании значения типа T, может быть доступно меньшее количество методов, чем при использовании указателя на *T;
  • Интерфейс скрывает конкретный тип и значения, которые он содержит. Можно использовать только методы, объявленные в интерфейсе, даже если конкретный тип имеет другие методы;
  • Тип interface{} является пустым интерфейсом и не накладывает требований на типы, которые ему соответствуют (может быть присвоен любой тип);
  • Создав значение interface{}, с любым значением, мы ничего не можем сделать с ним непосредственно, т.к. интерфейс не имеет методов;
  • Обычно нет необходимости объявлять отношения между конкретным типом и интерфейсом, которому он соответствует. Однако иногда полезно документировать и проверять эти отношения;
  • Объявление var w io.Writer = new(bytes.Buffer) проверяет, что значение типа *bytes.Buffer соответствует интерфейсу io.Writer во время компиляции. Нет необходимости создавать новую переменную, поскольку любое значение типа *bytes.Buffer будет подходить, даже nil, который можно записать как (*bytes.Buffer)(nil);
  • Непустым типам интерфейсов, таким как io.Writer, чаще всегда соответствуют типы указателей, особенно когда один или несколько методов интерфейса подразумевают некоторые изменения получателя, как метод Write. Особенно часто используется указатель на структуру;
  • Помимо указателей, можно использовать и другие ссылочные типы для удовлетворения интерфейсов, даже с методами, изменяющими получателя;
  • Фундаментальные типы могут соответствовать интерфейсам;
  • Тип time.Duration соответствует интерфейсу fmt.Stringer;
  • Один конкретный тип может соответствовать нескольким интерфейсам;
  • Используя интерфейсы, мы можем группировать конкретные типы по их общим аспектам и выражать их общие свойства;
  • Каждая группировка конкретных типов может быть выражена в виде интерфейса, и мы можем определять новые интерфейсы, когда в них нуждаемся, не изменяя при этом объявления конкретных типов. Это особенно удобно, когда мы используем конкретный тип из пакета другого автора. Однако все конкретные типы, которые мы хотим группировать с помощью интерфейса, должны удовлетворять базовым общим требованиям.