跳到主要內容

建構函式

建構函式是用於建立類例項的特殊函式。

Dart 實現了多種型別的建構函式。除了預設建構函式外,這些函式的名稱與類名相同。

生成式建構函式
建立新例項並初始化例項變數。
預設建構函式
當沒有指定建構函式時,用於建立新例項。它不接受引數且沒有名稱。
命名建構函式
闡明建構函式的目的,或允許為同一類建立多個建構函式。
常量建構函式
建立作為編譯時常量的例項。
工廠建構函式
建立子型別的新例項,或者從快取返回現有例項。
重定向建構函式
將呼叫轉發到同一類的另一個建構函式。

建構函式型別

#

生成式建構函式

#

要例項化一個類,使用生成式建構函式。

dart
class Point {
  // Instance variables to hold the coordinates of the point.
  double x;
  double y;

  // Generative constructor with initializing formal parameters:
  Point(this.x, this.y);
}

預設建構函式

#

如果你沒有宣告建構函式,Dart 會使用預設建構函式。預設建構函式是沒有引數或名稱的生成式建構函式。

命名建構函式

#

使用命名建構函式可以為一個類實現多個建構函式,或提供額外的清晰度

dart
const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  final double x;
  final double y;

  // Sets the x and y instance variables
  // before the constructor body runs.
  Point(this.x, this.y);

  // Named constructor
  Point.origin() : x = xOrigin, y = yOrigin;
}

子類不繼承超類的命名建構函式。要在子類中建立超類中定義的命名建構函式,請在子類中實現該建構函式。

常量建構函式

#

如果你的類產生不可變的物件,請將這些物件設為編譯時常量。要使物件成為編譯時常量,請定義一個 const 建構函式,並將所有例項變數設定為 final

dart
class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);

  final double x, y;

  const ImmutablePoint(this.x, this.y);
}

常量建構函式並非總是建立常量。它們可能在非 const 上下文中使用。要了解更多資訊,請查閱使用建構函式一節。

重定向建構函式

#

建構函式可以將呼叫重定向到同一類的另一個建構函式。重定向建構函式沒有函式體。建構函式在冒號 (:) 後使用 this 而不是類名。

dart
class Point {
  double x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  Point.alongXAxis(double x) : this(x, 0);
}

工廠建構函式

#

在實現建構函式時遇到以下兩種情況之一時,使用 factory 關鍵字

  • 建構函式並不總是建立其類的新例項。儘管工廠建構函式不能返回 null,但它可能會返回

    • 快取中的現有例項,而不是建立新例項
    • 子類的新例項
  • 在構造例項之前需要執行非平凡的工作。這可能包括檢查引數或執行初始化列表中無法處理的任何其他處理。

以下示例包含兩個工廠建構函式。

  • Logger 工廠建構函式從快取返回物件。
  • Logger.fromJson 工廠建構函式根據 JSON 物件初始化一個 final 變數。
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) {
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

像使用其他建構函式一樣使用工廠建構函式

dart
var logger = Logger('UI');
logger.log('Button clicked');

var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);

重定向工廠建構函式

#

重定向工廠建構函式指定了對同一類中另一個建構函式的呼叫,每當呼叫該重定向建構函式時都會使用此呼叫。

dart
factory Listenable.merge(List<Listenable> listenables) = _MergingListenable

普通工廠建構函式似乎可以建立並返回其他類的例項。這可能會使重定向工廠變得不必要。重定向工廠有幾個優點

  • 抽象類可以提供一個常量建構函式,它使用另一個類的常量建構函式。
  • 重定向工廠建構函式避免了轉發器重複形式引數及其預設值的需要。

建構函式擷取(tear-off)

#

Dart 允許你將建構函式作為引數提供,而無需呼叫它。這稱為 擷取(因為你 擷取 了括號),它充當一個閉包,使用相同的引數呼叫建構函式。

如果擷取是一個建構函式,其簽名和返回型別與方法接受的相同,你可以將該擷取用作引數或變數。

擷取與 lambda 或匿名函式不同。lambda 充當建構函式的包裝器,而擷取就是建構函式本身。

使用擷取

gooddart
// Use a tear-off for a named constructor:
var strings = charCodes.map(String.fromCharCode);

// Use a tear-off for an unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);

而不是 Lambda

baddart
// Instead of a lambda for a named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));

// Instead of a lambda for an unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));

要了解更多討論,請觀看關於擷取的這個 Decoding Flutter 影片。

在新標籤頁中在 YouTube 上觀看:“Dart Tear-offs | Decoding Flutter”

例項變數初始化

#

Dart 可以透過三種方式初始化變數。

在宣告中初始化例項變數

#

在宣告變數時初始化例項變數。

dart
class PointA {
  double x = 1.0;
  double y = 2.0;

  // The implicit default constructor sets these variables to (1.0,2.0)
  // PointA();

  @override
  String toString() {
    return 'PointA($x,$y)';
  }
}

使用初始化形式引數

#

為了簡化將建構函式引數賦值給例項變數的常見模式,Dart 引入了 初始化形式引數

在建構函式宣告中,包含 this.<propertyName> 並省略函式體。this 關鍵字指代當前例項。

當存在名稱衝突時,使用 this。否則,Dart 風格省略 this。生成式建構函式有一個例外,你必須以 this 為字首初始化形式引數名稱。

如本指南前面所述,某些建構函式和建構函式的某些部分無法訪問 this。其中包括

  • 工廠建構函式
  • 初始化列表的右側
  • 傳遞給超類建構函式的引數

初始化形式引數還允許你初始化不可為空或 final 的例項變數。這兩種型別的變數都需要初始化或預設值。

dart
class PointB {
  final double x;
  final double y;

  // Sets the x and y instance variables
  // before the constructor body runs.
  PointB(this.x, this.y);

  // Initializing formal parameters can also be optional.
  PointB.optional([this.x = 0.0, this.y = 0.0]);
}

私有欄位不能用作命名初始化形式引數。

dart
class PointB {
// ...

  PointB.namedPrivate({required double x, required double y})
      : _x = x,
        _y = y;

// ...
}

這也適用於命名變數。

dart
class PointC {
  double x; // must be set in constructor
  double y; // must be set in constructor

  // Generative constructor with initializing formal parameters
  // with default values
  PointC.named({this.x = 1.0, this.y = 1.0});

  @override
  String toString() {
    return 'PointC.named($x,$y)';
  }
}

// Constructor using named variables.
final pointC = PointC.named(x: 2.0, y: 2.0);

透過初始化形式引數引入的所有變數都是 final 的,並且僅在其初始化變數的作用域內。

要執行無法在初始化列表中表達的邏輯,請建立包含該邏輯的工廠建構函式靜態方法。然後,你可以將計算出的值傳遞給普通建構函式。

建構函式引數可以設定為可為空且不進行初始化。

dart
class PointD {
  double? x; // null if not set in constructor
  double? y; // null if not set in constructor

  // Generative constructor with initializing formal parameters
  PointD(this.x, this.y);

  @override
  String toString() {
    return 'PointD($x,$y)';
  }
}

使用初始化列表

#

在建構函式體執行之前,你可以初始化例項變數。使用逗號分隔初始化器。

dart
// Initializer list sets instance variables before
// the constructor body runs.
Point.fromJson(Map<String, double> json) : x = json['x']!, y = json['y']! {
  print('In Point.fromJson(): ($x, $y)');
}

在開發期間驗證輸入時,在初始化列表中使用 assert

dart
Point.withAssert(this.x, this.y) : assert(x >= 0) {
  print('In Point.withAssert(): ($x, $y)');
}

初始化列表有助於設定 final 欄位。

以下示例在初始化列表中初始化了三個 final 欄位。要執行程式碼,請點選 執行

import 'dart:math';

class Point {
  final double x;
  final double y;
  final double distanceFromOrigin;

  Point(double x, double y)
    : x = x,
      y = y,
      distanceFromOrigin = sqrt(x * x + y * y);
}

void main() {
  var p = Point(2, 3);
  print(p.distanceFromOrigin);
}

建構函式繼承

#

子類(或子類)不繼承其超類(或直接父類)的建構函式。如果一個類沒有宣告建構函式,它只能使用預設建構函式

類可以繼承超類的引數。這些引數稱為超引數

建構函式的工作方式與呼叫一系列靜態方法有些類似。每個子類都可以呼叫其超類的建構函式來初始化例項,就像子類可以呼叫超類的靜態方法一樣。這個過程不會“繼承”建構函式體或簽名。

非預設超類建構函式

#

Dart 按照以下順序執行建構函式

  1. 初始化列表
  2. 超類的未命名、無引數建構函式
  3. 主類的無引數建構函式

如果超類沒有未命名、無引數建構函式,請呼叫超類中的一個建構函式。在建構函式體(如果有)之前,在冒號 (:) 後指定超類建構函式。

在以下示例中,Employee 類的建構函式呼叫其超類 Person 的命名建構函式。要執行以下程式碼,請點選 執行

class Person {
  String? firstName;

  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person does not have a default constructor;
  // you must call super.fromJson().
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

void main() {
  var employee = Employee.fromJson({});
  print(employee);
  // Prints:
  // in Person
  // in Employee
  // Instance of 'Employee'
}

由於 Dart 在呼叫超類建構函式之前評估傳遞給它的引數,因此引數可以是函式呼叫等表示式。

dart
class Employee extends Person {
  Employee() : super.fromJson(fetchDefaultData());
  // ···
}

超引數

#

為了避免將每個引數傳遞給建構函式的超類呼叫,使用超初始化引數將引數轉發到指定的或預設的超類建構函式。你不能將此特性與重定向建構函式一起使用。超初始化引數的語法和語義與初始化形式引數類似。

如果超類建構函式呼叫包含位置引數,則超初始化引數不能是位置引數。

dart
class Vector2d {
  final double x;
  final double y;

  Vector2d(this.x, this.y);
}

class Vector3d extends Vector2d {
  final double z;

  // Forward the x and y parameters to the default super constructor like:
  // Vector3d(final double x, final double y, this.z) : super(x, y);
  Vector3d(super.x, super.y, this.z);
}

為了進一步說明,請看以下示例。

dart
  // If you invoke the super constructor (`super(0)`) with any
  // positional arguments, using a super parameter (`super.x`)
  // results in an error.
  Vector3d.xAxisError(super.x): z = 0, super(0); // BAD

這個命名建構函式試圖兩次設定 x 的值:一次在超類建構函式中,一次作為位置超引數。由於兩者都指向 x 位置引數,這會導致錯誤。

當超類建構函式有命名引數時,你可以在命名超引數(下例中的 super.y)和超類建構函式呼叫的命名引數(super.named(x: 0))之間分配它們。

dart
class Vector2d {
  // ...
  Vector2d.named({required this.x, required this.y});
}

class Vector3d extends Vector2d {
  final double z;

  // Forward the y parameter to the named super constructor like:
  // Vector3d.yzPlane({required double y, required this.z})
  //       : super.named(x: 0, y: y);
  Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);
}