Шаг 161.
Основы Kotlin.
Инициализация. Подводные камни инициализации

    На этом шаге мы рассмотрим несколько особых случаев инициализации.

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

class Player() {
    init {
      val healthBonus = health.times(3)
    }
    val health = 100
}

fun main() {
    Player()
}

    Этот код не скомпилируется, потому что свойство health не инициализировано перед его использованием в блоке init. Как отмечалось ранее, когда свойство используется внутри блока init, инициализация свойства должна произойти до обращения к нему. Если health объявить перед блоком инициализации, код скомпилируется:

class Player() {
  val health = 100
  init {
    val healthBonus = health.times(3)
  }
}

fun main() {
  Player()
}

    Есть пара похожих, но более коварных сценариев развития событий, которые могут произойти с неосведомленным программистом. Например, в следующем коде объявлено свойство name, а затем функция firstLetter() читает первый символ свойства:

class Player() {
  val name: String
  private fun firstLetter() = name[0] 

  init {
    println(firstLetter()) 
    name = "Madrigal"
  }
}

fun main() {
  Player()
}

    Этот код скомпилируется, потому что компилятор видит, что свойство name инициализируется в блоке init - подходящем месте для присваивания начального значения. Но запустив этот код, вы получите исключение NullPointerException, потому что функция firstLetter() (которая использует свойство name) вызывается раньше, чем свойство name получит начальное значение в блоке init.

    Компилятор не проверяет порядок инициализации свойств и вызовов функций, которые их используют в блоке init. Прежде чем объявить блок init, который вызывает функции, обращающиеся к свойствам, убедитесь, что инициализировали свойства до вызовов функций. Если name инициализировать до вызова firstLetter(), код скомпилируется и выполнится без ошибки:

class Player() {
  val name: String
  private fun firstLetter() = name[0] 
  
  init {
    name = "Madrigal"
    println(firstLetter())
  }
}

fun main() {
  Player()
}

    Еще один хитрый сценарий показан в следующем примере, в котором инициализируются два свойства:

class Player(_name: String) {
  val playerName: String = initPlayerName() 
  val name: String = _name
  
  private fun initPlayerName() = name
}

fun main() {
  println(Player("Madrigal").playerName)
}

    Код снова скомпилируется, так как компилятор видит, что все свойства инициализируются. Но запуск кода приведет к исключению обращения по ссылке null.

    В чем же тут проблема? Когда playerName инициализируется функцией initPlayerName(), компилятор предполагает, что name инициализировано, но когда происходит вызов initPlayerName(), оказывается, что name еще не инициализировано.

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

class Player(_name: String) {
  val name: String = _name
  val playerName: String = initPlayerName() 
  
  private fun initPlayerName() = name
}

fun main() {
  println(Player("Madrigal").playerName)
}

    На следующем шаге мы рассмотрим небольшой пример для самостоятельного выполнения.




Предыдущий шаг Содержание Следующий шаг