1. 程式人生 > 其它 >Flutter 陳航 06-Dart 基礎語法

Flutter 陳航 06-Dart 基礎語法

本文地址


目錄

目錄

06 | 基礎語法與型別變數

Dart 的變數與型別

和絕大多數編譯型語言一樣,Dart 要求以 main 函式作為執行的入口。

在 Dart 中,我們可以用 var 或者具體的型別來宣告一個變數

  • 當使用 var 定義變數時,表示型別是交由編譯器推斷決定的
  • 也可以用靜態型別去定義變數,這樣編輯器和編譯器就能使用這些靜態型別,向你提供程式碼補全或編譯警告的提示了

未初始化的變數的值都是 null

Dart 是型別安全的語言,並且所有型別都是物件型別,都繼承自頂層型別 Object,函式和 null 也都是繼承自 Object 的物件。

num、bool 與 String

Dart 的數值型別 num 只有兩種子類:

  • 64 位 int,代表整數型別
  • 符合 IEEE 754 標準的 64 位 double,代表浮點數型別
main() {
  print("$age - $size - ${size.round()} - $hex - $e"); // 30 - 5.678 - 6 - 255 - 1200.0
}

var age = 30;
double size = 5.678;
int hex = 0xFF;
double e = 1.2e3;

dart:math 庫提供了諸如三角函式、指數、對數、平方根等高階函式。

在 Dart 裡,只有兩個物件具有 bool 型別:true 和 false,它們都是編譯時常量。

Dart 的 String 由 UTF-16 的字串組成。構造字串字面量時既能使用單引號也能使用雙引號,還能在字串中使用 ${express} 嵌入變數或表示式。

為了獲得內嵌物件的字串,Dart 會呼叫物件的 toString() 方法。

可以通過三個單引號三個雙引號的方式宣告多行字串。

List 與 Map

main() {
  var arr1 = ["Tom", "Andy", "Jack"]; // List<String>
  var arr2 = List.of([1, 2, 3]); // List<int>
  arr1.add("BQT");
  arr2.insert(0, 30);
  print(arr1); // [Tom, Andy, Jack, BQT]
  arr2.forEach((v) => print(v));

  var map1 = {"name": "Tom", 'sex': 'male'}; // Map<String, String>
  var map2 = new Map();
  map1['name'] = 'BQT';
  map2['sex'] = '男';
  print(map1); // {name: BQT, sex: male}
  map2.forEach((k, v) => print('${k}: ${v}'));
}

Dart 會自動根據上下文對集合型別進行推斷,所以後續往容器內新增的元素也必須遵照這一型別。

如果編譯器自動推斷的型別不符合預期,我們可以在宣告時顯式地把型別標記出來,不僅可以讓程式碼提示更友好一些,還可以讓靜態分析器幫忙檢查字面量中的錯誤,解除型別不匹配帶來的安全隱患或是 Bug。

main() {
  var arr1 = <String>["Tom", "Andy", "Jack"];
  var arr2 = List<int>.of([1, 2, 3]);
  var map1 = <String, String>{"name": "Tom", 'sex': 'male'};
  var map2 = new Map<String, String>();

  print(arr1.runtimeType); // List<String>
  print(map1.runtimeType); // _InternalLinkedHashMap<String, String>
  print(arr2 is List<int>); // true
  print(map2 is Map<String, String>); // true
}

常量定義

  • const,表示變數在編譯期間即能確定的值 -- 編譯期常量
  • final 表示變數可以在執行時確定值,而一旦確定後就不可再變 -- 執行時常量
import 'dart:math';

main() {
  const size = 3; // 編譯期常量,當然也可以使用 final 定義
  final count = Random().nextInt(10); // 執行時常量,不能使用 const 定義
}

07 | 函式、類與運算子

函式

在 Dart 中,所有型別都是物件型別,函式也是物件,它的型別叫作 Function

main() {
  bool isZero(int number) => number == 0;

  void printInfo(int number, Function check) => print("$number is Zero: ${check(number)}");

  Function f = isZero;
  printInfo(10, isZero); // 10 is Zero: false
  printInfo(0, f); // 0 is Zero: true
}

可選命名引數和可選引數

Dart 認為過載會導致混亂,因此從設計之初就不支援過載,而是提供了可選命名引數和可選引數。

具體方式是,在宣告函式時:

  • 可選命名引數:給引數增加{},必須顯式宣告引數名,引數位置無所謂 -- 可理解為 map
  • 可忽略的引數:給引數增加[],這些引數可以忽略,但必須按順序擺放 -- 可理解為 連結串列(LinkedList)

在使用這兩種方式定義函式時,我們還可以在引數未傳遞時設定預設值

main() {
  // 可選命名引數
  String f1({int? age, String? name}) => "$age,$name ";
  print(f1(age: 30, name: "BQT") + f1(age: 30) + f1(name: "BQT") + f1()); // 30,BQT 30,null null,BQT null,null

  String f2({int? age = 0, String name = "xxx"}) => "$age,$name ";
  print(f2(age: 30, name: "BQT") + f2(age: 30) + f2(name: "BQT") + f2()); // 30,BQT 30,xxx 0,BQT 0,xxx

  // 可忽略的引數
  String f3(int? age, [String? name, bool meal = true]) => "$age,$name,$meal ";
  print(f3(30, "BQT", false) + f3(30, "BQT") + f3(30)); // 30,BQT,false 30,BQT,true 30,null,true 
}

類的定義

Dart 中並沒有 public、protected、private 這些關鍵字,我們只要在宣告變數與方法時,在前面加上 _ 即可作為 private 方法使用。如果不加 _,則預設為 public。

注意,_ 的限制範圍並不是類訪問級別的,而是庫(package)訪問級別。

class Point {
  num x, y;
  Point(this.x, this.y); //語法糖,等同於在函式體內執行 this.x = x; this.y = y;
  void printInfo() => print('($x, $y)');

  static num factor = 0;
  static void printF() => print('$factor');
}

main() {
  Point.factor = 8;
  Point.printF();

  Point point = new Point(100, 200); // 關鍵字 new 可以省略
  point.printInfo();
}

命名建構函式

除了可選命名引數和可選引數之外,Dart 還提供了命名建構函式的方式,使得類的例項化過程語義更清晰。

此外,與 C++ 類似,Dart 支援初始化列表。在建構函式的函式體真正執行之前,你還有機會給例項變數賦值,或者重定向至另一個建構函式。

class Point {
  num x, y, z;

  Point(this.x, this.y, [this.z = 8]); // 可選引數
  Point.bottom(num x) : this(x, x); // 命名建構函式 + 重定向
  Point.def() : x=0, y=0, z=0; // 命名建構函式 + 初始化列表

  void printInfo() => print('($x,$y,$z)');
}

main() {
  Point(6, 6).printInfo(); // (6,6,8)
  Point(6, 6, 6).printInfo(); // (6,6,6)
  Point.bottom(5).printInfo(); // (5,5,8)
  Point.def().printInfo(); // (0,0,0)
}

繼承與介面實現

在 Dart 中,你可以對同一個父類進行繼承或介面實現:

  • 繼承父類意味著,子類由父類派生,會獲取父類的成員變數和方法實現,可以根據需要覆寫建構函式及父類方法
  • 介面實現意味著,子類獲取到的僅僅是介面的成員變數符號和方法符號,需要重新實現成員變數和介面方法
class Point {
  num x = 0, y = 0;
  void printInfo() => print('($x,$y)');
}

// 繼承
class Vector extends Point {
  @override
  void printInfo() => print('(${x + 100},$y)'); // 覆寫了父類實現
}

// 介面實現,注意,這裡會將 class Point 當做介面
class Coordinate implements Point {
  @override
  num x = 0, y = 0; // 所有成員變數【必須】重新宣告【並實現】

  @override
  void printInfo() => print('(${x + 10},${y + 10})'); // 成員函式也必須重新宣告並實現
}

介面實現時,成員變數和方法必須重新宣告並實現,否則提示 Try implementing the missing methods, or make the class abstract

main() {
  Vector()
    ..x = 1
    ..y = 2 //級聯運算子,等同於 xxx.x=1; xxx.y=2;
    ..printInfo(); // (101,2)

  var yyy = Coordinate()
    ..x = 1
    ..y = 2
    ..printInfo(); // (11,12)
  print(yyy is Point); //true
}

混入 Mixin

除了繼承和介面實現之外,Dart 還提供了另一種機制來實現類的複用,即 混入 Mixin。混入可以被視為具有實現方法的介面,不僅可以解決 Dart 缺少對多重繼承的支援問題,還能夠避免由於多重繼承可能導致的歧義(菱形問題)。

備註:繼承歧義,也叫菱形問題,是支援多繼承的程式語言中一個相當棘手的問題。
當 B 類和 C 類繼承自 A 類,而 D 類繼承自 B 類和 C 類時會產生歧義。如果 A 中有一個方法在 B 和 C 中已經覆寫,而 D 沒有覆寫它,那麼 D 繼承的方法的版本是 B 類,還是 C 類的呢?

要使用混入,只需要 with 關鍵字即可。

class MixinP with Point {}

main() {
  var yyy = MixinP()
    ..x = 100
    ..printInfo(); // (100,0)

  print(yyy is Point); //true
}

通過混入,一個類可以以非繼承的方式使用其他類中的變數與方法。

Flutter 很多地方都用到了混入

順序: extends before with before implements

class E extends A with B, C implements C, D {} // extends > with > implements
class F extends A implements B, C {}
class G extends A with B, C {}       // 可以同時混入多個類
class H with B, C implements C, D {} // 可同時 with / implements 同一個 class

注意:宣告建構函式的類無法被別的類混入。The class 'XXX' can't be used as a mixin because it declares a constructor. (Documentation)

運算子

Dart 多了幾個額外的運算子,用於簡化處理變數例項為 null 的情況。

  • ?. 運算子:a?.xx() ,表示 a 為 null 的時候跳過,避免丟擲異常
  • ??= 運算子:a ??= value,如果 a 為 null,則給 a 賦值 value,否則跳過
  • ?? 運算子:a ?? b,如果 a 不為 null,則返回 a,否則返回 b

對於系統的運算子,一般情況下只支援基本資料型別和標準庫中提供的型別。而對於使用者自定義的類,如果想支援基本操作,比如比較大小、相加相減等,則需要使用者自己來定義關於這個運算子的具體實現。

Dart 提供了類似 C++ 的運算子覆寫機制,使得我們可以覆寫或者自定義運算子。

class Vector {
  num x, y;
  Vector(this.x, this.y);

  Vector operator +(Vector v) => Vector(x + v.x, y + v.y); // 自定義 + 運算子,實現向量相加
  bool operator ==(dynamic v) => x == v.x && y == v.y; // 覆寫 == 運算子,判斷向量相等

  String info() => "($x,$y)";
}

main() {
  var x = Vector(3, 3);
  var y = Vector(2, 2) + Vector(1, 1);
  print(y.info()); // (3,3)
  print(x == y); // true
}

operator 是 Dart 的關鍵字,與運算子一起使用,表示一個類成員運算子函式。在理解時,我們應該把 operator 和運算子作為整體,看作是一個成員函式名。

問:在覆寫相等運算子時為何需要傳入 dynamic 變數,而不能傳入 Vector 呢?
答:因為 operator== 是繼承自 Object 類,這個類的引數宣告就是 dynamic

08 | 綜合案例

class Meta {
  double price;
  String name;

  Meta(this.name, this.price); // 成員變數初始化
}

class Item extends Meta {
  Item(name, price) : super(name, price);

  Item operator +(Item item) => Item("BQT", price + item.price); // 運算子過載
}

abstract class PrintHelper {
  void printInfo() => print(getInfo());

  String getInfo();
}

// 以非繼承的方式複用另一個類的成員變數及函式
class ShoppingCart extends Meta with PrintHelper {
  DateTime date;
  String? code;
  late List<Item> bookings; // add an init expression, or init in constructor, or mark it 'late'

  double get price => bookings.reduce((v, e) => v + e).price; // 歸併求和

  ShoppingCart({name}) : this.withCode(name: name, code: null); // 重定向

  ShoppingCart.withCode({name, this.code})
      : date = DateTime.now(),
        super(name, 0); // 呼叫父類初始化方法

  @override
  getInfo() => '''
購物車資訊:
-----------------------------
  使用者名稱: $name
  優惠碼: ${code ?? "code 為空"}
  總價: $price
  Date: ${date.getDate()}
-----------------------------
''';
}

// 擴充套件方法 https://blog.csdn.net/weixin_43836055/article/details/126073256
extension DateTimeExt on DateTime {
  String getDate() => "$year.$month.$day $hour:$minute:$second";
}

void main() {
  ShoppingCart.withCode(name: '張三', code: '123456')
    ..bookings = [Item('蘋果', 6.0), Item('鴨梨', 3.0)]
    ..printInfo();

  ShoppingCart(name: '李四')
    ..bookings = [Item('香蕉', 8.0), Item('西瓜', 5.0)]
    ..printInfo();
}

2022-12-4