308 lines
14 KiB
Markdown
308 lines
14 KiB
Markdown
# Объектно-ориентированное программирование
|
||
|
||
В предыдущих двух разделах мы говорили о функциях и структурах, но рассматривали ли Вы когда-нибудь функции как поля структуры? В этом разделе я познакомлю Вас с еще одним видом функций, который называется "метод".
|
||
|
||
## Метод
|
||
|
||
Предположим, Вы определили структуру "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()
|
||
}
|
||
|
||
Сейчас Вы уже можете написать объектно-ориентированную программу. Методы, начинающиеся с заглавной буквы, являются публичными, со строчной - приватными.
|
||
|
||
## Ссылки
|
||
|
||
- [Содержание](preface.md)
|
||
- Предыдущий раздел: [Структуры](02.4.md)
|
||
- Следующий раздел: [Интерфейсы](02.6.md)
|