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

Основы Go: методы, получатели и указатели

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

Это означает, что получатель объявлен как *T для некоторого типа T. (И еще, T не может быть указателем сам по себе, как, например, *int.)

В следующем примере метод Scale объявлен для *Vertex.

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

При использовании получателя-значения, метод Scale работает с копией оригинального значения Vertex. (Это такое же поведение, как и для любого другого аргумента функции.) Метод Scale должен иметь указатель на получателя для того, чтобы изменить значение Vertex, объявленное в функции main.

package main

import (
  "fmt"
  "math"
)

type Vertex struct {
  X, Y float64
}

func (v Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
  v.X = v.X * f
  v.Y = v.Y * f
}

func main() {
  v := Vertex{3, 4}
  v.Scale(10)
  fmt.Println(v.Abs())
}

Вывод:

50

Указатели и функции

Здесь мы видим методы Abs и Scale переписанные в качестве функций.

package main

import (
  "fmt"
  "math"
)

type Vertex struct {
  X, Y float64
}

func Abs(v Vertex) float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v *Vertex, f float64) {
  v.X = v.X * f
  v.Y = v.Y * f
}

func main() {
  v := Vertex{3, 4}
  Scale(&v, 10)
  fmt.Println(Abs(v))
}

Вывод:

50

Методы и косвенная адресация указателей

Сравнивая две предыдущие программы, вы могли обратить внимание, что функции с аргументом указателем должны принимать указатель:

var v Vertex
ScaleFunc(v)  // Ошибка компиляции!
ScaleFunc(&v) // OK

тогда как методы объявленные с получателем-указателем могут принимать как значение, так и указатель при вызове:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

В выражении v.Scale(5), хотя v является значением, а не указателем, метод с получателем-указателем вызывается автоматически. Это потому, что Go в качестве удобства интерпретирует v.Scale(5) как (&v).Scale(5), т.к. метод Scale объявлен для получателя-указателя.

package main

import "fmt"

type Vertex struct {
  X, Y float64
}

func (v *Vertex) Scale(f float64) {
  v.X = v.X * f
  v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
  v.X = v.X * f
  v.Y = v.Y * f
}

func main() {
  v := Vertex{3, 4}
  v.Scale(2)
  ScaleFunc(&v, 10)

  p := &Vertex{4, 3}
  p.Scale(3)
  ScaleFunc(p, 8)

  fmt.Println(v, p)
}

Вывод:

{60 80} &{96 72}

То же самое происходит и в обратном направлении.

Функции с аргументом значением должны принимать значение конкретного типа:

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // Ошибка компиляции!

тогда как методы с получателем-значением могут принимать как значение, так и указатель при вызове:

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK

В этом случае вызов метода p.Abs() интерпретируется как (*p).Abs().

package main

import (
  "fmt"
  "math"
)

type Vertex struct {
  X, Y float64
}

func (v Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
  v := Vertex{3, 4}
  fmt.Println(v.Abs())
  fmt.Println(AbsFunc(v))

  p := &Vertex{4, 3}
  fmt.Println(p.Abs())
  fmt.Println(AbsFunc(*p))
}

Вывод:

5
5
5
5

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

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

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

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

В следующем примере у Scale и Abs в качестве получателя указан тип *Vertex, хотя метод Abs не имеет необходимости изменять получателя.

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

package main

import (
  "fmt"
  "math"
)

type Vertex struct {
  X, Y float64
}

func (v *Vertex) Scale(f float64) {
  v.X = v.X * f
  v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
  return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
  v := &Vertex{3, 4}
  fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
  v.Scale(5)
  fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

Вывод:

Before scaling: &{X:3 Y:4}, Abs: 5
After scaling: &{X:15 Y:20}, Abs: 25


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


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

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