From 7014f7b2a41e1dea172910dd7c6bdbf791acf4bc Mon Sep 17 00:00:00 2001 From: Slava Zgordan Date: Mon, 31 Aug 2015 06:17:50 +0200 Subject: [PATCH] 02.5 --- ru/02.5.md | 307 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 ru/02.5.md diff --git a/ru/02.5.md b/ru/02.5.md new file mode 100644 index 00000000..2471fd53 --- /dev/null +++ b/ru/02.5.md @@ -0,0 +1,307 @@ +# Объектно-ориентированное программирование + +В предыдущих двух разделах мы говорили о функциях и структурах, но рассматривали ли Вы когда-нибудь функции как поля структуры? В этом разделе я познакомлю Вас с еще одним видом функций, который называется "метод". + +## Метод + +Предположим, Вы определили структуру "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" (как методы классов в классических объектно-ориентированных языках). Как Вы можете заметить, функция и структура здесь - две независимые друг от друга сущности. + +Пока что это не является проблемой. Однако, если Вам нужно будет посчитать также площади круга, квадрата, пятиугольника или другой геометрической фигуры, Вам придется добавлять новые фукнции с похожими именами. + +![](images/2.5.rect_func_without_receiver.png?raw=true) + +Рисунок 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()) + } + +Примечания относительно использования методов: + +- Если у методов одинаковые имена, но они относятся к разным ресиверам - это разные методы. +- Методы имеют доступ к полям внутри ресивера. +- Для того, чтобы вызвать метод структуры, используйте `.`, аналогично тому, как Вы работаете с полями. + +![](images/2.5.shapes_func_with_receiver_cp.png?raw=true) + +Рисунок 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)