Классы придуманы программистами в первую очередь для структурирования данных. Мы с вами уже видели, что в более-менее сложных задачах часто бывает необходимо использование составных типов — таких, как списки или строки; кстати говоря, и список и строка тоже являются классами, но только классами, определёнными в библиотеке языка.

Списки, однако, ограничены использованием элементов одного типа. Кроме этого, доступ к элементам в них происходит по номеру, что не всегда удобно. Рассмотрим, например, простую задачу вычисления расстояния между точками p1 и p2:

Сразу же бросается в глаза, что в условии задачи мы говорили о двух точках, а в заголовке функции имеем вместо этого четыре параметра, по два для каждой из точек. Гораздо понятнее было бы иметь функцию в таком виде:

Можно ли так написать заголовок функции? Да, конечно! Но для этого придётся определить в программе тип Point. В Котлине, самый простой способ это сделать выглядит так:

Строчка class Point(…​) определяет новый класс. Такое определение всегда начинается с ключевого слова class, за которым следует имя класса. Имена классов формируются по обычным правилам; не забывайте, что их рекомендуется начинать с прописной буквы — в отличие от имён переменных и функций.

Конструкторы и свойства класса

В круглых скобках перечисляется, какие данные необходимы для создания новой точки. Создание новой точки — это функция, но функция специальная — конструктор. Поэтому и параметры этой функции определяются почти (но не совсем) так же, как и для обычной функции. В данном случае мы видим, что создания точки необходимо задать её вещественные координаты x и y.

Внимательный читатель заметит отличие в определении x и y от обычных параметров функции — оно заключается в добавлении ключевого слова val перед именем каждого параметра. Ключевое слово val перед параметром конструктора превращает его из простого параметра в свойство класса. Свойства в данном случае задают внутреннюю структуру каждой точки. Свойство, заданное через val, можно читать, используя имя переменной (или параметра) типа Point и символ точки: p1.x. Вместо val можно использовать var (мутирующее свойство), тогда свойство можно будет также изменять: p1.x = …​. Обращение к свойству напоминает вызов функции, имеющей получателя, но в нём отсутствуют круглые скобки и набор аргументов. В этом месте можно вспомнить и понять, что list.size является свойством списка (размер), а str.length — свойством строки (длина).

Итак, каждая наша точка имеет два свойства — x и y. Обратите внимание, что, определив класс, мы определили и тип, но не определили ни одной переменной данного типа. Создаются новые точки следующим простым образом:

Каждый вызов конструктора Point создаёт новую точку, или объект (синоним — экземпляр) класса Point. Вызов distance(a, b) рассчитывает расстояние между двумя созданными точками.

Функции класса

Определение класса Point, приведённое в начале этого раздела, было пустым. Это означает, что класс содержит только два свойства x и y. Немного изменим определение класса, перенеся функцию distance внутрь него. Программисты называют такие функции членами класса.

Такая функция вместо двух параметров имеет лишь один — точку other (другая). Однако определённая внутри класса функция имеет также получателя (мы уже сталкивались с такими функциями ранее, но ни разу не определяли их сами). Получатель функции — объект того класса, членом которого является данная функция. Например:

Здесь точка a стоит перед именем функции и используется функцией как получатель. Аргумент b используется как параметр other. Как результат, функция distance всё равно имеет доступ к двум точкам. Она работает с получателем, непосредственно используя свойства своего класса-владельца: x и y. Также она работает с параметром, используя его свойства как other.x и other.y.

Сравнение объектов класса на равенство

Как нам уже известно, на равенство == можно сравнивать не только числа, но и переменные более сложных типов, например, строки или списки. Часто необходимо уметь сравнить на равенство переменные с типом, определённым пользователем в виде класса. Например, в группе задач lesson8.task2 про шахматную доску и фигуры имеется класс Square, описывающий одну клетку шахматной доски. В наиболее простой форме он выглядел бы так:

Проверим, что будет, если сравнить две одинаковых клетки first и second на равенство:

Если запустить эту главную функцию, мы увидим на консоли результат false. Почему?

Всё дело в способе работы, принятом в JVM для любых объектов. Каждый раз, когда мы вызываем конструктор какого-либо класса, в динамической памяти JVM создаётся объект этого класса. Ссылка на него запоминается в стеке JVM (подробности будут в разделе 4.5). По умолчанию, при сравнении объектов на равенство сравниваются друг с другом ССЫЛКИ, а не содержимое объектов.

Немного изменим определение класса Square, добавив впереди модификатор data. Такое определение обычно читается как «класс с данными».

Запустив главную функцию ещё раз, мы увидим результат true. При наличии модификатора data, для объектов класса работает другой способ сравнения на равенство: все свойства первого объекта сравниваются с соответствующими свойствами второго. Поскольку для обоих объектов column = 3 и row = 6, данные объекты равны.

Помимо этой возможности, классы с данными позволяют представить объект в виде строки, например:

Эта функция выведет на консоль Square(x=3, y=6). Попробуйте теперь убрать модификатор data в определении класса и посмотрите, как изменится вывод. Заметим, что строковое представление используется не только при выводе на консоль, но и в отладчике.

Каким же образом осуществляется переопределение способа сравнения объектов и способа их представления в виде строки? Для этой цели в Java придуманы две специальные функции. Первая из них называется equals, она имеет объект-получатель, принимает ещё один объект как параметр и выдаёт результат true, если эти два объекта равны. Чуть ниже приведён пример переопределения equals для класса Segment.

Вторая функция называется toString. Она также имеет объект-получатель, но не имеет параметров. Её результат — это строковое представление объекта. Например:

Запустив главную функцию выше, мы увидим на консоли строку 6 - 3. Обратите внимание на модификатор overrideперед определением toString(). Он указывает на тот факт, что данная функция переопределяет строковое представление по умолчанию. Подробнее об этом опять-таки в разделе 9.

О других возможностях классов с данными можно прочитать здесь: https://kotlinlang.org/docs/reference/data-classes.html.

Включение классов

Система классов была бы очень неполноценной, если бы нам приходилось использовать классы сами по себе, в отрыве друг от друга. Поэтому у классов есть множество способов взаимодействовать друг с другом. Самый простой из них — включение объекта одного класса внутрь другого класса. Например:

Здесь треугольник (Triangle) имеет три свойства ab и c, каждое из которых, в свою очередь, имеет тип Point — точка. В таких случаях говорят, что треугольник включает три точки, состоит из трёх точек или описывается тремя точками. Отрезок (Segment) имеет два таких же свойства begin и end — то есть описывается своим началом и концом.

Точки, в свою очередь, описываются двумя вещественными координатами. Например:

При вызове println мы прочитали свойство x СВОЙСТВА b треугольника t. Для этого мы дважды использовали точку для обращения к свойству объекта.

Переопределение equals для класса

Рассмотрим пример переопределения equals для класса Segment. Дело в том, что для отрезка, вообще говоря, всё равно, в каком порядке в нём идут начало и конец, то есть отрезок AB равен отрезку BA. Применение способа сравнения на равенство, действующего для классов с данными по умолчанию, даст нам другой результат: AB не равно BA.

Модификатор override перед определением equals говорит о том, что мы хотим изменить уже имеющийся метод сравнения на равенство. Единственный параметр other данного метода обязан иметь тип Any?, то есть «любой, в том числе null». В Котлине действует правило: абсолютно любой тип является разновидностью Any?, то есть значение любой переменной или константы можно использовать как значение типа Any?. Это обеспечивает возможность сравнения на равенство чего угодно с чем угодно.

Результат equals имеет тип Boolean. В первую очередь, мы должны проверить, что переданный нам аргумент — тоже отрезок: other is Segment. Ключевое слово is в Котлине служит для определения принадлежности значения к заданному типу. Аналогично !is делает проверку на не принадлежность.

Если аргумент — отрезок, мы сравниваем точки двух имеющихся отрезков на равенство, с точностью до их перестановки. Если же аргумент — не отрезок, то логическое И в любом случае даст результат false. Обратите внимание, что справа от && мы вправе использовать other как отрезок (например, используя его begin и end), поскольку проверка этого факта была уже сделана.

Пример задачи на простые классы

Решим теперь с помощью классов Point и Triangle следующую задачу. Пусть имеется треугольник ABC, заданный координатами вершин, и точка P. Необходимо определить, лежит ли точка внутри треугольника.

Для решения задачи нам потребовалось определить три новых функции в классе Triangle.

Пойдём от простого к сложному. Функция halfPerimeter() считает полупериметр треугольника, то есть половину его периметра. Для этого мы считаем длину отрезков AB, BC и CA, суммируем эти длины и делим результат пополам. Длина отрезка AB (например) считается как a.distance(b) — мы используем ранее определённую функцию точки distance.

Функция area() считает площадь треугольника, используя для этой цели формулу Герона: S2 = p(p - AB)(p - BC)(p - CA). Здесь S — площадь, p — полупериметр, ABBC и CA — длины сторон. Для расчёта полупериметра мы используем уже готовую функцию halfPerimeter().

Наконец, функция contains() решает исходную задачу, то есть определяет, находится ли точка, заданная параметром p, внутри треугольника-получателя. Для этой цели, кроме уже существующего треугольника-получателя ABC, мы создаём три других: ABPBCPCAP и считаем площади всех четырёх треугольников. Проверьте, что в случае присутствия точки P внутри треугольника должно выполняться равенство: S(ABC) = S(ABP) + S(BCP) + S(CAP). Это становится очевидно, если нарисовать все эти треугольники.

Готовые классы с данными, деструктурирование

В Котлине имеются два готовых класса с данными, которые могут применяться в программе, если потребовалось объединить в один тип два или три связанных значения других типов. Это класс Pair<A, B> (пара) со свойствами first и second типов A и B и класс Triple<A, B, C> (тройка) со свойствами firstsecond и third типов AB и C. Например:

Такая функция комбинирует две пары в тройку, складывая второй элемент первой пары с первым элементом второй.

Пара и тройка полезны также в тех случаях, когда хочется получить от функции несколько результатов. Например:

Данная функция преобразует строку вида «11:34:45» в тройку (часы, минуты, секунды). Она может быть использована так:

Деструктурирование в последней строчке функции позволяет выполнить разбиение тройки на отдельные компоненты, создавая три переменных hoursminutesseconds. Та же операция доступна для любого другого класса с данными (data class). Другой пример его использования:

Функция list.withIndex() возвращает список объектов типа IndexedValue, содержащих индекс элемента списка и его значение. Класс IndexedValue определён следующим образом:

Такая функция test выведет на консоль строчки #0: abc и #1: def.

Упражнения

Откройте файл srс/lesson8/task1/Geometry.kt в проекте KotlinAsFirst.

Как скачать и подключить проект KotlinAsFirst смотрите во Введении.

Посмотрите на задачи в нём. Кроме уже рассмотренного класса Point, в данном уроке используются классы Circle (окружность), Segment (отрезок), Line(прямая). Попробуйте порешать задачи данного урока; рекомендуется делать это последовательно, от простого к сложному, с проверкой правильности решения каждой из задач с помощью тестов. Тесты, как и всегда, находятся в test/lesson8/task1/Tests.kt

Пройдите в этой группе задач так далеко, как сможете. Рекомендуется попробовать решить одну из двух очень сложных задач в конце файла. Если у вас возникают сложности с придумыванием алгоритма решения задачи, обсудите алгоритм с преподавателем.

Откройте теперь файл srс/lesson8/task1/Chess.kt. Файл содержит задачи на поиск траектории движения различных шахматных фигур из клетки в клетку доски — короля, ладьи, слона, коня. Правила передвижения фигур описаны в комментариях к функциям.

В этом файле рекомендуется решить, по крайней мере, две задачи про одну из фигур (на определение длины траектории и самой траектории). Имейте в виду, что поиск траектории для коня достаточно сложен; прежде, чем приступать к этой задаче, рекомендуется ознакомиться с содержимым раздела 8.5 про поиск пути на графах и примерами в src/lesson8/task3/Graph.kt (этот файл не содержит нерешённых задач).

Переходите к следующему разделу.

Добавить комментарий