flutter–Dart基礎語法(三)類和對象、泛型、庫

一、前言

Flutter 是 Google 開源的 UI 工具包,幫助開發者通過一套程式碼庫高效構建多平台精美應用,Flutter 開源、免費,擁有寬鬆的開源協議,支援移動、Web、桌面和嵌入式平台。

Flutter是使用Dart語言開發的跨平台移動UI框架,通過自建繪製引擎,能高性能、高保真地進行Android和IOS開發。Flutter採用Dart語言進行開發,而並非Java,Javascript這類熱門語言,這是Flutter團隊對當前熱門的10多種語言慎重評估後的選擇。因為Dart囊括了多數程式語言的優點,它更符合Flutter構建介面的方式。

本文主要就是簡單梳理一下Dart語言的一些基礎知識和語法。關於程式語言的基本語法無外乎那麼些內容,注釋、變數、數據類型、運算符、流程式控制制、函數、類、異常、文件、非同步、常用庫等內容,相信大部分讀者都是有一定編程基礎的,所以本文就簡單地進行一個梳理,不做詳細的講解。大家也可以參考 Dart程式語言中文網

上一篇文章主要是寫了Dart語言的流程式控制制、函數和異常處理,本文將接著上一篇文章繼續往後寫,本文將主要介紹Dart語言的類和對象、泛型以及庫的使用。

二、類和對象

Dart 是一種基於類和 mixin 繼承機制的面向對象的語言。 每個對象都是一個類的實例,所有的類都繼承於 Object。面向對象中非常重要的概念就是類,類產生了對象。接下來我們就具體來學習類和對象,但是Dart對類進行了很多其他語言沒有的特性,所以,這裡我會花比較長的篇幅來講解。

2.1  類的定義

在Dart中,定義類用class關鍵字。類通常有兩部分組成:成員(member)和方法(method)。定義類的偽程式碼如下:

class 類名 {
  類型 成員名;
  返回值類型 方法名(參數列表) {
    方法體
  }
}

編寫一個簡單的Person類:

  • 這裡有一個注意點: 我們在方法中使用屬性(成員/實例變數)時,並沒有加this
  • Dart的開發風格中,在方法中通常使用屬性時,會省略this,但是有命名衝突時,this不能省略
class Person {
  String name;

  eat() {
    print('$name在吃東西');
  }
}

我們來使用這個類,創建對應的對象:

  • 注意:從Dart2開始,new關鍵字可以省略。
main(List<String> args) {
  // 1.創建類的對象
  var p = new Person(); // 直接使用Person()也可以創建

  // 2.給對象的屬性賦值
  p.name = 'why';

  // 3.調用對象的方法
  p.eat();
} 

2.2 構造方法

Dart語言中構造方法分為普通構造方法、命名構造方法、重定向構造方法、常量構造方法、工廠構造方法以及初始化列表等多種。下面我們就一一給大家簡單解釋一下其中的區別。

2.2.1 普通構造方法

我們知道, 當通過類創建一個對象時,會調用這個類的構造方法。

  • 當類中沒有明確指定構造方法時,將默認擁有一個無參的構造方法
  • 前面的Person中我們就是在調用這個構造方法。

我們也可以根據自己的需求,定義自己的構造方法:

  • 注意一當有了自己的構造方法時,默認的構造方法將會失效,不能使用
    • 當然,你可能希望明確的寫一個默認的構造方法,但是會和我們自定義的構造方法衝突;
    • 這是因為Dart本身不支援函數的重載(名稱相同, 參數不同的方式)。
  • 注意二:這裡我還實現了toString方法
class Person {
  String name;
  int age;

  Person(String name, int age) {
    this.name = name;
    this.age = age;
  }

  @override
  String toString() {
    return 'name=$name age=$age';
  }
}

另外,在實現構造方法時,通常做的事情就是通過 參數屬性 賦值。為了簡化這一過程, Dart提供了一種更加簡潔的語法糖形式。上面的構造方法可以優化成下面的寫法:

Person(String name, int age) {
    this.name = name;
    this.age = age;
  }
  // 等同於
  Person(this.name, this.age);

2.2.2 命名構造方法

但是在開發中, 我們確實希望實現更多的構造方法,怎麼辦呢?因為不支援方法(函數)的重載,所以我們沒辦法創建相同名稱的構造方法。因此,我們需要使用命名構造方法:

class Person {
  String name;
  int age;

  Person() {
    name = '';
    age = 0;
  }
    // 命名構造方法
  Person.withArgments(String name, int age) {
    this.name = name;
    this.age = age;
  }

  @override
  String toString() {
    return 'name=$name age=$age';
  }
}

// 創建對象
var p1 = new Person();
print(p1);
var p2 = new Person.withArgments('why', 18);
print(p2);

在之後的開發中, 我們也可以利用命名構造方法,提供更加便捷的創建對象方式。比如開發中,我們需要經常將一個Map轉成對象,可以提供如下的構造方法

  // 新的構造方法
  Person.fromMap(Map<String, Object> map) {
    this.name = map['name'];
    this.age = map['age'];
  }

  // 通過上面的構造方法創建對象
  var p3 = new Person.fromMap({'name': 'kobe', 'age': 30});
  print(p3);

2.2.3 初始化列表

我們來重新定義一個類Point, 傳入x/y,可以得到它們的距離distance:

class Point {
  final num x;
  final num y;
  final num distance;

  // 錯誤寫法
  // Point(this.x, this.y) {
  //   distance = sqrt(x * x + y * y);
  // }

  // 正確的寫法
  Point(this.x, this.y) : distance = sqrt(x * x + y * y);
}

上面這種初始化變數的方法, 我們稱之為初始化列表(Initializer list)

2.2.4 重定向構造方法

在某些情況下, 我們希望在一個構造方法中去調用另外一個構造方法, 這個時候可以使用重定向構造方法

  • 在一個構造函數中,去調用另外一個構造函數(注意:是在冒號後面使用this調用)
class Person {
  String name;
  int age;

  Person(this.name, this.age);

  Person.fromName(String name) : this(name, 0);
}

2.2.5 常量構造方法

在某些情況下,傳入相同值時,我們希望返回同一個對象,這個時候,可以使用常量構造方法.

默認情況下,創建對象時,即使傳入相同的參數,創建出來的也不是同一個對象,看下面程式碼:

  • 這裡我們使用identical(對象1, 對象2)函數來判斷兩個對象是否是同一個對象:
main(List<String> args) {
  var p1 = Person('why');
  var p2 = Person('why');
  print(identical(p1, p2)); // false
}

class Person {
  String name;

  Person(this.name);
}

但是, 如果將構造方法前加const進行修飾,那麼可以保證同一個參數,創建出來的對象是相同的

  • 這樣的構造方法就稱之為常量構造方法
main(List<String> args) {
  var p1 = const Person('zhangsan');
  var p2 = const Person('zhangsan');
  const p3 = Person('zhangsan');
  var p4 = Person('zhangsan');
  var p5 = Person('lisi');

  print(identical(p1,p2)); //true
  print(identical(p1,p3)); //true
  print(identical(p1,p4)); //false
  print(identical(p1,p5)); //false

}

class Person {
  final String name;

  const Person(this.name);
} 

常量構造方法有一些注意點:

  • 注意一:擁有常量構造方法的類中,所有的成員變數必須是final修飾.
  • 注意二: 為了可以通過常量構造方法,創建出相同的對象,不再使用 new關鍵字,而是使用const關鍵字
    • 如果是將結果賦值給const修飾的標識符時,const可以省略.

2.2.6 工廠構造方法

Dart提供了factory關鍵字, 用於通過工廠去獲取對象

main(List<String> args) {
  var p1 = Person('why');
  var p2 = Person('why');
  print(identical(p1, p2)); // true
}

class Person {
  String name;

  static final Map<String, Person> _cache = <String, Person>{};

  factory Person(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final p = Person._internal(name);
      _cache[name] = p;
      return p;
    }
  }

  Person._internal(this.name);
}

2.3 setter和getter

默認情況下,Dart中類定義的屬性是可以直接被外界訪問的。但是某些情況下,我們希望監控這個類的屬性被訪問的過程,這個時候就可以使用setter和getter

main(List<String> args) {
  final d = Dog("黃色");
  d.setColor = "黑色";
  print(d.getColor);
}

class Dog {
  String color;

  String get getColor {
    return color;
  }
  set setColor(String color) {
    this.color = color;
  }

  Dog(this.color);
}

2.4 類的繼承

面向對象的其中一大特性就是繼承,繼承不僅僅可以減少我們的程式碼量,也是多態的使用前提。Dart中的繼承使用extends關鍵字,子類中使用super來訪問父類。父類中的所有成員變數和方法都會被繼承,但是構造方法除外

main(List<String> args) {
  var p = new Person();
  p.age = 18;
  p.run();
  print(p.age);
}

class Animal {
  int age;

  run() {
    print('在奔跑ing');
  }
}

class Person extends Animal {

}

子類可以擁有自己的成員變數, 並且可以對父類的方法進行重寫

class Person extends Animal {
  String name;

  @override
  run() {
    print('$name在奔跑ing');
  }
}

子類中可以調用父類的構造方法,對某些屬性進行初始化:

  • 子類的構造方法在執行前,將隱含調用父類的無參默認構造方法(沒有參數且與類同名的構造方法)。
  • 如果父類沒有無參默認構造方法,則子類的構造方法必須在初始化列表中通過super顯式調用父類的某個構造方法
class Animal {
  int age;

  Animal(this.age);

  run() {
    print('在奔跑ing');
  }
}

class Person extends Animal {
  String name;

  Person(String name, int age) : name=name, super(age);

  @override
  run() {
    print('$name在奔跑ing');
  }

  @override
  String toString() {
    return 'name=$name, age=$age';
  }
}

2.5 抽象類

我們知道,繼承是多態使用的前提。所以在定義很多通用的 調用介面 時, 我們通常會讓調用者傳入父類,通過多態來實現更加靈活的調用方式。但是,父類本身可能並不需要對某些方法進行具體的實現,所以父類中定義的方法,我們可以定義為抽象方法

什麼是 抽象方法? 在Dart中沒有具體實現的方法(沒有方法體),就是抽象方法。

  • 抽象方法,必須存在於抽象類中。
  • 抽象類是使用abstract聲明的類。

下面的程式碼中, Shape類就是一個抽象類, 其中包含一個抽象方法.

abstract class Shape {
  getArea();
}

class Circle extends Shape {
  double r;

  Circle(this.r);

  @override
  getArea() {
    return r * r * 3.14;
  }
}

class Reactangle extends Shape {
  double w;
  double h;

  Reactangle(this.w, this.h);

  @override
  getArea() {
    return w * h;
  }
}

注意事項:

  • 注意一:抽象類不能實例化.
  • 注意二:抽象類中的抽象方法必須被子類實現, 抽象類中的已經被實現方法, 可以不被子類重寫.

2.6 隱式介面

Dart中的介面比較特殊, 沒有一個專門的關鍵字來聲明介面。默認情況下,定義的每個類都相當於默認也聲明了一個介面,可以由其他的類來實現(因為Dart不支援多繼承)。在開發中,我們通常將用於給別人實現的類聲明為抽象類:

abstract class Runner {
  run();
}

abstract class Flyer {
  fly();
}

class SuperMan implements Runner, Flyer {
  @override
  run() {
    print('超人在奔跑');
  }

  @override
  fly() {
    print('超人在飛');
  }
}

2.7  Mixin混入

在通過implements實現某個類時,類中所有的方法都必須被重新實現 (無論這個類原來是否已經實現過該方法)

但是某些情況下,一個類可能希望直接復用之前類的原有實現方案,怎麼做呢?

  • 使用繼承嗎?但是Dart只支援單繼承,那麼意味著你只能復用一個類的實現。

Dart提供了另外一種方案: Mixin混入的方式

  • 除了可以通過class定義類之外,也可以通過mixin關鍵字來定義一個類。
  • 只是通過mixin定義的類用於被其他類混入使用,通過with關鍵字來進行混入。
main(List<String> args) {
  var superMan = SuperMain();
  superMan.run();
  superMan.fly();
}

mixin Runner {
  run() {
    print('在奔跑');
  }
}

mixin Flyer {
  fly() {
    print('在飛翔');
  }
}

// implements的方式要求必須對其中的方法進行重新實現
// class SuperMan implements Runner, Flyer {}

class SuperMain with Runner, Flyer {

} 

2.8 類成員和方法

前面我們在類中定義的成員和方法都屬於對象級別的, 在開發中, 我們有時候也需要定義類級別的成員和方法。在Dart中我們使用static關鍵字來定義,需要注意的是,類方法和類成員只能通過類名進行訪問,不能通過對象名進行訪問

main(List<String> args) {
  var stu = Student();
  stu.name = 'why';
  stu.sno = 110;
  stu.study();

  Student.time = '早上8點';
  // stu.time = '早上9點'; 錯誤做法, 實例對象不能訪問類成員
  Student.attendClass();
  // stu.attendClass(); 錯誤做法, 實現對象不能訪問類方法
}

class Student {
  String name;
  int sno;

  static String time;

  study() {
    print('$name在學習');
  }

  static attendClass() {
    print('去上課');
  }
}

三、 枚舉類型

枚舉在開發中也非常常見, 枚舉也是一種特殊的類, 通常用於表示固定數量的常量值

3.1 枚舉的定義

枚舉使用enum關鍵字來進行定義:

main(List<String> args) {
  print(Colors.red);
}

enum Colors {
  red,
  green,
  blue
}

3.2 枚舉的屬性

枚舉類型中有兩個比較常見的屬性:

  • index: 用於表示每個枚舉常量的索引, 從0開始.
  • values: 包含每個枚舉值的List.
main(List<String> args) {
  print(Colors.red.index);
  print(Colors.green.index);
  print(Colors.blue.index);

  print(Colors.values);
}

enum Colors {
  red,
  green,
  blue
}

枚舉類型的注意事項:

  • 注意一: 您不能子類化、混合或實現枚舉。
  • 注意二: 不能顯式實例化一個枚舉

四、 泛型

泛型的定義主要有以下兩種:
  1. 在程式編碼中一些包含類型參數的類型,也就是說泛型的參數只可以代表類,不能代表個別對象。(這是當今較常見的定義)
  2. 在程式編碼中一些包含參數的類。其參數可以代表類或對象等等。(人們大多把這稱作模板)不論使用哪個定義,泛型的參數在真正使用泛型時都必須作出指明。
一些強類型程式語言支援泛型,其主要目的是加強類型安全及減少類轉換的次數,但一些支援泛型的程式語言只能達到部分目的。在Dart的 API 文檔中你會發現基礎數組類型 List 的實際類型是 List<E> 。 <…> 符號將 List 標記為 泛型 (或 參數化) 類型。 這種類型具有形式化的參數。 通常情況下,使用一個字母來代表類型參數, 例如 E, T, S, K, 和 V 等。

4.1 為什麼使用泛型?

在類型安全上通常需要泛型支援, 它的好處不僅僅是保證程式碼的正常運行:

  • 正確指定泛型類型可以提高程式碼品質。
  • 使用泛型可以減少重複的程式碼。

如果想讓 List 僅僅支援字元串類型, 可以將其聲明為 List<String> (讀作「字元串類型的 list 」)。 那麼,當一個非字元串被賦值給了這個 list 時,開發工具就能夠檢測到這樣的做法可能存在錯誤。 例如:

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // 錯誤

另外一個使用泛型的原因是減少重複的程式碼。 泛型可以在多種類型之間定義同一個實現, 同時還可以繼續使用檢查模式和靜態分析工具提供的程式碼分析功能。

// 假設你創建了一個用於快取對象的介面:
abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

// 後來發現需要一個相同功能的字元串類型介面,因此又創建了另一個介面:
abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

// 後來,又發現需要一個相同功能的數字類型介面 … 這裡你應該明白了。

// 泛型可以省去創建所有這些介面的麻煩。 通過創建一個帶有泛型參數的介面,來代替上述介面:
abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

在上面的程式碼中,T 是一個備用類型。 這是一個類型佔位符,在開發者調用該介面的時候會指定具體類型。

4.2 List、Set、Map中泛型的使用

4.2.1 字面量中的泛型

List , Set 和 Map 字面量也是可以參數化的。 參數化字面量和之前的字面量定義類似, 對於 List 或 Set 只需要在聲明語句前加 <type> 前綴, 對於 Map 只需要在聲明語句前加 <keyTypevalueType> 前綴, 下面是參數化字面量的示例:

var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

4.2.2 使用泛型類型的構造函數

在調用構造函數的時,在類名字後面使用尖括弧(<...>)來指定泛型類型。 例如:

// 創建一個元素為字元串的Set集合
var nameSet = Set<String>.from(names);

// 下面程式碼創建了一個 key 為 integer, value 為 View 的 map 對象:
var views = Map<int, View>();

4.2.3 運行時中的泛型集合

Dart 中泛型類型是 固化的,也就是說它們在運行時是攜帶著類型資訊的。 例如, 在運行時檢測集合的類型:

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true

提示: 相反,Java中的泛型會被 擦除 ,也就是說在運行時泛型類型參數的資訊是不存在的。 在Java中,可以測試對象是否為 List 類型, 但無法測試它是否為 List<String> 。

4.3 創建類時限制泛型類型

使用泛型類型的時候, 可以使用 extends 實現參數類型的限制。

class Foo<T extends SomeBaseClass> {
  // Implementation goes here...
  String toString() => "Instance of 'Foo<$T>'";
}

class Extender extends SomeBaseClass {...}

// 可以使用 SomeBaseClass 或其任意子類作為通用參數:
var someBaseClassFoo = Foo<SomeBaseClass>();
var extenderFoo = Foo<Extender>();

// 也可以不指定泛型參數:
var foo = Foo();
print(foo); // Instance of 'Foo<SomeBaseClass>'

// 指定任何非 SomeBaseClass 類型會導致錯誤:
var foo = Foo<Object>();

4.4 使用泛型函數

最初,Dart 的泛型只能用於類。 新語法_泛型方法_,允許在方法和函數上使用類型參數

T first<T>(List<T> ts) {
  // Do some initial work or error checking, then...
  T tmp = ts[0];
  // Do some additional checking or processing...
  return tmp;
}

這裡的 first (<T>) 泛型可以在如下地方使用參數 T :

  • 函數的返回值類型 (T).
  • 參數的類型 (List<T>).
  • 局部變數的類型 (T tmp).

五 庫的使用

在Dart中,你可以導入一個庫來使用它所提供的功能。庫的使用可以使程式碼的重用性得到提高,並且可以更好的組合程式碼。Dart中任何一個dart文件都是一個庫,即使你沒有用關鍵字library聲明。

5.1 庫的導入

import語句用來導入一個庫,後面跟一個字元串形式的Uri來指定表示要引用的庫,語法如下:

import '庫所在的uri'

5.1.1 常見的庫URI有三種不同的形式

  • 來自dart標準版,比如dart:io、dart:html、dart:math、dart:core(但是這個可以省略)
    //dart:前綴表示Dart的標準庫,如dart:io、dart:html、dart:math
    import 'dart:io';
  • 使用相對路徑導入的庫,通常指自己項目中定義的其他dart文件
    //當然,你也可以用相對路徑或絕對路徑的dart文件來引用
    import 'lib/student/student.dart';
  • Pub包管理工具管理的一些庫,包括自己的配置以及一些第三方的庫,通常使用前綴package
    //Pub包管理系統中有很多功能強大、實用的庫,可以使用前綴 package:
    import 'package:flutter/material.dart';

5.1.2 庫文件中內容的顯示和隱藏

如果希望只導入庫中某些內容,或者刻意隱藏庫裡面某些內容,可以使用showhide關鍵字

  • show關鍵字:可以顯示某個成員(屏蔽其他)
  • hide關鍵字:可以隱藏某個成員(顯示其他)
// 只顯示Student, Person,其他的都屏蔽
import 'lib/student/student.dart' show Student, Person;

// 只屏蔽Person,其他的都顯示
import 'lib/student/student.dart' hide Person;

5.1.3 庫中內容和當前文件中的名字衝突

當各個庫有命名衝突的時候,可以使用as關鍵字來使用命名空間

import 'lib/student/student.dart' as Stu;

Stu.Student s = new Stu.Student();

5.2 庫的定義

5.2.1 library關鍵字

通常在定義庫時,我們可以使用 library 關鍵字給庫起一個名字。但目前我發現,庫的名字並不影響導入,因為import語句用的是字元串URI

library math;

5.2.2 part關鍵字

在開發中,如果一個庫文件太大,將所有內容保存到一個文件夾是不太合理的,我們有可能希望將這個庫進行拆分,這個時候就可以使用part關鍵字了。不過官方已經不建議使用這種方式了:

image-20190911173722226

// mathUtils.dart文件
part of "utils.dart";

int sum(int num1, int num2) {
  return num1 + num2;
}

// dateUtils.dart文件
part of "utils.dart";

String dateFormat(DateTime date) {
  return "2020-12-12";
}

// utils.dart文件
part "mathUtils.dart";
part "dateUtils.dart";

// test_libary.dart文件
import "lib/utils.dart";

main(List<String> args) {
  print(sum(10, 20));
  print(dateFormat(DateTime.now()));
}

5.2.3 export關鍵字

官方不推薦使用part關鍵字,那如果庫非常大,如何進行管理呢?

  • 將每一個dart文件作為庫文件,使用export關鍵字在某個庫文件中單獨導入
// mathUtils.dart文件
int sum(int num1, int num2) {
  return num1 + num2;
}

// dateUtils.dart文件
String dateFormat(DateTime date) {
  return "2020-12-12";
}

// utils.dart文件
library utils;

export "mathUtils.dart";
export "dateUtils.dart";

// test_libary.dart文件

import "lib/utils.dart";

main(List<String> args) {
  print(sum(10, 20));
  print(dateFormat(DateTime.now()));
}

最後,也可以通過Pub管理自己的庫自己的庫,在項目開發中個人覺得不是非常有必要,所以暫時不講解這種方式。