DEV Community

Ge Ji
Ge Ji

Posted on

Dart Lesson 11: Null Safety (Part 2) — Safe Operators Explained

Today, we'll dive deep into various safety operators and practical techniques in null safety. Mastering these will allow us to handle null values more flexibly and efficiently while ensuring code security.

I. Null-Check Operator (?.) : Elegantly Handling Nullable Objects

In null safety, directly accessing properties or methods of a nullable type will cause compilation errors. The null-check operator ?. helps us handle this scenario concisely: it first checks if the object is null – if so, it returns null; otherwise, it accesses the property or method normally.

1. Basic Usage

void main() {
  String? name = "Dart";
  int? length = name?.length; // Access length using ?.
  print(length); // Output: 4 (name is not null, returns length normally)

  name = null;
  length = name?.length; // name is null, returns null
  print(length); // Output: null
}
Enter fullscreen mode Exit fullscreen mode

Compared to traditional if checks, ?. makes code more concise:

// Traditional approach
int? getLength(String? str) {
  if (str != null) {
    return str.length;
  } else {
    return null;
  }
}

// Simplified with ?.
int? getLength(String? str) => str?.length;
Enter fullscreen mode Exit fullscreen mode

2. Chained Calls

?. supports chained calls, which is ideal for handling multi-level nested nullable objects. Note: If any link in the chain is null, subsequent assignment operations will not execute.

class Address {
  String? city; // City (nullable)
}

class User {
  Address? address; // Address (nullable, needs initialization first)
}

void main() {
  User? user = User();
  // Error example: user's address is uninitialized (null), assignment fails
  user?.address?.city = "Beijing";
  String? city1 = user?.address?.city;
  print(city1); // Output: null (assignment failed because address is null)

  // Correct example: Initialize all links before assignment
  user = User();
  user.address = Address(); // First initialize address
  user?.address?.city = "Beijing"; // All links in chain are non-null
  String? city2 = user?.address?.city;
  print(city2); // Output: Beijing (assignment successful)

  // When user is null, entire entire chain returns null
  user = null;
  String? city3 = user?.address?.city;
  print(city3); // Output: null
}
Enter fullscreen mode Exit fullscreen mode

Without ?., chained calls would require multiple if checks, making code cumbersome.


II. Null Assertion Operator (!) : When Must It Be Used?

The core purpose of the ! operator is to allow developers to explicitly guarantee a nullable variable's non-nullability when the compiler cannot confirm it through code logic. However, in scenarios where all code branches are covered, the compiler will automatically infer non-nullability, making ! unnecessary.

1. Scenarios Requiring !: Uncovered Branches Exist

void main() {
  String? value;

  // Conditional branches not fully covered (only handles even numbers)
  if (DateTime.now().second % 2 == 0) {
    value = "Even second";
  }

  // Compiler detects potential unassigned branch, ! is required
  print(value!.length); // Output: length (if assigned) or crash (if not)
}
Enter fullscreen mode Exit fullscreen mode

2. Scenarios Requiring !: All Branches Assign Values

void main() {
  String? value;

  // Conditional branches fully covered (both if and else assign values)
  if (DateTime.now().second % 2 == 0) {
    value = "Even second";
  } else {
    value = "Odd second";
  }

  // Compiler confirms all branches assign values, no ! needed
  print(value.length); // Output: string length
}
Enter fullscreen mode Exit fullscreen mode

3. Risk Warning

! essentially represents a "developer's guarantee to the compiler." If this guarantee fails (variable is null), it will crash immediately:

void main() {
  String? name = null;
  print(
    name!.length,
  ); // Runtime crash: Null check operator used on a null value
}
Enter fullscreen mode Exit fullscreen mode

Best Practice: Whenever you can use if (var != null) for null checking, avoid using !.


III. late Keyword: Lazy Initialization of Non-Nullable Variables

In some scenarios, we can't initialize non-nullable variables at declaration (e.g., dependent on external data loading) but can guarantee initialization before use. The late keyword allows lazy initialization of non-nullable variables.

1. Solving the "Non-Nullable Variable Must Be Initialized" Problem

class User {
  // Non-nullable variable that can't be initialized at declaration
  late String name; // Use late for lazy initialization

  // Initialize after constructor
  User(String userName) {
    name = userName; // Lazy initialization
  }
}

void main() {
  User user = User("Alice");
  print(user.name); // Output: Alice
}
Enter fullscreen mode Exit fullscreen mode

2. Lazy Loading Feature

late variables execute initialization logic only on first access, making them suitable for resource-intensive objects:

// Simulate time-consuming initialization
String fetchData() {
  print("Fetching data...");
  return "Remote data";
}

void main() {
  late String data = fetchData(); // fetchData() doesn't execute at declaration
  print("Before accessing data");
  print(data); // fetchData() executes on first access
  print(data); // Uses cached value on subsequent accesses
}

// Output:
// Before accessing data
// Fetching data...
// Remote data
// Remote data
Enter fullscreen mode Exit fullscreen mode

3. Risk: Crash If Accessed Before Initialization

Accessing a late variable before initialization throws a runtime exception, so ensure initialization before use:

void main() {
  late String value;
  // print(value); // Runtime crash: LateInitializationError: Field 'value' has not been initialized
  value = "Hello"; // Must initialize before use
}
Enter fullscreen mode Exit fullscreen mode

IV. Null Coalescing Operator (??) and Null Assignment Operator (??=)

Dart provides two additional useful null-handling operators:

1. Null Coalescing Operator (??) : Provide Default Values

?? returns a default value when the variable is null, otherwise returns the variable itself:

void main() {
  String? name = null;
  String displayName = name ?? "Guest"; // name is null, use default
  print(displayName); // Output: Guest

  name = "Bob";
  displayName = name ?? "Guest"; // name is not null, use name's value
  print(displayName); // Output: Bob
}
Enter fullscreen mode Exit fullscreen mode

2. Null Assignment Operator (??=) : Assign Only If Null

??= assigns a value only when the variable is null, avoiding overwriting existing values:

void main() {
  String? message = null;
  message ??= "Hello"; // message is null, assign value
  print(message); // Output: Hello

  message ??= "World"; // message is not null, no assignment
  print(message); // Output: Hello
}
Enter fullscreen mode Exit fullscreen mode

These operators are often used with ?. to handle complex null scenarios:

class User {
  String? name;
}

String? getUserName(User? user) {
  return user?.name ?? "Unknown"; // Return default if user or name is null
}

void main() {
  print(getUserName(null)); // Output: Unknown (user is null)
  print(getUserName(User())); // Output: Unknown (name is null)
  print(getUserName(User()..name = "Charlie")); // Output: Charlie
}
Enter fullscreen mode Exit fullscreen mode

V. Null Safety Best Practices

Mastering null safety requires not just syntax knowledge but also developing good programming habits:

  1. Prefer Non-Nullable Types: Choose non-nullable types by default, using nullable types only when necessary.
  2. Use ! Cautiously: Only use ! when the compiler truly cannot infer non-nullability and the developer is 100% certain the variable is non-null.
  3. Use late Appropriately: late is suitable for lazy initialization dependent on external conditions – avoid using it just to bypass compilation errors.
  4. Leverage Type Promotion: Let the compiler automatically infer non-nullability through if (var != null) to reduce explicit operators:
void printLength(String? text) {
  if (text != null) {
    // Compiler automatically promotes text to non-nullable
    print(text.length); // No need for !
  }
}
Enter fullscreen mode Exit fullscreen mode
  1. Initialize Non-Nullable Variables in Initializer Lists: Ensure non-nullable variables are initialized through constructor initializer lists:
class Person {
  String name;
  int age;

  // Assignment in initializer list
  Person({required String n, required int a}) : name = n, age = a;
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)