Flutter之旅:從原始碼賞析Dart物件導向

張風捷特烈發表於2019-07-05

前言

相信大家都是有過物件導向經驗的人,那物件導向是什麼感覺呢?
大概也就是一開始心跳加速,小鹿亂撞,之後平淡無奇,最後被她折磨到懷疑人生。
今天給你介紹個物件,她的名字叫Dart,還等什麼,趕快認識一下。

Flutter之旅:從原始碼賞析Dart物件導向


1.物件導向的條件

1.1:三大特性

首先房子、車子、票子要有的吧,不然還面個什麼物件?其次物件導向思想要到位,準備的三大件:封裝、繼承、多型

1.1.1:封裝的思想

下面這東西大家應該見過,是一個電子元件,它能很好地說明封裝的特性。每個電子元件都有暴露在外的引腳。這些腳分為輸入和輸出。使用一個電子元件時,我們只是關心輸入和輸出的情況,也就是它的真值表,對他內部的構成是不關心的。
所以一個電子元件對硬體組裝師而言,就是一個有引腳的黑塊,一個能夠完成特定功能的邏輯單元。類比一下三方類庫,在引入之後,不需要知道庫的具體實現邏輯,只要按照暴露的API(真值表),你進行一個API的呼叫(輸入),就會完成特定的功能(輸出)。

Flutter之旅:從原始碼賞析Dart物件導向


類也是這樣,最終目的是要實現邏輯的複用,讓其成為一個可獨立存在的邏輯單元,從而和其他類共同運作。但是現在與上面所不同的是:我們不單單是一個使用者,更多角色的是它的設計者。所以需要考慮的要點不僅是此類和其他類如何契合運作,更重要的是它的內部構成。
要知道,使用一個電子元件,和設計製造一個電子元件是完全不同的,而程式設計師顯然是後者。


1.1.2:繼承的思想

一個人的出生並非是一無所有,它享有著父母的資產,人脈,地位。這些都是他可以使用的資源。 這就無需艱苦奮鬥來達到當前的境地,從而能夠在未來的發展中更上一層。當然,類也是這樣,子類通過繼承可以享受到父類所帶來的'天賦'。

這就涉及到了一個概念,叫抽象。抽象並非隨便抽的,不以解決問題為前提的抽象都是耍流氓,通過抽象來提取物件的公共特徵,形成基類。這相當於是父母在努力打拼,想為孩子打造一個適宜的生存環境。

還有一點就是:孩子不一定繼承父親的一切,有些東西是父親不想給孩子的,這就涉及到繼承的訪問限制。


1.1.3:類的多型

一個人在社會中可以擁有多個角色,比如捷特在學校是一個學生、在公司是程式設計師、在週末是一個男朋友、在旅行中是一個遊客,這就是一個物件的不同狀態,簡稱:多型。
多型有什麼優勢,比如:有人喊,來個學生搬桌子,捷特就可以以學生的身份過去;週末妹子約,捷特就可以男朋友的身份過去;有人喊,來個程式設計師敲改bug,捷特也能跑過去。
下面寫段虛擬碼,這樣的優勢在於使用物件的那個方法只需要針對介面即可,並不需要傳入Me物件。如果pickDesk,date,fixBug全部傳入Me物件,雖然能執行,但是不利於擴充和維護。

class Me implements Pickable,Kissable,Codeable{

}

pickDesk(Pickable student){
  student.pick(this);
}

date(Kissable boyfriend){
  if(boyfriend.getId == this.id){
    boyfriend.kiss(this);
  }
}

fixBug(Codeable coder){
  coder.debug(this);
}
複製程式碼

好了,羅裡吧嗦一堆,下面開始進入正題,給你介紹Dart的物件導向,一般說物件導向都是個Person,在加個Student什麼的。
雖說沒什麼不好,但感覺很平淡無奇,既然我們們都是有物件導向經驗的人,直接去原始碼裡找物件面面唄,這樣才驚險,刺激。


2.從Size一族開始說起

我一直在想通過那個類的原始碼開始說比較好,最好不要太長,也不要太難,Size就比較完美。

2.1.類的定義

class 關鍵字定義類,沒毛病,extend 繼承關鍵字,也很OK。

---->[sky_engine/lib/ui/geometry.dart:347]-------
class Size extends OffsetBase {
複製程式碼

2.2.抽象類的定義

Size繼承自OffsetBase,那就來看一下OffsetBase。首先它是一個抽象類,使用的關鍵字也是abstract。
其中有一個構造方法,傳入兩個引數,分別是水平和豎直的值。注意構造方法的書寫形式是和Java有所區別的。
其中封裝了兩個私有屬性:_dx_dy

---->[sky_engine/lib/ui/geometry.dart:9]-------
abstract class OffsetBase {
  const OffsetBase(this._dx, this._dy);//建構函式

  final double _dx;
  final double _dy;
}
複製程式碼

2.3:方法

不知大家看到下面的程式碼感覺如何,反正我感覺挺彆扭,也許不太熟悉吧。

//是否無法衡量的
bool get isInfinite => _dx >= double.infinity || _dy >= double.infinity;
//是否有限
bool get isFinite => _dx.isFinite && _dy.isFinite;

看著有點嚇人,不過才剛開始,可以慢慢分析,上面這句如果看得眼花繚亂,
我改寫了一下,下面的應該可以看懂吧,意思就是如果_dx和_dy有一個超過double的範圍就返回true

bool isInfinite() {
    return _dx >=double.infinity || _dy >= double.infinity;
}

注意一點:在Java中常用isXXX,Dart 裡的get關鍵字可以讓呼叫簡潔,使用如下
var size= Size(30,40);
print(size.isInfinite);//false
複製程式碼

2.4:運算子過載

這段程式碼看了三秒鐘,箭頭太多,愣是沒反應過來,仔細一想,這不會是運算子過載吧,和C++的有點神似。下面是分別對<、<=、>、>=、==的運算子過載。


bool operator <(OffsetBase other) => _dx < other._dx && _dy < other._dy;
bool operator <=(OffsetBase other) => _dx <= other._dx && _dy <= other._dy;
bool operator >(OffsetBase other) => _dx > other._dx && _dy > other._dy;
bool operator >=(OffsetBase other) => _dx >= other._dx && _dy >= other._dy;

@override
bool operator ==(dynamic other) {
  if (other is! OffsetBase)//如果傳入物件不是OffsetBase
    return false;//直接滾
  final OffsetBase typedOther = other;
  return _dx == typedOther._dx &&//判斷是否相等
         _dy == typedOther._dy;
}

使用方式:
var size= Size(30,40);
var size2= Size(10,20);
print(size>size2);//true
複製程式碼

2.5:雜湊值和toString

和Java比較像,沒啥好說的。這裡要插一句,看原始碼的時候,最好自己留心一下原始碼中的書寫風格,畢竟和大佬看起是很必要的,比如命名的風格,註釋的風格等。

@override
int get hashCode => hashValues(_dx, _dy);

@override
String toString() => '$runtimeType(${_dx?.toStringAsFixed(1)}, ${_dy?.toStringAsFixed(1)})';
複製程式碼

OK,這樣一個OffsetBase物件就面完了,緊不緊張,刺不刺激。下面繼續看Size


2.6:構造Size物件方法

注意了,這裡圈起來,要考的。使用父類的構造方法來完成本類的構造個語法格式:類名(引數,...):super(引數,...)

  
class Size extends OffsetBase {
  /// 根據給定的寬高建立Size物件
  const Size(double width, double height) : super(width, height);
複製程式碼

在聯想一下初始專案中的讓人蒙圈的這句話,是不是豁然開朗。

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
複製程式碼

然後就是一堆構造Size物件的方法,總的來說就是,不管怎樣,你給我弄兩個值來當寬高

  Size.copy(Size source) : super(source.width, source.height);
  const Size.square(double dimension) : super(dimension, dimension);
  const Size.fromWidth(double width) : super(width, double.infinity);
  const Size.fromHeight(double height) : super(double.infinity, height);
  const Size.fromRadius(double radius) : super(radius * 2.0, radius * 2.0);

  static const Size zero = const Size(0.0, 0.0);
  static const Size infinite = const Size(double.infinity, double.infinity);
複製程式碼

2.7:Size中的屬性封裝

有了老爹之後,Size類就可以使用老爹的“財富”,這裡Size為了形象,將老爹的東西名字都給改了。
類存在的價值之一在於封裝屬性及調動屬性之間的關係完成特定功能,比如aspectRatio可以獲取寬高比。 對於任意Size物件,在任意時間,任意空間,都可以呼叫aspectRatio方法獲取寬高比,這是程式導向所不能及的。

  double get width => _dx;
  double get height => _dy;

  double get aspectRatio {
    if (height != 0.0)
      return width / height;
    if (width > 0.0)
      return double.infinity;
    if (width < 0.0)
      return double.negativeInfinity;
    return 0.0;
  }
複製程式碼

2.8:繼承的優勢

上面OffsetBase說到運算子過載,在Size類中也有運算子過載,這些是尺寸的四則運算,然而Size依舊可以使用OffsetBase中過載過的運算子,這就是有老爹的優勢。

OffsetBase operator -(OffsetBase other) {
  if (other is Size)
    return new Offset(width - other.width, height - other.height);
  if (other is Offset)
    return new Size(width - other.dx, height - other.dy);
  throw new ArgumentError(other);
}
Size operator +(Offset other) => new Size(width + other.dx, height + other.dy);
Size operator *(double operand) => new Size(width * operand, height * operand);
Size operator /(double operand) => new Size(width / operand, height / operand);
Size operator ~/(double operand) => new Size((width ~/ operand).toDouble(), (height ~/ operand).toDouble());
Size operator %(double operand) => new Size(width % operand, height % operand);
複製程式碼
2.9.看Offset類

繼承會讓孩子具有先天優勢,那麼兩個孩子豈不是更加物盡其用。定義一個父類就是為了能夠更好的擴充,OffsetBase自然也不例外。
Size物件描述了一個類似框框的物件,那麼Offset描述的便是位移。兩者有一個共同的特點,那就是有兩個數值類的屬性。這也是OffsetBase被抽象出來的原因。

class Offset extends OffsetBase {
  //構造方法
  const Offset(double dx, double dy) : super(dx, dy);
  factory Offset.fromDirection(double direction, [ double distance = 1.0 ]) {
    return new Offset(distance * math.cos(direction), distance * math.sin(direction));
  }
  
  static const Offset zero = const Offset(0.0, 0.0);
  static const Offset infinite = const Offset(double.infinity, double.infinity);

  double get dx => _dx; //水平位移
  double get dy => _dy; //數值位移
  
  //方法:屬性的處理
  double get distance => math.sqrt(dx * dx + dy * dy);//移動距離
  double get distanceSquared => dx * dx + dy * dy;//移動距離的平方
  Offset scale(double scaleX, double scaleY) => new Offset(dx * scaleX, dy * scaleY);//縮放
  Offset translate(double translateX, double translateY) => new Offset(dx + translateX, dy + translateY);//移動
  
 //運算子過載
  Offset operator -() => new Offset(-dx, -dy);
  Offset operator -(Offset other) => new Offset(dx - other.dx, dy - other.dy);
  Offset operator +(Offset other) => new Offset(dx + other.dx, dy + other.dy);
  Offset operator *(double operand) => new Offset(dx * operand, dy * operand);
  Offset operator /(double operand) => new Offset(dx / operand, dy / operand);
  Offset operator %(double operand) => new Offset(dx % operand, dy % operand);

  Rect operator &(Size other) => new Rect.fromLTWH(dx, dy, other.width, other.height);

  //略....

  @override
  int get hashCode => hashValues(dx, dy);

  @override
  String toString() => 'Offset(${dx?.toStringAsFixed(1)}, ${dy?.toStringAsFixed(1)})';
複製程式碼
2.10:小結

這是三個類比較簡單,很適合剛入門Dart的夥伴讀讀,關係簡單畫一下。
現在你應該對Dart中類的建立,屬性,方法的書寫以及類的繼承有所理解了吧。

Flutter之旅:從原始碼賞析Dart物件導向


3.Dart中的介面與列舉

與Java不同,Dart中的介面定義依然是abstract關鍵字,介面和抽象類本質上並沒有區別,都是對一類事物的抽象,只不過介面更傾向於提取事物的能力。比如Comparable介面和Pattern介面。

3.1 :介面的定義

Comparable定義了一個抽象方法compareTo,用來和另一個物件進行比較,也就是這個介面的功能是作比較。
Pattern介面的功能是:匹配字串,獲得一個可迭代的匹配結果集。

---->[sky_engine/lib/core/comparable.dart:72]----------
abstract class Comparable<T> {

  int compareTo(T other);
  
  static int compare(Comparable a, Comparable b) => a.compareTo(b);
}

---->[sky_engine/lib/core/pattern.dart:72]----------
abstract class Pattern {
  Iterable<Match> allMatches(String string, [int start = 0]);
  Match matchAsPrefix(String string, [int start = 0]);
}
複製程式碼

3.2:Dart中的介面

實現介面的類擁有該介面的功能。可以看出num是實現了Comparable介面的,可以說明它有作比較的能力。
介面的實現和Java一樣,是關鍵字implements。

abstract class num implements Comparable<num> {

複製程式碼

3.3:實現多個介面

Dart中的介面也是支援多實現的,用逗號隔開。比如String即實現Comparable,也實現了Pattern,
說明String同時可具有這兩種功能。

abstract class String implements Comparable<String>, Pattern {
複製程式碼

3.4:列舉型別

列舉通常用來表示相同型別的一組常量,用關鍵字enum來表示。
列舉物件可以結合switch做分支處理。
另外Dart中的列舉元素具有索引,從0開始,依次計數,用index屬性訪問。 說到列舉,我首先想到的就是Paint的頭,就用這個類來說明一下:

Flutter之旅:從原始碼賞析Dart物件導向

---->[sky_engine/lib/ui/painting.dart:833]----------
enum StrokeCap {
  butt,//無頭(預設)
  round,//圓型
  square,//方形
}

使用:
var paint= Paint();
paint.strokeCap=StrokeCap.round;//圓型
print(StrokeCap.round.index);//1
複製程式碼

看來原始碼中的幾個類,物件導向也基本上能有個認識。下面來自定義個二位向量總結一下。


4.自定義向量類Vector2

4.1:定義Vector2類與使用

這裡定義了Vector2類,包含兩個數值分別是橫縱座標,建構函式用最原始的方式

class Vector2{
  num x;
  num y;

  Vector2(num x, num y) {
    this.x = x;
    this.y = y;
  }
}

main(){//使用
  var v1=Vector2(3,4);
  print('(${v1.x},${v1.y})');//(3,4)
}
複製程式碼

4.2:建構函式的簡寫

通過簡寫,可以使建構函式簡潔明瞭,同時也能實現等價功能。

class Vector2{
  num x;
  num y;
  Vector2(this.x,this.y);
}
複製程式碼

4.3:命名建構函式

原始碼中經常會出現XXX.formXXX來構造物件

class Vector2{
  num x;
  num y;
  Vector2.fromMap(Map<String,num> point){
    this.x = point['x'];
    this.y = point['y'];
  }
}

main(){//使用
  var v2= Vector2.fromMap({'x':5,'y':6});
  print('(${v2.x},${v2.y})');//(5,6)
}
複製程式碼

4.4:定義方法
  double get distance => sqrt(x * x + y * y); //向量的模
  double get rad => atan2(y, x); //與x正方向夾角(弧度制)
  double get angle => 180 / pi * atan2(y, x); //與x正方向夾角(角度制)
複製程式碼

4.5:定義一個操作介面
abstract class Operable{
  void reflex();//反向
  void reflexX();//X反向
  void reflexY();//Y反向

  void scale(num xRate,num yRate);//縮放
  void translate(num dx,num dy);//平移
  void rotate(num deg,[isAnticlockwise=true]);//旋轉
}
複製程式碼

4.6:繼承介面,實現方法
class Vector2 implements Operable{
  num x;
  num y;
  Vector2(this.x,this.y);

  Vector2.fromMap(Map<String,num> point){
    this.x = point['x'];
    this.y = point['y'];
  }

  @override
  void reflex() {
    this.x=-x;
    this.y=-y;
  }

  @override
  void reflexX() {
    this.x=-x;
  }

  @override
  void reflexY() {
    this.y=-y;
  }

  @override
  void rotate(num deg, [isAnticlockwise = true]) {
    var curRad = rad + deg*pi/180;
    this.x=distance*cos(curRad);
    this.y=distance*sin(curRad);
  }

  @override
  void scale(num xRate, num yRate) {
    this.x *=xRate;
    this.y *=yRate;
  }

  @override
  void translate(num dx, num dy) {
    this.x +=dx;
    this.y +=dy;
  }
  
  @override
  String toString() {
    return '(${this.x},${this.y})';
  }
}
複製程式碼

4.7:運算子過載

現學現賣,運算子過載一下

//運算子過載
Vector2 operator +(Vector2 other) => Vector2(x + other.x, y + other.y);
Vector2 operator -(Vector2 other) => Vector2(x - other.x, y - other.y);
num operator *(Vector2 other) => x * other.x + y * other.y;
複製程式碼

這個類就先這樣,以後有需要繼續改進。


4.8:測試
main() {
  var v1 = Vector2(3, 4);
  print(v1); //(3,4)
  print(v1.distance); //5.0
  print(v1.angle); //53.13010235415598
  v1.rotate(37);
  print(v1); //(-0.011353562466313798,4.0000058005648444)
  
  var v2 = Vector2.fromMap({'x': 5, 'y': 6});
  print(v2); //(5,6)
  v2.reflex();
  print(v2);//(-5,-6)
  
  var v3 = Vector2(2, 2);
  var v4 = Vector2(3, 2);
  print(v4 - v3); //(1,0)
  print(v4 + v3); //(5,4)
  print(v4 * v3); //10
}
複製程式碼

本文到此接近尾聲了,另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,本人微訊號:zdl1994328,期待與你的交流與切磋。如果想快速嚐鮮Flutter,《Flutter七日》會是你的必備佳品。

下一篇將把語法掃個尾,介紹Dart中的其他語法。
最後說一句:祝大家早日能夠真正的面向你的物件來程式設計?

相關文章