做做Flutter-Day04:關於更複雜的型別大小事…

影山小麥機
13 min readJun 23, 2023

聊聊Generic、Typedefs、Type system

Dart Type System

前言

寫了一早上的iOS,才發現自己好像把Flutter晾在一邊了真糟糕。聊聊最近,最近我很常喝古吉咖啡豆,還有哥倫比亞的薇拉,薇拉是以前在苗栗的一間咖啡店喝到的咖啡,對它檸檬、甘蔗的酸甜的味道印象很深刻,所以就買來請家人烘了。

既然是手沖,也可以講講我的手法:

  1. 我最近粉水比都是330ml的水配一匙20g左右的咖啡豆

2. 研磨度是小飛馬601n的3.5左右,有時候會更細一點,Maybe刻度3。

3. 濾杯用珈堂的星芒濾杯,沖法的話大概都是悶蒸30秒後一刀流注完330ml的水。

之前有喝過說著一刀流注水40秒結束的,我覺得這樣還是太快,所以我大多都拉到90秒左右。星芒濾杯的好處就是可以大水,所以我也就大水給它處理,真的還是蠻好喝的。

好啦,總之,這幾天大概都是這樣過,我們來講講Dart的語法吧!

正文

泛型Generic

泛型的概念在Swift中也有,主要在使用的時候上的意義,就是可以不指定型別,可以想像就像玩Uno的時候有多顏色選擇這件事情。

在官方網站上有這樣的詮釋,你可以使用英文代號去設置你預設泛型的概念,比如下面的T就是你可以使用泛型的位置,它可以是任何型別:

abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}

如果是下面的實用舉例,我們在輸入值的部分就可以制定輸入型別,在輸出的tmp這個參數就會是T這個型別,你可以看到在List[T]的取得key也是個可以自己制定的型別:

T first<T>(List<T> ts) {
// Do some initial work or error checking, then...
T tmp = ts[0];
// Do some additional checking or processing...
return tmp;
}

大致上,泛型的好處就是如果這個function是需要重複利用的,使用泛型去定義你的傳出、傳入型別可能會是一個好事情:「因為它可以保持程式設計的彈性。」

但在使用它的時候也需要思考的事情是:「我設計的這個使用泛型的程式碼真的應該這樣用嗎?」

型別定義Typedefs

Typedefs的概念跟Swift的Typileas類似,意義類於給原本的型別或物件新的定義名稱,大概邏輯可以像是下面這樣:

typedef IntList = List<int>;
IntList il = [1, 2, 3];

在官方網站上的這個例子的作法就是List<int>在重新定義後的名字為IntList,所以你在看到這個重新定義的物件的時候,

我們可以再實際的看另外一個舉例:

typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.

這邊的型別定義也是將泛型插入,然後就可以取得內部的值,所以這邊可以看到m2的型別定義是可以讓定義的寫作更加的簡潔,

不過,使不使用重新定義型別名稱,取決於個人對於當下程式碼的可讀性,如果typedefs有助於讓程式碼的前後文的更好讀,而不會在開發者體驗上再花時間去追溯程式碼的定義,而搞不清楚這個物件的定義上在做些什麼。

型別系統Type System

Dart的型別系統,在目前的設計中是強型別的宣告方式,據我寫JavaScript的朋友們說,JavaScript在處理很多事情上很尷尬,有很多時候某個變數在程式邏輯的上下文中可能型別就會改變,雖然我本人覺得雖然有這樣的可能發生,但編程習慣應該更加的嚴謹或許就可以避免這種情形啦(by 我這個Swift使用者)

那Dart的型別系統可能會想詮釋些什麼事情呢?我們可以從某一個段落提到的重點切入:

Soundnes(型別安全)

或者我們可以稱之為強型別,在文件的某一段提到:

The benefits of soundness

A sound type system has several benefits:

  • Revealing type-related bugs at compile time.
    A sound type system forces code to be unambiguous about its types, so type-related bugs that might be tricky to find at runtime are revealed at compile time.
  • More readable code.
    Code is easier to read because you can rely on a value actually having the specified type. In sound Dart, types can’t lie.
  • More maintainable code.
    With a sound type system, when you change one piece of code, the type system can warn you about the other pieces of code that just broke.
  • Better ahead of time (AOT) compilation.
    While AOT compilation is possible without types, the generated code is much less efficient.

這一段文本主要提到的重點有四個:

  1. 在編譯時可以揭示型別相關的錯誤
  2. 易讀的程式碼
  3. 更好維護的程式碼
  4. 更好的靜態編譯

其實這樣強型態的宣告系統,在Swift中就有類似的概念了,不過這邊可能要對官方網站或者我們自己舉的一些例子進行分析與解構:

void printInts(List<int> a) => print(a);

void main() {
final list = [];
list.add(1);
list.add('2');
printInts(list);
}

比如像是上面這個例子,我們其實知道printInts這個function在做的事情就是印數字出來,但是在下面這行:

list.add('2');

這邊新增的是一個字串的值進到list這個array裡面,所以實際上在執行printInts的時候必定會報錯囉,因為list在宣告的時候就是一個數字的型別。

系統大概會報這個List的型別是dynamic(動態的)。

實際上有個做法應該會是解決手段,如下:

void printInts(List<int> a) => print(a);

void main() {
final list = <int>[];
list.add(1);
list.add(2);
printInts(list);
}

上面這邊就明確、直接的把新增進list的值都改成數字。

不過,如果要實際的解釋物件導向的階層關係(hierarchy),還是以官網的舉例來說明:

上面這個舉例想表達的意思,是指Alligator(短吻鱷)、Cat、HoneyBadge(蜜獾)這幾類都是Animal動物這個類以下的分支,而Lion、MaineCoon(緬因貓)則是Cat的個類下面的分支。

我們到這邊大致上知道類之間的關係,所以接下來會從語法的角度繼續詮釋:

初始化物件

Cat c = Cat(); ///可以通過編譯

Animal c = Cat(); ///可以通過編譯

MaineCoon c = Cat(); ///會報錯

上面的邏輯我們可以依照階層去推理,如果初始化的物件型別是更下面、更細節的類別,它就不適合做為宣告的型別,這樣會報錯,這邊的邏輯其實就有點像Swift中UIView是做UI的算底層的物件,只要有好的邏輯基本上可以做任何的靜態UI,所以可以以UIView作為宣告型別去處理。

List物件

List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;

在上面的邏輯規則中,因為List的宣告型別是MaineCoon,所以可以符合List<Cat>這個物件的宣告規則,這樣的編譯是合理的。

但有另外一個例子是這樣:

List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals; ///報錯

Animal是比較底層的型別,如果這個時候宣告它,但要它跟Cat這個型別的List去做相等的邏輯,則會有階層的問題。

但在Dart中還是有另一種聰明的設計,就是向下轉型,這個做法在Swift中也有:

List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;

我們只要用as這個關鍵字就可以讓物件的型別轉為List<Cat>,這樣就可以編譯了。

關於繼承關係:覆寫(override)

下面我們定義一個Animal的類別,這個類別有兩個功能:

  1. chase
  2. parent
class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}

上面也就是我們在很前面給的圖的基本類別,這個時候我要創建一個類別叫做HoneyBadger來示範語法:

Case1

class HoneyBadger extends Animal {
@override
void chase(Animal a) { ... } ///可以覆寫完成編譯

@override
HoneyBadger get parent => ... ///可以編譯過
}

上面這個範例可以編譯過,理由是因為parent在Aninal這個基本類別裡具有這個function,所以是合理的階層關係。

Case2

class HoneyBadger extends Animal {
@override
void chase(Animal a) { ... } ///可以覆寫完成編譯

@override
Root get parent => ... ///沒辦法編譯
}

上面這個類別可能沒辦法編譯,因為這邊要覆寫的function並沒有使用Animal底下,所以這邊會報錯。

不過關於覆寫,還有另外一種可能:

class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}

我們一樣是使用Animal這個物件當作是基類,底下分別舉兩種Case來示範:

class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}

Case1:

這邊的Object是所有類別的基類,所以可以接受任何的類型的加入。

class HoneyBadger extends Animal {
@override
void chase(Object a) { ... } /// 可以覆寫成功,因為Object是基本類別

@override
Animal get parent => ... ///可以覆寫成功
}

Case2:

上面任意一種

class Mouse extends Animal {...}

class Cat extends Animal {
@override
void chase(Mouse x) { ... } ///可能會報錯
}

上面這個例子Mouse是Object的子類,所以這邊在文法上並不適合這樣使用。

所以,順著上面的解釋邏輯,在下面a物件宣告為Animal物件的類別,但在調用chase方法中塞入Alligator這個初始化的物件,可能會有型別認定上的物件而報錯:

Animal a = Cat();
a.chase(Alligator()); // Not type safe or feline safe.

那如果要解決上面可能會報錯的問題,我們應該怎麼修正它呢?

我們可以定義一個Cat物件讓chase可以接受Alligator:

class Cat extends Animal {
@override
void chase(Alligator x) { ... }
}

下面這樣就可以編譯了:

Cat a = Cat();
a.chase(Alligator()); // Correct

最後要講一個蠻有好的語法叫做dynamic:

官網下了一個蠻大的標題叫:Don’t use a dynamic list as a typed list

A dynamic list is good when you want to have a list with different kinds of things in it. However, you can’t use a dynamic list as a typed list.

意思是如果你想讓你的List可以容納各樣的型別,這樣是有類似的寫法如下,但你不能把它變成固定宣告的型別:

class Cat extends Animal { ... }

class Dog extends Animal { ... }

void main() {
List<Cat> foo = <dynamic>[Dog()]; // Error
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

上面這個例子想要說的事情是這樣,如果你固定讓List裡面指定了Cat,但卻在實際上的List裡面新增的是Dog物件,這樣會產生問題,就算在陣列前你給了dynamic的標記。

但假如在宣告List的物件時,就已經給了dynamic型別,這樣就算實際上你給這個物件是Dog()、Cat()等的物件,都可以被成功編譯。

然後,也有一種是Run time時候的報錯:

void main() {
List<Animal> animals = [Dog()];
List<Cat> cats = animals as List<Cat>; /// 這邊轉型會出現問題
}

上面的轉型,因為內部是Dog物件,但轉型的時候轉成Cat,這樣會造成Runtime編譯上的問題。

欸不過dynamic看起來還是很好用XD

Type Inference 型別推斷

以下是宣告一個arguments的物件,但我們可以用dynamic給它彈性,讓它可以包含各種型別,而且不會編譯錯誤。

Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};

或者,其實你如果不使用dynamic去指定你的型別要什麼,你也可以自己用var或final來定義它,這個時候它的型別推斷就會是Object,如下:

var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>

其實主要在使用的時候應該還是會有蠻多需要對語法trouble shooting的部分,有機會再分享遇到的問題怎麼解。

後記

最近我爸不知道為啥很愛黃金曼特寧,烘了 一堆到espresso等級的焙度,看了頭都有點痛,畢竟我平常不太喝深烘的,只能全部把它做成冷萃的冰咖啡,這樣至少在晚上嘴饞的時候可以不用攝取太多咖啡因(笑)

不過還是有做一點像是中深烘焙度的咖啡,手沖才不會都是喝深烘的焦味。

然後今天早上在看他挑非洲之王果丁丁的生豆,不知道這次風味會長怎麼樣(笑)

Sign up to discover human stories that deepen your understanding of the world.

影山小麥機
影山小麥機

Written by 影山小麥機

本職為Mobile工程師,熱愛分享視野,也樂意站在ChatGPT的肩膀上。訂閱小麥機,收割技術、職涯、人生的難題。

No responses yet

Write a response