系统化掌握Dart编程之空安全

388 阅读7分钟

image.png

前言

在现代编程中,空值null) 处理一直是开发者面临的挑战之一。空引用可能导致程序崩溃产生难以调试的错误,尤其是在大型项目中。

时至今日,空安全已经是一个屡见不鲜的话题,目前像主流的编程语言KotlinSwiftRust 等都对空安全有自己的支持。

Dart 作为一种现代化的编程语言,自版本 2.12 开始引入了 空安全Null Safety特性从根本上解决了这一问题

空安全不仅提高了代码的健壮性可预测性,还使得开发者能够更加自信地构建和维护复杂的应用程序。

千曲而后晓声,观千剑而后识器。虐它千百遍方能通晓其真意

一、空值问题

空值null) 表示一个未初始化不存在的对象引用。虽然空值有时是必要的,但不当使用导致程序崩溃产生难以调试的错误。例如,在 JavaC# 中,访问一个 null 引用的属性或方法会导致 NullPointerException类似的异常

String name = null;
int length = name.length(); // 这里会抛出 NullPointerException

空指针异常搞得很多人怀疑人生

image.png

二、什么是空安全?

空安全Null Safety) 是一种编程语言特性,旨在防止因变量对象引用null 而导致的错误。在支持空安全的语言中,类型系统能够区分 非空类型Non-nullable Types) 和 可空类型Nullable Types),从而在编译期就捕捉到潜在的空指针异常减少运行时错误的发生

空安全会在编译期防止意外访问 null 变量的错误的产生。

健全的空安全通过对非空变量的未被初始化或以 null 初始化的情况进行标记,把潜在的 运行时错误 转变成了 编辑时 的分析错误。

简而言之,空安全是通过类型系统重构编译时静态验证为主运行时兜底为辅的核心机制,将运行时NPE转化为编译时可预防的错误

此特性在开发应用的过程中修复这类错误:

  • 没有以非空的值初始化
  • 赋了 null 值。

示例代码

// 声明非空变量
var i = 42; // Inferred to be an int.
//声明可空变量
int? aNullableInt = null;

有了空安全后的状态

image.png

三、空安全的原则

空安全支持基于以下两条核心原则

  • 1、默认不可空。除非将变量显式声明为可空,否则它一定是非空的类型。经研究后发现,非空是目前的 API 中最常见的选择,所以选择了非空作为默认值
  • 2、完全可靠Dart空安全是非常可靠的,意味着编译期间包含了很多优化。如果类型系统推断出某个变量不为空,那么它 永远 不为空。

做个遵守原则的人:

image.png

四、空安全的核心理念

通过编译期检查来确保代码不会意外地尝试使用 null 值。具体来说:

  • 非空类型:默认情况下,所有类型都是非空的,意味着它们不能为 null。例如,String 表示一个非空字符串
  • 可空类型:如果一个变量可以为 null,则需要显式声明为可空类型,如 String?

通过这种区分,编译器可以在编译阶段检测到可能的空值问题,并要求开发者明确处理这些情况,从而避免运行时错误。

五、空安全的优势

  • 减少运行时错误:通过编译期检查,提前捕获潜在的空指针异常,减少运行时错误的发生
  • 提高代码质量:明确的空值处理规则使得代码逻辑更加清晰,减少了不必要的 if (x != null) 检查。
  • 增强团队协作:空安全特性有助于团队成员更好地理解代码意图,确保每个人都遵循一致的空值处理规则
  • 优化性能:避免不必要的空值检查防御性编程,可以使代码更加简洁高效。

六、引入空安全前后类型系统的变化

  • 1、在引入空安全前Dart类型系统是这样的: image.png 这意味着在之前,所有的类型都可以为Null,也就是Null类型被看作是所有类型的子类
  • 2、在引入空安全之后: image.png 可以看出,最大的变化是将Null类型独立出来了,这意味着Null不再是其它类型的子类型,所以对于一个非Null类型的变量传递一个Null值时会报类型转换错误

七、空安全操作符

7.1、空合并操作符 (??)

左侧表达式null 时,使用右侧表达式的值作为默认值

void main() {
  String? nullableName;

  // 如果 nullableName 为 null,则使用 "Guest" 作为默认值
  String displayName = nullableName ?? "Guest";
  print(displayName); // 输出: Guest
}

7.2、空感知赋值操作符 (??=)

只有在左侧表达式null 时,才执行赋值操作。

void main() {
  String? nullableName;

  // 如果 nullableName 为 null,则赋值为 "Guest"
  nullableName ??= "Guest";
  print(nullableName); // 输出: Guest

  // 如果已有值,则不再赋值
  nullableName ??= "Alice";
  print(nullableName); // 输出: Guest
}

7.3、安全调用操作符 (?.)

允许安全地访问对象的属性或调用方法,如果对象为null,则返回 null

void main() {
  String? nullableName;

  // 如果 nullableName 为 null,则 length 也为 null
  int? length = nullableName?.length;
  print(length); // 输出: null
}

7.4、断言操作符 (!)

用于断言某个表达式不为空。使用时需谨慎,确保表达式确实不会为 null,否则会导致运行时异常

void main() {
  String? nullableName;

  // 如果 nullableName 为 null,则会抛出异常
  String nonNullName = nullableName!;

  // 更安全的做法是先检查是否为 null
  if (nullableName != null) {
    String nonNullName = nullableName!;
  }
  
  // ! 另外一个常见的用处:
  bool isEmptyList(Object object) {
   if (object is! List) return false;
   return object.isEmpty;
  }
}

八、延迟初始化late的使用

对于无法在定义时进行初始化,并且又想避免使用?.,那么延迟初始化可以帮到你。通过late修饰的变量,可以让开发者选择初始化的时机,并且在使用这个变量时可以不用?.

void main() {
 late List urls;//延时初始化 
 setUrls(List urls) {
   this.urls=urls; 
 } 
 int getUrlLen() { 
  return urls.length; 
 }

延时初始化虽然能为我们编码带来一定便利,但如果使用不当会带来空异常的问题,所以在使用的时候一定保证赋值和访问的顺序,切莫颠倒

九、required 的作用

required 关键词主要用于函数参数构造函数参数中,以确保调用者必须提供这些参数的值。这是空安全(null safety)特性的一部分,旨在提高代码的健壮性可预测性

9.1、构造函数中的required

当定义一个类时,使用 required 可以确保每个实例都必须初始化某些字段。这对于那些对对象状态至关重要的属性非常有用,可以防止忘记设置必要的参数

class Person {
  final String name;
  final int age;

  // 使用 required 确保 name 和 age 必须被传递
  Person({required this.name, required this.age});
}

void main() {
  // 正确的调用方式
  var person = Person(name: "Alice", age: 30);
  print(person.name); // 输出: Alice
  print(person.age);  // 输出: 30

  // 错误的调用方式(缺少 required 参数)
  // var person2 = Person(); // 编译错误:缺少 name 和 age 参数
}

9.2、函数参数中的required

除了构造函数,required 也可以用于普通函数的命名参数,确保调用者必须提供这些参数。

void greet({required String name, String greeting = "Hello"}) {
  print('$greeting, $name!');
}

void main() {
  // 正确的调用方式
  greet(name: "Alice"); // 输出: Hello, Alice!
  greet(name: "Bob", greeting: "Hi"); // 输出: Hi, Bob!

  // 错误的调用方式(缺少 required 参数)
  // greet(); // 编译错误:缺少 name 参数
}

9.3、required 的作用与优势

  • 强制性:确保关键参数不会被遗漏,提高了代码的可靠性。
  • 清晰性:明确哪些参数是必需的,使得 API 更加直观易懂。
  • 空安全:结合 Dart 的空安全机制,帮助避免潜在的 null 值问题。

9.4、注意事项

  • required 只能用于命名参数,不能用于位置参数。位置参数本身就要求按顺序提供。
  • required 参数必须在调用时显式提供,即使它们有默认值。

十、总结

空安全是一种重要的编程语言特性,通过区分非空类型可空类型,并结合专用的操作符,它能够在编译期捕捉潜在的空指针异常减少运行时错误的发生。掌握空安全不仅提升了代码的质量健壮性,还帮助开发者编写更加简洁高效的代码。

码字不易,记得 关注 + 点赞 + 收藏 + 评论