Dart Sound Null Safety in-depth analysis

This article is a simple summary of learning Understanding Null safety

Nullability in a type system

Null is very useful and can represent a nonexistent value, but can cause an exception if left unattended.

Null is essentially an instance of a null class, which can be treated as a subtype of any other type. It’s actually a form of polymorphism. The reason for the null pointer calling method/property error (i.eNoSuchMethodErrorAn exception comes from calling a method or property that does not exist in NULL.

Nullable and non-nullable in the type system

If you change the type hierarchy of Null, no type can be nullable directly. This variable is non-nullable.

A nullable type is more like a combination of a primitive data type and Null (e.g. String?). That means Null will be a subtype of any Nullable type.

Given the following example, info[‘familyName’] will be fetched as a null value, Type ‘Null’ is not a subtype of type ‘String’ in type cast Null is not a non-Nullable subtype. But what if I convert it to String? It works, combined with the second graph, because null is a String, right? Subtype of the Nullable type.

  Map<String.dynamic> info = {'name': 'laozhang'};
  // String familyName = info['familyName'] as String; // error
  String? familyName = info['familyName'] as String?; // ok
  print(familyName);
Copy the code

All types seem to be split into two, and non-nullable types allow you to access methods/properties without worrying about nullable Pointers. Non-nullable variables can be assigned to nullable variables because they are safe, but not vice versa.

Top and bottom types

Before null-safey, Object was the top type in the type hierarchy, while NULL was the bottom type. In a non-nullable type, Object? Will be its top type and Never will be its bottom type. That is, if you want to receive a value of any type in null-safey, use theObject?Rather thanObjectType. Indicates the use of a bottom typeNeverRather thanNull.

Characteristics and analysis for optimization and improvement

The following optimizations make static analysis smarter and more sensitive.

The return value

In null-safety, a warning is given if a value is not returned by return, and null is returned by default. Null-safety does not allow this for a non-nullable return value; the return value must be explicit.

Variable initialization

Non-nullable variables must be initialized before they can be used:

  1. Global and static variable declarations must be initialized
// Using null safety:
int topLevel = 0;
class SomeClass {
 static int staticField = 0;
}
Copy the code
  1. Initialize instance variables when they are declared directly or via a constructor (or using late)
class SomeClass {
int atDeclaration = 0;
int initializingFormal;
int initializationList;

SomeClass(this.initializingFormal)
    : initializationList = 0;
}
Copy the code
  1. Local variables can be assigned at any time, but must have been assigned before use. Okay
// Using null safety:
int tracingFibonacci(int n) {
int result;
if (n < 2) {
  result = n;
} else {
  result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
}

print(result);
return result;
}
Copy the code
  1. The named parameter must have a default value or be a Request keyword modifier.

Type of ascension

Null-safety solves the optimization of type promotion analysis. The following code can also run normally. Object instance will be promoted to List instance after IS judgment

bool isEmptyList(Object object) {
  if (object is! List) return false;
  return object.isEmpty; // <-- Error!
}
Copy the code

Never

Never can be used to interrupt or throw exceptions. Can be used as a type

Never wrongType(String type, Object value) {
  throw ArgumentError('Expected $type, but was ${value.runtimeType}. ');
}
Copy the code

The assignment

Analysis of final variable assignments has become more flexible and clever, and the following code no longer gives an error at null-safety.

// Using null safety:
int tracingFibonacci(int n) {
  final int result;
  if (n < 2) {
    result = n;
  } else {
    result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1);
  }

  print(result);
  return result;
}
Copy the code

Null checks for type promotion

Nullable variables == null or! Dart raises variable types to non-nullable, and arguments! List<String>? Promoted to List<String>

Note that type promotion has no effect on fields in a class. Because field usage is too flexible, static analysis can’t tell where is being used and where is being checked

// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if(arguments ! =null) {
    result += ' ' + arguments.join(' '); }}// or
// Using null safety:
String makeCommand(String executable, [List<String>? arguments]) {
  var result = executable;
  if (arguments == null) return result;
  return result + ' ' + arguments.join(' ');
}
Copy the code

Unnecessary Code Warnings

Perform a null check on a non-nullable type, as in:? ., == null,! Static analysis throws warnings or errors.

// Using null safety:
String checkList(List? list) {
  if (list == null) return 'No list';
  if(list? .isEmpty) {// list? .isEmpty Unnecessary code
    return 'Empty list';
  }
  return 'Got something';
}
Copy the code

Nullable type processing

Null-aware operator (null-aware)

Before null-safTY, all property/method calls were nullable because it was not known which node in the call chain might be null. .

String? notAString = null;
print(notAString? .length? .isEven);Copy the code

In null-safty, once the caller is null, the rest of the methods in the chain are skipped and not executed, i.e. short-circuited.

thing? .doohickey.gizmoCopy the code

Similar null-sensitive operators are:? . ? []

// Null-aware cascade:receiver? . method();// Null-aware index operator:receiver? [index];Copy the code

Null Assertion operator

When a nullable variable can be confirmed not to be null, you can use the as cast or! To assert that it will not be NULL. If the conversion or assertion fails, an exception is thrown; otherwise, a non-Nullable type is cast.

// Using null safety:
String toString() {
  if (code == 200) return 'OK';
  return 'ERROR $code ${(error as String).toUpperCase()}';
}

// Using null safety:
String toString() {
  if (code == 200) return 'OK';
  return 'ERROR $code ${error! .toUpperCase()}';
}
Copy the code

Newest variable

Non-nullable attributes or fields of a class must be initialized before they can be used (either by initializing the list in the constructor or by default), but you can also use the late modifier, which will constrain variables from compile to run time. Note, however, that using it at run time without assignment also throws an exception.

Lazy loading

Another benefit of late is that variables can be lazily loaded. The _temperature variable is not created directly at instance construction time but is deferred until it is first accessed. If this operates on the _temperature variable. For some operations that consume a lot of resources, you can postpone them when necessary.

By default, methods/properties of the instance are not allowed to be initialized because the instance has not yet been constructed. However, due to lazy loading, this initialized value now has access to the current instance’s methods/attributes, such as the _readThermometer() method because of the late keyword.

// Using null safety:
class Weather {
  late int _temperature = _readThermometer();
}
Copy the code

Newest final combination

Non-nullable variables do not need to be initialized at declaration time or constructor initialization and must be assigned once and only.

The Required modifier

Prevent non-nullable named parameters from being null. Type checking requires that named parameters use the required modifier or give the parameter a default value.

Nullable field processing

Type promotion has no effect on fields in a class.

class Coffee {
  String? _temperature;

  void heat() { _temperature = 'hot'; }
  void chill() { _temperature = 'iced'; }

  void checkTemp() {
    if(_temperature ! =null) {
      print('Ready to serve ' + _temperature + '! '); }}String serve() => _temperature! + ' coffee';
}
Copy the code

There are several ways to handle this. One is to add the assertion operator to _temperature directly! . The other is to copy variables into local variables and then do type promotion. If local variables change their value, remember to change the assignment value back to the field.

// Using null safety:
void checkTemp() {
  var temperature = _temperature;
  if(temperature ! =null) {
    print('Ready to serve ' + temperature + '! '); }}Copy the code

The nullability of generics

T The value can be a Nullable or non-nullable type.

// Using null safety:
class Box<T> {
  final T object;
  Box(this.object);
}

main() {
  Box<String> ('a string');
  Box<int?> (null);
}
Copy the code