首頁 > 軟體

flutter--Dart基礎語法(三)類和物件、泛型、庫

2021-02-03 08:30:07

一、前言

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關鍵字了。不過官方已經不建議使用這種方式了:

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管理自己的庫自己的庫,在專案開發中個人覺得不是非常有必要,所以暫時不講解這種方式。

 

 


IT145.com E-mail:sddin#qq.com