# [null safety](https://dart.dev/null-safety/understanding-null-safety) ![top-and-bottom](https://hackmd.io/_uploads/BkTzDgtV1g.png) ![bifurcate](https://hackmd.io/_uploads/SJB7PeYVyl.png) null只有少數的屬性或方法可以使用, 例如: hashCode, toString(), ==, 所以var a=null時, 對a呼叫一般的方法或屬性會發生執行時錯誤, null safety就是要把錯誤限制在編譯時期發生. null safety的目的就是把上圖右側的可空類型想辦法變成左側的非空類型, 這樣才能 **安全的** 叫用類型的方法和屬性. 可以使用 ? 控制變數是否允許變數的值為null. ``` String a='aaa'; // 不可為null, 強迫宣告時要賦值 String? b; // 可為null ``` nullable變數的初始值就是null ``` int? a; assert(a==null); ``` nullable的變數在沒有初始化之前, 會限制其屬性或方法的存取 ``` Object? d; // print(d.isEven); // 不允許 ``` ## 全域變數(main()以外宣告的變數) 在宣告時就要初始化由於可以從程式中的任何位置存取和分配這些變量,因此編譯器不可能保證變數在使用之前已被賦予值。唯一安全的選擇是要求聲明本身俱有一個產生正確類型值的初始化表達式: ``` int a=23; void main() { print(a.isEven); // print: false } ``` ``` int? a; void main() { print(a?.isEven); // print: null } ``` main()內的頂層變數可以延遲宣告 ``` void main() { int a; a=23; print(a); } ``` ## class 的靜態欄位 必須宣告時就初始化, 因為和全域變數一樣, 可以在程式任意地方調用`SomeClass.staticField` ``` class SomeClass { static int staticField = 0; } ``` ## class 的實例欄位 ### class 的非空 field 必須在宣告時設定初始值 or 使用初始化形式參數 or 在建構函式的初始化清單中進行初始化 ``` class SomeClass { int atDeclaration = 0; // 直接設定 int initializingFormal; // 初始化形式參數 設定 int initializationList; // 初始化清單 設定 SomeClass(this.initializingFormal) : initializationList = 0; } ``` ### [class 的可空 field](https://dart.dev/null-safety/understanding-null-safety#working-with-nullable-fields) **只有** final+private instance field 可以獲得類型提升 類型提升最初僅適用於(函數內的)局部變量,現在從 Dart 3.2 開始也適用於class的 **private+final fields** ``` class MyClass { final String? name; // final but not private MyClass(this.name); // name仍然有可能是null void printName() { if (name != null) { // Error: Property 'length' cannot be accessed on 'String?' because it is potentially null. print(name.length); // name不是函數裡的區域變數, 流分析無法分析 } } } ``` 但如果 _name 是 final+private field, 則流分析會接受它, 因為: private: 會確保_name不會被lib外的subClass修改 final: 會確保_name不會被lib內的程式碼修改 結論: 建構子`MyClass(this._name);` 執行過後, _name的值就永遠固定了, 這使得printName()內部的`if (_name != null)`非空檢查是有意義的. ``` // ok class MyClass { final String? _name; // final + private MyClass(this._name); // _name仍然有可能是null void printName() { if (_name != null) { // 這裡的流分析會生效 print(_name.length); // _name will promote to String type } } } ``` 另一種解決方法是把nullable class field複製到函數裡面成為該函數的區域變數, 這樣flow analysis就會很樂意去檢查它. ``` // ok class MyClass { String? name; // 我就是不希望 name 是 final and/or private field MyClass(this.name); // 呼叫者有可能用 null 建構新物件 void printName() { // 把 name 複製到函數裡, 交給 flow analysis 處理 String? nameInFunc = name; if (nameInFunc != null) { print(nameInFunc.length); } } } ``` 也可以相信自己的判斷, 用非空斷言解決 ``` // OK class MyClass { String? name; // 我就是不希望 name 是 final and/or private field MyClass(this.name); // 呼叫者有可能用 null 建構新物件 void printName() { if (name != null) { print(name!.length); // 我手動加斷言 } } } ``` ## 函數內的區域變數 (局部變量) 可以交給flow analysis來判斷 non-null變數 是否在使用前有適當初始化(class內的field是沒有流分析的, 一律要初始化) ``` // flow analysis 會檢查result的賦值 int tracingFibonacci(int n) { int result; // 宣告時可以不初始化 if (n < 2) { result = n; } else { result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1); } print(result); return result; } ``` ``` int bigger(int a, int b) { if (a > b) { return a; } else if (a < b) { return b; } else { return 0; } } void main() { // main也是函數, 所以也有流分析 int c = 12; int d = 55; int ret; // flow analysis will determin ret's value ret = bigger(c, d); print(ret); } ``` ## 用 late 延遲檢查 用 **late** 修飾子延遲賦值, 編譯時期不會檢查其是否為空, 只有在執行時期, 第一次存取時才會檢查是否為空值(有可能造成執行期錯誤) ``` // 如果沒有late, 全域變數會強迫初始化 late var description; void main() { description = 'Feijoada!'; print(description); } ``` 對於下列場合, late會很方便: 某變數可能不會被叫用,並且初始化它的成本很高。 ``` // temperature也許永遠不會被存取, 但readThermometer()執行很花時間 late String temperature = readThermometer(); ``` 正在初始化一個實例變量,其初始化程序需要存取 this ``` class A { var c; late var d = c + 12; A(this.c); info() => this.d; } void main() { var a = A(1); print(a.info()); } ``` 函數的選擇性參數(named or position variables) 可選參數必須有預設值, 所以如果沒有賦值, 系統會為其賦值null, 但如果參數類型是不可空就會失敗. 所以需要將其設為nullable或指定有效的非null預設值 ``` // word1可為空, 初始值是null // word2不可為空, 要先賦值 // word3不可為空, 有強迫叫用者賦值 void say(name, {String? word1, String word2='',required String word3}) { print('$name says: $word1 $word2 $word3'); } ``` ## control flow analysis, type promotion, Never * **control flow analysis** is **Definite assignment analysis** (明確賦值分析) 流分析只會對函數的區域變數進行分析, 確定它們在被acess前都已被賦值 ``` int tracingFibonacci(int n) { // 流分析決定result是否有被賦值 int result; if (n < 2) { result = n; } else { result = tracingFibonacci(n - 2) + tracingFibonacci(n - 1); } print(result); return result; } ``` ``` // ok main()也是一個函數, 所以流分析會確定a=10 void main() { int a; bool condition = true; if (condition) { a = 10; } print(a + 12); } ``` ``` // Error: condition在函數以外, 流分析偵測不到 bool condition = true; void main() { int a; if (condition) { a = 10; } print(a + 12); } ``` ``` // 對函數參數進行流分析 bool isEmptyList(Object object) { if (object is List) { return object.isEmpty; // <-- OK! } else { return false; } } ``` * **Never** 底部類型Never沒有值, Never意味著表達式永遠無法成功完成執行。表達式一定會拋出異常、中止或以其他方式確保表達式被終止。 ``` Never wrongType(String type, Object value) { throw ArgumentError('Expected $type, but was ${value.runtimeType}.'); } ``` * type promotion 如果一個nullable變數經過流分析的驗證, 它有被適當的初始化, 不會是null, 則系統會把此可空變數提升為非空變數, 這樣才能叫用該類型的屬性或方法 ``` class Point { final double x, y; bool operator ==(Object other) { if (other is! Point) wrongType('Point', other); // Never意味著到這裡程式一定會終止 // 到此已確定other is Point, 就會promote other to non-null Point type // 此時認定other是Point類型, 才能呼叫other.x和other.y return x == other.x && y == other.y; } Point(this.x,this.y); } ``` ``` // Using null safety: String makeCommand(String executable, [List<String>? arguments]) { var result = executable; // 我們手動檢查 arguments 是否為空 if (arguments != null) { // arguments被提升為 非空的List<String> 類別 result += ' ' + arguments.join(' '); } return result; } ``` * 手動檢查變數是否為空, ==null, !=null, is, as, ! * 因為流分析已經很先進, 所以如果有不必要的null檢查會被流分析警告 ``` String checkList(List<Object>? list) { if (list == null) return 'No list'; if (list?.isEmpty ?? false) { // 上一行已經確定不會有null list return 'Empty list'; } return 'Got something'; } void main() { // output: // Warning: Operand of null-aware operation // '?.' has type 'List<Object>' which excludes null. } ``` ## Smarter null-aware methods 流分析僅對局部變數、參數和final field有幫助, 所以手動使用 **空感知** 來擴大流分析的範圍到每個語句 ### ?. ``` String? notAString = null; print(notAString?.length); // print: null ``` ``` String? notAString = null; // 鍊狀取值中, 只要遇到null, 後面就直接短路 print(notAString?.length.isEven); // print: null ``` ### ?..method() ``` class A { void say() => print('done'); } void main() { A? b; print(b?..say()); // print: null } ``` ### ?[ ] ``` List? a; print(a?[0]); // print: null ``` ### 函數 函數沒有空感知operator, 但可以這樣寫: `function?.call(arg1, arg2); ` ``` void say(String words) => print(words); void main() { Function f=say; print(f.call('hello')); // print: hello Function? v=null; print(v?.call('hello')); // print: null } ``` ### Non-null assertion operator 非空斷言操作符 ! 流分析無法判斷可空變數是否為空, 但我知道, 則手動加斷言 ``` class HttpResponse { final int code; final String? error; HttpResponse.ok() : code = 200, error = null; HttpResponse.notFound() : code = 404, error = 'Not found'; @override String toString() { // 此句已排除error=null的可能 if (code == 200) return 'OK'; // Error: Method 'toUpperCase' cannot be called on 'String?' because it is potentially null. return 'ERROR $code ${error.toUpperCase()}'; } } ``` 兩種寫法都可以: ``` @override String toString() { if (code == 200) return 'OK'; // 我斷言 return 'ERROR $code ${error!.toUpperCase()}'; //return 'ERROR $code ${(error as String).toUpperCase()}'; } ``` 但 ! 和 as在功能上還是有所不同: * a!: 我斷言變數 a 不是 null (但不一定是想要的非空類型) * a as T: 我要把 a 強制轉換成類型 T (T 也有可能是可空類型) ## 其他處理可空變數的方法 ### late modifier 延遲到執行時, 第一次使用該變數時才檢查是否非空 ``` // Using null safety, incorrectly: class Coffee { // Error: Field '_temperature' should be initialized because its // type 'String' doesn't allow null. String _temperature; void heat() { _temperature = 'hot'; } void chill() { _temperature = 'iced'; } String serve() => _temperature + ' coffee'; } void main() { var coffee = Coffee(); // 流分析無法確定會先呼叫coffee.heat(); coffee.heat(); coffee.serve(); } ``` 使用late來修飾而不是String?, 可以保持_temperature仍然只能是non-nullable String的語意, 維護時也會特別注意執行時期_temperature有沒有被適當初始化 ``` class Coffee { // 第一次使用時才檢查非空性 late String _temperature; void heat() { _temperature = 'hot'; } void chill() { _temperature = 'iced'; } String serve() => _temperature + ' coffee'; } ``` **Lazy initialization 延遲初始化** 當初始化表達式成本高昂且可能不需要時,這會很方便 ``` // Using null safety: class Weather { // 有用到才會執行 _readThermometer() late int _temperature = _readThermometer(); } ``` 實例欄位初始時無法存取this ,因為在所有欄位初始值設定項完成之前無法存取此物件。但對於late字段, 情況不再如此,因此可以存取this 、呼叫方法或存取實例上的字段。 ``` class A { late var d = c + 12; var c; A(this.c); info() => this.d; } ``` **Late final** 通常non-null final field在宣告時一定要初始化, 加上late就可以延遲到執行階段才檢查. final同樣只能對變數賦值一次, 所以late final變數是儲存 **物件狀態** 的好方法. ``` class Coffee { late final String _temperature; void heat() { _temperature = 'hot'; } void chill() { _temperature = 'iced'; } String serve() => _temperature + ' coffee'; } ``` ### Required named parameters 如果想要一個具有non-null類型且沒有預設值的命名參數, 這意味著我希望要求呼叫者始終會為它賦值 ``` function({int? a, required int b, int c=0, required int? d}) {} ``` a沒預設值, 初始值是null b非空但強制叫用者去初始化, c有預設值, d可空但強制叫用者要賦值(但叫用者也可能給null) ### Abstract fields ``` abstract class Cup { // 不會報錯 String get contents; set contents(String value); } abstract class Cup2 { // Error: Field 'contents' should be initialized because its type 'String' doesn't allow null. String contents; } ``` 照理說, 直接宣告field和宣告field的getter和setter是等效的, 但Cup2中, compiler不知道contents是抽象的, 會強制它在宣告時必須賦值, 我們可以加上abstract顯式的宣告content是抽象成員, 不用初始化. ``` abstract class Cup2 { // 不會報錯 abstract String contents; } ``` ## Nullability and generics 泛型的空安全 ``` // Using null safety: class Box<T> { final T object; Box(this.object); } void main() { Box<String>('a string'); Box<int?>(null); } ``` 觀察上例可知, 泛型由於更靈活, 所以可空的處理會更複雜, 結論就是用之前的技巧去限制泛型的可空性. ### 限制泛型的繼承類型 ``` class Box<T extends Object> { // T must be non-nullable T value; // 必不為空 Box(this.value); } void main() { Box<int> nonNullBox = Box(42); // Valid // Box<int?> nullableBox = Box(null); // Error } ``` ### flow analysis+type promotion ``` class Box<T> { T? value; // 顯式宣告 value is nullable Box(this.value); void printValue() { // 把函數外的field複製進來 var inValue = value; if (inValue is int) { // 排除 inValue 為空的可能, inValue is promoted to int print(inValue + 1); } else { print('unknow'); } } } ``` ``` // Using null safety: class Interval<T extends num?> { T min, max; // T is num? 可為空 Interval(this.min, this.max); bool get isEmpty { var localMin = min; var localMax = max; if (localMin == null || localMax == null) return false; return localMax <= localMin; } } ``` ### 有初始值 ``` T getDefaultValue<T extends Object>(T? value, T defaultValue) { return value ?? defaultValue; } ``` ### as ``` class Box<T> { T? object; Box.empty(); Box.full(this.object); T unbox() => object as T; // 強制類型轉換, 不可用 ! } ``` ## dart core 中有關 null safety 的變動 ### Map[key] 的返回值是 nullable ``` // map[key] is nullable type var map = {'key': 'value'}; print(map['key']); // print: value print(map['key2']); // print: null ``` map[key]當key不存在時, 會返回null, 所以以下的.length操作會出錯 ``` print(map['key'].length); // map['key']的返回值類型是可空的, 不允許.length呼叫 ``` 用空斷言修改: ``` print(map['key']!.length); // OK. ``` ### [No unnamed List constructor](https://dart.dev/null-safety/understanding-null-safety#no-unnamed-list-constructor) 使用List()來產生一個空列表[], 已完全被刪除, 想產生空列表可以使用` List.empty()` ### [Cannot set a larger length on non-nullable lists](https://dart.dev/null-safety/understanding-null-safety#cannot-set-a-larger-length-on-non-nullable-lists) ### [Cannot access Iterator.current before or after iteration](https://dart.dev/null-safety/understanding-null-safety#cannot-access-iterator-current-before-or-after-iteration) ## [Summary](https://dart.dev/null-safety/understanding-null-safety#summary)