--- title: 'Dart 異常處理、函數方法、引用庫' disqus: kyleAlien --- Dart 異常處理、函數方法、引用庫 === ## Overview of Content 如有引用參考請詳註出處,感謝 :cat: :::success * 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/) 本篇文章對應的是 [**Dart 函數與方法、異常處理、引用庫 | Java 比較**](https://devtechascendancy.com/dart-java-compare_exception_function-methods/) ::: :::info 以下使用 Dart SDK `3.4.3` 版本 ::: [TOC] ## Dart 函數與方法 :::info * **應該稱為「方法」還是「函數」**? 在物件導向中,會將我們傳統所學的命令是編成得函數(`function`)稱之為方法(`method`) 而在 Dart 中既有方法也有函數,因為它既可以作為物件導向設計,也可以作為命令式設計… Dart 的方法就是放置在類內的函數(這時會稱之為方法),而放置在最頂層的函數就稱之為函數 ```java= // Dart 類 public class Hello { // 宣告在 Dart 類內,稱之為「方法」 void sayHello() { System.out.println("Hello~ Java method"); } } // 宣告在 Dart 頂層,稱之為「函數」 void sayHello() { print("Hello~ Dart method") } ``` ::: ### Dart、Java 方法的差異 * 以下我們以 Java 這門語言的方法(`Method`)來與 Dart 這門語言的方法相比,看看 Dart 跟 Java 在方法的表達上差異在哪… 1. **方法的位置**:Dart 多了「**函數**」 Dart 與 Java 方法較大的差異在,**Java 方法的方法就會存在類中 (class),而 Dart 中的方法可以單獨存在檔案中**,這種方法稱為 **頂層函數**(`top-level function`) Java 方法如下: ```java= // Java public class Hello { void sayHello() { System.out.println("Hello~ Java method"); } } ``` Dart 頂層函數如下: ```dart= void sayHello() { print("Hello~ Dart method"); } ``` 2. **參數的表達**: Java 方法重載機制,大多數時候是因為有多種不同的參數所需,所以需要多個相同名稱的方法(但參數需不同),範例如下… 我們可以看到,這這對於生產程式的效率上並不佳 ```java= class Hello { void sayHello() { System.out.println("Hello~ Java method"); } // 為了參數方法重載 void sayHello(String personName) { System.out.println("Hello~ Java method, " + personName); } // 為了參數方法重載 void sayHello(String personName, int count) { for (int i = 0; i < count; i++) { System.out.println("Hello~ Java method, " + personName); } } } ``` 而在 Dart 中則是以 **==可選位置參數==、==命名參數==來處理**,並且 Dart 的方法 **允許參數有默認值** > Dart 的可選位置參數、命名參數會在接下來小節會舉例說明 另外在 **Dart 中並沒有方法重載機制**,下圖中證明 Dart 不支援方法重載 > ![image](https://hackmd.io/_uploads/SkITcCb9R.png) ### 可選位置參數:預設參數 * 在一般的方法參數設置下,呼叫方法時,方法的 ^1.^ 參數都是必須設置,並且 ^2.^ 不允許有預設參數,只要有一個條件不符合就無法通過編譯 ```dart= void printMsg(String msg, int count = 1) { for (int i = 0; i < count; i++) { print("($i) $msg"); } } ``` 如下圖所示,不使用位置、命名參數的話,就不允許有預設值 > ![image](https://hackmd.io/_uploads/B1vnnA-qA.png) * **可選「位置」參數** 的使用方式: 可選位置參數是 **使用中括號(`[]`)包裹參數**,並且它的重點在於「==**位置**==」,它會限制使用者一定要按照方法參數的位置做設置,不能指定參數名去賦予值; 範例如下 ```java= void testPosition() { // 全部參數使用預設值 printInfo(); printInfo("Alien", 998877); printInfo("Alien", 1111, 998877); // printInfo(123); // 順序不對,前面必須要指定 name // printInfo(id : 123); // 無法指定 } void printInfo([String name = "Hello", // 有預設參數 int id = 9527, // 有預設參數 int? phone]) { print("name: $name, id: $id, phone: $phone"); } ``` > ![](https://i.imgur.com/F5d4Zaz.png) ### 可選命名參數 - 預設參數 * **可選「命名」參數** 的使用方式: 可選位置參數是 **使用花括號(`{}`)包裹參數**,它與位置參數不同,使用可選命名參數時並 **不在意參數的順序**,因為使用它時 **需要我們手動為方法的每個參數指定數值**; 範例如下 ```dart= void testSetName() { // 全部參數使用預設值 printInfo_2(); // 按照順序命名(okay) printInfo_2(name: "Alien", id: 123, phone: 666666); // 不按照順序命名(okay) printInfo_2(phone: 9999, id: 777, name: "Kyle"); } void printInfo_2({String name = "World", int id = 9527, int? phone}) { print("name: $name, id: $id, phone: $phone"); } ``` > ![](https://i.imgur.com/7uWarLB.png) :::warning * 使用命名參數時不允許沒有指定參數名稱 > ![image](https://hackmd.io/_uploads/BJyP11z5R.png) ::: ### 一級方法物件:Function、typedef 使用 * **Dart 的一級方法物件特點如同 [Java 的 Lambda 表達式](https://devtechascendancy.com/java-functional-lambda-methods/),又如 groovy 的閉包概念,==把方法作為參數傳入==,在方法的內部可以依據需要隨時調用這個一級方法物件** * 在 Dart 中使用一級方法物件時常會接觸的兩個關鍵字如下表 | 使用關鍵字 | 解釋 | | - | - | | Function | **`Function` 對於 Dart 來說是一種「類型」**,我們所以的方法其實都是 `Function` 類型;但是 `Function` 本身不具有參數檢查功能 | | typedef | 重新命名(如同 Shell `alias` 的功能),**配合 `Function` 使用時,可以在編譯期間進行 ==參數進行檢查==** | 1. 使用 Function 類型:如下範例中,我們可以看到 `printItem`、`showMsg` 兩個方法雖然參數不同,但是都屬於 Function 類型 ```dart= void printItem(String item) { print("myFunction be call, element $item"); } void showMsg(String msg, int count) { for (int i = 0; i < count; i++) { print("($i), msg: $msg"); } } // 接收 Function 類型 void receiveFunction(Function function) { print("Function runtimeType: ${function.runtimeType}"); } void main() { receiveFunction(printItem); receiveFunction(showMsg); } ``` > ![image](https://hackmd.io/_uploads/H1FJNJMqR.png) 2. 使用 `typedef` 定義 Function 類型:以下我們來改進 `receiveFunction` 方法,讓其接收指定類型的 Function ```dart= void printItem(String item) { print("myFunction be call, element $item"); } void showMsg(String msg, int count) { for (int i = 0; i < count; i++) { print("($i), msg: $msg"); } } // 定義 Function 類型的細節 typedef ShowItemFunc = void Function(String); // 接收指定 Function 類型 void receiveFunction(ShowItemFunc function) { print("Function runtimeType: ${function.runtimeType}"); } void main() { receiveFunction(printItem); // Error! 因為並不是所需類型的 Function receiveFunction(showMsg); } ``` > ![image](https://hackmd.io/_uploads/HkmdVJfcC.png) ## Dart 異常 Dart 異常與 Java 不同,Dart 屬於 **非檢查異常**:^1^ 方法不必聲明他們所拋出的異常,也就是呼叫方法時 ^2^ 不必捕捉異常 另外,Dart 提供了 `Exception`、`Error` 類型(還有一些子類),還可以自定義拋出任何的類型,**甚至可以拋出方法、類 (基本上就是一切皆可拋出)** :::info * Java 則是混合型異常,既有檢查異常(規定呼叫者一定要處理),也有非檢查異常(嚴重錯誤類型),想了解更多 Java 異常的細節,請點擊 [**深入理解 Java 異常處理:從基礎概念到最佳實踐指南**](https://devtechascendancy.com/java-jvm-exception-handling-guide/) ```java= class JavaException { void nonCheckThrow() { throw new RuntimeException(); } void checkThrow() throws IOException { throw new IOException(); } } ``` ::: ### Dart 異常處理:try-catch-finally、不同的 catch * 雖然 Dart 可以不捕捉異常,但是它也同樣支持 `try / catch / finally` 操作 **Dart 與 Java 捕捉異常的不同之處在於 ==`catch`==,Dart 的 `catch` 只有兩個預設參數 ^1.^`e(error)` 拋出的異常物件、^2.^`s(stack)` 錯誤棧 可以使用** > ==並且這兩個參數類型皆為 `dynamic`== (可使用 runtimeType 去查看類型) ```java= void main() { try { testException("帥到被捕捉"); } catch(e, s) { // 無法定義類型的參數 print(e); print("runtimeType: ${e.runtimeType}\n"); print(s); print("runtimeType: ${s.runtimeType}"); } finally { print("被捉住哭哭"); } } void testException(String str) { throw new Exception(str); } ``` > ![](https://i.imgur.com/ntxRaPW.png) ### 捕捉指定類型異常:on、再次拋出:rethrow * 上面有說到 Dart 的 catch 只提供兩個參數 (並且這兩個參數皆為 dynamic 類型),那難道就不能捕捉指定類型的異常了嗎? 不,其實是可以的,但若 Dart 要捉取指定的類型就要使用 `on` 關鍵字(`on <類型> catch(e, s)`),範例如下: ```dart= void tryThrow() { throw UnsupportedError('This method not implement'); } void main() { try { tryThrow(); } on UnsupportedError catch (e) { print("Use `on` to catch unsupported error: ${e.message}"); } } ``` > ![image](https://hackmd.io/_uploads/Sy1mpJf90.png) * 另外 Dart 也可以使用 **rethrow 將捕捉的異常再次拋出**(但這並不算是例外轉譯),範例如下… 1. 定義異常函數: ```dart= void say() { print("說不出的帥"); } void testThrowFunc() { // 可以拋出方法 throw say; } ``` 2. 使用 `on` 來捕捉 Function 類型的異常,並在執行過後再次透過 `rethrow` 把相同的異常拋 ```dart= void main() { try { testThrowFunc(); } on Function catch(e) { print("e: $e"); print("e(): ${e()}"); print("e.runtimeType: ${e.runtimeType}"); rethrow; // 把捕獲的異常重新拋出 } finally { print("- v -+"); } } ``` > ![image](https://hackmd.io/_uploads/ByXIR1f5R.png) ### Assert 斷言 * 在 Dart 中,assert 是一個常用的調試工具,用於在開發階段驗證程式的假設,它的主要目的是在開發過程中檢查程式的內部狀態是否符合預期,從而幫助開發者及早發現和修正錯誤(`assert` 通常用於檢查函數參數是否符合預期,這在函數的開發和調試過程中特別有用) `assert` 的特性如下: 1. **僅在調試模式下啟用**: assert 語句僅在調試模式(`debug mode`)下執行,在生產環境(`release mode`)中,所有的 assert 語句都會被忽略,不會執行 > 因此,不應該依賴 assert 來進行生產環境中的程式邏輯控制 2. **提升程式的可靠性**: 通過在程式的關鍵位置添加 assert,可以及早發現潛在的錯誤,從而提高程式的可靠性和穩定性 * 範例如下,**當 assert 內部的判斷為 false 時發生中斷**,並拋出 AssertionError ```java= void main() { var a = 100; assert(a == 99); } ``` > ![](https://i.imgur.com/nC0dNoG.png) ## Dart 引用庫 在 Dart 中,透過 import 關鍵字和指定路徑,開發者可以匯入各種類型的函式庫,包括 Dart 標準函式庫、第三方函式庫以及專案內的自訂函式庫 根據函式庫的來源和用途,Dart 使用不同的路徑前綴來明確區分這些函式庫的類型 | 庫分類 | 關鍵字 | 範例 | | - | - | - | | Dart 語言(標準庫) | dart: | dart:io | | 第三方庫(通常可以到 [pub.dev](https://pub.dev/) 找) | package: | package:hello/world.dart | ### 指定庫別名:as * 在 Dart 中,有時候你可能會遇到不同的函式庫中存在相同名稱的類別、函數或變數 為了避免命名衝突,並提高程式碼的可讀性和可維護性,Dart 提供了 `as` 關鍵字,用於為匯入的程式庫指定一個別名 | 關鍵字 | 解釋 | | - | - | | as | 庫另外命名 | 概念範例如下 ```dart= // 舉例 (並沒有這個第三方庫) 假設內部都有 Json class import 'package:lib1/json1.dart'; import 'package:lib2/json2.dart' as Json2; void main() { // lib 1 中的 Json Json j = new Json(); // lib 2 中的 Json Json2.Json j2 = new Json2.Json(); } ``` ### 庫部分引用 * Dart 提供了靈活的方式來引用庫中的部分內容,以便在程式碼中只引入必要的部分;這不僅可以減少不必要的依賴,還可以提高程式碼的可讀性和效能 使用關鍵字如下表 | 關鍵字 | 解釋 | | - | - | | show | 部分引用 | | hide | 隱藏特定部分之外都引用| | deferred as | '**庫的懶加載**' 並在合適的時候使用 `loadLibrary()` 方法 | 概念範例如下 ```dart= // lib1 指使用 Json class import 'package:lib1/json1.dart' show Json; // lib2 除了 Json class 之後都使用 import 'package:lib2/json2.dart' hide Json; // 改名為 T import 'package:xxx/ui/fragment/transfer/Transfer.dart' as T;' void main() { T.Transfer(); // 建構函數上就必須加上 T popCalendarDialog(); } import 'package:xxx/utils/Calendar.dart' deferred as Calendar;' void popCalendarDialog() async { await Calendar.loadLibrary(); // 開始加載庫 Calendar.open(); } ``` ## Appendix & FAQ :::info ::: ###### tags: `Flutter`、`Dart`