Показаны сообщения с ярлыком методы в Go. Показать все сообщения
Показаны сообщения с ярлыком методы в Go. Показать все сообщения

среда, 17 ноября 2021 г.

Go для Java разработчиков: методы и интерфейсы

Метод выглядит как обычное определение функции, за исключением того, что у него есть получатель. Получатель аналогичен ссылке this в методе экземпляра Java.

type MyType struct { i int }

func (p *MyType) Get() int {
    return p.i
}

var pm = new(MyType)
var n = pm.Get()

Это объявляет метод Get, связанный с MyType. Получатель назван p в теле функции.

Методы объявлены для определенных типов. Если вы преобразовываете значение в другой тип, новое значение будет иметь методы нового типа, а не методы старого типа.

Вы можете определить методы для встроенного типа, объявив новый определенный тип, производный от него. Новый тип отличается от встроенного.

type MyInt int

func (p MyInt) Get() int {
    return int(p) // Требуется преобразование.
}

func f(i int) {}
var v MyInt

v = v * v // Операторы базового типа все еще применяются.
f(int(v)) // int(v) не имеет объявленных методов.
f(v)      // INVALID

Интерфейсы

Интерфейс Go похож на интерфейс Java, но любой тип, который предоставляет методы, названные в интерфейсе Go, можно рассматривать как реализацию этого интерфейса. Никакого явного объявления не требуется.

Предположим, что этот интерфейс определен:

type MyInterface interface {
    Get() int
    Set(i int)
}

Поскольку MyType уже имеет метод Get, мы можем заставить MyType удовлетворить интерфейс, добавив

func (p *MyType) Set(i int) {
    p.i = i
}

Теперь любая функция, которая принимает MyInterface в качестве параметра, будет принимать переменную типа *MyType.

func GetAndSet(x MyInterface) {}

func f1() {
    var p MyType
    GetAndSet(&p)
}

В терминах Java определение Set и Get для *MyType привело к тому, что *MyType автоматически реализует MyInterface. Тип может удовлетворять нескольким интерфейсам. Это форма неявной типизации (утиная типизация (англ. Duck typing)).

Когда я вижу птицу, которая ходит, как утка, плавает, как утка, и крякает, как утка, я называю эту птицу уткой. - Джеймс Уиткомб Райли

Встраивание (делегирование)

Встраивание типа как анонимного поля может использоваться для реализации формы подтипов.

type MySubType struct {
    MyType
    j int
}

func (p *MySubType) Get() int {
    p.j++
    return p.MyType.Get()
}

Это реализация MySubType как подтипа MyType.

func f2() {
    var p MySubType
    GetAndSet(&p)
}

Метод Set унаследован от MyType, поскольку методы, связанные с анонимным полем, повышаются до методов включающего типа.

В этом случае, поскольку MySubType имеет анонимное поле типа MyType, методы MyType также становятся методами MySubType. Метод Get был переопределен, а метод Set был унаследован.

Это не то же самое, что подкласс в Java, а форма делегирования. Когда вызывается метод анонимного поля, его получателем является поле, а не окружающая структура. Другими словами, методы анонимных полей не отправляются динамически. Если вам нужен эквивалент динамического поиска метода Java, используйте интерфейс.

func f3() {
    var v MyInterface

    v = new(MyType)
    v.Get() //  Вызов метода Get для *MyType.

    v = new(MySubType)
    v.Get() // Вызов метода Get для *MySubType.
}

Утверждения типа

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

type Printer interface {
    Print()
}

func f4(x MyInterface) {
    x.(Printer).Print() // утверждения типа для Printer
}

Преобразование в Printer полностью динамическое. Оно будет работать до тех пор, пока динамический тип x (фактический тип значения, хранящегося в x) определяет метод Print.


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


пятница, 26 июля 2019 г.

ООП в Golang: методы не для классов, но для типов

В Go нет классов, но вы можете определять методы для типов.

Вы можете определить методы для любого типа, объявленного в определении типа.

Метод - это функция с дополнительным аргументом получателя. Получатель находится между ключевым словом func и именем метода. В следующем примере метод HasGarage связан с типом House. Получатель метода назван p.

type House struct {
    garage bool
}

func (p *House) HasGarage() bool { return p.garage }

func main() {
    house := new(House)
    fmt.Println(house.HasGarage()) // Печатает "false" (нулевое значение)
}

Преобразования и методы

Если вы преобразуете значение в другой тип, новое значение будет иметь методы нового типа, но не старого.

type MyInt int

func (m MyInt) Positive() bool { return m > 0 }

func main() {
    var m MyInt = 2
    m = m * m // Операторы базового типа все еще применяются.

    fmt.Println(m.Positive())        // Печатает "true"
    fmt.Println(MyInt(3).Positive()) // Печатает "true"

    var n int
    n = int(m) // Требуется преобразование.
    n = m      // Недопустимо
}

../main.go:14:4: cannot use m (type MyInt) as type int in assignment

В Go идиоматично преобразовывать тип выражения для доступа к определенному методу.

var n int64 = 12345
fmt.Println(n)                // 12345
fmt.Println(time.Duration(n)) // 12.345µs

(Основной тип time.Duration - int64, а тип time.Duration имеет метод String, который возвращает продолжительность, отформатированную как время.)


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


четверг, 6 июня 2019 г.

Спецификация Go: значения метода

Если выражение x имеет статический тип T и M находится в наборе методов типа T, x.M называется значением метода. Значение метода x.M - это значение функции, которое можно вызывать с теми же аргументами, что и вызов метода x.M. Выражение x вычисляется и сохраняется во время оценки значения метода; сохраненная копия затем используется в качестве получателя в любых вызовах, которые могут быть выполнены позже.

Тип T может быть интерфейсным или неинтерфейсным типом.

Рассмотрим тип структуры T с двумя методами: Mv, получатель которого имеет тип T, и Mp, получатель которого имеет тип *T.

type T struct {
  a int
}
func (tv  T) Mv(a int) int         { return 0 }  // получатель значение (value receiver)
func (tp *T) Mp(f float32) float32 { return 1 }  // получатель указатель (pointer receiver)

var t T
var pt *T
func makeT() T

Выражение

t.Mv

дает значение функции типа

func(int) int

Следующие два вызова эквивалентны:

t.Mv(7)
f := t.Mv; f(7)

Аналогично, выражение

pt.Mp

дает значение функции типа

func(float32) float32

Как и в случае селекторов, ссылка на неинтерфейсный метод с получателем значения, использующим указатель, автоматически разыменует этот указатель: pt.Mv эквивалентен (*pt).Mv.

Как и в случае вызовов методов, ссылка на неинтерфейсный метод с получателем указателя, использующим адресуемое значение, автоматически получит адрес этого значения: t.Mp эквивалентно (&t).Mp.

f := t.Mv; f(7)   // то же что и t.Mv(7)
f := pt.Mp; f(7)  // то же что и pt.Mp(7)
f := pt.Mv; f(7)  // то же что и (*pt).Mv(7)
f := t.Mp; f(7)   // то же что и (&t).Mp(7)
f := makeT().Mp   // недопустимо: результат makeT() не адресуем

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

var i interface { M(int) } = myVal
f := i.M; f(7)  // то же что и i.M(7)


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


Спецификация Go: выражения методов (method expressions)

Если M находится в наборе методов типа T, T.M - это функция, которая вызывается как обычная функция с теми же аргументами, что и M, с префиксом дополнительного аргумента, который является получателем метода.

MethodExpr    = ReceiverType "." MethodName .
ReceiverType  = Type .

Рассмотрим тип структуры T с двумя методами: Mv, чей получатель имеет тип T, и Mp, чей получатель имеет тип *T.

type T struct {
  a int
}
func (tv  T) Mv(a int) int         { return 0 }  // получатель значение (value receiver)
func (tp *T) Mp(f float32) float32 { return 1 }  // получатель указатель (pointer receiver)

var t T

Выражение

T.Mv

возвращает функцию, эквивалентную Mv, но с явным получателем в качестве первого аргумента; ее сигнатура

func(tv T, a int) int

Эта функция может вызываться с явным получателем, поэтому следующие пять вызовов эквивалентны:

t.Mv(7)
T.Mv(t, 7)
(T).Mv(t, 7)
f1 := T.Mv; f1(t, 7)
f2 := (T).Mv; f2(t, 7)

Аналогично, выражение

(*T).Mp

возвращает значение функции, представляющее Mp с сигнатурой

func(tp *T, f float32) float32

Для метода с получателем значения можно получить функцию с получателем явного указателя, поэтому

(*T).Mv

возвращает значение функции, представляющее Mv с сигнатурой

func(tv *T, a int) int

Такая функция перенаправляется через получатель для создания значения, которое передается в качестве получателя базовому методу; метод не перезаписывает значение, адрес которого передается в вызове функции.

Последний случай, функция получения значения для метода указатель-получатель, является недопустимым, поскольку методы-указатели-получатели не входят в набор методов типа значения.

Значения функций, полученные из методов, вызываются с помощью синтаксиса вызова функций; получатель предоставляется в качестве первого аргумента вызова. То есть, учитывая, что f := T.Mv, f вызывается как f(t, 7), а не как t.f(7). Чтобы создать функцию, которая связывает получателя, используйте литерал функции или значение метода.

Допустимо получить значение функции из метода типа интерфейса. Результирующая функция принимает явный получатель этого типа интерфейса.


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


среда, 5 июня 2019 г.

Спецификация Go: объявления методов

Метод - это функция с получателем (receiver). Объявление метода привязывает идентификатор, имя метода к методу и связывает метод с базовым типом получателя.

MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .
Receiver   = Parameters .

Получатель указывается в разделе дополнительных параметров, предшествующих имени метода. В этом разделе параметров должен быть объявлен единственный невариантный (non-variadic) параметр - получатель. Его тип должен быть определенного типа T или указателем на определенный тип T. T называется базовым типом получателя. Базовый тип получателя не может быть указателем или типом интерфейса, и он должен быть определен в том же пакете, что и метод. Считается, что метод связан с базовым типом получателя, а имя метода отображается только в селекторах типа T или *T.

Непустой идентификатор получателя должен быть уникальным в сигнатуре метода. Если на значение получателя не ссылаются внутри тела метода, его идентификатор может быть опущен в объявлении. То же самое относится в целом к параметрам функций и методов.

Для базового типа непустые имена связанных с ним методов должны быть уникальными. Если базовый тип является типом структуры (struct type), непустой метод и имена полей должны быть разными.

Дан определенный тип Point, следующие объявления

func (p *Point) Length() float64 {
  return math.Sqrt(p.x * p.x + p.y * p.y)
}

func (p *Point) Scale(factor float64) {
  p.x *= factor
  p.y *= factor
}

привязывают методы Length и Scale с типом получателя *Point к базовому типу Point.

Тип метода - это тип функции с получателем в качестве первого аргумента. Например, метод Scale имеет тип

func(p *Point, factor float64)

Однако функция, объявленная таким образом, не является методом.


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


пятница, 15 марта 2019 г.

Go Code Review Comments: Тип получателя (Receiver Type)

Выбор того, использовать ли получатель значения (value receiver) или получатель указателя (pointer receiver) в методах, может быть трудным, особенно для новых программистов на Go. В случае сомнений используйте указатель, но бывают случаи, когда получатель значения имеет смысл, обычно по соображениям эффективности, например, для небольших неизменяющихся структур или значений базового типа. Вот некоторые полезные рекомендации:

  • Если получателем является карта, func или chan, не используйте указатель на них. Если получатель является срезом (slice), а метод не срезает и не перераспределяет срез, не используйте указатель на него.
  • Если метод должен изменять получатель, получатель должен быть указателем.
  • Если получатель является структурой, которая содержит sync.Mutex или подобное поле синхронизации, получатель должен быть указателем, чтобы избежать копирования.
  • Если получатель является большой структурой или массивом, указатель получателя является более эффективным. Насколько большой структурой или массивом? Предположим, что это эквивалентно передаче всех его элементов в качестве аргументов методу. Если он кажется слишком большим, он также слишком велик для получателя.
  • Могут ли функции или методы, одновременно или при вызове из этого метода, изменять получателя? Тип значения создает копию получателя при вызове метода, поэтому внешние обновления не будут применяться к этому получателю. Если изменения должны быть видны в исходном получателе, получатель должен быть указателем.
  • Если получатель является структурой, массивом или срезом (slice) и любой из его элементов является указателем на что-то, что может изменяться, предпочтите получатель указателя, поскольку это сделает намерение более понятным для читателя.
  • Если получатель представляет собой небольшой массив или структуру, которая, естественно, является типом значения (получатель значения) (например, что-то типа time.Time), без изменяемых полей и указателей, или является простым базовым типом, таким как int или string, получатель значения имеет смысл. Получатель значения может уменьшить количество мусора, который может быть сгенерирован; если значение передается в метод значения, вместо размещения в куче (heap) может использоваться копия в стеке (stack). (Компилятор старается избегать этого выделения, но не всегда может быть успешным.) Не выбирайте тип получателя значения по этой причине без предварительного профилирования.
  • Наконец, если есть сомнения, используйте получатель указателя.

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


четверг, 14 марта 2019 г.

Go Code Review Comments: Имена получателей

Имя получателя метода должно отражать его идентичность; часто достаточно одной или двух буквенных аббревиатур этого типа (например, «c» или «cl» для «Client»). Не используйте общие имена, такие как «me», «this» или «self», идентификаторы, типичные для объектно-ориентированных языков, которые придают методу особое значение. В Go получатель метода - это просто еще один параметр, и поэтому он должен иметь соответствующее имя. Имя не должно быть таким же описательным, как у аргумента метода, так как его роль очевидна и не имеет никакой документальной цели. Оно может быть очень коротким, так как будет отображаться почти в каждой строке каждого метода типа; знакомство допускает краткость. Будьте последовательны: если вы вызываете получатель «c» в одном методе, не называйте его «cl» в другом.


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


суббота, 16 февраля 2019 г.

Go FAQ: Почему T и *T имеют разные наборы методов?

Как сказано в спецификации Go, набор методов типа T состоит из всех методов с типом приемника T, в то время как соответствующий указатель типа *T состоит из всех методов с получателем *T или T. Это означает, что набор методов *T включает в себя T, но не наоборот.

Это различие возникает потому, что если значение интерфейса содержит указатель *T, вызов метода может получить значение путем разыменования указателя, но если значение интерфейса содержит значение T, для вызова метода нет безопасного способа получить указатель. (Это позволило бы методу изменить содержимое значения внутри интерфейса, что не разрешено спецификацией языка.)

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

var buf bytes.Buffer
io.Copy(buf, os.Stdin)

будет копировать стандартный ввод в копию buf, а не в сам buf. Это почти никогда не является ожидаемым поведением.


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


пятница, 15 февраля 2019 г.

Go FAQ: Следует ли определять методы для значений или указателей?

// метод для указателя
func (s *MyStruct) pointerMethod() { } 

// метод для значения
func (s MyStruct)  valueMethod()   { } 

Для программистов, не привыкших к указателям, различие между этими двумя примерами может сбивать с толку, но на самом деле ситуация очень проста. При определении метода для типа получатель (s в приведенных выше примерах) ведет себя точно так же, как если бы это был аргумент метода. Определять ли получатель как значение или как указатель - это тот же вопрос что и, должен ли аргумент функции быть значением или указателем? Есть несколько соображений по этому поводу.

Во-первых, и это наиболее важно, метод должен изменить получатель? Если это так, получатель должен быть указателем. (Срезы и карты выступают в качестве ссылок, поэтому их история немного более тонкая, но, например, для изменения длины среза в методе получатель должен быть указателем.) В приведенных выше примерах, если pointerMethod изменяет поля s, вызывающая сторона увидит эти изменения, но valueMethod вызывается с копией аргумента вызывающей стороны (это определение передачи значения), поэтому изменения, которые он вносит, будут невидимы для вызывающей стороны.

Кстати, в Java приемники методов всегда указатели, хотя их указательный характер несколько замаскирован (существует предложение добавить приемники значения к языку).

Во-вторых, учет эффективности. Если приемник большой, например, большая struct, это будет намного дешевле использовать указатель приемника.

Следующее - последовательность. Если некоторые из методов типа должны иметь приемник указателя, остальные тоже должны, поэтому набор методов соответствует независимо от того, как используется тип.

Для таких типов, как базовые типы, срезы и небольшие struct, получатель значения очень дешев, поэтому, если семантика метода требует указателя, получатель значения эффективен и понятен.


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