iT邦幫忙

2023 iThome 鐵人賽

DAY 2
1

前面有提到 Dart 是 Flutter 所使用的核心語言。因此在我們投入 App 開發前,先來了解一下 Dart 的一些基礎語法吧!

DartPad

跟許多程式語言相同,Dart 也有推出類似 Playground 的線上編譯器,名叫 DartPad。您可以盡情在裡面嘗試 Dart 的語法。DartPad 傳送門

宣告修飾字

在談論變數型態前,我們先來討論幾個宣告變數的修飾字。

var

我們可以使用 var 來進行變數宣告,Dart 會自動的針對給定的值來推定型態,並且該型態一旦推論完後,便不得再更改

// name 已經自動推論型態為 String
var name = 'Micro Jordan';

// 此時會報錯,因為嘗試將 Int 型態的值存入 String 的變數中
name = 1;

dynamicObject

Object 是所有型態的根類別,所以每種型態都是 Object 的子類別。也就意味著我們可以將任何值放入以 Object 宣告的變數。
dynamic 是 Dart 中的動態型別,不為在編譯時進行型別檢查,也就意味著可以自由的變換型態。

// Object 因為是根類別,所以什麼型態都能接受
Object obj1 = "I'm an object";
obj1 = 1;

// dynamic 則是自動變化,現在是 String
dynamic obj2 = "I'm an dynamic";
// 現在又變成 Int
obj2 = 1;

// 此時 Dart 會標註此行錯誤,原因是 Object 並沒有提供 length 這個屬性
print(obj1.length);
// 由於是 dynamic 讓編譯器跳過檢查,不過執行的時候就會真的出錯了
print(obj2.length);

finalconst

有時我們會希望宣告某個值為常數,這時我們可以考慮使用 finalconst 。兩者皆為把某值設為常數的語法,差距在於 const 設定的值在編譯時會直接被替換為某值,因此要使用 const 時請確保你要設定的值在程式執行前就已經確定好;相反的 final 雖然也是設定變數,但可以是在運算後得到的結果。

// 假設我們在根據資料筆數計算總頁數
// 每頁固定 10 筆資料,這在編譯前便可以確定,因此宣告為 const
const pageSize = 10;

// 但由於資料筆數不固定,因此 totalPage 無法在編譯前就先行確定,需要經過運算 total / pageSize 才可以得出總頁數
// 因此宣告為 final
final totalPage = 20;

null-safety

null 代表著「無」,在 Dart 中用於標示某個變數或對象的值不存在,並且可以被賦值給任何型態的變數。
Null safety 強制區分了 nullnon null 的值。在 Dart 預設中所有類型皆預設為 non null ,這也意味不能接收 null 值。若需要使用空值,則必須將該類型加入 ? 修飾,來表示該變量為 nullable 。透過這樣的分別能使程式碼更加安全而減少出現預期外的錯誤。
舉例來說,當你預期取得的為一個 int 變數但卻獲得 null 時,程式就會在執行時產生 runtime error 。因此正確應將該變數宣告為 int?

截至寫文章的今天 Dart 的版本為 3.1.1 ,自從 3.0 版本開始, Dart 強制採用 null-safety ,這可以非常有效的避免寫出執行時產生難以預料結果的程式碼。
我們透過例子來說明

// 在未有 null safety 前,此函式的傳入型態可能是 null 或 non null
bool isEmpty(String string) {
  // 當 string 為 null 時,會因為沒有 length 屬性而出現 runtime error
  return string.length == 0;
}

main() {
  isEmpty(null);
}

但有了 null safety 後便可以更有彈性的去調用這個函式

// String? 表示該值可為 String 或 null 皆可
bool isEmpty(String? string) {
  if (string != null) {
    return string.length == 0;
  } 
  return false;
}

void main() {
  isEmpty(null);
}

變數型態

在 Dart 中,與其他語言相同也提供了幾種常見基本資料型態,以下將一一介紹:

Numbers (數字)

Dart 的數字型態定義為 num class,底下有兩個子型態分別為

  1. int - 整數型態,至於整數的範圍則要取決於執行的平台
  2. double - 64 bit 的雙精度浮點數
    將目前我們認識的型態位階畫出來可以得到這張圖,越上面的就代表位階越高。至於怎麼看這張圖呢?可以說
    • doublenum 的一種 ✅
    • double 也是 Object 中的一種 ✅
    • intdouble 的一種 ❌

https://ithelp.ithome.com.tw/upload/images/20230917/20135082oaInhjXru3.png

// 在 dartpad 中試試吧!
// 宣告變數方式與多數語言相同
//[變數型態] [變數名稱] = [變數值];
int a = 1; // 標註實際型態
num b = 1; // 標註父類別型態
Object c = 1; // 標註祖父(?)類別型態

print(a.runtimeType);
print(b.runtimeType);
print(c.runtimeType);
// 以上三個變數在執行時都會被認定為 int 型態

// 這時候 dartpad 應該會自動把 a 用紅色底線標著
// 因為不能將 double 的值放到 int 變數中  
a = 1.2; 

// 這裡就不會跳錯,因為 double 也屬於 num 的子類別,因此可以自由的轉型
b = 1.2;

// 此外 Dart 也提供了一種自動推論型態的變數宣告方式
var d = 1;
// 這樣其實 Dart 也能自動的判別出 x 的型態為 int
print(d.runtimeType);
// 但是謹記!! var 會自動推論型態,一但給定型態後便無法再進行轉換
d = 1.2;

Strings (字串)

Dart 的字串是以 UTF-16 編碼為單位,用單引號或雙引號括起來的都可以是字串。

String contest = '鐵人賽';
String name = "Micro Jordan";

// 串接字串也很簡單
print('2023 ' + contest + ' ' + name); // 2023 鐵人賽 Microjordan
// 也可以這樣做,就不用一直加來加去
print('2023 $contest $name');
// 若要運算則必須寫成 ${expression},例如:
int year = 2021;
print('${year + 2} $contest $name');

Booleans (布林)

僅有 truefalse 兩種值,用於條件判斷式

String message = "Hello Flutter";

// 在 Flutter 中與多數程式語言一樣使用 "==" 作為判斷前後是否相符的運算子
// 所以這行看的順序是先看 message 變數是否與 "Hello Flutter" 的字串相符,並將 true / false 放入 isGreeting 變數中
bool isGreeting = message == "Hello Flutter";

// true
print(isGreeting);

Runes and grapheme clusters (符文和字型叢集)

Unicode 是國際標準的字符集,編碼了世界上大多數的文字。因此無論是數字、英文字母、中文字甚至是表情符號 emoji 都可以對應要一組編碼,並且只要採用 Unicode 標準的皆通用。
前面有提到 Dart 的 String 型態使用的 UTF-16 為編碼單位,我們這裡並不細講編碼的細節,我們只需要知道 UTF-16 有編碼的極限,也就意味著有些字符並無法被容納在單個 UTF-16 編碼之中。
俗稱🐎:一個便當吃不夠,可以吃兩個!!一個 UTF-16 字元裝不下,可以用第二個來裝。
在 Dart 中定義了 runes 的 class,讓我們可以看到每個字元的編碼

var string = '鐵人賽';
print(string.runes.toList()); // [37941, 20154, 36093]
print(string.length); // 3

var emoji = '😊';
print(emoji.runes.toList()); // [128522]
print(emoji.length); // 2

從上述雖然我們得到了 😊 符號的編碼,但由於一個 UTF-16字元裝不完,需要兩個才能完整的表示該字元。也因此在印出字串長度時,明明是一個字元卻會得到 2 這個結果。
所以這時候就需要用到 Flutter 官方提供的 characters 套件,使用 character API 就可以正常的印出正確的長度拉。

import 'package:characters/characters.dart';

void main() {
  var emoji = '😊';
  print(emoji.runes.toList()); // [128522]
  print(emoji.length); // 2
  print(emoji.characters.length); // 1
}

Symbols

Symbol 是一種代表符號的特殊類型,通常用於識別 Dart 程式碼中的符號名稱。但因為並非常用的資料型態,因此我們就先略過。
我們上面介紹了數種變數型態,都圍繞在單一物件的範疇,下部分我們會談談 Dart 中將多個單一物件包在一起時有哪些東西可以使用。


Records

需要 Dart version >= 3.0 才支援
Records 是可將多個物件放入一個單一物件,並且大小是固定的。

// 宣告方式
var ithome = {2023, name: 'Micro Jordan', language: 'Flutter', '鐵人賽'};

// 取用每個變數
print(ithome.$1); // 因為 2023 並沒有名稱可以調用,所以用 $1 表示取用第 1 個匿名的值
print(ithome.name); // Microjordan
print(ithome.language); // Flutter
print(ithome.$2); // 取用第 2 個匿名的值

我們也可以標註 record 的型態

// 宣告 phone 是一個由 (String, int) 組成的 record
(String, int) phone1;
phone1 = ('iPhone', 14);

// 使用加上 name 讓給定的值到對應的 name 時就要加上 {} 才能宣告
({String model, int generation}) phone2;
phone2 = (model: 'iPhone', generation: 14);

Lists (陣列)

與多數的程式語言相同,Dart 也有提供陣列的使用,也與多數語言相同,陣列統一是從 index 為 0 的地方開始存放。

// 宣告陣列的方式
var nums = [1, 2, 3, 4];

// 此時由於是 var,因此會自動推論為 List<int> 型態,也就是存放整數型態的陣列

// 陣列的操作
nums[0] = 20; // 可直接更動某值   [20, 2, 3, 4]
nums.add(2); // 新增元素至陣列尾端 [20, 2, 3, 4, 2]
nums.remove(2); // 刪除第一個值為 2 的元素 [20, 3, 4, 2]
nums.removeAt(0); // 刪除 index 為 0 的元素 [3, 4, 2]
nums.insert(2, 3333); // 在 index 為 2 的地方插入 3333. [3, 4, 3333, 2]

在 Dart 所定義的 List 中其實有分兩種,一種為固定長度;另一種是可變長度的。預設是採用可變長度的 List,也因此可以使用 addremove 這些變動陣列長度的 API。

https://api.dart.dev/stable/3.1.1/dart-core/List-class.html

Sets (集合)

Sets 是物件的集合,並且內容不重複。換句話說就是同樣的內容,只會在 Set 中看到一次,如果有第二個相同的內容要加入時,Set 會判別到相同內容而略過操作。
在 Dart 中其實提供多種 Set 的實作,內容不重複的精神是一樣的,不過在於實現的資料結構不同而使的有不同特性。

  1. LinkedHashSet : Dart 預設的 Set 實作,內容會按照存放的順序而依序存放。
  2. HashSet: 無序的存放所有內容
  3. SplayTreeSet: 使用 self-balancing binary tree 實作,在保證存放速度的同時,也使存放的內容得以有序的存放。
// 因為 HashSet / SplayTreeSet 在 collection 中,因此要先引入
import 'dart:collection';
void main() {
  var set1 = <String>{'Zebra', 'Ant', 'Ballon', 'Candy'};
  print(set1); // 按照輸入順序輸出

  var set2 = HashSet<String>();
  set2.addAll({'Zebra', 'Ant', 'Ballon', 'Candy'});
  print(set2); // 原則上是無序輸出,存放順序是按照 hashCode 來決定,所以這裡看不出來無序的部分

  var set3 = SplayTreeSet<String>();
  set3.addAll({'Zebra', 'Ant', 'Ballon', 'Candy'});
  print(set3); // 按照字母的順序輸出,因為字母順序是 A -> B -> C -> Z

  // set 的基本操作
  set1.add('Banana'); // 加入元素
  set1.remove('Zebra'); // 刪除元素
  print(set1.contains('Banana')); // 是否包含元素
  print(set1.length); // set 的長度
}

Map (字典)

Map 這個資料型態跟字典一樣,我們會預期查找字典時,一個單字對應到的是一個解釋。因此 Map 在設定時需要同時給定 Key (單字) 和 Value (對應的解釋),並且 Key 是唯一且不重複的。

// 定義 dictionary 變數為 Map 型態,且 Key 為 String / Value 也為 String 
var dictionary = Map<String, String>();

// 加入元素至 Map 中
dictionary['apple'] = '平果';
dictionary['banana'] = '香蕉';

// 直接修改 Key 對應的 Value
dictionary['apple'] = '蘋果';

因為 Map 中的 Key 是唯一且不重複的,聽起來跟 Set 的特性很像對吧!所以正是因為如此,Map 也提供了三種 Map 的實作:
1. LikedHashMap
2. HashMap
3. SplayTreeMap
細節我們也不談,總之就大同小異拉~

今日總結

因為篇幅的關係我們沒辦法一一的講述每個型態底下各自的方法,畢竟我們的目的的實戰,而不是翻譯官方文件。簡單總結一下今天的內容:
- 要充分瞭解各修飾字的用途
1. int - 整數
2. double - 倍精度浮點數,也就是可用來表示小數點
3. String - 字串,用單引號或雙引號刮起來的都是
4. Boolean - 布林值,用於判斷式
5. Records - 固定長度可存放多個物件的單一物件,然後要 Dart version >= 3 才支援
6. Lists - 陣列,長度是可變動的
7. Sets - 集合,用於存放不重複的內容
8. Map - 字典,需要一對 key / value 來表示,key 值不得重複

今天的內容如果是有程式基礎的人來看應該很快就可以瀏覽過去,未來 2 ~ 3 天也是著重在 Dart 語法相關的介紹,畢竟工欲善其事,必先利其器,先打好對這個語言的基礎絕對不是壞事拉 XD

明天我們會講解關於迴圈、判斷式、函式的相關內容。明天見囉~


上一篇
[Day 01] 什麼是 Flutter?
下一篇
[Day 03] Dart 基礎語法 Part 2
系列文
Flutter 從零到實戰 - 30 天の學習筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言