Функции-создатели
Функции-создатели в некотором смысле являются заменой конструкторам. Они предназначены для создания объекта, реализующего тот или иной интерфейс; при этом, естественно, для их написания должна существовать реализация данного интерфейса и функция-создатель обычно вызывает внутри себя тот или иной конструктор, иногда делая выбор из нескольких вариантов. Известными примерами функций-создателей являются listOf(…)
, mutableListOf(…)
, setOf(…)
, mutableSetOf(…)
. Для матрицы, подобная функция может быть определена так:
fun <E> createMatrix(height: Int, width: Int, e: E): Matrix<E> = TODO()
Здесь fun <E>
говорит о том, что функция использует настраиваемый тип E
— тип элементов матрицы. Первый и второй параметр задают высоту и ширину матрицы, а третий — значение элемента, который при создании матрицы будет записан во все ячейки. Результатом функции должна стать вновь созданная матрица, но, поскольку реализации интерфейса Matrix<E>
ещё нет, вместо вызова конструктора в теле фигурирует TODO()
— специальная функция, бросающая исключение UnsupportedOperationException
.
Имея функцию-создатель, мы можем описывать более сложные операции с матрицами, которые не только изменяют существующие матрицы, но и создают новые — по-прежнему не зная ничего о реализации. Например, транспонирование меняет местами ряды и колонки в матрице:
fun <E> transpose(matrix: Matrix<E>): Matrix<E> { if (matrix.width < 1 || matrix.height < 1) return matrix val result = createMatrix(height = matrix.width, width = matrix.height, e = matrix[0, 0]) for (i in 0 until matrix.width) { for (j in 0 until matrix.height) { result[i, j] = matrix[j, i] } } return result }
Так мы создаём новую матрицу, меняя местами ширину и высоту старой, а затем в цикле переписываем элементы из старой матрицы в новую — с учётом того, что ряды стали колонками и наоборот.
При попытке протестировать эту функцию мы получим исключение UnsupportedOperationException
при создании матрицы — до тех пор, пока не сделаем её реализацию и не используем её в функции-создателе.
Скелет реализации интерфейса
Для того, чтобы создать реализацию интерфейса — то есть класс, который умеет делать все описанные в интерфейсе операции — необходимо для начала написать примерно следующий “скелет”.
class MatrixImpl<E> : Matrix<E> { override val height: Int = TODO() override val width: Int = TODO() override fun get(row: Int, column: Int): E = TODO() override fun get(cell: Cell): E = TODO() override fun set(row: Int, column: Int, value: E) { TODO() } override fun set(cell: Cell, value: E) { TODO() } override fun equals(other: Any?) = TODO() override fun toString(): String = TODO() }
Заголовок class MatrixImpl<E> : Matrix<E>
говорит о том, что мы определяем класс MatrixImpl<E>
, который является реализацией интерфейса Matrix<E>
и использует настраиваемый тип E
. Далее перечисляются все свойства и функции, имеющиеся в Matrix<E>
; перед каждым из них добавляется модификатор override
— он сигнализирует об определении свойства / функции, уже имеющихся в интерфейсе. Класс, в отличие от интерфейса, должен содержать реальные тела функций и реальные значения свойств — но в скелете они заменяются на TODO()
. В конце класса перечисляются две упоминавшиеся ранее функции equals
и toString
— первая для сравнения (матриц) на равенство, а вторая для представления матрицы в виде строки.
Здесь въедливый читатель, заметив перед equals
и toString
модификатор override
, может задаться вопросом — а две этих функции тоже определены в каком-нибудь интерфейсе? Это предположение не вполне верно. Определения двух этих функций имеются в специальном классе Any
, определяющем тип “любой”. Напомним, что в Котлине любой тип является разновидностью типа Any?
, то есть множество значений Any?
— вообще все значения, которые могут существовать в программе на Котлине. Any
без вопроса имеет то же множество значений, за вычетом специального null. Это, в частности, значит, что сравнение на равенство и представление в виде строки в Котлине можно выполнить для чего угодно.
Варианты реализации интерфейса
Теперь поговорим о том, как можно скелет реализации заменить на настоящую реализацию. Почти всегда, когда речь идёт о более-менее сложных понятиях, это можно сделать несколькими способами, какой из них лучше подходит для конкретной задачи — решает программист.
Начать нужно всегда с ответа на вопрос — какие данные описывают интересующий нас объект (матрицу) и как их можно представить на данном языке программирования? Для матрицы первая часть ответа такова — высота и ширина матрицы (целые числа) и набор элементов матрицы (типа E
). Поскольку имеющиеся у матрицы функции не предполагают изменения её высоты и ширины, их лучше всего объявить как свойства в конструкторе матрицы:
class MatrixImpl<E>(override val height: Int, override val width: Int //, something other? ) : Matrix<E> { // Attention: no more height / width here override fun get(row: Int, column: Int): E = TODO() // Other functions... }
Обратите внимание, что определения свойств высоты и ширины исчезли из тела класса и переехали в конструктор — при этом сохранив необходимый модификатор override
.
Что касается набора элементов, то здесь актуальна вторая часть вопроса — как представить этот набор? Для этого нужен некоторый контейнер, ссылка на который хранилась бы в ещё одном свойстве матрицы. Лучше, чтобы это свойство было закрытым, чтобы возможные действия с матрицей ограничивались лишь свойствами и функциями из интерфейса Matrix<E>
. Существует несколько вариантов такого контейнера. Рассмотрим некоторые из них.