Dart語法篇之物件導向基礎(五)

mikyou發表於2019-11-12

簡述:

從這篇文章開始,我們繼續Dart語法篇的第五講, dart中的物件導向基礎。我們知道在Dart中一切都是物件,所以物件導向在Dart開發中是非常重要的。此外它還和其他有點不一樣的地方,比如多繼承mixin、構造器不能被過載、setter和getter的訪問器函式等。

一、屬性訪問器(accessor)函式setter和getter

在Dart類的屬性中有一種為了方便訪問它的值特殊函式,那就是setter,getter屬性訪問器函式。實際上,在dart中每個例項屬性始終有與之對應的setter,getter函式(若是final修飾只讀屬性只有getter函式, 而可變屬性則有setter,getter兩種函式)。而在給例項屬性賦值或獲取值時,實際上內部都是對setter和getter函式的呼叫

1、屬性訪問器函式setter

setter函式名前面新增字首set, 並只接收一個引數。setter呼叫語法於傳統的變數賦值是一樣的。如果一個例項屬性是可變的,那麼一個setter屬性訪問器函式就會為它自動定義,所有例項屬性的賦值實際上都是對setter函式的呼叫。這一點和Kotlin中的setter,getter非常相似。

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);
  set right(num value) => left = value - width;//使用set作為字首,只接收一個引數value
  set bottom(num value) => top = value - height;//使用set作為字首,只接收一個引數value
}

main() {
  var rect = Rectangle(3, 4, 20, 15);
  rect.right = 15;//呼叫setter函式時,可以直接使用類似屬性賦值方式呼叫right函式。
  rect.bottom = 12;//呼叫setter函式時,可以直接使用類似屬性賦值方式呼叫bottom函式。
}
複製程式碼

對比Kotlin中的實現

class Rectangle(var left: Int, var top: Int, var width: Int, var height: Int) {
    var right: Int = 0//在kotlin中表示可變使用var,只讀使用val
        set(value) {//kotlin中定義setter
            field = value
            left = value - width
        }
    var bottom: Int = 0
        set(value) {//kotlin中定義setter
            field = value
            top = value - height
        }
}

fun main(args: Array<String>) {
    val rect = Rectangle(3, 4, 20, 15);
    rect.right = 15//呼叫setter函式時,可以直接使用類似屬性賦值方式呼叫right函式。
    rect.bottom = 12//呼叫setter函式時,可以直接使用類似屬性賦值方式呼叫bottom函式。
}
複製程式碼

2、屬性訪問器函式getter

Dart中所有例項屬性的訪問都是通過呼叫getter函式來實現的。每個例項數額行始終都有一個與之關聯的getter,由Dart編譯器提供的。

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);
  get square => width * height; ;//使用get作為字首,getter來計算面積.
  set right(num value) => left = value - width;//使用set作為字首,只接收一個引數value
  set bottom(num value) => top = value - height;//使用set作為字首,只接收一個引數value
}

main() {
  var rect = Rectangle(3, 4, 20, 15);
  rect.right = 15;//呼叫setter函式時,可以直接使用類似屬性賦值方式呼叫right函式。
  rect.bottom = 12;//呼叫setter函式時,可以直接使用類似屬性賦值方式呼叫bottom函式。
  print('the rect square is ${rect.square}');//呼叫getter函式時,可以直接使用類似讀取屬性值方式呼叫square函式。
}
複製程式碼

對比kotlin實現

class Rectangle(var left: Int, var top: Int, var width: Int, var height: Int) {
    var right: Int = 0
        set(value) {
            field = value
            left = value - width
        }
    var bottom: Int = 0
        set(value) {
            field = value
            top = value - height
        }

    val square: Int//因為只涉及到了只讀,所以使用val
        get() = width * height//kotlin中定義getter
}

fun main(args: Array<String>) {
    val rect = Rectangle(3, 4, 20, 15);
    rect.right = 15
    rect.bottom = 12
    println(rect.square)//呼叫getter函式時,可以直接使用類似讀取屬性值方式呼叫square函式。
}
複製程式碼

3、屬性訪問器函式使用場景

其實,上面settergetter函式實現的目的,普通函式也能做到的。但是如果用setter,getter函式形式更符合編碼規範。既然普通函式也能做到,那具體什麼時候使用setter,getter函式,什麼時候使用普通函式呢。這不得不把這個問題和另一問題轉化一下成為: 哪種場景該定義屬性還是定義函式的問題(關於這個問題,記得很久之前在討論Kotlin的語法詳細介紹過)。我們都知道函式一般描述動作行為,而屬性則是描述狀態資料結構(狀態可能經過多個屬性值計算得到)。 如果類中需要向外暴露類中某個狀態那麼更適合使用setter,getter函式;如果是觸發類中的某個行為操作,那麼普通函式更適合一點。

比如下面這個例子,draw繪製矩形動作更適合使用普通函式來實現,square獲取矩形的面積更適合使用getter函式來實現,可以仔細體會下。

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  set right(num value) => left = value - width; //使用set作為字首,只接收一個引數value
  set bottom(num value) => top = value - height; //使用set作為字首,只接收一個引數value
  get square => width * height; //getter函式計算面積,描述Rectangle狀態特性
  bool draw() {
    print('draw rect'); //draw繪製函式,觸發是動作行為
    return true;
  }
}

main() {
  var rect = Rectangle(3, 4, 20, 15);
  rect.right = 15; //呼叫setter函式時,可以直接使用類似屬性賦值方式呼叫right函式。
  rect.bottom = 12; //呼叫setter函式時,可以直接使用類似屬性賦值方式呼叫bottom函式。
  print('the rect square is ${rect.square}');
  rect.draw();
}
複製程式碼

二、物件導向中的變數

1、例項變數

例項變數實際上就是類的成員變數或者稱為成員屬性,當宣告一個例項變數時,它會確保每一個物件例項都有自己唯一屬性的拷貝。如果要表示例項私有屬性的話就直接在屬性名前面加下劃線 _,例如_width_height

class Rectangle {
  num left, top, _width, _height;//宣告瞭left,top,_width,_height四個成員屬性,未初始化時,它們的預設值都是null
}  
複製程式碼

上述例子中的left, top, width, height都是會自動引入一個gettersetter.事實上,在dart中屬性都不是直接訪問的,所有對欄位屬性的引用都是對屬性訪問器函式的呼叫, 只有訪問器函式才能直接訪問它的狀態。

2、類變數(static變數)與頂層變數

類變數實際上就是static修飾的變數,屬於類的作用域範疇;頂層變數就是定義的變數不在某個具體類體內,而是處於整個程式碼檔案中,相當於檔案頂層,和頂層函式差不多意思。static變數更多人願意把它稱為靜態變數,但是在Dart中靜態變數不僅僅包括static變數還包括頂層變數

其實對於類變數和頂層變數的訪問都還是通過呼叫它的訪問器函式來實現的,但是類變數和頂層變數有點特殊,它們是延遲初始化的,在getter函式第一次被呼叫時類變數或頂層變數才執行初始化,也即是第一次引用類變數或頂層變數的時候。如果類變數或頂層變數沒有被初始化預設值還是null.

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {
    Cat() {
        print("I'm a Cat!");
    }
}
//注意,這裡變數不定義在任何具體類體內,所以這個animal是一個頂層變數。
//雖然看似建立了Cat物件,但是由於頂層變數延遲初始化的原因,這裡根本就沒有建立Cat物件
Animal animal = Cat();
main() {
    animal = Dog();//然後又將animal引用指向了一個新的Dog物件,
}
複製程式碼

頂層變數是具有延遲初始化過程,所以Cat物件並沒有建立,因為整個程式碼執行中並沒有去訪問animal,所以無法觸發第一次getter函式,也就導致Cat物件沒有建立,直接表現是根本就不會輸出I'm a Cat!這句話。這就是為什麼頂層變數是延遲初始化的原因,static變數同理。

3、final 變數

在Dart中使用 final 關鍵字修飾變數,表示該變數初始化後不能再被修改。final 變數只有 getter 訪問器函式,沒有 setter 函式。類似於Kotlin中的val修飾的變數。宣告成final的變數必須在例項方法執行前進行初始化,所以初始化final變數有很多中方法。注意: 建議儘量使用final來宣告變數

class Person {
    final String gender = '男';//直接在宣告的時候初始化,這種方式比較侷限,針對基本資料型別還可以,但如果是一個物件型別就顯示不合適了。
    final String name;
    final int age;
    Person(this.name, this.age);//利用建構函式為final變數初始化。
    //上述程式碼等價於下面實現
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}
複製程式碼

finalconst的區別,就好比Kotlin中的valconst val之間的區別,const是編譯期就進行了初始化,而 final則是執行期進行初始化

4、常量物件

在dart有些物件是在編譯期就可以計算的常量,所以在dart中支援常量物件的定義,常量物件的建立需要使用 const 關鍵字。常量物件的建立也是呼叫類的建構函式,但是注意必須是常量建構函式,該建構函式是用 const 關鍵字修飾的。常量建構函式必須是數字、布林值或字串,此外常量建構函式不能有函式體,但是它可以有初始化列表。

class Point {
    final double x, y;
    const Point(this.x, this.y);//常量建構函式,使用const關鍵字且沒有函式體
}

main() {
    const defaultPoint = const Point(0, 0);//建立常量物件
}
複製程式碼

二、建構函式

1、主建構函式

主建構函式是Dart中建立物件最普通一種建構函式,而且主建構函式只能有一個,如果沒有指定主建構函式,那麼會預設自動分配一個預設無參的主建構函式。此外dart中建構函式不支援過載

class Person {
    var name;
    //隱藏了預設的無參建構函式Person();
}
//等價於:
class Person {
    var name;
    Person();//一般把與類名相同的函式稱為主建構函式
}
//等價於
class Person {
    var name;
    Person(){}
}

class Person {
    final String name;
    final int age;
    Person(this.name, this.age);//顯式宣告有參主建構函式
    Person();//編譯異常,注意: dart不支援同時過載多個建構函式。
}
複製程式碼

建構函式初始化列表 :

class Point3D extends Point {
    double z;
    Point3D(a, b, c): z = c / 2, super(a, b);//初始化列表,多個初始化步驟用逗號分隔;先初始化z ,然後執行super(a, b)呼叫父類的建構函式
}
//等價於
class Point3D extends Point {
    double z;
    Point3D(a, b, c): z = c / 2;//如果初始化列表沒有呼叫父類建構函式,
    //那麼就會存在一個隱含的父類建構函式super呼叫將會預設新增到初始化列表的尾部
}
複製程式碼

初始化的順序如下圖:

Dart語法篇之物件導向基礎(五)

初始化例項變數幾種方式

//方式一: 通過例項變數宣告時直接賦預設值初始化
class Point {
    double x = 0, y = 0;
}

//方式二: 使用建構函式初始化方式
class Point {
    double x, y;
    Point(this.x, this.y);
}

//方式三: 使用初始化列表初始化
class Point {
    double x, y;
    Point(double a, double b): x = a, y = b;//:後跟初始化列表
}

//方式四: 在建構函式中初始化,注意這個和方式二還是有點不一樣的。
class Point {
    double x, y;
    Point(double a, double b) {
        x = a;
        y = b;
    }
}
複製程式碼

2、命名建構函式

通過上面主建構函式我們知道在Dart中的建構函式是不支援過載的,實際上Dart中連基本的普通函式都不支援函式過載。 那麼問題來了,我們經常會遇到建構函式過載的場景,有時候需要指定不同的建構函式形參來建立不同的物件。所以為了解決不同引數來建立物件問題,雖然拋棄了函式過載,但是引入命名建構函式的概念。它可以指定任意引數列表來構建物件,只不過的是需要給建構函式指定特定的名字而已。

class Person {
  final String name;
  int age;

  Person(this.name, this.age);

  Person.withName(this.name);//通過類名.函式名形式來定義命名建構函式withName。只需要name引數就能建立物件,
  //如果沒有命名建構函式,在其他語言中,我們一般使用函式過載的方式實現。
}

main () {
  var person = Person('mikyou', 18);//通過主建構函式建立物件
  var personWithName = Person.withName('mikyou');//通過命名建構函式建立物件
}
複製程式碼

3、重定向建構函式

有時候需要將建構函式重定向到同一個類中的另一個建構函式重定向建構函式的主體為空,建構函式的呼叫出現在冒號(:)之後

class Point {
    double x, y;
    Point(this.x, this.y);
    Point.withX(double x): this(x, 0);//注意這裡使用this重定向到Point(double x, double y)主建構函式中。
}
//或者
import 'dart:math';

class Point {
  double distance;

  Point.withDistance(this.distance);

  Point(double x, double y) : this.withDistance(sqrt(x * x + y * y));//注意:這裡是主建構函式重定向到命名建構函式withDistance中。
}
複製程式碼

4、factory工廠建構函式

一般來說,建構函式總是會建立一個新的例項物件。但是有時候會遇到並不是每次都需要建立新的例項,可能需要使用快取,如果僅僅使用上面普通建構函式是很難做到的。那麼這時候就需要factory工廠建構函式它使用factory關鍵字來修飾建構函式,並且可以從快取中的返回已經建立例項或者返回一個新的例項。在dart中任意建構函式都可以被替換成工廠方法, 它看起來和普通建構函式沒什麼區別,可能沒有初始化列表或初始化引數,但是它必須有一個返回物件的函式體

class Logger {
  //例項屬性
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache =
      <String, Logger>{};//類屬性

  factory Logger(String name) {//使用factory關鍵字宣告工廠建構函式,
    if (_cache.containsKey(name)) {
      return _cache[name]//返回快取已經建立例項
    } else {
      final logger = Logger._internal(name);//快取中找不到對應的name logger,呼叫_internal命名建構函式建立一個新的Logger例項
      _cache[name] = logger;//並把這個例項加入快取中
      return logger;//注意: 最後返回這個新建立的例項
    }
  }

  Logger._internal(this.name);//定義一個命名私有的建構函式_internal

  void log(String msg) {//例項方法
    if (!mute) print(msg);
  }
}
複製程式碼

三、抽象方法、抽象類和介面

抽象方法就是宣告一個方法而不提供它的具體實現。任何例項的方法都可以是抽象的,包括getter,setter,操作符或者普通方法。含有抽象方法的類本身就是一個抽象類,抽象類的宣告使用關鍵字 abstract.

abstract class Person {//abstract宣告抽象類
  String name();//抽象普通方法
  get age;//抽象getter
}

class Student extends Person {//使用extends繼承
  @override
  String name() {
    // TODO: implement name
    return null;
  }

  @override
  // TODO: implement age
  get age => null;
}
複製程式碼

在Dart中並沒有像其他語言一樣有個 interface的關鍵字修飾。因為Dart中每個類都預設隱含地定義了一個介面

abstract class Speaking {//雖然定義的是抽象類,但是隱含地定義介面Speaking
  String speak();
}

abstract class Writing {//雖然定義的是抽象類,但是隱含地定義介面Writing
  String write();
}

class Student implements Speaking, Writing {//使用implements關鍵字實現介面
  @override
  String speak() {//重寫speak方法
    // TODO: implement speak
    return null;
  }

  @override
  String write() {//重寫write方法
    // TODO: implement write
    return null;
  }
}
複製程式碼

四、類函式

類函式顧名思義就是類的函式,它不屬於任何一個例項,所以它也就不能被繼承。類函式使用 static 關鍵字修飾,呼叫時可以直接使用類名.函式名的方式呼叫。

class Point {
    double x,y;
    Point(this.x, this.y);
    static double distance(Point p1, Point p2) {//使用static關鍵字,定義類函式。
        var dx = p1.x - p2.x;
        var dy = p1.y - p2.y;
        return sqrt(dx * dx + dy * dy);
    }
}
main() {
    var point1 = Point(2, 3);
    var point2 = Point(3, 4);
    print('the distance is ${Point.distance(point1, point2)}');//使用Point.distance => 類名.函式名方式呼叫
}
複製程式碼

總結

到這裡有關dart中物件導向基礎部分已經介紹完畢,這篇文章主要介紹了dart中常用的建構函式以及一些物件導向基礎知識,下一篇我們將繼續dart中物件導向一些高階的東西,比如物件導向的繼承和mixin. 歡迎關注~~~

我的公眾號

這裡有最新的Dart、Flutter、Kotlin相關文章以及優秀國外文章翻譯,歡迎關注~~~

Dart語法篇之物件導向基礎(五)

Dart系列文章,歡迎檢視:

相關文章