類別 class

什麼是類與物件

類就是分類、歸類,把我們身邊真實存在的事物抽象化,歸納成同一種型別。舉例來說,將全世界所有人抽象成「人類」這個類;而 Bob、Tom 這一個個具體的人,就是物件。

就像是一張「設計圖」或是「樣板」,定義了物件會擁有的結構與行為。

一樣拿人類來舉例,人類這個類就是一套樣板,人具備哪些特徵?有姓名、手腳等器官,也能說話、思考。像姓名、年齡這類資訊,就屬於類的屬性特徵;說話、思考這類動作,則是類的行為。簡單來說,類就是從現實事物中抽取出同一種模板,有了類之後,就能依此建立一個個獨立物件,像是 Jack、Jim 這些人。

前面提到類的屬性(姓名、年齡)與類的行為(說話、思考),在程式裡分別會用變數與函數來表示。

類會規範物件必須具備哪些 屬性(變數) 以及 方法(函數)

類好比烘焙用的模具,它本身不是實體成品;透過同一個模具,我們才能做出一個個實際的麵包。

程式語言發展初期,並沒有「類」的概念,像是 C 語言就只有函數、變數與各式敘述,這種寫法稱為程序導向。隨著軟體功能越做越複雜,開發者發現純程序導向的程式會出現大量重複碼、程式架構雜亂、層次不清、邏輯毫無條理。後來有人提出物件導向的思維:把程式邏輯抽象成一個個物件,模擬真實世界的運作模式。就像一間公司,一開始老闆包辦所有工作;後來招募員工分層負責,要統計報表就找財務、要招募新人就找人資,各司其職之後整體流程會清晰許多。

物件 是類的一個 實例(Instance)

透過 new 關鍵字或是建構函式建立時,類就會產生一個對應的物件。

物件是程式執行時真實存在的實體,可以存放資料、呼叫方法。

類與物件的關係

  • 類是 設計藍圖,物件是 按照藍圖蓋好的實際房屋
  • 同一個類可以建立多個物件,每個物件都擁有獨立的資料。
  • 所有由同一類建立的物件,共用類裡定義的行為(方法)。

接下來我們把焦點轉到 Dart 語言

本章會學習以下重點內容

Dart 是完整的物件導向語言,所有值都是類的實例。除了 Null 以外,所有類都繼承自基底類別 Object

Dart 透過「類別 + Mixin」的機制實現繼承

單一繼承規則:每個類只能繼承一個父類,但 Mixin 可以重複复用多套類別邏輯

擴充方法(Extension Method):不用修改原始類別、也不用建立子類,就能替現有類新增功能

類修飾符:控制其他程式庫能否繼承、實作該類別

現在我們開始學習 Dart 的類別寫法

類成員

類成員包含方法與變數,所謂成員,就是組成類別的所有元素。

使用 . 存取實例成員;?. 是空安全存取運算子,避免空指標例外

下面是完整範例,示範定義類別、從類別建立物件、透過物件名稱呼叫成員

// 定義類別,等同建立一個樣板模具 
class Point {
  double x, y;
  Point(this.x, this.y);

  // 實例方法:計算兩點之間的距離平方
  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return dx * dx + dy * dy;
  }
}

void main() {
  // 透過 Point 類建立物件 p,後續所有操作都透過物件執行
  var p = Point(2, 2);
  
  // 讀取實例變數(隱式 getter)
  print(p.y); //2.0

  // 呼叫實例方法
  double dist = p.distanceTo(Point(4, 4));
  print(dist); // 8.0

  // ?. 空安全存取
  Point? nullablePoint;
  var a = nullablePoint?.y; // 若物件為 null,a 直接賦值 null,不會拋出例外
  print(a); // null
}Code language: Dart (dart)

建構函式(Constructors)

一般建構函式

Dart 和其他 C 系列語言有明顯差異,它支援具名建構函式;多數 C 衍生語言的建構函式名稱必須和類別同名,Dart 則允許自訂名稱作為建構函式。

格式:類別名稱() / 類別名稱.識別字()

new 關鍵字可省略(Dart 2 版本之後新增)

class Point {
  double x, y; //兩個變數屬於類成員,用來儲存類別屬性 
  
  // 預設建構函式,初始化上方兩個變數 x y
  Point(this.x, this.y);
  
  // 具名建構函式 Point.fromJson:透過 JSON 資料初始化 x y
  Point.fromJson(Map<String, double> json)
      : x = json['x']!,
        y = json['y']!;
		
}

void main() {
  var p1 = Point(2, 2);//呼叫預設建構函式,傳入 2、2,初始化 p1 的 x=2、y=2
  
  var p2 = Point.fromJson({'x': 1, 'y': 2}); //使用 fromJson 具名建構函式傳入 JSON
  
  // new 可寫可不寫,執行效果完全相同
  var p3 = new Point(3, 3); //同樣使用預設建構函式
  
  print("p1:(${p1.x},${p1.y}), p2:(${p2.x},${p2.y}), p3:(${p3.x},${p3.y})");
  //輸出:p1:(2.0,2.0), p2:(1.0,2.0), p3:(3.0,3.0)
}Code language: Dart (dart)

常數建構子 const

在建構函式加上 const,可建立編譯期常數物件;傳入完全相同參數的常數會共用同一個實例

class ImmutablePoint {
  final double x, y;
  // 常數建構函式規則:所有成員都必須標註 final,加上 const 修飾後 x、y 只能是 final
  const ImmutablePoint(this.x, this.y);
}

void main() {
  // 常數實例
  var a = const ImmutablePoint(1, 1); //建立常數物件時也要加上 const 
  var b = const ImmutablePoint(1, 1);
  print(identical(a, b)); // true,兩者參考同一個物件
  
  var m = const ImmutablePoint(1, 1); 
  var n = const ImmutablePoint(2, 1);
  print(identical(m, n)); // false,x 數值不同就是獨立物件

  // 沒有 const 為一般物件,不會共用實例
  var c = ImmutablePoint(1, 1);
  print(identical(a, c)); //false

  // const 上下文內層可省略 const
  const pointMap = {
    'p1': [ImmutablePoint(0, 0)],
    'p2': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)]
  };
  
  print(pointMap); //{p1: [Instance of 'ImmutablePoint'], p2: [Instance of 'ImmutablePoint', Instance of 'ImmutablePoint']}

  print(identical(p1, p2)); // false
  print(identical(p2, p3)); // false
  print(identical(p1, p3)); // false
}Code language: Dart (dart)
常數建構子只有所有傳入參數完全一致時,才會共用同一實例;任一參數不同就會建立兩個獨立物件

取得物件執行時型別 runtimeType

透過 物件.runtimeType 取得執行期型別;優先使用型別判斷運算子 is,而非 runtimeType 比對相等

class Point {
  double x, y;
  Point(this.x, this.y);

  // 實例方法:計算兩點距離平方
  double distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return dx * dx + dy * dy;
  }
}

void main() {
  var p = Point(2, 3);
  print("p 的型別:${p.runtimeType}"); // Point

  // 推薦穩定寫法
  if (p is Point) {
    print("p 屬於 Point 型別");
  }
  
  // 正式環境不建議使用
  if (p.runtimeType == Point) {
	print("p 屬於 Point 型別 ");
  }
}Code language: Dart (dart)

實例變數 Instance Variables

  1. 可空型別未初始化預設為 null;非可空變數必須給予初始值
  2. 所有實例變數自動產生 getter;非 final、未初始化 late final 會自動產生 setter
  3. 一般實例變數的初始化敘述不能存取 this;late 變數初始化時可以使用 this
  4. final 實例變數只能賦值一次:宣告時、建構參數、初始化清單三擇一

隱式 get/set

class Point {
  double? x; // 初始 null,自動產生 getter + setter
  double? y;
  double z = 0; // 預設初始值 0
}

void main() {
  var point = Point();
  point.x = 4; // 呼叫隱式 setter
  assert(point.x == 4); // 呼叫隱式 getter
  assert(point.y == null);
  print(point.z); // 0
}
Code language: JavaScript (javascript)

初始化敘述 this 存取限制

double globalX = 1.5;
class Point {
  // 合法:存取外部全域變數,不依賴 this
  double? x = globalX;

  // 編譯錯誤:一般初始化不能使用 this
  // double? y = this.x;

  // 合法:late 標記變數初始化可使用 this
  late double? z = this.x;

  // 合法:建構參數語法糖 this.x 不屬於初始化敘述
  Point(this.x, this.y);
  double? y;
}

void main() {
  var p = Point(null, 5);
  print(p.z); // null
}
Code language: JavaScript (javascript)

final 實例變數三種初始化方式

class ProfileMark {
  // 方式1:宣告時直接賦值
  final DateTime start = DateTime.now();
  final String name;

  // 方式2:透過建構參數賦值
  ProfileMark(this.name);

  // 方式3:建構初始化清單賦值;具名建構子 unnamed 預設 name 為空字串
  ProfileMark.unnamed() : name = '';
}

void main() {
  var m1 = ProfileMark("user1");
  var m2 = ProfileMark.unnamed();
  print(m1.name);
  print(m2.name);
}
Code language: PHP (php)

隱式介面 Implicit interfaces

Dart 每個類別自帶隱式介面,內含該類所有公開實例成員。

  • implements:實作介面,必須完整覆寫介面所有方法
  • extends:繼承父類,直接复用父類實作邏輯
  • 底線開頭的私有變數 _xxx 不會暴露到介面中

1 實作單一介面

class Person {
  final String _name;
  Person(this._name);
  // 介面公開方法
  String greet(String who) => "Hello $who, I'm $_name";
}

// 實作 Person 介面,必須覆寫全部介面成員
class Impostor implements Person {
  String get _name => "";
  @override
  String greet(String who) => "Hi $who, guess who?";
}

String greetBob(Person p) => p.greet("Bob");

void main() {
  print(greetBob(Person("Kathy")));
  print(greetBob(Impostor()));
}
Code language: PHP (php)

子類覆寫父介面方法時,加上 @override 標註,寫法和 Java 類似;使用 implements 關鍵字實作介面類別

2 實作多個介面

//抽象類別
abstract class Comparable {
  int compareTo(Object other);
}

//抽象類別
abstract class Location {
  double get x;
  double get y;
}

//同時實作兩個介面
class Point implements Comparable, Location {

  @override
  double x, y;

  Point(this.x, this.y);

  @override
  int compareTo(Object other) {
    if (other is Point) {
      return (x + y).compareTo(other.x + other.y);
    }
    return -1;
  }
}

void main() {
  var p = Point(2, 3);
  print(p.x);
  print(p.compareTo(Point(1, 1)));
}
Code language: PHP (php)

@override
double x, y;

這個標註代表:這兩個屬性是實作父介面 Location 定義的抽象 getter

類別靜態變數 & 靜態方法 static

1. 靜態變數 static(類別變數,全域單一)

  • 屬於類別本身,不屬於物件實例;延遲初始化,第一次使用時才配置記憶體
  • 常數型靜態變數建議使用小駝峰命名
class Queue {
  static const initialCapacity = 16; //靜態常數變數
  static int count = 0; //靜態可變變數
  Queue() {
    count++;
  }
}

void main() {
  assert(Queue.initialCapacity == 16); //存取時才初始化,預設值16
  Queue(); //count +1
  Queue();
  print(Queue.count); // 2
}
Code language: JavaScript (javascript)

2. 靜態方法 static(類別方法)

  • 沒有 this,無法存取實例成員,只能存取靜態變數
  • 直接使用 類別名稱.方法() 呼叫
  • 可作為編譯期常數傳入常數建構函式
import 'dart:math';

class Point {
  double x, y;
  Point(this.x, this.y);

  // 靜態方法:計算兩點直線距離
  static double distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var dist = Point.distanceBetween(a, b);
  print(dist); // 約 2.828
}Code language: JavaScript (javascript)

靜態方法隸屬於類別,所以必須透過類別名稱呼叫,並傳入兩個 Point 物件實例做參數。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *