Null safety в dart. Кратко и самое нужное

обновлено: 21.08.2023

Ссылки для изучения, официальная документация:

Далее я опишу кратко понимание sound null safety. Кратко не получилось, но зато постарался наиболее понятно.

dart 2.12.0 вышел 3 марта 2021 года. С этой версии по умолчанию применяется Null Safety. С этой версии код, написанный не в рамках null safety начал вызывать ошибку.

Sound null safety – можно перевести как надежная “нулевая” безопасность. Или непротиворечивый (в данном случае под этим понимается, что если у переменной объявлен тип int, то ничего не может там другого(противоречивого) находиться.)

В основе sound null safety лежит 3 принципа:

  1. Non-nullable by default (по умолчанию переменные не могут быть null)
  2. Incrementally adoptable
  3. 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,
      ),
    );
  }
}
Рубрика: dart

2 ответа к «Null safety в dart. Кратко и самое нужное»

  1. Спасибо за статью! очень крутая!

    вопросы:
    в 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. в 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, и новую версию флаттера, возникла такая проблема. это относится даже к флаттеру, что тут нужно указывать другой тип. Сделал, на случай, чтобы находили эту статью по этой ошибке, если она еще у кого-то будет.
    =====================
    Спасибо! Подправлю опечатки! Всегда приятно видеть обратную связь!
    Сорри, что ответил не сразу)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *