如有引用參考請詳註出處,感謝
如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 DevTech Ascendancy Hub
本篇文章對應的是 深入解析 Dart 語言:命名慣例、類特性、建構函數與抽象特性
以下使用 Dart SDK 3.4.3
版本
先了解一下 Java、Dart 對於檔案取名方式的差異 (以下是取名慣例)
Java 語言
語言類型 | 檔案取名 | 方法取名 |
---|---|---|
Java | 開頭大寫 + 駝峰 | 開頭小寫 + 駝峰 |
Dart 語言
語言類型 | 檔案取名 | 方法取名 |
---|---|---|
Dart | 開頭小寫 + 底線 | 開頭小寫 + 駝峰 |
接下來我們再來了解 Dart 語言所創造的類的特性、特點(以下會跟 Java 語言做比較)
Java 對於類中的屬性有詳細的描述 (public
、protected
、private
、package
),而 Dart 並沒有這些描述字,Dart 預設所有的成員皆是公開的
如果要表達「私有」的特性,Dart 是透過底線(_
)來將屬性、方法私有化
範例如下:
創建 info_bean.dart
檔案:並在內部設置私有、公開成員
創建 main.dart
檔案:
Dart 這種添加底線的私有的特性是 對於不同檔案才有用,無法使用在相同檔案中
也就是說私有方法、成員放在同個檔案中,就可以自由地被相同檔案中的程式存取
範例如下:
如下圖所見,我們可以看到就算使用底線描述的成員,由於在相同檔案中,仍可被其他的方法、類訪問
屬性(Property
)是 Java 中有被提議但尚未實作的功能,它鑑於成員變量與方法之間… 使用屬性時,外部呼叫者看起來就是如同呼叫成員,而內部的實作則是方法
而在 Dart 中,每個成員都可以透過 get
、set
關鍵字將成員轉為屬性使用
Dart 屬性操控 | 格式 | 注意 |
---|---|---|
get | <類型> get <屬性名稱> => 操作函數 | - |
set | <類型> set <屬性名稱> (<參數>) => 操作函數 | 不可以為 final 常量 |
當操作函數只有一行時可以使用 =>
描述函數體,當超過一行時就必須使用 {}
描述
另外在使用 get
屬性時,若參數為空也可以省略參數括號
Dart 使用屬性的範例如下
類中普通的成員
在類中宣告 get 屬性:在內部使用起來就如同使用方法一般,如果符合 Dart 語法規則就可以簡化為單行
在類中宣告 set、get 屬性:對於 Set 屬性則必須接收一個參數,該參數是外部使用者傳入的數值,通常會搭配一個私有數值保存真正的數值
使用範例:在外部使用者使用其來就如同使用類的成員一樣,不會感知到它是屬性的特性
適時的使用屬性可以增加程式可讀性與彈性,並減少複雜度並隱藏細節,第二個使用屬性的範例如下:
Dart 操作符重載,類似於 C++ 操作符多載 (而 Java 就不支持操作符重載),同樣可以重新定義操作符號;透過 關鍵字 operator 就可以重載操作符,而且 Dart 比起 C++ 更靈活,返回值不一定要是自身類
Dart 可重載的操作符如下表(全部的操作符請點擊 Operator 連結 去官網訪站查看)
符號 | 符號 | 符號 | 符號 |
---|---|---|---|
< | > | <= | >= |
- | + | * | / |
~/ | % | [] | []= |
| | & | << | >> |
~ | == | % |
Dart 有一個 call 函數,它相當於小括號 ()
符號的重載(也就是呼叫符號重載)
透過定義它我們只要使用 ()
就可以省略省去呼叫 call
函數… 使用範例如下
如同 Java 的枚舉類,每個枚舉類型都有一個 index 的屬性可使用(用來標示元素的位置),並且可以透過 values
來取得 Enum 的集合
Dart Enum 類使用的範例如下:
Dart 的建構函數可以使用 可選命名參數({}
符號)、可選位置參數([]
符號)來包裹參數達到函數重載的特性 (因為 Dart 沒有函數重載)
命名建構函數:當要載入特定建構函數,可以使用指定的命名方式
使用 「類名.屬性名」,使用屬性名稱命名,可以更清晰的知道自己創建的建構函數使用到哪個屬性,增加程式的可讀性 (以往建構函數並不具有可讀性,因為它沒有名稱,所以對於類的使用者來說缺乏語意)
有可選位置參數的話,為什麼還需要命名建構函數?
同樣是為了更好的可讀性,它可以更好的去了解到目前使用的類有作用在哪,而不用每個建構參數都去了解!
使用範例如下:
定義命名建構函數
使用命名建構函數
初始化列表:這種初始化列表的使用方式如同 C++ 的建構函數一樣,使用冒號(:
)開頭,後面接續需要賦值的屬性
初始化列表很常被使用在定義私有的成員
使用範例如下:
在建構函數後使用初始化列表
使用命名建構函數與初始化列表
Dart 初始化列表與 C++ 初始化列表區別
C++ 11 以後也有初始化列表,不過 C++ 的初始化列表不能使用在類別宣告 (除了 inline 函數),因為 C++ 會把宣告與實現分開,而 Dart 不會
重新定向建構函數:java 在方法體內部使用 this 呼叫另一個建構函數,而 Dart 也可以使用 初始化列表的方式呼叫別的建構函數
建構函數重新定向
呼叫重定向的建構函數
如下圖所見,呼叫重定向的建構函數後,它確被重新導向 InfoBean._nameAge
命名函數中
重新定向建構函數限制
重新定向建構函數不可以使用 this
初始化
重新定向建構函數不能有方法體
const 是在編譯期間就確定,並且它也可以說用在建構函數上… 而需要實現這個功能就要一些條件(或是說限制) 1. 使用 const
修飾建構函數,並 2. 聲明類的 所有 成員為 final
,3. 使用時必須以 const
宣告
const 可以減少動態規劃記憶體空間的耗能,提高運行效能
const 建構函數範例如下
所有的成員都要初始化:
以下是個錯誤示範,由於 name
成員沒有被定義,所以不能使用 const
宣告建構函數
所有的成員皆為 final
描述:
以下是個錯誤示範,雖然建構函數中都有賦予值,但是由於 number
成員沒有被 final
描述,所以也無法建構 const
建構函數
使用 const
建構函數:用法如同呼叫一般建構函數,但是要注意,若要發揮出 const
建構函數的較能,則呼叫時也要用 const
宣告
如下圖,我們可以發現一件很有趣的事情
使用 const 創建的物件,若是成員數值相同則會被歸類為同一個物件,規劃在同一塊記憶體上
Dart 有提供 factory
關鍵字,讓自動產生一個 工廠設計模式 的方法(想了解更多工廠設計請點擊連結),而 factory
這種提供方式就像是替我們創建了一個靜態方法,而該方法專門產生類的實體(instance
)
但這個產生的實體並非是單例,而是新物件
factory & static 方法差異
factory 強制返回的就是該類的物件,而 static 則可以返回不同的物件 (或是不返回)
factory
範例如下
以下使用 Dart 的 factory
實現 單一工廠 模式
實現單例的手法仍然相同,重點就是 1. 私有化建構函數、2. 使用靜態變量保存單例物件、3. 使用靜態方法(而這裡就改成使用 factory
關鍵字)往外提供單例物件
實現物件單例:
使用單例物件
想了解更多單例設計方式,請查看 Singleton 單例模式、設計 | 解說實現 | Android Framework Context Service
由於 Dart 是單執行緒,所以不用擔心同步問題 (不像 java 必須使用 volatile
、synchronized
關鍵字)
同樣的,我們也可以使用 factory
關鍵字來實現 覆用工廠
其重點就是 1. 私有化建構函數、2. 使用 Map 變量保存物件、3. 使用靜態方法(而這裡就改成使用 factory
關鍵字)往外提供覆用物件
實現覆用物件:
使用覆用物件:
想了解夠多的工廠設計方法,請查看 Factory 工廠方法模式 | 解說實現 | Java 集合設計
Java 的抽象設計有 abstract class
、interface
並且它是單一繼承制度,而 Dart 在物件導向的設計上給予了不同於 Java 的設計(接下來這小節要介紹的)
使用 abstract 修飾符定義抽象類(abstract class
)
這個抽象類如同 Java 抽象類不能實例化
繼承抽象類如同 Java 使用關鍵字 extends
,若是有抽象方法就必須實作,並且透過 super 呼叫父類建構函數、方法、成員
範例如下
透過 abstract
關鍵字定義抽象類
定義子類,透過 extends
關鍵字繼承於父類
使用抽象類範例:
Dart 不支援內部類,Java 才支援內部類
Dart 並沒有 interface
關鍵字,Dart 中每個類都隱式的定義了一個包含實例成員的介面(不管抽象 or 實體類)
那要怎麼分辨當前類是使用「繼承」的抽象類,還是使用「實作」的介面類?
這其實要依靠類的實現關鍵字來決定
如果實現類要使用繼承類,那就使用 extends
關鍵字
如果要使用實作介面類,那就要使用 implements
關鍵字
接下來我們看實現類如何透過實作,實體類(class
)的介面、抽象類(abstract class
)的介面
實作「實體類」的介面
定義實體類
由於實體類的方法必須定義方法體,所以以下使用方法的空實現
實現類把實體類作為介面使用
使用 implements
關鍵字實作實體類的介面方法… 由於 IInterface
的方法中有方法體,所以這邊的實現算是覆寫(override
)
實作「抽象類」的介面
定義抽象類
由於使用抽象類,所以抽象方法不需要方法體
實現類把抽象類作為介面使用
同樣使用 implements
關鍵字來實作抽象類的方法
Dart 與一般的高級語言(像是 Java
、Kotlin
、Swift
)不同的特點之一就是「多重繼承」(但 Dart 其實並非真正實現多重繼承,而是使用 Mixins 手動來達成「類似多重繼承」的效果)
Dart 以 mixin
關鍵字來定義「混合類」,並以 with
關鍵字來使用混合類
混合類範例如下
當然 mixin
關鍵字不只可以使用在類上,以可以在另一个 mixin 中使用 mixin
在 Dart 中,mixin
是一組可以應用到其他類上的功能,而 mixin class
它具有一定的限制,這些限制幫助避免多重繼承的菱形問題(最後會說明菱形問題);
mixin 類的限制如下所示
混合類除了空建構函數之外,不能有有參建構函數
混合類除了 Object 類以外,不能在繼承其它類,利用這個規則來避免多重繼承的菱形問題!
什麼是菱形問題?混合類(mixin class
)有多重繼承的菱形問題?
在多重繼承裡有個較為麻煩的「菱形問題」:菱形問題是說,假設繼承兩個混合類,而這兩個混合類中又有相同的方法,那呼叫時到底該定位到哪個實現呢?
傳統多重繼承導致的菱形問題概念圖如下(下圖問題不會出現在 Dart 中)
而 Dart 透過限制混合類(mixin class
)的繼承來抑制這種菱形問題
如上面小節所述,Dart 可以實現類似多重繼承的功能,並且雖然透過一些限制來避免了菱形問題,但還有另一個問題是「Dart 如何定位 mixin class
的方法」
如下圖所示,D 類別繼承了 B、C 類,但雙方都實現了同名的方法,那在呼叫方法 method
時該定位到哪個方法呢?
對於這個問題,Dart 使用了 方法解析順序(Method Resolution Order
, MRO) 來確保在使用 mixin
和 mixin class
時,方法的調用順序是可預測的和一致的,從而解決了多重繼承定位方法的問題
使用範例(測試)如下
定義多個 mixin class
,並類這些混合類中都擁有相同的方法
定義兩個實現類,這個個實現類都繼承多個混合類,並呼叫混合類中相同的方法(describe()
),不過 繼承的順序不同!
測試多重混合繼承後,對於相同的方法會被定位到哪類上
如下圖所見,我們可以看到混合方法會依照順序被覆蓋,由於 VoyageFinalHostel
類最後繼承的混合類是 Hostel
,所以 describe()
就會定位到 Hostel;而 VoyageFinalCoffee
類最後的繼承混合類是 Coffee
,所以會定位到 Coffee
Flutter
、Dart