Урок 12: Конструкторы в Kotlin (primary, secondary). Блоки инициализации init

primary secondary. Блоки инициализации init - Android [Kotlin] для начинающих

Что такое конструктор в Kotlin

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

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

Конструктор вызывается всегда при создании объекта класса. Этот механизм служит для проведения внутри себя множества так называемых организационных действий для успешно создания объекта. Свойства, которые мы передаем в класс, на самом деле передаются в конструктор. Именно внутри конструктора происходит инициализация этих свойств (выделение памяти, присвоение значений свойств переменным и так далее). Потому что свойства – это по сути обычные переменные внутри класса. И это тоже надо запомнить, пригодится для дальнейшего понимания.

Чтобы не путаться, будем разделять эту историю на логические блоки.

Блок 1: пустой конструктор

Чтобы наглядно увидеть это, давайте создадим новый класс. Вспоминаем ТЗ для приложения с рецептами из прошлого урока. У нас уже есть класс “блюдо”, предлагаю создать новую сущность Ingredient, потому что у отдельного игредиента тоже могут быть различные свойства. Свойства класса будем писать в теле, скобки можно опустить и это будет корректно. Таким образом мы создадим класс без конструктора, но Kotlin все равно сгенерирует этот конструктор, просто он будет без параметров. Но оставим их пустыми для явной демонстрации пустого конструктора.

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

class Ingredient() {

   var name = "какое то название"
   var weight = 999
   var count = 888

}

Что мы сейчас сделали. Создали класс с некими проинициализированными полями. Как видите, в сигнатуре класса (там где пустые круглые скобки) свойств нет, класс создастся с пустым конструктором.

Запомним. В Kotlin такой конструктор называется первичным или primary constructor. Причем мы можем явно его указать с помощью ключевого слова constructor. А так как при отсутствии аннотаций и дополнительных модификаторов доступа его можно опустить – студия разработки подсветит его серым и предложит убрать это ключевое слово за ненадобностью.

Теперь фиксируем озвученное. В ранее созданном классе Main (или в любом удобном для вас месте в рамках пакета) создадим объект класса Ingredient.

val ingredient1 = Ingredient()

Выглядит знакомо. Мы создали объект класса, все поля его уже проинициализированы и к их значениям можно обратиться. Кроме того, для этого экземпляра можно задать новые значения конкретных полей.

val ingredient1 = Ingredient()
ingredient1.name = "Репчатый лук"
println(ingredient1.name)

С полями понятно. Теперь следим за пустым primary constructor. При наведении курсора на пустые скобки класса подсвечивается его полное название. Кстати, если навести курсор на конструктор класса Dish, также увидим его сигнатуру со всеми прописанными свойствами.

Блок 2: основной конструктор (primary constructor)

Итак, создание класса с пустым конструктором создает сложности с созданием объектов класса с уникальной инициализацией полей. Поэтому был создан конструктор, в который мы передаем необходимые параметры. Напомню, перед скобками указывается ключевое слово constructor, но в текущих условиях его можно опустить. Пропишем параметры. Причем сейчас мы прописываем их туда без ключевых слов var/val. Как параметры, которые приходят в обычную функцию. Потому что мы пока еще не создаем переменные класса. Они по прежнему остаются в теле.

class Ingredient(name: String, weight: Int, count: Int) {

   var name = "какое то название"
   var weight = 999
   var count = 888

}

Теперь при создании объекта необходимо инициализировать его поля. Но чем бы мы не инициализировали новые объекты, создаваться они будут все с теми же данными, которые прописаны в теле класса. Убедимся в этом. В рабочем классе Main создадим объект с одним названием, распечатаем его и увидим другое название. Чтобы инициализация объекта происходила корректно, необходимо инициализировать поля класса полями из конструктора. Их названия можно оставить одинаковыми, но для наглядности можно или переименовать, или добавить нижнее подчеркивание перед названием свойства. А вообще, при клике на свойство подсветится его отношение к свойству из конструктора.

class Ingredient(name: String, weight: Int, count: Int) {

   var name = name
   var weight = weight
   var count = count

}

В этом случае объекты будут создаваться правильно. Создадим экземпляр класса. Проинициализируем поля какими-нибудь данными и проверим, распечатав одно из свойств.

val ingredient1 = Ingredient("Картошка", 100, 1)
println(ingredient1.name)

Вернемся в класс, чтобы проследить что происходит. Когда мы создаем объект, в круглые скобки прописываем свойства. Эти свойства поставляются в конструктор класса. И если немного упрощенно рассуждать, то значения из сигнатуры присваиваются ранее созданным полям класса. Точнее сказать в переменные записываются ссылки на созданные в памяти объекты со значениями. Таким образом в конкретный объект класса записываются те самые данные, которые мы указали при создании.

Блок 3: упрощенная форма основного конструктора

Прелесть языка в том, что можно записать свойства в более упрощенном виде. Так мы объявляли класс в предыдущем уроке. Перед именем параметра прописываем var или val и таким образом мы также создаем переменные класса сразу в конструкторе. Их по прежнему можно сразу инициализировать значениями по умолчанию. В этом случае переменные в теле уже не нужны.

class Ingredient(val name: String, val weight: Int, val count: Int) {

}

Блок 4: вторичный конструктор (secondary constructor)

Бывают случаи, иногда, когда необходимо применить альтернативный способ создания класса. Например, инициализировать объекты разным количеством свойств. Как правило основного конструктора бывает достаточно за счет его гибкости. Но этот способ нужно иметь в виду. Для таких случаев используется вторичный или вспомогательный конструктор (secondary constructor).

Его синтаксис будет следующим: внутри класса пишем ключевое слово constructor. Если основной конструктор не пустой, вторичный конструктор должен его вызывать с перечислением всех полей. И только потом добавлять дополнительные свойства. Для этого в круглых скобках перечисляем параметры без val или var (так как эти переменные уже объявлены). Далее добавим новое поле, например, Boolean переменную isNeedToPrepare, что будет означать нужно ли предварительно готовить ингредиент. Далее пишем двоеточие, ключевое слово this и в круглых скобках перечисление свойств основного конструктора – это будет говорить об обращении к нему (или его вызове).

class Ingredient(val name: String, val weight: Int, val count: Int) {

   constructor(
		name: String, 
		weight: Int, 
		count: Int, 
		isNeedToPrepare: Boolean
): this(name, weight, count) {

   }

В дополнительном конструкторе нельзя объявлять переменные класса. В основном он используется для определения дополнительных параметров, через которые можно передавать данные для инициализации экземпляра класса. Также здесь можно прописывать какую-то логику. Чтобы новое свойство проинициализировалось корректно, сначала создадим его поле в теле класса с ключевым словом var, проинициализировав значением по умолчанию false.

Затем в конструкторе мы должны, обратите внимание, сначала обратиться к переменной из тела класса и присвоить ей значение (переменную) которое приходит в свойствах конструктора. То есть мы пишем isNeedToPrepare = isNeedToPrepare, но так как мы находимся в области видимости вторичного конструктора, компилятор не понимает откуда куда нужно присваивать значение. Поэтому перед первой переменной (куда нужно присвоить значение) пишем this с точкой.

class Ingredient(val name: String, val weight: Int, val count: Int) {

	var isNeedToPrepare = false

	constructor(
		name: String,
		weight: Int,
		count: Int,
		isNeedToPrepare: Boolean
	) : this(name, weight, count) {

		this.isNeedToPrepare = isNeedToPrepare

	}

}

Теперь ошибки нет, и мы явно указали с помощью this, что эта переменная относится к переменной класса, а не конструктора. Если покликать на обе переменные, мы увидим их принадлежность к соответствующим местам в виде подсветки в студии разработки. Ключевое слово this не нужно, если переменные будут называться по разному. Например, добавим нижний слэш в название свойства конструктора и в теле соответственно. Теперь this можно убрать и ошибки не возникнет.

class Ingredient(val name: String, val weight: Int, val count: Int) {

   var isNeedToPrepare = false

   constructor(
      name: String,
      weight: Int,
      count: Int,
      _isNeedToPrepare: Boolean
   ) : this(name, weight, count) {

      isNeedToPrepare = _isNeedToPrepare

   }

}

Окей. Перейдем к созданию объектов, чтобы наглядно увидеть как работают два конструктора. Теперь мы можем создать два ингредиента. При заполнении свойств, в одном из объектов, свойство _isNeedToPrepare уже будет не обязательным к заполнению. Но оно будет хранить этот параметр со значением по умолчанию false.

val ingredient1 = Ingredient("Картошка", 100, 1)
val ingredient2 = Ingredient("Морковь", 50, 1, true)

Инициализатор

Когда при создании объекта (не важно с помощью какого конструктора) нужно выполнить какой то блок кода (логику или ряд вызовов методов) применяются блоки инициализации. Они прописываются в теле класса, обозначаются ключевым словом init. Напишем такой блок внутри которого выведем информационное сообщение.

init {
		println("Ингредиент создан")
}

Теперь при создании экземпляра класса будет вызван блок init и в консоли распечатается информационное сообщение. В классе может быть один или несколько блоков инициализации и выполняться они будут последовательно.

Наконец, перечислим правильную очередность вызовов всех блоков кода во время создания экземпляра класса:

  • вторичный конструктор
  • первичный конструктор
  • инициализация полей класса и блоков init в порядке расположения их в коде
  • выполнение кода в теле вторичного конструктора

Для тех, кто собрался стать Android-разработчиком

Пошаговаясхема
Пошаговая
схема

Описание процесса обучения от основ Kotlin до Android-разработчика

Бесплатныеуроки
Бесплатные
уроки

Авторский бесплатный курс по основам языка программирования Kotlin

Обучающийбот
Обучающий
бот

Тренажер и самоучитель по Котлин – бесплатные тесты и практика

Поделиться уроком

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *