Null и nullable-типы

Последнее обновление: 30.05.2021

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

val n = null
println(n)  // null

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

Однако переменным стандартных типов, например, типа Int или String или любых других классов, мы не можем просто взять и присвоить значение null:

val n : Int = null   // ! Ошибка, переменная типа Int допускает только числа

Мы можем присвоить значение null только переменной, которая представляет тип Nullable. Чтобы превратить обычный тип в тип nullable, достаточно поставить после названия типа вопросительный знак:

// val n : Int = null  //! ошибка, Int не допускает значение null
val d : Int? = null // норм, Int? допускает значение null

При этом мы можем передавать переменным nullable-типов как значение null, так и конкретные значения, которые укладываются в диапазон значений данного типа:

var age : Int? = null
age = 34              // Int? допускает null и числа
var name : String? = null
name = "Tom"        // String? допускает null и строки

Nullable-типы могут представлять и создаваемые разработчиком классы:

fun main() {
    
    var bob: Person = Person("Bob")
    // bob = null // ! Ошибка - bob представляет тип Person и не допускает null
    var tom: Person? = Person("Tom")
    tom = null  // норм - tom представляет тип Person? и допускает null
}
class Person(val name: String)

В то же время надо понимать, что String? и Int? - это не то же самое, что и String и Int. Nullable типы имеют ряд ограничений:

  • Значения nullable-типов нельзя присвоить напрямую переменным, которые не допускают значения null

    var message : String? = "Hello"
    val hello: String = message		// ! Ошибка - hello не допускает значение null
    
  • У объектов nullable-типов нельзя вызвать напрямую те же функции и свойства, которые есть у обычных типов

    var message : String? = "Hello"
    // у типа String свойство length возвращает длину строки
    println("Message length: ${message.length}")	// ! Ошибка
    
  • Нельзя передавать значения nullable-типов в качестве аргумента в функцию, где требуется конкретное значение, которое не может представлять null

Оператор ?:

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

var name : String?  = "Tom"
val userName: String = name	// ! Ошибка

Переменная name хранит строку "Tom". Переменная userName представляет тип String и тоже может хранить строки, но тем не менее напрямую в данном случае мы не можем передать значение из переменной name в userName. В данном случае для компилятора неизвестно, каким значением инициализирована переменная name. Ведь переменная name может содержать и значение null, которое недопустимо для типа String.

В этом случае мы можем использовать оператор ?:, который позволяет предоставить альтернативное значение, если присваиваемое значение равно null:

var name : String?  = "Tom"
val userName: String = name ?: "Undefined"	// если name = null, то присваивается "Undefined"

var age: Int? = 23
val userAge: Int = age ?:0	// если age равно null, то присваивается число 0

Оператор ?: принимает два операнда. Если первый операнд не равен null, то возвращается значение первого операнда. Если первый операнд равен null, то возвращается значение второго операнда.

То есть это все равно, если бы мы написали:

var name : String?  = "Tom"
val userName: String
if(name!=null){

	userName = name
}

Но оператор ?: позволяет сократить подобную конструкцию.

Оператор ?.

Оператор ?. позволяет объединить проверку значения объекта на null и обратиться к функциям или свойствам этого объекта.

Например, у строк есть свойство length, которое возвращает длину строки в символах. У объекта String? мы просто так не можем обратиться к свойству length, так как если объект String? равен null, то и строки как таковой нет, и соответственно длину строки нельзя определить. И в этом случае мы можем применить оператор ?.:

var message : String? = "Hello"
val length: Int? = message?.length

Если переменная message вдруг равна null, то переменная length получит значение null. Если переменная name содержит строку, то возвращается длина этой строки. По сути выражение val length: Int? = message?.length эквивалентно следующему коду:

val length: Int?
if(message != null)
	length = message.length
else
	length = null

С помощью оператора ?. подобным образом можно обращаться к любым свойствам и функциям объекта.

Также в данном случае мы могли совместить оба выше рассмотренных оператора:

val message : String?  = "Hello"
val length: Int = message?.length ?:0

Теперь переменная length не допускает значения null. И если переменная name не определена, то length получает число 0.

Используя этот оператор, можно создавать цепочки проверок на null:

fun main() {

    var tom: Person? = Person("Tom")
    val tomName: String? = tom?.name?.uppercase()
    println(tomName)        // TOM

    var bob: Person? = null
    val bobName: String? = bob?.name?.uppercase()
    println(bobName)        // null

    var sam: Person? = Person(null)
    val samName: String? = sam?.name?.uppercase()
    println(samName)        // null

}
class Person(val name: String?)

Здесь класс Person в первичном конструкторе принимает значение типа String?, то есть это можт быть строка, а может быть null.

Допустим, мы хотим получить переданное через конструктор имя пользователя в верхнем регистре (заглавными буквами). Для перевода текста в верхний регистр у класса String есть функция uppercase(). Однако может сложиться ситуация, когда либо объект Person равен null, либо его свойство name ( которое представляет тип String?) равно null. И в этом случае перед вызовом функции uppercase() нам надо проверять на null все эти объекты. А оператор ?. позволяет сократить код проверки:

val tomName: String? = tom?.name?.uppercase()

То есть если tom не равен null, то обращаемся к его свойству name. Далее если name не равен null, то обращаемся к ее функции uppercase(). Если какое-то звено в этой проверки возвратит null, переменная tomName тоже будет равна null.

Но здсь мы также можем избежать финального возвращения null и присвоить значение по умолчанию:

val tomName: String = tom?.name?.uppercase() ?: "Undefined"

Оператор !!

Оператор !! (not-null assertion operator) принимает один операнд. Если операнд равен null, то генерируется исключение. Если операнд не равен null, то возвращается его значение.

fun main() {
    try {
        val name : String?  = "Tom"
        val id: String = name!!
        println(id)
    } catch (e: Exception) { println(e.message)}
}

Поскольку данный оператор возвращает объект, который не представляет nullable-тип, то после применения оператора мы можем обратиться к методам и свойствам этого объекта:

val name : String?  = null
val length :Int = name!!.length
Помощь сайту
WebMoney
  • P378451176208
  • Z280152397659
ЮMoney/Яндекс-Деньги
  • 410011174743222
PayPal
  • metanit22@mail.ru
Перевод на карту
  • Номер карты: 4048415020898850