動態

詳情 返回 返回

Flutter/Dart第15天:Dart類構造函數 - 動態 詳情

Dart官方文檔:https://dart.dev/language/constructors

重要説明:本博客基於Dart官網文檔,但並不是簡單的對官網進行翻譯,在覆蓋核心功能情況下,我會根據個人研發經驗,加入自己的一些擴展問題和場景驗證。

如下代碼樣例,和Java類似,最常用的生成式構造函數:

class Point {
  double x = 0;
  double y = 0;

  Point(double x, double y) {
    this.x = x;
    this.y = y;
  }
}

最佳實戰:在Dart中,僅當命名衝突時,才使用this關鍵字,否則一般可以省略this關鍵字。

初始化參數列表

如上最常用的構造函數,Dart可以進一步優化如下初始化參數形式。同時,也可以為非空變量設置默認值。

class Point {
  final double x;
  final double y;

  // 在構造函數體執行之前,初始化實例變量
  Point(this.x, this.y);
}

默認構造函數

和Java類似,類如果沒有申明構造函數,那麼它會有個默認的構造函數。默認構造函數沒有入參,它只會調用父類的沒有入參的構造函數。

構造函數無法繼承

子類無法繼承父類的構造函數,如果子類沒有申明構造函數,那麼這個子類就只有默認構造函數(無論父類是否有其他構造函數)。

命名構造函數

在Dart中,通過命名構造函數,可以為類提供多個構造函數,並且在創建對象時更加清晰。

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

class Point {
  final double x;
  final double y;

  Point(this.x, this.y);

  // `origin`命名構造函數
  Point.origin()
      : x = xOrigin,
        y = yOrigin;
}

特別注意:如上節提到,子類無法繼承父類的構造函數,包括父類的命名構造函數。如果子類想使用父類的某個命名構造函數,那麼子類必須實現該命名構造函數。

調用父類構造函數

默認情況下,子類無入參的非命名構造函數會調用父類的無入參的非命名構造函數。父類的構造函數在構造函數體之前被調用。如果有初始化參數列表,那麼初始化參數列表在父類構造函數調用之前被調用。

構造函數相關的調用順序如下:

  • 初始化參數列表
  • 父類無入參的構造函數
  • 子類無入參的構造函數

特別注意:如果父類沒有通過無入參且非命名的構造函數,那麼我們必須手工調用父類的一個構造函數,通過冒號:後面緊跟父類構造函數。

如下代碼樣例,Person是父類,它僅申明瞭一個命名構造參數。Employee是繼承Person的子類,由於父類沒有申明無入參非命名的構造函數,因此在它構造函數都必須手工調用父類的某個構造函數。如命名構造函數fromJson後面,通過冒號:調用了父類的命名構造函數。

// 未申明:無入參、非命名的構造函數
class Person {
  String? firstName;

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

class Employee extends Person {
  // 手工調用父類的構造函數:super.fromJson()
  Employee.fromJson(super.data) : super.fromJson() {
    print('in Employee');
  }
}

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

特別注意:由於構造函數參數是在調用構造函數之前計算,因此構造函數的參數可以是表達式,如函數調用等。父類的構造函數不能使用this.關鍵字,因為參數可以是表達式、靜態函數等,並不一定是類實例。

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

手工調用父類的構造函數,然後逐一設置入參比較繁瑣,如果我們想要簡化,那麼可以父類的初始值構造函數。這個功能不能與重定向構造函數一起使用(因為語法衝突)。

class Vector2d {
  final double x;
  final double y;

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

class Vector3d extends Vector2d {
  final double z;

  // 默認情況下,我們的使用方法:
  // Vector3d(final double x, final double y, this.z) : super(x, y);
  
  // 簡化版本:
  Vector3d(super.x, super.y, this.z);
}

如下代碼樣例,父類的初始化構造函數可以通過命名參數調用。

class Vector2d {
  // ...

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

class Vector3d extends Vector2d {
  // ...

  Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);

  // 等價調用
  // Vector3d.yzPlane({required double y, required this.z}) : super.named(x: 0, y: y);
}

初始化列表

在構造函數執行之前,我們可以調用父類的構造函數,還可以初始化實例變量。實例變量初始化通過逗號分隔。

Point.fromJson(Map<String, double> json)
    : x = json['x']!,
      y = json['y']! {
  print('In Point.fromJson(): ($x, $y)');
}

開發階段,我們可以在初始化列表中增加斷言:

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

初始化列表在設置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);
  // 輸出:3.605551275463989
}

重定向構造函數

重定向構造函數,就是使用類的其他的構造函數,重定向到的構造函數使用this關鍵字:

class Point {
  double x, y;

  // 主構造函數
  Point(this.x, this.y);

  // 重定向到主構造函數
  Point.alongXAxis(double x) : this(x, 0);
}

常量構造函數

如果對象的數據不會改變,這些對象可以作為編譯期常量。

常量構造函數要求:實例變量都是final不可變量,定義一個const修飾符的構造函數。

特別注意:上一文我們有提到常量構造函數,常量構造函數創建的對象並不一定<?都是常量(當創建的對象沒有const修飾符,或者對象不是在const常量上下文中,那麼該對象就不是常量)!

如下代碼樣例,ImmutablePoint有常量構造函數,它創建的3個對象中,前面2個是常量,後面1個並非常量。

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

  final double x, y;

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

// `a`和`b`對象是常量,且它們屬於同一個實例
var a = const ImmutablePoint(1, 2);
var b = const ImmutablePoint(1, 2);

assert(identical(a, b));

// `c`對象並非常量,它也和`a`或者`b`不是同一個實例
var c = ImmutablePoint(1, 2);
assert(!identical(a, b));

工廠構造函數

在一個不總是創建實例的類中,使用factory關鍵字實現一個構造函數,即為工廠構造函數。

工廠構造函數常用使用場景:如通過構造函數,從緩存獲取對象,或者創建其子類,或者創建不可變常量但是又不想提供初始化參數列表等。

如下代碼樣例,Logger工廠構造函數優先從緩存獲取對象,而Logger.fromJson()工廠構造函數則初始化了一個final不可實例變量:

class Logger {
  final String name;
  bool mute = false;

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

  // 優先從緩存獲取對象,如果不存在則新增
  factory Logger(String name) {
    print('默認構造函數:$name');
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }

  // 命名構造函數,通過工廠構造函數獲取對象(緩存,或新增)
  factory Logger.fromJson(Map<String, Object> json) {
    print('命名構造函數:$json');
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

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

工廠構造函數的使用,和普通構造函數無本質區別:

var logger = Logger('UI');
logger.log('Hi NTopicCN.');
// 結果:
// 默認構造函數:UI
// Hi NTopicCN.

var loggerJson = Logger.fromJson({'name': 'UI'});
loggerJson.log('Hello Logger.');
// 結果:
// 命名構造函數:{name: UI}
// 默認構造函數:UI
// Hello Logger.

我的本博客原地址:https://ntopic.cn/p/2023102101


Add a new 評論

Some HTML is okay.