算是一些筆記,雖然 Dart 可以應用的場合挺多的,不過我目前主要目標還是放在瀏覽器上,工作上還是能夠自己寫一些前端比較方便,希望可以找到一個好用的前端語言。
總之,先從基本開始: 在 Get Started 裡頭介紹了 DartPad 可以在瀏覽器裡測試程式碼。
DartPad 的說明文件好像是放在 Github 上的 Wiki 裡,比如關於嵌入網頁的說明: Embedding Guide。
然後有分平台:
- webdev: 應該就是環繞在 Dart2JS 的應用程式開發,最後執行環境是瀏覽器。
- Flutter: Mobile App 的開發,不能使用 dart:mirrors 以及 dart:html 函式庫。後者不意外,前者應該是因為 iOS 平台沒有辦法嵌 DartVM 的關係。感覺主要就是用語言的部份,不知道執行時期環境跟其他平台有共用多少。
- DartVM: 跑在 DartVM 上,主要是 Server-side 或是 Console 介面的程式。
在 Sample Code 有一些常見語言邏輯片段的程式,比如: 變數宣告、邏輯判斷、迴圈、類別宣告、類別繼承、非同步操作、例外... 等等。
在 Language Tour 介紹了主要的語言元素,包含基本概念、關鍵字、內建型別、函數、運算子、類別、封裝、還有各種 Dart 特有的特性。
幾個我覺得比較特殊的:
- 所有東西都是物件,繼承自 Object 類別。
- 函數可以在頂層名稱空間 (就像 JavaScript 或是 C/C++ 那樣,跟 Java 不同) 的,也可以是類別的方法,也可以有 closure (nested or local function)
- 沒有 public, protected, private 之類的存取控制關鍵字,封裝是以函式庫 (Library) 為單位封裝,以底線 (_) 開頭的東西只能在函式庫內使用。
- 語言中的 expression 跟 statement 會有不同的意義,後續看文件要特別注意。
變數在明確初始化的場合下,會預設的被初始化為 null 值,即便是數值 (num, int) 也一樣。 (因為所有東西都是物件)
變數可以不明確寫出型別,不過原則上建議明確寫出變數或參數的型別,但 lambda 之類很短的程式片段,因為夠短,語言引擎應該可以正確的進行型別推理,所以建議基於可讀性而把型別標註 (Type annotation) 省略掉。
值僅在編譯時期決定的常數應該用 const 關鍵字宣告,值的繫結只會發生在編譯時期。如果是指定值後不會再變更的則使用 final 宣告,值的繫結發生在執行時期,且只會發生一次。
數值型別有 int 跟 double 兩種,都是原則上為 64 位元的有號數,分別表示整數與浮點數,這兩個數值型別的上層類別是 num 型別。 Dart 本身沒有規定儲存 int 型別的位元數,但是會受到平台的限制,因此原則上是 64 位元的。
數學操作有些定義在數值類別上,比如 .abs(), .ceil(), .floor(), .round() 方法,或是 int 的 << 跟 ~ 等運算子。有些是定義在 dart:math 裡,比如 sqrt() 或是 cos() 之類的。
字串可以用雙引號或單引號,不論是雙引號或單引號,脫逸字元 (如: \n) 都是有效的。另外也像 Python 一樣,有 r"Raw String" 可以使用,脫逸字元在 raw string 中是無效的。還有多行的字串,也是跟 Python 用法相同使用三個引號來定義。
鄰接的字串定值 (string literal) 會自動的在編譯時期被接合成一個字串,或是也可以像執行時期一樣用 + 來接合字串。
字串中可以使用 ${expression}
來進行值的嵌入,最後 expression 被評估出的值物件中的 .toString() 會被用來產生遷入的字串值。
當字串嵌入中的 expression 只是一個識別字 (identifier, 通常就是變數名稱) 時,大括號可以省略。不過個人不太喜歡這樣用法,眼睛太大很容易會看錯...
在 if 中的判斷式,應該明確的評估出 true 或是 false 值來,否則行為會依平台有不同的結果。這點跟 Java 相同,但執行時期反應是依平台而定。
陣列 (array) 就是 List 物件,串列的大小是可以透過 .add() 延伸的,這點跟 Python 類似。要宣告內容不可變更的常數的串列,直接使用 const 關鍵字,寫法大概是 var a = const [1, 2, 3]
這個樣子。
也有字典,就是 Map 物件,使用起來也是類似 Python 的 dict 操作。與 List 一樣可以利用 const 關鍵字來製作不可變得常數字典,大概是 var d = const {1: "one", 2: "two",}
這樣的寫法。
Symbol 感覺像是取位址,語法是 #identifier
。基本上所有識別字的位址都可以取,變數、函數、方法、類別都可以,主要是搭配 reflection 來使用。
函數的參數除了一般的以順序指定參數之外,也可以有使用選用的名稱的參數。參數大概是 (必要的依順序指定參數, {選用的依名稱指定參數}) 或是 (必要的依順序指定參數, [依序選用的依順序指定參數]) 兩種定義方式。一旦參數放到大括號中,就必須使用名稱指定。直接用程式來測試語法:
匿名函數是可以的,定義方式有點像 JavaScript: (param1, param2) { /* code */ }
另外也有 lambda 的語法,使用 => 來定義: (param1, param2) => /* expression */
,基本上就是等價於 (param1, param2) { /* return expression; */ }
函數都會傳回值,沒有明確傳回或是定義為 void 的函數,會自動傳回 null 值。這點跟 Python 的行為相同。
運算子跟大部份語言差不多,不過多了個 ..
(cascade operator) 可以連續在同一個物件上操作,而不用重複物件變數的識別字。
強制轉型的語法是 (variable as Type)
,型別測試則是 (variable is Type)
或是 (variable is! Type)
。
賦值的部份,比較特殊的是 variable ??= value
這個語法,等價於 variable = ((null == variable) ? value : variable)
。
條件評估的部份,比較特殊的是 (expression1 ?? expression2)
,等價於 (null == expression1) ? expression2 : expression1
,不過評估不會有重複的問題。
物件成員存取除了一般的語法之外,還支援物件為 null 的存取,語法是 (obj?.member)
,等價於 (null == obj) ? null : obj.member
。
邏輯判斷和迴圈跟 Java 差不多,大概就是 if - else if - else
, for ( ; ; )
, for ( in )
, while ( )
, do-while
, switch-case-default
這些,此外 continue
與 break
也有支援。其中 for ( in )
的語法是針對 Iterable 與 Iterator 類別容器的遊走,內建的 List 物件支援 Iterable 但是 Map 則沒有,不過 Map 有支援 .forEach() 方法。
例外部份跟 Java 也是很類似,不過語法有點不一樣:
... } on OutOfLlamasException { ...
- 攔截特定例外類別,但不處理例外實體。
... } on Exception catch (e) { ...
- 攔截特定例外類別,並處理例外實體。
... } catch (e) { ...
- 擷取所有其餘的例外。
擷取例外實體的 catch()
可以接收 (e)
或是 (e, s)
兩種參數傳入的方式,前面的 e 是例外實體,後面的 s 是堆疊紀錄 (stack trace) 實體。
也支援 ... } finally { ...
語法,來處理不論是不是發生例外的資源釋放作業。
物件的建構子語法大概是 Constructor( /* 參數 */ ) : /* 其他建構子呼叫或值初始化 */, ... { /* 後續初始化作業 */ } 這樣。
針對 factory 模式,有個 factory 關鍵字。如果一個類別的建構子是 factory constructor 的話,則建立物件時必須使用 new 來建立,詳細的用法看文件範例 (Factory Constructors) 比較清楚。
有 property 的 getter, setter 的支援,可以讓 assignment 的 left-hand side 與 right-hand side 反應到比較複雜的邏輯上,而不只是單純的取值或賦值,語法看文件 (Getters and Setters) 比較快。
抽象類別跟運算子覆載跟 C++ 很像: Abstract Classes, Overridable Operators。
每個 Class 會自動的定義一個 Interface 出來,可以說 Interface 就是把函數實作去掉的 Class 定義。保留下來的有: 屬性、方法原型。要把 Class 當 Interface 用,就是在宣告實作的 Class 時用 implement 關鍵字來引入即可。
還可以實作 Mix-in 這種語言模式,要被 Mix-in 的 Class 必須滿足以下要件:
- 不能繼承自 Object 以外的物件,
- 不能有建構子,
- 不能引用上層物件,也就是不能參考到 super 參考。
條件跟平台有關,上面的條件是針對網頁的 dart2js 平台,是目前各平台中比較嚴苛的條件,也許未來隨著新技術的導入可以再更放寬。
要把類別當成 Mix-in 混入,就是在宣告實作的 Class 時用 with 關鍵字把要混入的 Class 導入即可。
類別實體也可以當成函數物件呼叫,只要加上稱為 call()
的方法就好了: Callable Classes。
也有支援泛型,長的很像 Java: Generics。
匯入函式庫是用 import 關鍵字,預設是直接匯入所有套件內的 symbol 到全域空間中: import 'dart:html';
。
可以用 show 來僅匯入部份 symbol: import 'package:lib1/lib1.dart' show foo;
。
可以用 hide 來遮蔽部份匯入的 symbol: import 'package:lib2/lib2.dart' hide foo;
。
另外也可以用 as 關鍵字讓匯入的符號包裝在指定的名稱內: import 'package:lib2/lib2.dart' as lib2;
使用時的語法 lib2.Element element2 = new lib2.Element();
。這樣的用法在文件內稱為 library prefix: Specifying a library prefix。
匯入時 as 跟 show/hide 也可以混用: import 'package:lib2/lib2.dart' as lib2 show foo1, foo2, foo3;
。
在套件路徑中:
- 如果是 dart: 開頭的,屬於核心函式庫,自 SDK 提供;
- 如果是 package: 開頭的,則是透過 pub 工具所管理的套件函式庫,看 pubspec.yaml 中導入了哪些套件,有需要的話也可以將自己當成套件函式庫匯入;
- 沒有特殊的開頭的話,就是相對目前的 .dart 檔的相對路徑。
可以參考建立套件函式庫的指引來瞭解關於函式庫的事情,即使只是使用函式庫沒有要製作函式庫: Create Library Packages。
如果要製作函式庫的話,可以參考 shelf: Web server middleware for Dart 專案的作法,這個函式庫被用在 pub 中,應該會持續積極維護吧!
非同步的部份,有蠻多東西的... 在語言本身有 await 與 async 兩個關鍵字,搭配核心函式庫的 Future 與 Stream 兩個類別。
關鍵字 async 是用來修飾一個函式,表示函式是以非同步的方式傳出參數。用 async 修飾過的函式,其函數原型必須定義為傳回 Future
要等待 Future 完成再往下執行的話,可以使用 await 或是 Future 物件的 .then() 方法。使用 await 的話,函數本身也必須用 async 修飾,因為函式的結束時機也變成不一定。使用 Future 物件的 .then() 方法的話,就無法確保函式結束時非同步的程式碼已經完成。用簡單的程式來測試 await 跟 Future.then() 兩種同步方式:
Stream 是代表一連串非同步傳回的結果物件,對應的同步方法是使用 await for (variable-declaration in expression-result-into-stream) { ... }
語法,詳細的說明可以看 Using asynchronous for loops with Streams。
另外有兩篇講非同步的文章:
- Dart Language Asynchrony Support: Phase 1:
-
這篇講的是比較基本的 async, await 的用法,大概就上面那些。
- Dart Language Asynchrony Support: Phase 2 - async*, sync*, and all the rest:
-
這篇講到進階的用法,包含怎麼寫 generator 函數,同步的部分就會用到 sync*, yield,非同步的部分就會用到 async*, yield。
還有提到撰寫 generator 時,如果要把另一個 iterator 的元素一個一個丟出去,除了寫迴圈一個一個 yield 之外,可以使用 yield* 語法。
在 C/C++ 會見到的 typedef 在 Dart 也是有,不過目前只能用來定義函數介面的別名: Typedefs。
可以用 @annotation 來幫程式中的元素加註,可以用在:
- library
- class
- typedef
- type parameter
- constructor
- factory
- function
- field
- parameter
- variable declaration
- import directive
- export directive
關於名稱 annotation 與 metadata 的差異,似乎是在程式碼裡是 annotation,等到了執行時期變成物件可以被程式使用了,就轉而叫做 metadata 的樣子。也許是類似 class 跟 object instance 的關係吧。
依照文件所說,取出 metadata 的 dart:mirrors 函式庫介面仍然是不穩定的,不過還是做了一個小測試來看看大概介面大概長什麼樣子。
加註的語法跟 Python 的 @decorator 差不多,但是實質上有非常大的差別,要小心不要弄混了。
註解寫法跟 C++, Java 差不多,另外還有類似 C# 的產生文件用註解語法。
程式風格可以參考 Effective Dart 的建議。列出幾個比較特殊的:
-
識別字 (Identifiers) 的大小寫:
- 類別名稱 (含要用來做 Annotation 的類別): UpperCamelCase
- 檔案與目錄的名稱: lowercase_with_underscores
- 利用 import-as 匯入函式庫時所設定的別名或前置名稱 (prefix): lowercase_with_underscores
- 常數: lowerCamelCase (他們覺得 UPPERCASE_WITH_UNDERSCORE 不好看: PREFER using lowerCamelCase for constant names)
- 其他 (區域變數、方法、屬性): lowerCamelCase
- 不要用 Tab: DO NOT use Tabs ... 個人是不太認同,不過是在網頁開發界常見的觀點,而且有提供 dartfmt 格式化工具,這可以解決一些用空白縮排沒對好但沒看出來的問題。
- 文件撰寫指引裡面講到一些寫作的方法: Documentation。
- 不要用 .length 檢查 Collection 實體內有沒有東西,速度會比較慢: DO NOT use .length to see if a collection is empty。
- 避免使用 Iterable.forEach() 來進行元素遊走,沒特別講原因,不過可能是會略影響效能吧,雖然是可以在編譯時期最佳化去克服: AVOID using Iterable.forEach() with a function literal。
- Nested function 功能等同建立一個區域的函數物件實體的變數,所以不需要使用建匿名函數再賦值到變數上的方式: DO use a function declaration to bind a function to a name。
- 忽略區域變數的型別指引,因為型別推理應該會成功: CONSIDER omitting the types for local variables。
- 儘量使用 await 來進行非同步資料的使用,這樣程式比較好懂: PREFER async/await over using raw futures。
- 在 Design 裡面,講到很多讓程式碼比較清楚的技巧。
在 Library Tour 介紹了內建的基礎函式庫,大概是這些:
- dart:core
- 基礎型別,包含: 數值、字串、正規表示式、網址、日期時間、各種容器 (List, Map, Set) 跟相關類別 (Comparable, Iterable, Iterator)、例外。
- dart:async
- 基本上就是非同步操作相關的類別與函式,不過還是鼓勵利用 async, await 之類操作: dart:async - asynchronous programming。
- dart:math
- 數學函式庫,包含三角函數、最大值、最小值、亂數,可以考慮使用
import 'dart:math' as math;
的方式引入來區隔來自 dart:code 的函式: dart:math - math and random。 - dart:html
- 針對瀏覽器平台的函式庫,包含 DOM, CSS 操作、事件處理、HttpRequest (AJAX)、WebSocket 等介面。針對比較新的進階應用,有另外封裝在別的內建函式庫中: dart:html - browser-based apps - more information。
- dart:io
- 這個函式庫只能用在命令列介面中,可能就是只在 DartVM 平台上實作吧... 主要是檔案跟資料夾的 I/O、HTTP 與 WebSocket 的 server 與 client 的實作、子行程、Socket、標準輸出入。
- dart:convert
- 一些轉來轉去用的函式: JSON 轉換,UTF-8 字串的轉換、Base64 轉換、HTML 脫逸... 之類的。
- dart:mirrors
- 作 Reflection 用的函式庫,除非是要開發接近語言底層的工具,不然看起來是少碰為妙。
線上的函式庫 API 文件: Dart SDK - https://api.dartlang.org/stable/。