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

 


Связанные темы