Skip to content

Latest commit

 

History

History
307 lines (218 loc) · 14 KB

File metadata and controls

307 lines (218 loc) · 14 KB

Объектно-ориентированное программирование

В предыдущих двух разделах мы говорили о функциях и структурах, но рассматривали ли Вы когда-нибудь функции как поля структуры? В этом разделе я познакомлю Вас с еще одним видом функций, который называется "метод".

Метод

Предположим, Вы определили структуру "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()
}

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

Ссылки