Классы придуманы программистами в первую очередь для структурирования данных. Мы с вами уже видели, что в более-менее сложных задачах часто бывает необходимо использование составных типов — таких, как списки или строки; кстати говоря, и список и строка тоже являются классами, но только классами, определёнными в библиотеке языка.
Списки, однако, ограничены использованием элементов одного типа. Кроме этого, доступ к элементам в них происходит по номеру, что не всегда удобно. Рассмотрим, например, простую задачу вычисления расстояния между точками p1
и p2
:
fun distance(x1: Double, y1: Double, x2: Double, y2: Double) = sqrt(sqr(x2 - x1) + sqr(y2 - y1))
Сразу же бросается в глаза, что в условии задачи мы говорили о двух точках, а в заголовке функции имеем вместо этого четыре параметра, по два для каждой из точек. Гораздо понятнее было бы иметь функцию в таком виде:
fun distance(p1: Point, p2: Point) = ...
Можно ли так написать заголовок функции? Да, конечно! Но для этого придётся определить в программе тип Point
. В Котлине, самый простой способ это сделать выглядит так:
class Point(val x: Double, val y: Double) fun distance(p1: Point, p2: Point) = sqrt(sqr(p2.x - p1.x) + sqr(p2.y - p1.y))
Строчка class Point(…)
определяет новый класс. Такое определение всегда начинается с ключевого слова class, за которым следует имя класса. Имена классов формируются по обычным правилам; не забывайте, что их рекомендуется начинать с прописной буквы — в отличие от имён переменных и функций.
Конструкторы и свойства класса
В круглых скобках перечисляется, какие данные необходимы для создания новой точки. Создание новой точки — это функция, но функция специальная — конструктор. Поэтому и параметры этой функции определяются почти (но не совсем) так же, как и для обычной функции. В данном случае мы видим, что создания точки необходимо задать её вещественные координаты x
и y
.
Внимательный читатель заметит отличие в определении x
и y
от обычных параметров функции — оно заключается в добавлении ключевого слова val перед именем каждого параметра. Ключевое слово val перед параметром конструктора превращает его из простого параметра в свойство класса. Свойства в данном случае задают внутреннюю структуру каждой точки. Свойство, заданное через val, можно читать, используя имя переменной (или параметра) типа Point
и символ точки: p1.x
. Вместо val можно использовать var (мутирующее свойство), тогда свойство можно будет также изменять: p1.x = …
. Обращение к свойству напоминает вызов функции, имеющей получателя, но в нём отсутствуют круглые скобки и набор аргументов. В этом месте можно вспомнить и понять, что list.size
является свойством списка (размер), а str.length
— свойством строки (длина).
Итак, каждая наша точка имеет два свойства — x
и y
. Обратите внимание, что, определив класс, мы определили и тип, но не определили ни одной переменной данного типа. Создаются новые точки следующим простым образом:
fun usePoints() { // Здесь мы просто перечисляем аргументы val a = Point(0.0, 3.0) // А здесь перед каждым аргументом мы написали имя параметра, которому он соответствует val b = Point(x = 4.0, y = 0.0) println(distance(a, b)) // 5.0 }
Каждый вызов конструктора Point
создаёт новую точку, или объект (синоним — экземпляр) класса Point
. Вызов distance(a, b)
рассчитывает расстояние между двумя созданными точками.
Функции класса
Определение класса Point
, приведённое в начале этого раздела, было пустым. Это означает, что класс содержит только два свойства x
и y
. Немного изменим определение класса, перенеся функцию distance
внутрь него. Программисты называют такие функции членами класса.
class Point(val x: Double, val y: Double) { fun distance(other: Point): Double = sqrt(sqr(x - other.x) + sqr(y - other.y)) }
Такая функция вместо двух параметров имеет лишь один — точку other
(другая). Однако определённая внутри класса функция имеет также получателя (мы уже сталкивались с такими функциями ранее, но ни разу не определяли их сами). Получатель функции — объект того класса, членом которого является данная функция. Например:
fun usePoints() { val a = Point(0.0, 3.0) val b = Point(4.0, 0.0) println(a.distance(b)) // 5.0 }
Здесь точка a
стоит перед именем функции и используется функцией как получатель. Аргумент b
используется как параметр other
. Как результат, функция distance
всё равно имеет доступ к двум точкам. Она работает с получателем, непосредственно используя свойства своего класса-владельца: x
и y
. Также она работает с параметром, используя его свойства как other.x
и other.y
.