Kotlin. Null-безпека. Загальні поняття

Null-безпека. Загальні поняття. Символ ?. Оператори ?. та ?:


Зміст


Пошук на інших ресурсах:

1. Поняття null-значення. Елементи, що отримують null-значення

У сучасних мовах програмування (зокрема Java, C# тощо) виникають ситуації, коли величина не містить ніякого значення. Терміни null-значення і нульове значення – це різні речі.

У мові Kotlin null-значення означає, що для змінних, які оголошені як val або var, не існує значення на даний момент. Виходячи з цього контексту, елементи мови Kotlin поділяються на два види:

  • nullable – це елементи, які можуть приймати null-значення;
  • non-nullable – елементи, які не можуть приймати null-значення.

При написанні програм важливим є правильне використання елементів, що підтримують null-значення з метою уникнення помилок. В інших мовах програмування (зокрема Java) неправильне використання nullable-елементів призводить до виникнення помилок на етапі виконання, що є дуже небажаним. Як наслідок, генерується виключення (у Java це виключення java.lang.NullPointerException) яке є наслідком того, що компілятор не зміг виявити помилку під час компіляції програми.

У мові Kotlin система типів організована так, що помилки з використанням nullable-типів, виявляються вже на етапі компіляції. Тобто, помилку одразу можна визначити і виправити, інакше програмний код не буде скомпільовано. Це є суттєва перевага мови Kotlin.

 

2. Оголошення змінних, що підтримують null-значення (nullable-змінних). Символ ?

На відміну від мови Java (а також інших мов), мова Kotlin дає можливість програмісту безпосередньо вказувати типи, які підтримують null-значення. Це здійснюється з допомогою додавання символу ? (знак питання) до назви типу

Type?

де

  • Type –   будь-який базовий тип мови Kotlin (Int, Double, Byte, Short, UByte, Char, String та інші).

Якщо змінна оголошена як nullable-змінна, то їй можна присвоювати значення null.

Приклад.

У прикладі демонструється оголошення та використання nullable-змінних різних типів.

fun main(args: Array<String>) {
  var i1 : Int? = null // nullable-змінна типу Int
  val d1 : Double? = null // nullable-змінна типу Double
  val s1 : String? = null // nullable-змінна типу String
  var b1 : Boolean? = null // nullable-змінна типу Boolean
  var c1 : Char? = null // nullable-змінна типу Char

  println(i1)
  println(d1)
  println(s1)
  println(b1)

  // nullable-змінним, які оголошені як var,
  // можна присвоювати й інші значення
  i1 = 25; println(i1);
  b1 = true; println(b1);
  c1 = '+'; println(c1);
}

Результат

null
null
25
true
+

 

3. Забезпечення null-безпеки в мові Kotlin

Якщо в програмі оголошується значення типу з підтримкою null, то на етапі компіляції буде здійснюватись перевірка на null-допустимі значення. Таким чином, коло можливих операцій звузиться. Якщо операція не забезпечує null-безпеку, то буде виведено повідомлення про помилку. Нижче наведено можливі випадки таких помилок та спроби їх усунення

 

3.1. Повернення з функції результату, який може бути null

Наприклад, у наступному коді буде виникати помилка компіляції:

// Функція, яка повертає Int з null-рядка
fun GetLenString(str : String?) : Int {
  return str.length // тут помилка
}

При спробі скомпілювати дану функцію буде виведено помилку

Only safe (?.) or non-null asserted (!!.) calls are allowed on a nullable receiver of type String?

Дійсно, як можна повернути довжину рядка типу String?, який може бути рівний null, тобто не існувати.

Щоб виправити ситуацію, потрібно в тіло функції GetLenString() ввести код перевірки на null-значення на кшталт

// Функція, яка повертає Int з null-рядка
fun GetLenString(str : String?) : Int {
  if (str!=null) // перевірка на null
    return str.length
  else
    return 0
}

 

3.2. Присвоєння nullable-значення змінній, яка не підтримує null

Якщо оголошено змінну, яка не підтримує null-значення, то спроба присвоєння цій змінній значення nullable-змінної викличе помилку компіляції.

Наприклад, у наступному коді

fun main(args: Array<String>) {
  // 1. Змінна, яка підтримує null
  var X : Double? = 23.8

  // 2. Змінна, яка не підтримує null
  var Y : Double = 33.1

  // 3. Спроба присвоєння
  Y = X // помилка
}

при спробі компіляції у рядку присвоєння

Y = X

компілятор видасть помилку

Type mismatch: inferred type is Double? but Double was expected

Щоб компілятор не видавав помилку, потрібно реалізувати перевірку на null, наприклад, так

fun main(args: Array<String>) {
  // 1. Змінна, яка підтримує null
  var X : Double? = 23.8

  // 2. Змінна, яка не підтримує null
  var Y : Double = 33.1

  // 3. Спроба присвоєння,
  //    перевірка на null
  if (X!=null)
    Y = X // працює, помилки немає
  else
    Y = 0.0
}

 

3.3. Передача null-параметру в функцію, в якій оголошено параметр, що не підтримує null-значення

Якщо оголошується функція, яка отримує параметр, що не підтримує null-значення, то спроба передачі nullable-значення в цю функцію завершиться невдачею.

Наприклад, у нижченаведеному коді

// Функція, яка отримує значення, яке не підтримує тип null
fun ShowInteger(num : Int) : Unit {
println("num = " + num)
} 

fun main(args: Array<String>) {
  var num1:Int = 10 // не nullable-тип
  var num2:Int? = 20 // nullable-тип
  ShowInteger(num1); // працює
  ShowInteger(num2) // тут помилка
}

компілятор в рядку

ShowInteger(num2)

видасть помилку

Type mismatch: inferred type is Int? but Int was expected

Для усунення помилки потрібно щоб функція ShowInteger() отримувала nullable-тип Int?. У результаті код функції може бути приблизно таким

// Функція, яка отримує значення, яке не підтримує тип null
fun ShowInteger(num : Int?) : Unit {
  // Перевірка на null
  if (num!=null)
    println("num = " + num)
}

 

4. Оператор безпечного виклику ?.. Приклад

З допомогою оператора безпечного виклику ?. можна реалізувати перевірку на null та виклик методу. У найбільш загальному випадку, використання оператора ?. виглядає наступним чином

obj?.MethodName()

тут

  • obj – ім’я змінної (об’єкту);
  • MethodName() – ім’я функції (методу), що викликається з екземпляру (об’єкту) obj. Виклик MethodName() може бути з передачею йому параметрів.

Код виклику obj?.MethodName() конвертується компілятором у код, що містить перевірку на null

if (obj!=null)
  obj.MethodName()
else
  null

На рисунку 1 показано роботу оператора ?..

Kotlin. Null-безпека. Оператор ?.

Рисунок 1. Оператор ?.

Приклад.

У прикладі використовується модифікована функція GetLengthString(), яка використовує оператор ?. .

// Функція, яка повертає nullable-значення String?
fun GetLengthString(s : String?) : Int? {
  return s?.length // оператор ?.
}

fun main(args: Array<String>) {
  // Використання оператора ?. для отримання довжини null-рядка
  var s : String? = "abcde"
  println(GetLengthString(s))
  s = null
  println(GetLengthString(s))
}

Результат

5
null

 

5. Оператор присвоєння значення null за замовчуванням ?: (Елвіс)

Оператор ?: реалізує перевірку значення на null на основі двох операндів. Цей оператор ще називається оператор об’єднання по null (null coalescing operator). У найбільш загальному випадку використання оператора ?: виглядає так

op1 ?: op2

тут op1, op2 – деякі операнди, які приймають участь в отриманні результату. Результат операції формується на основі рисунку 2.

Kotlin. Null-безпека. Оператор ?:

Рисунок 2. Оператор ?:

Приклад.

fun main(args: Array<String>) {
  // Використання оператора ?:

  // 1. Для типу String
  var s1 : String? = "abcd"

  var s2 : String = s1 ?: "" // s2 = "abcd"
  println(s2)

  s1 = null
  s2 = s1 ?: "null value" // s2 = "null value"
  println(s2)

  // 2. Для типу Int
  var i1 : Int? = 25
  var i2 : Int = i1 ?: 0 // i2 = 25
  println(i2)
  i1 = null
  i2 = i1 ?: 0 // i2 = 0
  println(i2)
}

Результат

abcd
null value
25
0

 

6. Перевірка на null. Оператор !!

З допомогою оператору !! можна здійснити приведення до типу, що не підтримує значення null. Якщо приводиться саме значення null, то Kotlin згенерує виключну ситуацію. Використання оператора !! виглядає наступним чином

obj!!

тут obj – деяка змінна (об’єкт).

Робота оператора !! зображена на рисунку 3.

Kotlin. Null-безпека. Оператор !!

Рисунок 3. Оператор !!

Приклад 1. Генерується виключення для типу String.

fun main(args: Array<String>) {
  // Оператор !!

  // Використання для типу String
  var s1 : String? = "abcd"
  var s2 : String = s1!!   // s2 = "abcd"
  println(s2)

  s1 = null
  s2 = s1!!   // виключення NullPointerException
}

Результат

abcd
Exception in thread "main" kotlin.KotlinNullPointerException
at TrainKt.main(train.kt:16)

Приклад 2. Генерується виключення для типу Int.

fun main(args: Array<String>) {
  // Оператор !!
  // Використання для типу Int
  var i1 : Int? = 25
  var i2 : Int
  i2 = i1!!   // i2 = 25
  println(i2)

  i1 = null
  i2 = i1!!   // генерується виключення NullPointerException
  println(i2)
}

Результат

25
Exception in thread "main" kotlin.KotlinNullPointerException
at TrainKt.main(train.kt:13)

 

7. Функція let

Функція let застосовується для лямбда-виразів, які допускають значення null. Дана функція у поєднанні з оператором ?. використовується для зручного запису nullable-виразів.

Загальна форма використання функції виглядає наступним чином

obj2 = obj1?.let {
  // код лямбда виразу
  // використовується it
  // ...
}

тут

  • obj1, obj2 – змінні (об’єкти), які мають nullable-тип.

На рисунку 4 зображено роботу функції let.

Kotlin. Null-безпека. Функція let. Безпечний виклик лямбда-виразу

Рисунок 4. Функція let. Безпечний виклик лямбда-виразу

Як видно з рисунку, якщо obj!=null, то виконується лямбда-вираз. Якщо obj==null, то нічого не відбувається і лямбда-вираз не виконується.

Зі збільшенням розміру обчислюваного виразу, ефект від використання функції let суттєво зростає.

Приклад.

fun main(args: Array<String>) {
  // Функція let в поєднанні з ?.

  // 1. Для типу String
  // 1.1. Оголошено nullable-змінні
  var s1 : String?
  var s2 : String?

  // 1.2. Не null-значення
  s1 = "abcd"

  s2 = s1?.let { // Лямбда-вираз
    it.reversed() // Реверсування рядка
  }
  println(s2) // "dcba"

  // 1.3. null-значення
  s1 = null
  s2 = s1?.let {
    it.reversed()
  }
  println(s2) // null

  // 2. Використання з типом Double
  // 2.1. Оголосити дві nullable-змінні
  var d1 : Double?
  var d2 : Double?

  // 2.2. Не null-значення
  d1 = 2.33
  d2 = d1?.let { // помножити на 3
    it * 3
  }
  println(d2) // 6.99

  // 2.3. null-значення
  d1 = null
  d2 = d1?.let {
    it * 3
  }
  println(d2) // null
}

Результат

dcba
null
6.99
null

 


Споріднені теми