flutter–Dart基礎語法(二)流程式控制制、函數、異常
一、前言
Flutter 是 Google 開源的 UI 工具包,幫助開發者通過一套程式碼庫高效構建多平台精美應用,Flutter 開源、免費,擁有寬鬆的開源協議,支援移動、Web、桌面和嵌入式平台。
Flutter是使用Dart語言開發的跨平台移動UI框架,通過自建繪製引擎,能高性能、高保真地進行Android和IOS開發。Flutter採用Dart語言進行開發,而並非Java,Javascript這類熱門語言,這是Flutter團隊對當前熱門的10多種語言慎重評估後的選擇。因為Dart囊括了多數程式語言的優點,它更符合Flutter構建介面的方式。
本文主要就是簡單梳理一下Dart語言的一些基礎知識和語法。關於程式語言的基本語法無外乎那麼些內容,注釋、變數、數據類型、運算符、流程式控制制、函數、類、異常、文件、非同步、常用庫等內容,相信大部分讀者都是有一定編程基礎的,所以本文就簡單地進行一個梳理,不做詳細的講解。大家也可以參考 Dart程式語言中文網。
上一篇文章主要是寫了Dart語言的一些基本語法,本文將接著上一篇文章繼續往後寫。
二、Dart中的流程式控制制
流程式控制制涉及到的基本語法其實很簡單,但是這一塊也是程式語言基礎中最難的一部分,主要難點在於解決問題的邏輯思路,流程式控制制知識實現我們解決問題的邏輯思路的一種表達形式。所以,大家在學習程式語言的過程中,學習基本語法是一部分,更重要的部分其實是鍛煉自己解決問題的邏輯能力,而這一塊的加強,必須加以大量的練習才能熟練掌握。本文主要是給大家羅列一下Dart中的流程式控制制相關的基本語法。
流程式控制制主要涉及到的內容無外乎條件分支結構、switch分支結構和循環結構,此外,還有一些特殊的語法break、continue等。對於有過編程經驗的同學而言,這些內容都是so easy。下面就簡單給大家羅列一下。
2.1 條件分支結構
Dart 中的條件分支結構就是 if - else
語句,其中 else
是可選的,Dart 的if判斷條件必須是布爾值,不能是其他類型。比如下面的例子。
if (isRaining()) { you.bringRainCoat(); } else if (isSnowing()) { you.wearJacket(); } else { car.putTopDown(); }
2.2 switch分支結構
在 Dart 中 switch 語句使用 ==
比較整數,字元串,或者編譯時常量。 比較的對象必須都是同一個類的實例(並且不可以是子類), 類必須沒有對 ==
重寫。 枚舉類型 可以用於 switch
語句。在 case
語句中,每個非空的 case
語句結尾需要跟一個 break
語句。 除 break
以外,還有可以使用 continue
, throw
,者 return
。當沒有 case
語句匹配時,執行 default
程式碼
var command = 'OPEN'; switch (command) { case 'CLOSED': executeClosed(); break; case 'PENDING': executePending(); break; case 'APPROVED': executeApproved(); break; case 'DENIED': executeDenied(); break; case 'OPEN': executeOpen(); break; default: executeUnknown(); }
// case 程式示例中預設了 break 語句,導致錯誤 switch (command) { case 'OPEN': print('open'); // ERROR: 丟失 break case 'CLOSED': print('close'); break; }
// Dart 支援空 case 語句, 允許程式以 fall-through 的形式執行 var command = 'CLOSED'; switch (command) { case 'CLOSED': // Empty case falls through. case 'NOW_CLOSED': // Runs for both CLOSED and NOW_CLOSED. executeNowClosed(); break; } // 在非空 case 中實現 fall-through 形式, 可以使用 continue 語句結合 lable 的方式實現 var command = 'CLOSED'; switch (command) { case 'CLOSED': executeClosed(); continue nowClosed; // Continues executing at the nowClosed label. nowClosed: case 'NOW_CLOSED': // Runs for both CLOSED and NOW_CLOSED. executeNowClosed(); break; }
2.3 循環結構
和其他程式語言中的循環結構一樣,Dart中的循環結構也是有for、while、do…while三種,這三種循環結構可以相互轉換,大家根據自己的編程習慣進行選擇即可。
2.3.1 for循環
進行迭代操作,可以使用標準 for
語句。 例如:
var message = StringBuffer('Dart is fun'); for (var i = 0; i < 5; i++) { message.write('!'); }
閉包在 Dart 的 for
循環中會捕獲循環的 index 索引值, 來避免 JavaScript 中常見的陷阱。 請思考示例程式碼:
var callbacks = []; for (var i = 0; i < 2; i++) { callbacks.add(() => print(i)); } callbacks.forEach((c) => c());
和期望一樣,輸出的是 0
和 1
。
// 如果要迭代一個實現了 Iterable 介面的對象, 可以使用 forEach() 方法, 如果不需要使用當前計數值, 使用 forEach() 是非常棒的選擇 candidates.forEach((candidate) => candidate.interview()); //實現了 Iterable 的類(比如, List 和 Set)同樣也支援使用 for-in 進行迭代操作 iteration var collection = [0, 1, 2]; for (var x in collection) { print(x); // 0 1 2 }
2.3.2 while和do…while循環
// while 循環在執行前判斷執行條件: while (!isDone()) { doSomething(); } // do-while 循環在執行後判斷執行條件: do { printLine(); } while (!atEndOfPage());
2.4 break和continue語句
使用 break
停止程式循環:
while (true) { if (shutDownRequested()) break; processIncomingRequests(); }
使用 continue
跳轉到下一次迭代:
for (int i = 0; i < candidates.length; i++) { var candidate = candidates[i]; if (candidate.yearsExperience < 5) { continue; } candidate.interview(); }
如果對象實現了 Iterable 介面 (例如,list 或者 set)。 那麼上面示例完全可以用另一種方式來實現:
candidates .where((c) => c.yearsExperience >= 5) .forEach((c) => c.interview());
2.5 assert語句
如果 assert
語句中的布爾條件為 false , 那麼正常的程式執行流程會被中斷。 下面是一些示例:
// 確認變數值不為空。 assert(text != null); // 確認變數值小於100。 assert(number < 100); // 確認 URL 是否是 https 類型。 assert(urlString.startsWith('https'));
提示: assert 語句只在開發環境中有效, 在生產環境是無效的; Flutter 中的 assert 只在 debug 模式 中有效。 開發用的工具,例如 dartdevc 默認是開啟 assert 功能。 其他的一些工具, 例如 dart 和 dart2js, 支援通過命令行開啟 assert :
--enable-asserts
。
- assert 的第一個參數可以是解析為布爾值的任何表達式。 如果表達式結果為 true , 則斷言成功,並繼續執行。 如果表達式結果為 false , 則斷言失敗,並拋出異常 (AssertionError) 。
- assert 的第二個參數可以為其添加一個字元串消息。
assert(urlString.startsWith('https'), 'URL ($urlString) should start with "https".');
三、Dart中的函數
Dart 是一門真正面向對象的語言, 甚至其中的函數也是對象,並且有它的類型 Function 。 這也意味著函數可以被賦值給變數或者作為參數傳遞給其他函數。 也可以把 Dart 類的實例當做方法來調用。
3.1 函數的定義
下面是函數實現的示例:
// 模板 returnType funcName(paramsList) { // function code // return statement } bool isNoble(int atomicNumber) { return _nobleGases[atomicNumber] != null; }
3.1.1 可選參數
函數有兩種參數類型: required(必需參數,函數調用時不傳就會報錯) 和 optional(可選參數,函數調用時可以不傳)。 required 類型參數在參數最前面, 隨後是 optional 類型參數。 命名的可選參數也可以標記為 「@required」 。
可選參數可以是命名參數或者位置參數,但一個參數只能選擇其中一種方式修飾。
- 命名可選參數:定義函數時,使用
{param1, param2, …}
來指定命名參數,並且可以使用 @required 注釋表示參數是 required 性質的命名參數。調用函數時,可以使用指定命名參數paramName: value
。// 定義函數是,使用 {param1, param2, …} 來指定命名參數: void enableFlags({bool bold, bool hidden}) {...} // 調用函數時,可以使用指定命名參數 paramName: value。 例如: enableFlags(bold: true, hidden: false); // 使用 @required 注釋表示參數是 required 性質的命名參數, 該方式可以在任何 Dart 程式碼中使用(不僅僅是Flutter)。 // 此時 Scrollbar 是一個構造函數, 當 child 參數缺少時,分析器會提示錯誤。 const Scrollbar({Key key, @required Widget child})
-
位置可選參數:將參數放到
[]
中來標記參數是可選的,調用函數時,按位置順序傳遞參數。// 將參數放到 [] 中來標記參數是可選的: String say(String from, String msg, [String device]) { var result = '$from says $msg'; if (device != null) { result = '$result with a $device'; } return result; } // 下面是不使用可選參數調用上面方法 的示例: assert(say('Bob', 'Howdy') == 'Bob says Howdy'); // 下面是使用可選參數調用上面方法的示例: assert(say('Bob', 'Howdy', 'smoke signal') == 'Bob says Howdy with a smoke signal');
3.1.2 默認參數
在定義方法的時候,可以使用 =
來定義可選參數的默認值。 默認值只能是編譯時常量。 如果沒有提供默認值,則默認值為 null。
注意:舊版本程式碼中可能使用的是冒號 (:
) 而不是 =
來設置參數默認值。 原因是起初命名參數只支援 :
。 這種支援可能會被棄用。 建議 使用 =
指定默認值。
下面是設置可選參數默認值示例:
/// 設置 [bold] 和 [hidden] 標誌 ... void enableFlags({bool bold = false, bool hidden = false}) {...} // bold 值為 true; hidden 值為 false. enableFlags(bold: true);
下面示例演示了如何為位置參數設置默認值:
String say(String from, String msg, [String device = 'carrier pigeon', String mood]) { var result = '$from says $msg'; if (device != null) { result = '$result with a $device'; } if (mood != null) { result = '$result (in a $mood mood)'; } return result; } assert(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');
3.1.3 返回值
所有函數都會返回一個值。 如果沒有明確指定返回值, 函數體會被隱式的添加 return null;
語句。
foo() {} assert(foo() == null);
3.2 main()函數
任何應用都必須有一個頂級 main()
函數,作為應用服務的入口。 main()
函數返回值為空,參數為一個可選的 List<String>
。
下面是 web 應用的 main()
函數:
void main() { querySelector('#sample_text_id') ..text = 'Click me!' ..onClick.listen(reverseText); }
下面是一個命令行應用的 main()
方法,並且使用了輸入參數:
// 這樣運行應用: dart args.dart 1 test void main(List<String> arguments) { print(arguments); assert(arguments.length == 2); assert(int.parse(arguments[0]) == 1); assert(arguments[1] == 'test'); }
3.3 匿名函數
多數函數是有名字的, 比如 main()
和 printElement()
。 也可以創建沒有名字的函數,這種函數被稱為 匿名函數。 匿名函數可以賦值到一個變數中, 舉個例子,在一個集合中可以添加或者刪除一個匿名函數。
匿名函數和命名函數看起來類似— 在括弧之間可以定義一些參數或可選參數,參數使用逗號分割。後面大括弧中的程式碼為函數體:
([[Type] param1[, …]]) { codeBlock; }; // 下面例子中定義了一個包含一個無類型參數 item 的匿名函數。 list 中的每個元素都會調用這個函數,列印元素位置和值的字元串。 var list = ['apples', 'bananas', 'oranges']; list.forEach((item) { print('${list.indexOf(item)}: $item'); });
3.4 箭頭函數
不管是匿名函數還是命名函數,如果函數中只有一句表達式,可以使用箭頭語法,簡寫如下:
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;
=> expr
語法是{ return expr; }
的簡寫。=>
符號 有時也被稱為 箭頭 語法。
提示: 在箭頭 (=>) 和分號 (;) 之間只能使用一個 表達式 ,不能是 語句 。 例如:不能使用 if 語句 ,但是可以是用 條件表達式.
3.5 函數是一等對象
一個函數可以作為另一個函數的參數。 例如:
void printElement(int element) { print(element); } var list = [1, 2, 3]; // 將 printElement 函數作為參數傳遞。 list.forEach(printElement);
同樣可以將一個函數賦值給一個變數,例如:
// 使用匿名函數 var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!'; assert(loudify('hello') == '!!! HELLO !!!');
3.6 變數的作用域
Dart 是一門詞法作用域的程式語言,就意味著變數的作用域是固定的, 簡單說變數的作用域在編寫程式碼的時候就已經確定了。 花括弧內的是變數可見的作用域。下面示例關於多個嵌套函數的變數作用域:
bool topLevel = true; void main() { var insideMain = true; void myFunction() { var insideFunction = true; // 注意 nestedFunction() 可以訪問所有的變數, 一直到頂級作用域變數。 void nestedFunction() { var insideNestedFunction = true; assert(topLevel); assert(insideMain); assert(insideFunction); assert(insideNestedFunction); } } }
3.7 閉包
3.7.1 閉包的概念
閉包這個概念好難理解,身邊朋友們好多都稀里糊塗的,我也是學習了很久才理解這個概念。下面請大家跟我一起理解一下,如果在一個函數的內部定義了另一個函數,外部的我們叫他外函數,內部的我們叫他內函數。
閉包: 在一個外函數中定義了一個內函數,內函數里運用了外函數的臨時變數,並且外函數的返回值是內函數的引用。這樣就構成了一個閉包。
一般情況下,在我們認知當中,如果一個函數結束,函數的內部所有東西都會釋放掉,還給記憶體,局部變數都會消失。但是閉包是一種特殊情況,如果外函數在結束的時候發現有自己的臨時變數將來會在內部函數中用到,就把這個臨時變數綁定給了內部函數,然後自己再結束。
函數可以封閉定義到它作用域內的變數。 接下來的示例中, makeAdder()
捕獲了變數 addBy
。 無論在什麼時候執行返回函數,函數都會使用捕獲的 addBy
變數。
/// 返回一個函數,返回的函數參數與 [addBy] 相加 Function makeAdder(num addBy) { // //返回的函數就是一個閉包,封閉了局部變數 addBy return (num i) => addBy + i; } void main() { // 創建一個加 2 的函數。 var add2 = makeAdder(2); // 創建一個加 4 的函數。 var add4 = makeAdder(4); assert(add2(3) == 5); assert(add4(3) == 7); }
3.7.2 閉包的特點
由於變數的作用域的限制,全局變數可以在整個程式碼範圍內使用,但是帶來的問題就是任何地方都可以修改該全局變數,函數內局部變數又只能在函數內部使用。所以閉包就讓外部訪問函數內部變數成為可能,同時也讓局部變數可以常駐在記憶體中。
- 讓外部訪問函數內部變數成為可能;
- 局部變數會常駐在記憶體中;
- 可以避免使用全局變數,防止全局變數污染;
- 會造成記憶體泄漏(有一塊記憶體空間被長期佔用,而不被釋放)
閉包就是可以創建一個獨立的環境,每個閉包裡面的環境都是獨立的,互不干擾。閉包會發生記憶體泄漏,每次外部函數執行的時候,外部函數的引用地址不同,都會重新創建一個新的地址。但凡是當前活動對象中有被內部子集引用的數據,那麼這個時候,這個數據不刪除,保留一根指針給內部活動對象。
閉包記憶體泄漏為: key = value,key 被刪除了 value 常駐記憶體中; 局部變數閉包升級版(中間引用的變數) => 自由變數;
四、異常
Dart 程式碼可以拋出和捕獲異常。 異常表示一些未知的錯誤情況。 如果異常沒有被捕獲, 則異常會拋出, 導致拋出異常的程式碼終止執行。和 Java 有所不同, Dart 中的所有異常是非檢查異常。 方法不會聲明它們拋出的異常, 也不要求捕獲任何異常。
Dart 提供了 Exception 和 Error 類型, 以及一些子類型。 當然也可以定義自己的異常類型。 但是,此外 Dart 程式可以拋出任何非 null 對象, 不僅限 Exception 和 Error 對象。
4.1 拋出異常 throw
下面是關於拋出或者 引發 異常的示例:
throw FormatException('Expected at least 1 section');
也可以拋出任意的對象:
throw 'Out of llamas!';
因為拋出異常是一個表達式, 所以可以在 => 語句中使用,也可以在其他使用表達式的地方拋出異常:
void distanceTo(Point other) => throw UnimplementedError();
4.2 異常處理 try…catch…finally
Dart中的異常處理和Java中的比較類似,也是使用try…catch…finally的語句進行處理,不同的是,Dart中海油一個特殊的關鍵字on。使用 on 來指定異常類型, 使用 catch 來 捕獲異常對象,捕獲語句中可以同時使用 on 和 catch ,也可以單獨分開使用。
捕獲異常可以避免異常繼續傳遞(除非重新拋出( rethrow )異常)。 可以通過捕獲異常的機會來處理該異常:
try {
breedMoreLlamas();
} on OutOfLlamasException {
buyMoreLlamas();
}
通過指定多個 catch 語句,可以處理可能拋出多種類型異常的程式碼。 與拋出異常類型匹配的第一個 catch 語句處理異常。 如果 catch 語句未指定類型, 則該語句可以處理任何類型的拋出對象:
// 捕獲語句中可以同時使用 on 和 catch ,也可以單獨分開使用。 使用 on 來指定異常類型, 使用 catch 來 捕獲異常對象。 try { breedMoreLlamas(); } on OutOfLlamasException { // 一個特殊的異常 buyMoreLlamas(); } on Exception catch (e) { // 其他任何異常 print('Unknown exception: $e'); } catch (e) { // 沒有指定的類型,處理所有異常 print('Something really unknown: $e'); }
catch()
函數可以指定1到2個參數, 第一個參數為拋出的異常對象, 第二個為堆棧資訊 ( 一個 StackTrace 對象 )。
try { // ··· } on Exception catch (e) { print('Exception details:\n $e'); } catch (e, s) { print('Exception details:\n $e'); print('Stack trace:\n $s'); }
如果僅需要部分處理異常, 那麼可以使用關鍵字 rethrow
將異常重新拋出。
void misbehave() { try { dynamic foo = true; print(foo++); // Runtime error } catch (e) { print('misbehave() partially handled ${e.runtimeType}.'); rethrow; // Allow callers to see the exception. } } void main() { try { misbehave(); } catch (e) { print('main() finished handling ${e.runtimeType}.'); } }
不管是否拋出異常, finally
中的程式碼都會被執行。 如果 catch
沒有匹配到異常, 異常會在 finally
執行完成後,再次被拋出。如果catch捕獲到異常,那麼先執行catch中的處理程式碼,然後再執行finally中的程式碼。總而言之,finally語句塊中的程式碼一定會被執行,並且是在最後被執行。
try { breedMoreLlamas(); } finally { // Always clean up, even if an exception is thrown. cleanLlamaStalls(); } // 任何匹配的 catch 執行完成後,再執行 finally try { breedMoreLlamas(); } catch (e) { print('Error: $e'); // Handle the exception first. } finally { cleanLlamaStalls(); // Then clean up. }