iT邦幫忙

2024 iThome 鐵人賽

DAY 4
0

本篇內容我們將開始介紹 Dart 的一些基礎知識以及在使用 Flutter 時常見的語法或功能。畢竟,我們的目標是開發 Flutter 應用程式。

Google I/O Flutter 3.22 & Dart 3.4

Dart 語言基礎

Dart 由 Google 主導於 2011 年公開。其開發團隊由 Lars Bak 和 Kasper Lund 領導,Lars Bak 是知名的虛擬機專家,曾領導開發 V8 JavaScript 引擎。

V8 的 8 表示作者 Lars Bak 寫的第 8 個虛擬機,之前的包含 Java、Smalltalk 等。

這個章節我們將涵蓋基礎語法、變數、型別、控制流程、函式等基礎。現今許多程式語言都會借鑑其他已存在語言的優點,Dart 也不例外,集合了許多高階成熟語言的功能。

  • 生產力工具包含分析程式碼,編輯器擴充套件,套件生態圈
  • 垃圾回收機制
  • 靜態型別
  • 型別註釋
  • 可移植性
  • 非同步

在深入各種語法之前,先讓我們稍微認識一下這個語言,其中一個比較特別的是;Dart 可以在兩種環境下運行:

  • Dart VM
  • JavaScript 編譯

且執行 Dart 程式碼有兩種模式 JIT 和 AOT

  • JIT 編譯; 即使編譯就是程式碼只有在需要的時候才編譯。Dart VM 會載入和即時編譯程式碼為原生機械碼。這種模式通常用在“使用指令執行程式”或者在開發行動應用程式時支援像是除錯和熱重載。
  • AOT 編譯:將 Dart VM 和程式預先編譯,VM 的功能更類似於 Dart 執行環境系統,提供垃圾回收,和其他SDK 支援的原生方法。這種方式的好處在於效能優勢,但無法支援熱重載。

所謂 Dart VM 你可以看成是一個用 C++ 開發彙整以下功能的一個程式

  • 編譯器
  • SDK
  • 執行環境需要的功能如垃圾回收

在 JIT 模式下,功能就是即時編譯器把程式碼轉換成機械碼,但是在 AOT 模式下會把用到的執行環境功能和程式碼一併打包編譯。因此,對於最終用戶來說,他們不需要在他們的作業系統上安裝任何關於 Dart 的執行環境或工具。

Dart 參考了許多已存在的語言因此開發者一般很容易從其他語言切換到 Dart,尤其是如果你有 C 或 JavaScript 的經驗,對於 Dart 的語法你應該不陌生。

Dart 使用 OO 的概念設計,也就是基於物件的概念。物件使用 class 定義,遵循了 OO 的原則,Dart 得益於封裝(Encapsulation),繼承(Inheritance),組合(Composition ),抽象化,多型多態性(Polymorphism)。

Dart 的型別系統不只讓開發者可以在編譯時期發現問題,而且也支援強大的自動補齊功能。除了一般的型別外,Dart 也支援 Null 安全。

不同於某些語言,Dart 也具備嚴謹的 Null 安全機制。空值(null)是程式語言中常見的概念 — 它簡單表示沒有值。與 JavaScript 不同,Dart 沒有未定義(undefined)的概念。Null 安全允許開發者指定和識別哪些變數或參數可以為空值,或哪些方法可以返回空值。有了這些識別,開發者可以根據需要調整其程式碼以應對可能的空值。

Dart 分析器還可以識別潛在的空指針異常(null pointer exceptions,即空值導致代碼失敗的情況),並迫使開發者透過額外檢查或通過緊縮類型來排除空值,從而準備應對這種情況。

變數

type identifier = value;

通常我們開發時直接使用 var 讓編譯器自行推斷,如果無法推斷則為 dynamic 型別。

變數命名規則:

  • 不可使用關鍵字如 newclass
  • 須使用英數字元。
  • 不能包含空白和特殊字元,_$ 例外。
  • 不可使用數字開頭。

這些和大部分語言差不多。

var inferredString = 'Hello';
String explicitString = "World";

Null 安全

Dart 的變數可以「沒有」值,稱為 nullnull 值時在 Dart 2.12 版本加入的,我們可以賦值 null

在 2.12 版本之前下面程式碼是正確的

int n; // n 初始化為 null
print(n); // null
n = 42;
print(n); // 輸出 42

在 2.12 之後的版本,上面寫法會顯示錯誤。如果要允許某個變數可以為 null 需要進一步處理,有 2 種方式:

  • ? 宣告:int? n;
  • late 宣告:late n; 。很多時候你知道某個變數在使用之前會設定值,但是在宣告的時候無法立刻用值初始化。在 Flutter 中的一個例子是,一個變數被宣告時無法被賦值,但在 Widget 初始化的時候會立即設定上去。這種情況就應該使用 late 型別。

存取可 null 的變數

如同我們的猜測,如果一個變數可以是 null 那麼我們需要在使用之前檢查它是否為 null,假如我們有一個變數負責計算儲存分數,且比賽開始之前為 null

int? goals;
print(goals + 2);

輸出 goals 變數把 null + 2 是會產生錯誤。為了解決這個問題,你可以顯式的檢查變數只有在變數不等於 null 才存取:

int? goals;
if (goals != null) {
  print(goals + 2);
}

通過 if 陳述式,我們檢查了 goals確定不等於 null。Dart 會記得這個檢查並允許 + 2。

當 Dart 支援 Null 安全時,團隊決定改變語言的預設行為。團隊決定強制開發者重新評估他們的程式關於 Null 安全的重要性,而不是讓既有的程式繼續運作。雖然過程相當痛苦,很多 Bug 和程式需要改寫,但這讓 Flutter 生態圈變的更好。

內建型別

Dart 為型別安全的程式語言,意味在撰寫程式和編譯時每個變數必須要定義型別。雖然型別是強制的,但型別註釋(Type annotations)不是。

這裡我們先提到 Type Inference v.s. Type Annotation。所謂型別註釋就只是明確宣告型別如 String str 而型別推斷則是讓編譯器幫我們分析推斷 var str

也就是說如果 Dart 可以推斷的話,我們就不需要明確的宣告型別。下面是一些內建的 Dart 資料型別:

  • 數字如 numintdouble
  • 布林如 bool
  • 集合如列表,陣列,Map
  • 字串函 Rune(表達 Unicode 字元)

數字:

  • 整數(int):64位有符號非小數整數值,範圍從 -2^63 到 2^63-1。例如 27、-1 和 534。

  • 雙精度浮點數(double):Dart 使用 64 位雙精度浮點數來表示小數數值。例如 1.0、-57.00001 和 0.2。

兩者都是 num 型別,此外 Dart 還支援 dart:math 函式庫協助計算。

⚠️ 有些行為會根據平台有些不同。例如當執行在 Web 情況下 int 和 double 會編譯成 JavaScript 的 number ,那麼精度就只有 -2^53 到 2^53 -1

Dart 還支援 BigInt 型別其限制取決於系統的記憶體,但要小心使用,其效能不比 num

布林:

Dart 同樣也提供常見的 bool 型別 truefalse。布林是簡單的真值,用於邏輯處理。但不像 JavaScript

JavaScript 中 Falsy 有 false0-0,空字串,nullundefinedNaN 除了這些其他都是值 true

Dart 的布林型別相對嚴格,並且不採用跟 JavaScript 一樣的行為。

列表:

在 Dart 語言中,列表包含了其他語言中陣列(array)和列表(List)類型的功能。正如其名所示,列表存放一系列的值,其中這些值的順序是重要的。例如,一個有優先順序的活動列表或一個基於時間的事件列表可以存放在 Dart 的列表中。列表中的每個值都有一個索引(index)。

  • [index] 語法可用索引存取列表中的值
  • + 可以串接兩個列表
  • add 可以將值加入列表後面
  • length 可以檢索長度
  • remove 可以移除

⚠️ 列表 List 預設並沒有長度限制。我們應使用 [] 來建立列表,舊版使用 List 型別的建立方式已棄用:

List list = [];
print(list.length);
list.add('Hello');
List nums = [1, 2, 3];

建立列表時,可以設定一個長度來強制固定大小。固定大小的列表無法擴展:

List fixedList = List.filled(3, 'World');
fixedList.add('Hello'); // Error

在很多 OO 語言中會使用 new Type 的方式建立物件, Dart 過去也是這樣,但現在已不再使用 new 關鍵字來創建實例。但請注意,new 仍然是一個保留關鍵字,所以您不能將變量命名為 new

Map

在 Dart 中 Map 是動態鍵值對集合,可以通過鍵來存取和編輯值。這和 List 非常類似,除了用索引來存取外,在 Map 是使用 Key。同時 Key 和 Value 可以是任何型別。跟 List 不同,Map 沒有排序的概念雖然其中一些 Map 型別可以執行排序。關於 Map 也提供一些方便的方法:

  • [key] 運算子來存取值
  • length 可以取得 Map 長度
  • remove 方法可以移除 Map 中的元素

Map 應使用 {} 大括號來建立

Map nameAgeMap = {};
nameAgeMap['Alice'] = 23;
Map preFilledMap = {"Search": 1, "Alex": 2};

String

在 Dart,字串是一系列的字元 - UTF-16,主要是用來呈現文字。Dart 字串可以是單行或多行,搭配成對的單引號或雙引號來包住字元。

String a = 'Here is a signle quote string';
String b = "Here is a double quote string";

此外,多行字串可以使用 '''""" 建立(Python 也有類似的 f"""

String str = '''Here is a multi-line single
	quote string''';

注意到第二排的縮排也會包含在字串中。

字串可以使用 + 號串接,此外 * 可以用來重複指定的數量,[index] 可以檢索字元。

String s1 = 'Here is a ';
String s2 = s1 + 'concatenated string';
print(s2[0]);

字串插值

字串插值 String interpolation / variable expansion 是一個決定字串中佔位符 Placeholder 的動作,然後結合成結果。Dart 支援簡單的語法 ${}$ 符號是標記佔位符要被計算的地方。如果要計算的位置是一個變數,那麼大括號可以省略,如果不能被省略那麼會出現警告。當一個佔位符涉及超過 1 個變數時,大括號就是用來註記邊界的。

String a = "Happy string";
print("The string is: $a"); // The string is: Happy string
print("The string length is ${$a.length}"); // The string length is 12
print("$a.length"); // 這樣會輸出 Happy string.length

Dart 也支援 Rune 來表示 UTF-32

字面量 Literals

字面量是一種標記方式,用來表示固定的值。我們在前面已經見過了

  • int : 10, 1, -1, 5
  • double: 1.2
  • bool: true, false
  • String: "Dart"
  • List: [1, 2, 3]
  • Map: {"a": 1, "b": 2}

finalconst

有時候我們希望變數的值可以固定,要達成這個需求我們可以使用 constfinal 來確保值是固定的。

final String a = 'Staithes';
const int n = 3;

兩者有些許的差異,主要是這個值能否在編譯時期或執行時期被計算。如果一個變數在編譯時就可以決定其值,那麼應該使用 const。如果其值需要在運行時才能確定,例如某個值需要在物件生命週期執行時才能賦值且只有一次,那就使用 final

動態型別和使用 as

有時候你會從沒有提供資料型別的來源取得資料。例如從 API 取得 JSON 資料。這種情況下資料會被註記為 dynamic 然後編譯器會允許你直接操作資料,因為它無法檢查資料。如同你的猜想,假如資料型別不是我們預期的那樣,會造成執行時期的錯誤。

舉例來說,如果你使用 dio 套件呼叫一個 API ,接著取得 Map<String, dynamic> 型別的回應和資料。其中 Key 是資料名稱,Value 這是動態型別的內容。

import 'package:dio/dio.dart';

void main() {
  var dio = Dio();
  try {
    Response response = await dio.get('https://api.example.com/data');
    Map<String, dynamic> data = response.data;
    print(data['key']); 
  } catch (e) {
    print(e);
  }
}

如果你非常肯定 dynamic 值屬於某個型別,那麼你可以使用 as 關鍵字告訴編譯器。從這個時候開始,編譯器會認為它的型別是安全的,但這些全部源自於你假設你知道和註記了正確的型別。如果我們知道 JSON 回應的屬性 id 是字串我們可以如下

final id = json['id'] as String;

那麼 id 變數現在開始就是字串了,編譯器知道該如何檢查。然而如果結果證明 id 是可以為空的 String? 型別,那麼 as String 會在執行時期收到 null 時會失敗。現在我們概略了解型別安全、Null 安全、不可變(Immutable)等方式是如何運作了。

Dart 運算子

在 Dart,運算子只是「特殊語法的方法 Method」。當我們使用如 == 運算子時,就像這樣執行方法 x.==(y) 比對 x 和 y 變數是否相同(這形式的方法設計和 Ruby 類似)。不像 Java 等語言具有原始基本型別的概念例如 intboolean 這些型別並不是物件,而是直接對應底層硬體的數據型別。上面的 x 是一個物件實例因此有自己的方法,也意味著運算子是可以被複寫,因此你可以在 class 中撰寫它們的邏輯。

後續我們會在函式和方法的章節深入介紹,因此你可能會回到這個段落重新閱讀。到此我們理解了為何運算子可以提供不同的功能,是取決於作用的型別。

算術運算子

Dart 同樣支援大多數程式語言常見的算術運算子

+ 加
- 減號和負數
* 乘號
/ 除
~/ 普通 / 除號得到的值會是 double。要單純只取得整數的部分,其他語言通常會需要其他操作;轉型。而 「~/」 只會返回整數的部分
% 取餘數

一些運算子根據左邊運算元的型別有不同的行為,例如 + 不只可以讓數字相加也可以串接字串。

同樣的,Dart 也支援計算縮寫:

int goals = 2;
goals += 3;

遞增 ++ 和遞減 -- 運算子

void main() {
  var a = 5;
  var b = a++; // 後置增量: 先賦值再遞增
  print("a: $a, b: $b"); // a: 6, b: 5

  var c = 5;
  var d = ++c; // 前置增量: 先遞增再賦值
  print("c: $c, d: $d"); // c: 6, d: 6
}

關係運算子

  • ==
  • !=
  • >
  • <
  • >=
  • <=

Dart 只有 == 運算子,Dart 是強型別語言,不會像 JavaScript 那樣進行自動類型轉換。例如,"1" == 1 在Dart 中會是 false,而在 JavaScript 中會是 true。對於基本資料型別 (如int, double, bool),== 比較的是值。物件類型預設是參考(Reference)。但比較特別的是 == 是可以覆寫的,例如 String 類別重寫了 == 運算子,比較的是字串的內容(值),而不是引用。

// 基本類型比較
print(1 == 1);  // true
print(1 == 1.0);  // true
print("1" == 1);  // false

// 字符串比較
print("hello" == "hello");  // true, 比較的是值

// 列表比較
print([1, 2] == [1, 2]);  // true, 比較的是內容

// 對象比較
class Person {
  String name;
  Person(this.name);
}

var p1 = Person("Alice");
var p2 = Person("Alice");
print(p1 == p2);  // false, 比較的是引用

// 自定義 == 運算子
class Dog {
  final String name;

  Dog({required this.name});

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Dog &&
          runtimeType == other.runtimeType &&
          name == other.name;

  @override
  int get hashCode => name.hashCode;
}

邏輯運算子

Dart 的邏輯運算子應用在 bool 運算元。可以是變數,表達式或條件式。此外,可以結合複雜的表達式。

  • !expression 表達式結果的相反
  • ||OR 邏輯
  • && AND 邏輯

上面我們快速的介紹了語法和其與其他語言運作的差異。同時,你可能也看到各種語言的身影如 Ruby、Python 等。在這個時間學習 Dart 確實不能算是非常容易,因為其已經經歷了幾次的重大轉變也加入了更多的功能。

早期的 Dart 確實更傾向於動態類型,隨著時間推移 2018 年引入了強型別系統,2019 年引入了 UI-as-code 概念,2.12 版本引入了 Null 安全特性,到了 Dart 3 更加入了 Pattern Matching 。這些功能都是基於社群的反饋和意見。總之,我們將循序漸進的掌握這些技能。另外,若想更即時的得知第一手的消息,我們也可以關注 Github dart-langflutter


上一篇
Day 3 快速入門
下一篇
Day 5 Dart 基礎 (下)
系列文
Flutter 開發實戰 - 30 天逃離新手村38
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言