Thursday, June 15, 2017

Dart 基本語言元素

算是一些筆記,雖然 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 宣告,值的繫結發生在執行時期,且只會發生一次。

數值型別有 intdouble 兩種,都是原則上為 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 這些,此外 continuebreak 也有支援。其中 for ( in ) 的語法是針對 IterableIterator 類別容器的遊走,內建的 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 必須滿足以下要件:

  1. 不能繼承自 Object 以外的物件,
  2. 不能有建構子,
  3. 不能引用上層物件,也就是不能參考到 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 兩個關鍵字,搭配核心函式庫的 FutureStream 兩個類別。

關鍵字 async 是用來修飾一個函式,表示函式是以非同步的方式傳出參數。用 async 修飾過的函式,其函數原型必須定義為傳回 Future 物件,但仍然可以直接用 return 回傳結果的值,實際上函式的回傳值會自動地被包裝成 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 的建議。列出幾個比較特殊的:

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/