Null safety в dart. Кратко и самое нужное
обновлено: 21.08.2023
Ссылки для изучения, официальная документация:
- https://dart.dev/null-safety Перевод – https://habr.com/ru/post/513466/
- https://dart.dev/null-safety/migration-guide
- https://dart.dev/null-safety/understanding-null-safety Полезная статья, однако! я не нашел перевода на русском языке! Вот такое у нас русскоязычное сообщество! (умное, наверное, очень 🙂 )
Далее я опишу кратко понимание sound null safety. Кратко не получилось, но зато постарался наиболее понятно.
dart 2.12.0 вышел 3 марта 2021 года. С этой версии по умолчанию применяется Null Safety. С этой версии код, написанный не в рамках null safety начал вызывать ошибку.
Sound null safety – можно перевести как надежная “нулевая” безопасность. Или непротиворечивый (в данном случае под этим понимается, что если у переменной объявлен тип int, то ничего не может там другого(противоречивого) находиться.)
В основе sound null safety лежит 3 принципа:
- Non-nullable by default (по умолчанию переменные не могут быть null)
- Incrementally adoptable
- Fully sound – полная надёжность или абсолютная непротиворечивость
1 принцип – Non-nullable by default
Теперь при использовании null safety c версии dart >=2.12.0 следующий код вызовет ошибку на этапе компилятора и приложение даже не запустится!:
void main() {
int num;
print(num);
}
//The non-nullable local variable 'num' must be assigned before it can be used.
//Try giving it an initializer expression, or ensure that it's assigned on every execution path.dart(not_assigned_potentially_non_nullable_local_variable)
Избежать её можно назначив значение переменной при ее обозначении:
void main() {
int num = 1;
//num = null; // выведет ошибку, т.к. нельзя переменной присваивать null
print(num);
}
Если мы всё таки хотим присвоить переменной null, то нужно её объявить через знак “?” или через var. А именно:
void main() {
int? num;
var name;
print(num); //выведет null без ошибки. //выведет null. Теперь нам не обязательно назначать значение переменной при ее обозначении, кроме того, мы может в любом участке кода её присвоить null.
print(name); //выведет null без ошибки, т.к. мы объявили переменную name через var
}
2 принцип – Incrementally adoptable – постепенная адаптируемость (к null safety)
Надо сказать. что с введением Null safety в dart произошло фундаментальное изменением типов. Поэтому мы в проекте можем отключить принудительное Null safety. Или переходить на него частично, поэтапно. Можно комбинировать – наше приложение может не поддерживать null safety, а пакеты могут поддерживать null safety.
3 принцип – Fully sound
Что переводиться полная надежность. Т.е. dart гарантирует, что переменные, которые используют тип null safety никогда не может быть null, тем самым исключая падение вашего приложение с критической ошибкой из-за null в переменной.
Кроме того, компилятор Ahead of time с использованием null safety может производить более краткий нативный код, что сказывается в лучшую сторону на производительности.
Понимание типов в Null safety
Какие были системы типов в dart до v. 2.12 (Null safety). До null safety статическая система типов в dart позволяла значению null подменить экземпляр любого типа, это видно на схеме:
В следствии чего, возникали критические ошибки, например, при таком коде:
bad(String maybeString) {
print(maybeString.length); // у null нет свойства length!
}
main() {
bad(null);
}
В null safety null теперь null больше не является подтипом всех типов. А существует наравне с другими типами. И соответственно все типы в dart по умолчанию стали non-nullable типами.
Top и bottom
Итак системы типов dart до и после null safety. В любой системе типов языка есть Top и Bottom тип.
Top – от которого наследуются всего объекты. Bottom – тип, который наследуется от всех типов объектов, которые существуют. Раньше это был Object и Null. А сейчас это Object? и новый тип – Never. Object? имеет потомков Object (non-nullable) и непосредственно Null. И теперь тип Never является подтипом всех типов.
И теперь каждый тип со знаком вопроса (?) имеет 2 вариации: non-nullable и null:
В конечном итоге теперь у нас 2 группы типов: non-nullable (без знакака вопроса) и nullable (со знаком вопроса):
Тип never используется довольно редко и используется как возвращаемый тип при ошибке.
Поведение Promotion
Подразумевает обязательную проверка через if для безопасного использования переменной. Эту проверку мы можем делать только в рамках локальных переменных. Такая проверка не пройдет с глобальными переменными:
void main() {
print(method(null));
}
int method(int? value) {
if (value == null) {
//return 0; или
return valueIsNotDefined();
}
return value;
}
Never valueIsNotDefined() {
throw ArgumentError('Value is not defined');
}
Поведение Definite Assignment
Также предоставляется анализатором потока выполнения. Dart уверен, через логику, что переменной присваивается какое-либо значение, отличное от null:
void main() {
int a;
if (25 > 0) {
a = 1;
} else {
a = -1;
}
print(a); //1
}
Операторы Null-aware
Рассмотрим 6 операторов.
1. Заменяемой if на тернарный оператор – ??
void main() {
print(method(null)); //1
}
int method(int? value) {
// return value == null ? 0 : value;
//равноцено:
return value ?? 0;
}
2. Оператор присваивания – ??=
Легко читается: Если переменная равна null, то присваиваем ей значение.
void main() {
int? number;
number ??= 999; // Оператор присваивания срабатывает, если значения переменной = null
print(number); //999
}
3. Оператор доступа – ?.
Мы не можем быть уверены, ведь в противном случае у Null не вызовется метод, например:
void main() {
int? distance;
print(distance?.abs()); //null
print(distance?.abs() ?? 0); //0 (можно также применить оператор if null)
}
4. Оператор bang (Оператор утверждения) – !
Применяется, когда мы уверены на 100%, что переменная будет содержать нужное значение, иначе в процессе исполнения программы вылезет ошибка (исключение):
void main() {
int? distance;
print(distance!); //Unhandled exception!
}
5. Оператор преобразования типа – as
Преобразует тип, если типы сопоставимы:
void main() {
int? distance;
distance = 10;
print(distance as num ); //10
}
6. Оператор каскадный null-aware – ?..
Если переменная у нас Non-nullable, то доступ к методам осуществляется через .. Каскадный оператор ниже выполняется, когда myCar не равен null
void main() {
Car myCar = Car();
myCar.buy('Roy');
myCar.sales('Richard');
//Равнозначно
myCar
..buy('Roy')
..sales('Richard');
}
class Car {
void sales(String name) {}
void buy(String name) {}
}
А теперь напишем, когда myCar может быть null
void main() {
Car? myCar;
myCar?.buy('Roy');
myCar?.sales('Richard');
//Равнозначно
myCar
?..buy('Roy')
..sales('Richard');
}
class Car {
void sales(String name) {}
void buy(String name) {}
}
Модификатор late:
1. Late variables
Если в конструкторе приравнивать значение свойств, ошибки не будет:
class Car {
int id;
String name;
int price;
Car(this.id, this.name, this.price);
//Car(): id=2, name='Volvo', price = 390; //или так
String printCar() {
print('Car is free');
return 'Car: id: $id, name: $name, price: $price';
}
}
Однако, если приравнивать значение свойств в методе, допустим printCar(), то возникнет ошибка, для этого и нужен модификатор late:
void main() {
Car car = Car();
print(car.printCar());
}
class Car {
late int id;
late String name;
late int price;
String printCar() {
id = 2;
name = 'Volvo';
price = 390;
print('Car is free');
return 'Car: id: $id, name: $name, price: $price';
}
}
2. Late lazy initialization
Мы можем переменной присвоить метод экземпляра класса. Но! Выполняться этот метод будет тогда, когда мы что-либо будем делать с этой переменной!
void main() {
late var car = Car().printCar();
//print(car); //только в этом случае в методе printCar() будет выводиться и выполняться
}
class Car {
late int id;
late String name;
late int price;
String printCar() {
id = 2;
name = 'Volvo';
price = 390;
print('Car is free'); // Не выводится!
return 'Car: id: $id, name: $name, price: $price'; // Не выполняется
}
}
3. Late final variables
Если мы объявляем переменную как final, то мы сразу должны дать ей значение. Однако использование late – также избавляет нас дать сразу же значение.
void main() {
var car = Car().printCar();
}
class Car {
late final int price; //без late нужно было б сразу присваивать значение, т.к. final
void printCar() {
price = 390;
//price = 390;// можно присваивать 1 раз, иначе ошибка
}
}
List<>, Map<,>
List<>
??=, ??
если null, то присваиваем значение
void main() {
List list = ['A', null, 'b', 'c', null, null];
list[1] ??= 'e'; // если null, то присваиваем значение
print(list); //[A, e, b, c, null, null]
String name = list[4] ?? 'empty'; //в случае null, присваиваем значение
}
!, ?[] null-aware оператор, доступ по индексу
void main() {
List? list;
//1 пример
list[0]; //Ошибка, компилятор не запуститься
//2 пример
// говорим, что точно не null. (хотя у нас значение null)
list![0]; // компилятор запускается, но потом ошибка
//3 пример
//?[] null-aware оператор дооступ по индексу
print(list?[0]); //выводит null, критической ошибки нет.
}
Map<,>
??, ??= присвоение значения, в случае null
void main() {
Map categories = {'first': 1, 'second': 2};
print(categories['third']); //выведет null
int third1 = categories['third'] ?? 3; //не записывает в categories
print(categories); //{first: 1, second: 2}
int third2 = categories['third'] ??= 3; //записывает в categories
print(categories); //{first: 1, second: 2, third: 3}
}
Обращение по ключу:
void main() {
Map categories = {'first': 1, 'second': 2};
int value = categories['first']; //ошибка, компилятор не запустится
int? value2 = categories['first']; //работает
int? value3 = categories['first2']; //работает
int value4 = categories['first']!; //работает
int value5 = categories['first2']!; //не работает, ошибка при выполнении!
}
Работа с классом
void main() {
//var myHotel = Hotel(id: 1, name:'asdf', price: 55);
var myHotel = Hotel(id: 1, name: 'NYC', price: 400);
var myHotel2 = Hotel(id: 1, name: 'NYC', price: 400);// именнованные, обязательные, допускающие NULL
print(myHotel.printHotel());
}
class Hotel {
final int? id;
final String? name;
final int? price;
late final bool is_big;
Hotel({
required this.id,
//required this.id = 2, //ошибка! requered нельзя присваивать
required this.name,
required this.price
}) {
is_big = true;
}
String printHotel() {
print('Hotel is working...');
return "$id $name $price $is_big";
}
}
The argument type ‘Function’ can’t be assigned to the parameter type ‘void Function()?’
Эта проблема появилась, при переходе на Null safety, прежде рабочий код вызывает такую ошибку. Что делать
Старый код:
class _CartCounterState extends State {
int numOfItems = 1;
@override
Widget build(BuildContext context) {
return Row(
children: [
countersButtons(
icon: Icons.remove,
press: () {
if (numOfItems > 1) {
setState(() {
numOfItems--;
});
}
}),
],
);
}
SizedBox countersButtons({IconData? icon, Function? press}) {
return SizedBox(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(13),
),
),
child: Icon(icon),
onPressed: press, //The argument type 'Function?' can't be assigned to the parameter type 'void Function()?'
),
);
}
}
Новый код. Нужно поменять Function? на voidCallBack?
class _CartCounterState extends State {
int numOfItems = 1;
@override
Widget build(BuildContext context) {
return Row(
children: [
countersButtons(
icon: Icons.remove,
press: () {
if (numOfItems > 1) {
setState(() {
numOfItems--;
});
}
}),
],
);
}
SizedBox countersButtons({IconData? icon, VoidCallback? press}) {
return SizedBox(
child: OutlinedButton(
style: OutlinedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(13),
),
),
child: Icon(icon),
onPressed: press,
),
);
}
}
Спасибо за статью! очень крутая!
вопросы:
в 2. Оператор присваивания – ??=
print(number); //1 – почему 1? может 999
в List, Map List ??=, ??
print(list); //[A, e, b, c, null] – может [A, e, b, c, null, , null] должно быть?
в The argument type ‘Function’ can’t be assigned to the parameter type ‘void Function()?’
можно ли привести более короткий пример кода?)) (как-то тяжело в ваш пример вникать без большого опыта разработки во Flutter)
еще раз спасибо!)
в 2. Оператор присваивания – ??=
print(number); //1 – почему 1? может 999
– да! всё правильно! будет 999. вы молодец! это у меня неправильный комментарий!
=====================
в List, Map List ??=, ??
print(list); //[A, e, b, c, null] – может [A, e, b, c, null, , null] должно быть?
да, все верно, опять я из-за скорости не перепроверил! Вы правы!
код:
List list = [‘A’, null, ‘b’, ‘c’, null, null];
list[1] ??= ‘e’; // если null, то присваиваем значение
//конечно, в итоге получается
print(list); //[A, e, b, c, null, null]
=====================
Не обращайте внимание там где Нужно поменять Function? на voidCallBack?.
Это у меня был проект, который я переводил на Null safety, и новую версию флаттера, возникла такая проблема. это относится даже к флаттеру, что тут нужно указывать другой тип. Сделал, на случай, чтобы находили эту статью по этой ошибке, если она еще у кого-то будет.
=====================
Спасибо! Подправлю опечатки! Всегда приятно видеть обратную связь!
Сорри, что ответил не сразу)