В предыдущих двух разделах мы говорили о функциях и структурах, но рассматривали ли Вы когда-нибудь функции как поля структуры? В этом разделе я познакомлю Вас с еще одним видом функций, который называется "метод".
Предположим, Вы определили структуру "rectangle"(прямоугольник), и Вам нужно вычислить его площадь. Обычно для этого мы используем следующий код:
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func area(r Rectangle) float64 {
return r.width*r.height
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Площадь r1: ", area(r1))
fmt.Println("Площадь r2: ", area(r2))
}
Этот код вычисляет площадь прямоугольника. Мы используем для этого функцию area
, но это не метод структуры "rectangle" (как методы классов в классических объектно-ориентированных языках). Как Вы можете заметить, функция и структура здесь - две независимые друг от друга сущности.
Пока что это не является проблемой. Однако, если Вам нужно будет посчитать также площади круга, квадрата, пятиугольника или другой геометрической фигуры, Вам придется добавлять новые функции с похожими именами.
Рисунок 2.8 Связь между функцией и структурой
Очевидно, что это не очень хорошо. Площадь должна быть свойством круга или прямоугольника.
По этой причине в Go есть концепция метода
. Метод
привязывается к типу данных. У него такой же синтаксис, как и у функции, за исключением дополнительного параметра, идущего после ключевого слова func
и называемого ресивер
, который является основным телом метода.
В этом же примере Rectangle.area()
мог бы принадлежать непосредственно rectangle
, а не являться внешней функцией. Еще точнее, length
, width
и area()
все принадлежат rectangle
.
Как сказал Rob Pike:
"Метод - это функция, где первым указанным является аргумент, называемый ресивером."
Синтаксис метода:
func (r ТипРесивера) funcName(параметры) (результаты)
Давайте изменим наш пример, используя методы
:
package main
import (
"fmt"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width*r.height
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Площадь r1: ", r1.area())
fmt.Println("Площадь r2: ", r2.area())
fmt.Println("Площадь c1: ", c1.area())
fmt.Println("Площадь c2: ", c2.area())
}
Примечания относительно использования методов:
- Если у методов одинаковые имена, но они относятся к разным ресиверам - это разные методы.
- Методы имеют доступ к полям внутри ресивера.
- Для того, чтобы вызвать метод структуры, используйте
.
, аналогично тому, как Вы работаете с полями.
Рисунок 2.9 Методы отличаются друг от друга, если принадлежат разным структурам
В указанном выше примере методы area() есть у структуры Rectangle и у Circle соответственно, поэтому ресиверами этих методов являются Rectangle и Circle.
Стоит отметить, что метод с многоточием означает, что ресивер передается по значению, а не по ссылке. Различие в том, что когда ресивер передается по ссылке, метод может менять его значение, а когда ресивер передается по значению, метод работает с его копией.
Можем ли ресивер быть только лишь структурой? Конечно, нет. Ресивером может быть любой тип данных. Если у Вас возникла неясность в связи с типами, создаваемыми пользователями - структура является одним из них, но их может быть и больше.
Чтобы создать свой тип, используйте следующий формат:
type typeName typeLiteral
Примеры типов, созданных пользователем:
type ages int
type money float32
type months map[string]int
m := months {
"Январь":31,
"Февраль":28,
...
"Декабрь":31,
}
Я надеюсь, теперь Вы поняли, как использовать такие типы. Так же, как typedef
используется в C, в вышеприведенном примере можно использовать ages
для замены int
.
Но давайте вернемся к методам
.
В созданных пользователем типах можно использовать столько методов, сколько захотите.
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //срез, состоящий из элементов типа Box
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (bl BoxList) BiggestsColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if b.Volume() > v {
v = b.Volume()
k = b.color
}
}
return k
}
func (bl BoxList) PaintItBlack() {
for i, _ := range bl {
bl[i].SetColor(BLACK)
}
}
func (c Color) String() string {
strings := []string {"белый", "черный", "синий", "красный", "желтый"}
return strings[c]
}
func main() {
boxes := BoxList {
Box{4, 4, 4, RED},
Box{10, 10, 1, YELLOW},
Box{1, 1, 20, BLACK},
Box{10, 10, 1, BLUE},
Box{10, 30, 1, WHITE},
Box{20, 20, 20, YELLOW},
}
fmt.Printf("В наборе имеются %d коробок\n", len(boxes))
fmt.Println("Объем первой из них равен ", boxes[0].Volume(), "cm³")
fmt.Println("Цвет последней - ",boxes[len(boxes)-1].color.String())
fmt.Println("Самая большая из них имеет цвет: ", boxes.BiggestsColor().String())
fmt.Println("Давайте покрасим их все в черный цвет")
boxes.PaintItBlack()
fmt.Println("Цвет второй коробки - ", boxes[1].color.String())
fmt.Println("Очевидно, что цвет самой большой коробки теперь ", boxes.BiggestsColor().String())
}
Мы определили несколько констант и своих типов:
- Мы использовали
Color
как синонимbyte
. - Определили структуру
Box
, у которой есть поля height, width, length и color (высота, ширина, длина и цвет соответственно - прим.пер.). - Определили тип
BoxList
, содержащий элементы типаBox
.
Затем мы определили методы для наших созданных типов:
- Volume() использует Box как ресивер и возвращает объем Box.
- SetColor(c Color) изменяет цвет Box.
- BiggestsColor() возвращает цвет самой большой коробки.
- PaintItBlack() устанавливает цвет всех коробок (Box) в BoxList как черный.
- String() использует Color как ресивер и возвращает название цвета как строку.
Когда мы используем слова для того, чтобы описать свои потребности, все становится яснее. Мы часто записываем свои потребности перед тем, как начать писать код.
Давайте посмотрим на метод SetColor
. Его ресивером является указатель на Box. Да, можно использовать *Box
в качестве ресивера. Почему мы использовали здесь указатель? Потому что в этом методе мы хотим изменить цвет коробки (Box). Если бы мы не использовали указатель, метод бы изменил цвет лишь у копии Box.
Если мы видим, что ресивер - первый аргумент метода, несложно понять, как это работает.
Вы можете спросить, почему мы не написали (*b).Color=c
вместо b.Color=c
в методе SetColor(). Но все в порядке, поскольку Go знает, как интерпретировать это выражение. Не правда ли, Go восхитителен?
Вы также можете спросить, не должны ли мы использовать (&bl[i]).SetColor(BLACK)
в PaintItBlack
, ведь мы передаем в SetColor
указатель. Опять же, и здесь все в порядке, поскольку Go знает, как интерпретировать и это!
В предыдущем разделе мы изучили наследование полей. Аналогично этому в Go мы можем наследовать методы. Если анонимное поле содержит методы, то структура, которая содержит это поле, также располагает всеми методами этого поля:
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human // анонимное поле
school string
}
type Employee struct {
Human
company string
}
// определяем метод в Human
func (h *Human) SayHi() {
fmt.Printf("Привет, меня зовут %s, можете позвонить мне по телефону %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Марк", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Сэм", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
Если мы хотим, чтобы у Employee был свой метод SayHi
, мы можем определить метод с таким именем в Employee, и когда мы будем его вызывать, он скроет метод с тем же именем в Human.
package main
import "fmt"
type Human struct {
name string
age int
phone string
}
type Student struct {
Human
school string
}
type Employee struct {
Human
company string
}
func (h *Human) SayHi() {
fmt.Printf("Привет, меня зовут %s, можете позвонить мне по телефону %s\n", h.name, h.phone)
}
func (e *Employee) SayHi() {
fmt.Printf("Привет, меня зовут %s, я работаю в %s. Звоните мне по телефону %s\n", e.name,
e.company, e.phone) //Да, здесь можно разбить строку на две.
}
func main() {
mark := Student{Human{"Марк", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Сэм", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}
Сейчас Вы уже можете написать объектно-ориентированную программу. Методы, начинающиеся с заглавной буквы, являются публичными, со строчной - приватными.
- Содержание
- Предыдущий раздел: Структуры
- Следующий раздел: Интерфейсы