一、前言
在dart 2.12版本之前,任何一種類型都可以為null,所有的類型默認(rèn)都是null,這樣也就意味著我們每次使用一個(gè)變量的時(shí)候,可能都需要判斷一個(gè)變量是否為空,否則就可能拋出NoSuchMethodError異常,這樣在我們的開(kāi)發(fā)中就很頭痛,一不小心就會(huì)出現(xiàn)NoSuchMethodError異常,導(dǎo)致程序崩潰,為了減少這種情況的發(fā)生,我們就不得不在代碼中添加很多的冗余判斷代碼。
后來(lái)官方可能也是實(shí)在容忍不了這種現(xiàn)象,就像其他語(yǔ)言比如Swift、Kotlin一樣,引入了健全的空安全檢查機(jī)制。
下面這個(gè)例子,就是目前我們?cè)跊](méi)有引入空安全時(shí),要面臨的問(wèn)題。我們聲明一個(gè)變量,并且調(diào)用它的一些API,在使用之前必須要判斷是否為空:
int nullableInt; //null
if (nullableInt != null) {
print('nullableInt ==== ${nullableInt.toDouble()}');
} else {
print('nullableInt ==== $nullableInt');//打印結(jié)果:nullableInt ==== null
}
//使用前沒(méi)有判空
print('nullableInt ==== ${nullableInt.toDouble()}');
//拋出異常
Unhandled exception:
NoSuchMethodError: The method 'toDouble' was called on null.
Receiver: null
Tried calling: toDouble()
二、什么是空安全?
空安全是一種對(duì)可能為空的變量在編譯期就進(jìn)行校驗(yàn)的一種審查機(jī)制。這種檢查機(jī)制對(duì)所有變量的默認(rèn)值也進(jìn)行了改變,以前默認(rèn)為null,現(xiàn)在除非時(shí)候顯式聲明為可為空,所有都按照非空處理,在可能為null的情況下,如果不進(jìn)行非空判斷,dart的分析器就會(huì)報(bào)錯(cuò),從而減少我們?cè)陂_(kāi)發(fā)中的NoSuchMethodError情況的出現(xiàn)。
三、使用
要使用空安全機(jī)制,需要把dart版本設(shè)置為2.12版本及以上,具體環(huán)境配置在pubspec.yaml文件中,如果想聲明一個(gè)可空變量,需要顯示在類型后面添加?。
name: dart_study
description: A simple command-line application.
# version: 1.0.0
# homepage: https://www.example.com
environment:
sdk: '>=2.12.0 <3.0.0' #修改dart環(huán)境版本,支持空安全
#dependencies:
# path: ^1.7.0
dev_dependencies:
pedantic: ^1.9.0
3.1 基本類型
創(chuàng)建變量時(shí),如果想要一個(gè)變量可為null,需要顯示聲明為nullable類型,具體就是使用?進(jìn)行聲明。比如String?、int?、double?等等。看下面的實(shí)例:
例子1:顯式創(chuàng)建一個(gè)可為null的int類型變量
int? nullableInt;
print(nullableInt.toDouble()); //The method 'toDouble' can't be unconditionally invoked because the receiver can be 'null'.
這種變量在使用時(shí),必須要判斷是否為空,直接使用話,編譯不通過(guò),會(huì)拋出異常,告知使用了可空的變量
bin/dart_study.dart:4:21: Error: Method 'toDouble' cannot be called on 'int?' because it is potentially null.
Try calling using ?. instead.
print(nullableInt.toDouble());
^^^^^^^^
正確的寫法應(yīng)該是
print(nullableInt?.toDouble() ?? '為空了');
例子2:創(chuàng)建一個(gè)非空的int類型變量,如果判斷是否為null時(shí),會(huì)有個(gè)警告,這時(shí)我們就可以減少不必要的判斷,而直接使用變量。
int nonNullInt = 7;
print(nonNullInt);
if (nonNullInt != null) {//The operand can't be null, so the condition is always true.
print(nonNullInt);
}
3.2 集合類型
對(duì)于List、Map、Set類型來(lái)說(shuō),在使用時(shí),不僅要看集合變量是否可空,還要看里面的數(shù)據(jù)類型是否可空。對(duì)于List和Set我們可以大致總結(jié)為下面的表格:
| 類型 | 列表是否可為空 | 元素是否可為空 | 說(shuō)明 |
|---|---|---|---|
| List<String> | 否 | 否 | 一個(gè)包含非空字符串的非空列表 |
| List<String>? | 是 | 否 | 一個(gè)包含非空字符串的可空列表 |
| List<String?> | 否 | 是 | 一個(gè)包含可空字符串的非空列表 |
| List<String?>? | 是 | 是 | 一個(gè)包含可空字符串的可空列表 |
var nullableList = [1, null, 3];
print(nullableList[0]?.toDouble()); //校驗(yàn)空
var nonNullList = [1, 2, 3];
print(nonNullList[0].toDouble());//無(wú)需校驗(yàn)空
同樣,對(duì)于Map類型也可以總結(jié)為一個(gè)表格:
| 類型 | Map是否可為空 | 元素是否可為空 | 說(shuō)明 |
|---|---|---|---|
| Map<String: int> | 否 | 否 | 所有元素不可空的非空Map,但取值結(jié)果表面不可為空,如果使用的是無(wú)效Key,取出的是空 |
| Map<String: int>? | 是 | 否 | 所有元素不可空的可空Map,但取值結(jié)果表面不可為空,如果使用的是無(wú)效Key,取出的是空 |
| Map<String: int?> | 否 | 是 | 元素可空的非空Map,key是否有效取值都可為空 |
| Map<String: int?>? | 是 | 是 | 元素可空的可空Map,key是否有效取值都可為空 |
Map和List、Set不同的一點(diǎn)是,盡管Map規(guī)定了元素非空,但是如果使用的是無(wú)效Key取值的話,仍然返回的是一個(gè)可空值,所以默認(rèn)情況下,我們?cè)谑褂米兞拷邮杖〕龅闹禃r(shí),一般都需要使用可空類型接收。
var nullableMap = <String, int?>{'one': 1};
int? result = nullableMap['two'];
print('取出的值為${result?.toDouble()}'); //必須校驗(yàn)空
var nonNullMap = <String, int>{'one': 1};
print('取出的值為${nonNullMap['two']?.toDouble()}'); //必須校驗(yàn)空
四、空斷言運(yùn)算符
當(dāng)然,如果我們確定某個(gè)Key是有值的,但是編輯器認(rèn)為它可能為null,這個(gè)時(shí)候我們不想判斷一次,我們可以使用!進(jìn)行一次強(qiáng)轉(zhuǎn),
但是如果不能確定有值的情況,萬(wàn)萬(wàn)不可使用這種方式。
var nullableMap = <String, int?>{'one': 1};
int resultOne = nullableMap['one']!; //我們能夠確?!痮ne‘一定有值
print('取出的值為${resultOne.toDouble()}');
五、關(guān)鍵字 late
顧名思義,晚點(diǎn)。。。我們可以使用這個(gè)關(guān)鍵字對(duì)變量進(jìn)行修飾,使用late修飾的變量,可以認(rèn)為是一種懶加載,分析器也不會(huì)要求我們立刻對(duì)一個(gè)非空類型進(jìn)行賦值,只要我們能夠保證在第一次使用的時(shí)候進(jìn)行賦值,就可以了。
比如我們定義一個(gè)類Person:
class Person {
int age;
String name;
///
/// Non-nullable instance field 'age' must be initialized. (Documentation)
/// Try adding an initializer expression, or add a field initializer in this constructor,
/// or mark it 'late'.
///
Person();
}
我們可以通過(guò)添加late關(guān)鍵字,延遲加載:
class Person {
late int age;
late String name;
Person(); //編譯器不會(huì)報(bào)錯(cuò)
}