Dart語法篇之面向對象基礎(五)

簡述:緩存

從這篇文章開始,咱們繼續Dart語法篇的第五講, dart中的面向對象基礎。咱們知道在Dart中一切都是對象,因此面向對象在Dart開發中是很是重要的。此外它還和其餘有點不同的地方,好比多繼承mixin、構造器不能被重載、setter和getter的訪問器函數等。數據結構

1、屬性訪問器(accessor)函數setter和getter

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

一、屬性訪問器函數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函數。
}
複製代碼

二、屬性訪問器函數getter

Dart中全部實例屬性的訪問都是經過調用getter函數來實現的。每一個實例數額行始終都有一個與之關聯的getter,由Dart編譯器提供的。post

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實現ui

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函數。
}
複製代碼

三、屬性訪問器函數使用場景

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

好比下面這個例子,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();
}
複製代碼

2、面向對象中的變量

一、實例變量

實例變量實際上就是類的成員變量或者稱爲成員屬性,當聲明一個實例變量時,它會確保每個對象實例都有本身惟一屬性的拷貝。若是要表示實例私有屬性的話就直接在屬性名前面加下劃線 _,例如_width_heightspa

class Rectangle {
  num left, top, _width, _height;//聲明瞭left,top,_width,_height四個成員屬性,未初始化時,它們的默認值都是null
}  
複製代碼

上述例子中的left, top, width, height都是會自動引入一個gettersetter.事實上,在dart中屬性都不是直接訪問的,全部對字段屬性的引用都是對屬性訪問器函數的調用, 只有訪問器函數才能直接訪問它的狀態。

二、類變量(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變量同理。

三、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則是運行期進行初始化

四、常量對象

在dart有些對象是在編譯期就能夠計算的常量,因此在dart中支持常量對象的定義,常量對象的建立須要使用 const 關鍵字。常量對象的建立也是調用類的構造函數,可是注意必須是常量構造函數,該構造函數是用 const 關鍵字修飾的。常量構造函數必須是數字、布爾值或字符串,此外常量構造函數不能有函數體,可是它能夠有初始化列表。

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

main() {
    const defaultPoint = const Point(0, 0);//建立常量對象
}
複製代碼

2、構造函數

一、主構造函數

主構造函數是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調用將會默認添加到初始化列表的尾部
}
複製代碼

初始化的順序以下圖:

初始化實例變量幾種方式

//方式一: 經過實例變量聲明時直接賦默認值初始化
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;
    }
}
複製代碼

二、命名構造函數

經過上面主構造函數咱們知道在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');//經過命名構造函數建立對象
}
複製代碼

三、重定向構造函數

有時候須要將構造函數重定向到同一個類中的另外一個構造函數重定向構造函數的主體爲空,構造函數的調用出如今冒號(:)以後

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中。
}
複製代碼

四、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);
  }
}
複製代碼

3、抽象方法、抽象類和接口

抽象方法就是聲明一個方法而不提供它的具體實現。任何實例的方法均可以是抽象的,包括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;
  }
}
複製代碼

4、類函數

類函數顧名思義就是類的函數,它不屬於任何一個實例,因此它也就不能被繼承。類函數使用 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系列文章,歡迎查看:

相關文章
相關標籤/搜索