Как и все остальные встроенные типы, классы по умолчанию представляют тип, которые не допускает значение null. Однако иногда бывает необходимо, чтобы объект класса
мог хранить значение null
. Например, один из сценариев - мы отправляем запрос к сетевому ресурсу и в ответ мы можем получить какие-то конкретные данные, либо null, если
запрошенные данные на ресурсе не найдены. Для определения объекта класса, который может хранить значение null, можно использовать nullable-класс, то есть добавить к определению типа оператор ?:
void main (){ Person? sam; print(sam); // null - sam допускает значение null Person tom; // print(tom); // ! Ошибка, tom не может принимать null }
В случае с вызовом print(tom)
, как и в случае с переменными других типов, которые не могут принимать значение null, необходимо присвоить
переменной начальное значение перед ее использованием.
В тоже время при использовании nullable-классов мы можем попасть в следующую ситуацию. Как писалось выше, для обращения к полям и методам объекта используется оператор точка (.), после которой указывается имя поля/метода. Однако, как мы знаем, если переменным nullable-классов не присвоено значение, то они по умолчанию имеют значение null. Это необходимо учитывать. Например, обратимся к полям объекта через переменную, которая не инициализирована:
class Person{ String name = ""; int age = 0; void display() => print("Name: $name \tAge: $age"); } void main (){ Person? tom; tom.age = 38; // Ошибка, tom = null }
При выполнении строки кода tom.age = 38
программа сгенерирует ошибку и завершит свое выполнение. Потому что
мы не создали объект класса Person c помощью конструктора. Поэтому переменная tom имеет значение null, то есть фактически ничего, объекта нет.
Соответственно обратиться к поля несуществующего объекта мы не можем. Чтобы избежать подобной проблемы для обращения к полям мы можем проверять переменную на null:
if(tom != null){ // если tom не равна null tom.age = 38; // то присваиваем полю age число }
Но для упрощения проверки Dart позволяет использовать другой оператор - ?. (null-aware access operator):
class Person{ String name = ""; int age = 0; void display() => print("Name: $name \tAge: $age"); } void main (){ Person? tom; tom?.age = 38; // не выполняется tom?.display(); // не выполняется tom = Person(); // объект создан tom.age = 38; // выполняется tom.name = "Tom"; // выполняется tom.display(); // выполняется }
Оператор ?. проверят значение переменной, если переменная не равна null, то происходит обращение к ее полям и методам. Если же она равна null, то обращение к переменной игнорируется. Например, в следующем случае:
Person? tom; tom?.age = 38;
переменная tom равна null, поэтому строка кода tom?.age = 38
не окажет никакого влияния - значение age не изменится, а ошибки не будет, программа продолжит выполнение.
Обратите внимание, что после создания объекта оператор ?. не примеряется:
tom = Person(); // объект создан tom.age = 38; // выполняется
Дело в том, что анализатор Dart может автоматически вывести, что здесь не требуется проверка на null, так как объект Person создан. Данная ситуация называется type promotion или продвижение типа - Dart "продвигает" тип Person? в Person.
Однако не во всех ситуациях Dart может вывести, что объект не null:
void printPerson(Person? person){ person?.display(); } class Person{ String name = ""; int age = 0; void display() => print("Name: $name \tAge: $age"); } void main (){ Person? tom; printPerson(tom); tom = Person(); printPerson(tom); }
В функции printPerson получаем в качестве параметра значение Person? и вызываем у него метод display. Поскольку для анализатора Dart неизвестно, какое же значение будет передано в функцию - null или Person, то при вызове метода display у Person применяется оператор ?.:
person?.display();
Если person будет равен null, то вызова метода просто не будет, и программа продолжит свою работу. Если person не равен null, то будет вызываться метод display.
Особую форму этого оператора представляет оператор ?.. (null-aware cascade operator), который позволяет использовать каскадную нотацию, если переменная не равна null.
void setDefaultValues(Person? person){ person ?..name = "Tom" ..age = 38 ..display(); } class Person{ String name = ""; int age = 0; void display() => print("Name: $name \tAge: $age"); } void main (){ Person? tom; setDefaultValues(tom); tom = Person(); setDefaultValues(tom); }
Здесь функция setDefaultValues принимает некоторое значение Person? и, если это значение не равно null, с помощью каскадной нотации устанавливает для его полей некоторые значения и вызывает метод display.
В некоторых ситуациях переменная может быь определена как переменная nullable-типа, тем не менее при этом могут быть исключены ситуации, что данная переменная будет
хранить null
. И если мы точно уверены, что эта переменная в процессе работы программы не получит значение null
,
то в этом случае мы можем принимать оператор ! (null assertion operator), который ставится после названия объекта.
Например, возьмем следующий код:
class Password{ String? text; // текст пароля String get(){ if(text == null){ text = "qwerty"; } return text; // ! Ошибка } } void main (){ Password myPassword = Password(); print(myPassword.get()); }
Здесь в классе Password поле text, которое хранит текст пароля, определено как значение String?
, то есть по умолчанию оно имеет значение null
. В методе get(), если text равно null, определяем для
него некоторое начальное значение. Причем логика метода get() такова, что метод в любом случае возвратить значение String - строку, а не null. Ведь даже если text равно null,
мы все равно присваиваем ему строку. И кажется, что пробелем никаких быть не должно. Но при выполнении программы мы столкнемся с ошибкой:
main.dart:9:16: Error: A value of type 'String?' can't be returned from a function with return type 'String' because 'String?' is nullable and 'String' isn't.
return text;
^
Здесь мы уверены, что метод в любом случае возвратит строку String, поэтому мы можем после названия объекта поставить оператор !:
return text!;
Полный код:
class Password{ String? text; String get(){ if(text == null){ text = "qwerty"; } return text!; // метод возвращает String } } void main (){ Password myPassword = Password(); print(myPassword.get()); }
Но следует еще раз отметить, что данный оператор следует использовать, когда у нас есть стопроцентная уверенность, что значение НЕ равно null. Если же такой уверенности нет, то лучше не злоупотреблять данным оператором и не использовать его.
Дополнительный оператор !. позволяет обратиться к полям или методам объекта, который представляет nullable-тип, но не равен null. Например:
class Password{ String? text; bool isValid(){ if(text == null){ text = ""; } return text.length > 6; // ! Ошибка } } void main (){ Password myPassword = Password(); bool isValid = myPassword.isValid(); print("Is Valid: $isValid"); }
Здесь та же ситуация, что и в предыдущем примере про оператор !. В методе isValid() мы проверяем длину текста пароля, используя поле length класса String, и возвращаем true, если длина пароля больше 6 символов (то есть пароль действителен). Причем мы уверены, что текст пароля - поле text не равно null, так как мы также устанавливаем для него значение, если о вдруг равен null. Но компилятор в этом не уверен. И чтобы компилятор тоже был уверен, для доступа к полям и методам объекта, который может быть равен null, используем оператор !.:
return text!.length > 6;
Полный код:
class Password{ String? text; bool isValid(){ if(text == null){ text = ""; } return text!.length > 6; // больше 6 символов } } void main (){ Password myPassword = Password(); bool isValid = myPassword.isValid(); // false print("Is Valid: $isValid"); // Is Valid: false }