Skip to main content

Nullable типи

Ми розглянули об'єкти й тепер прийшов час розглянути випадки, коли у нас можуть бути відсутні якісь дані. Що таке nullable? Nullable – це властивість типу не набувати значення, а мати null (тобто нічого). Тобто:

val string: String? = null

Як ми бачимо у нас додався знак питання та значення тепер дорівнює null (тобто нічого немає). Для чого використовується ?? Насправді він використовується для того, щоб вказати, що змінна або функція може не мати значення. За замовчуванням, типи завжди мають мати якесь значення, але іноді потрібно вказати, що даних немає, тому ми й використовуємо nullable-типи.

Цікаво знати

Але чому за замовчуванням не можна вказувати null? В багатьох мовах програмування, зазвичай, не вказують додатково, що змінна або функція може не мати значення, але, якщо трішки подумати, то можна зрозуміти, що в будь-який момент можна спіткатись через помилку під час виконанння програми, коли ми забудемо перевірити чи є дані, чи їх немає (дізнайтесь більше про помилку на мільярд доларів).

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

Тому, Котлін вирішив боротись з цим додавши null-safety.

Null-safety

Що ж це таке? У додачу до вказування знаку запитання, Kotlin зобов'язує перевіряти ці типи перед використанням. Тобто, спробувавши наступне:

fun main() {
val string: String = someFunction()
println(string)
}

fun someFunction(): String? = null
Type mismatch: inferred type is String? but String was expected

Ми не можемо використовувати nullable типи, там де не очікується null. Тоді як?

Smart cast

Один з найпростіших способів перевірити тип та його використовувати, це звичайний if:

fun main() {
val foo: String? = getFooOrNull()
if(foo != null)
println(foo) // тут вже foo типу String
else println('Немає даних')
}

fun getFooOrNull(): String? {
if(Random.nextBoolean())
"Foo"
else null
}

В цьому випадку у нас спрацювало автоматичне приведення типів. Наскільки ви вже знаєте, в Kotlin є оператор as, який приводить одну сутність до іншої, якщо це можливо. В такому випадку нам це не потрібно — ми маємо смарткасти, що роблять це автоматично, приводячи тип з String? до String.

До речі

Також разом зі знаком запитання, за код стилем, зазвичай, функції, що повертають nullable-значення, називають наступним патерном – getXOrNull(). Тобто дописують orNull, щоб це було більш очевидно для того, хто буде читати код.

Також, це до речі працює ось таким чином:

fun main() {
val foo: String? = getFooOrNull()
if(foo == null)
return // виходимо з програми

println(foo) // після if компілятор вже розуміє, що тут нічого окрім String бути не може
}

Після if компілятор вже розуміє, що тут нічого окрім String бути не може. Так працює не лише з if, а, наприклад, також з when.

До речі, варто враховувати, що smart-cast працює тільки з іммутабельними (незмінними змінними, тобто значеннями) даними. Все через те, що компілятор не може перевірити, чи змінюємо ми ці дані десь паралельно (наприклад, якщо у нас багатопоточна програма).

Safe call оператор (оператор безпечного виклику)

Іншим видом перевірки є безпечний виклик. Що це таке? Розглянемо на практиці:

fun main() {
val foo: String? = getFooOrNull()
println(foo?.plus("Bar")) // до речі, для такого й використовується подібний запис операторів
}

Що ж це означає? Насправді все дуже просто – якщо вираз зліва (перед ?) дорівнює null вираз справа виконуватись не буде (тобто весь вираз поверне null). То у нас виходить, що параметром в println у нас все ж таки буде null? Як ми можемо вказати, що у нас немає даних? Насправді ми можемо зробити через те й же if:

fun main() {
val foo: String? = getFooOrNull()
println(if(foo == null) "Немає даних" else foo + "Bar")
}

І це буде працювати. Але.. трішки заскладно, тому Котлін зробив наступне.

Elvis-оператор

Для того, щоб вказати значення (за замовчуванням) виразу, який може мати null придумали elvis-оператор. Розгляньмо на попередньому прикладі:

fun main() {
val foo: String? = getFooOrNull()
println(foo?.plus("Bar") ?: "Немає даних")
}

Тобто, ми додали справа ?:, а після нього вже вираз, який буде виконуватись, якщо в результаті виконання виразу зліва буде null. Взагалі, з ним також можна робити наступні речі:

fun main() {
val foo: String? = getFooOrNull() ?: return // виходимо з програми, якщо null
println(foo + "Bar")
}

Тобто, за аналогією з if, ми також можемо вказати, що якщо null, то далі виконування не йде (про контракт Nothing поговоримо пізніше).

Not-null assertion (Double-bang) оператор

І останній вид операторів, який працює з nullable значеннями – not-null assertion оператор. Працює дуже просто – показує компілятору, що значення, яке марковане як nullable, точно не null.

fun main() {
val foo: String? = getFooOrNull()
println(foo!! + "Bar")
}

Але, вартно враховувати, що якщо там все ж таки буде null, то при виконуванні ми отримаємо помилку. Тож будьте обачні. Цей вид оператору слід використовувати тоді, коли ми не можемо використовувати, наприклад, смарткасти, але ми впевнені, що дані все ж є. Наприклад:

var string: String? = null

...

fun main() {
if(string != null)
println(string!!)
}

В інших ситуаціях, краще його не використовувати, якщо не хочете мати помилок в роботі програми.

Лайфхак

До речі, у випадках, коли у нас є якась nullable змінна, ми можемо створити її копію, яка вже буде працювати зі смарткастами:

var string: String? = null

fun main() {
// магія областей видимості
val string = string ?: return
println(string)
}

Я, зазвичай, використовую саме цей спосіб.