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!';
提示: 高質量的生產環境程式碼通常會實現 Error 或 Exception 型別的異常丟擲。
因為丟擲異常是一個表示式, 所以可以在 => 語句中使用,也可以在其他使用表示式的地方丟擲異常:
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. }
&n