Dart 裡的型別系統

戀貓de小郭發表於2021-05-31

原文連結: recipes.tst.sh/docs/faq/ty…

什麼是型別?

型別是用於描述例項介面的節點,如下示例和註釋所示:


// Foo is now an interface type.
class Foo {}

// FooFn is now an alias of the `Foo Function()` type.
typedef FooFn = Foo Function();

// You can now create interface types of Bar with any subtype of Foo as the type argument.
class Bar<T extends Foo> {
  // T is a subtype of Foo in this context.
}
複製程式碼

在最頂層的級別裡,只有少數幾種型別:

  • dynamic
  • void
  • interface 型別
  • function 型別
  • parameter 型別

最常見的是 interface 型別,它描述了類和決定了型別引數。

dart:core 包含了一堆具有特殊型別屬性的類,下面將介紹這些類。

github.com/dart-lang/s…

例項

在物件的整個生命週期中,它只有一個型別,該型別在構造時確定並且永遠不能更改:

int x = 2;
num y = x;
print(x is int); // true
print(y is int); // true
int z = y as int; // works
複製程式碼

用於宣告變數型別的只是 interface ,它可以儲存任何實現了該 interface 的子型別。

方法

當在例項上呼叫方法時,建立例項的型別始終決定了該方法的實現,例如:

class Foo {
  void hi() => print("i am foo");
}

class Bar implements Foo {
  void hi() => print("i am bar");
}

void callHi(Foo foo) => foo.hi();

void main() {
  callHi(Bar()); // prints "i am bar"
}

複製程式碼

如上述例子所示,Bar 實現的 hi 將始終覆蓋來自其例項的呼叫結果,而不管它在什麼上下文中。

在 Dart 程式碼所有可見的型別,都是 Object 的子型別,並繼承其預設實現的 interface

Dart 是強型別的語言,這意味著編譯器可以在執行時,對值的型別做出強有力的保證。

當然,強型別並不意味著方法一定存在,如果呼叫時缺少方法,Dart 會呼叫預設情況下呼叫 noSuchMethod 會丟擲 NoSuchMethodError

(42 as dynamic).foo(); // throws NoSuchMethodError
複製程式碼

例項上在 Dart 裡的所有欄位訪問,都是通過對 settergetter 方法的呼叫來完成,當在類中宣告一個欄位時,它隱式宣告瞭讀取和寫入內部變數的 settergetter 方法。

class Foo {
  // This declares both set:a and get:a
  int a;
}

class Bar extends Foo {
  // This overrides get:a without touching set:a
  int get a => super.a * 2; 
}

main() {
  var foo = Bar();
  foo.a = 2;
  print(foo.a); // prints 4
}
複製程式碼

子型別

變數可以包含不是其宣告型別的實際子型別的值,除了 null

int x;
print(x is int); // false
複製程式碼

這段程式碼會列印 false因為 is 運算子是子型別檢查,而不是可分配性檢查

而另一方面,as 操作會進行可分配性檢查

int x;
print(x as int); // null, works
複製程式碼

這是因為,在以下情況下 x 可以是 T 的子型別:

  • x 的執行時型別是 T 的子型別。
  • x 為空並且 T 可以為空。

Null vs void vs dynamic vs Object

Null 物件是特殊的,當不是 get:hashCodeget:runtimeTypeoperator== 的方法被呼叫,它丟擲一個格式為 NoSuchMethodError 的異常。

dynamicvoid 型別都是 Object 的有效別名,但它們改變了一些可見的方法:

  • 使用 Object,只能方法 Object 的介面(如普通類),例如 hashCode
  • 使用 void,可以儲存和轉換,但不能訪問任何方法。
  • 使用 dynamic,可以訪問任何方法,並使用任何引數呼叫它,這些返回值也被視為 dynamic

閉包

提取是將例項方法轉換為閉包的過程,這通常稱為 tear-off

如果在一個物件上呼叫函式並省略了括號, Dart 稱之為 ”tear-off” :一個和函式使用同樣引數的閉包,當呼叫閉包的時候會執行其中的函式,比如:names.forEach(print); 等同於 names.forEach((name){print(name);});

可以通過呼叫名稱為 getter 的方法來提取方法:

typedef ToStringFn = String Function();
ToStringFn getToString(Object x) => x.toString;
複製程式碼

在這個例子中,我們從一個任意物件中 x 中提取了 toString方法,通過閉包,就可以像呼叫上的常規例項一樣呼叫 x

typedef ToStringFn = String Function();
ToStringFn getToString(Object x) => x.toString;
main() {
  var foo = 111;
  var a = getToString(foo);
  print(a());
}
複製程式碼

實際上,上面的程式碼與以下程式碼相同,除了前者效率更高一些。

typedef ToStringFn = String Function();
ToStringFn getToString(Object x) => () => x.toString();
複製程式碼

Functions 非常特殊,它們實際上可以指兩個不同的東西:

  • 用引數和返回型別宣告的函式型別,即 void Function() foo;
  • Function 類作為介面型別,任何方法的父類。

Function 型別類似於泛型介面型別,但可以描述引數名稱和型別。

所有函式型別都是 Function 的子型別,無論它們的返回型別和引數如何:

print(print is Function); // true
複製程式碼

這裡做一個有趣的實驗,如下程式碼所示:


void main() {
  void foo() {}
  int bar([int aaa]) {}
  Null biz({int aaa}) {}
  int baz(int aa, {int aaa}) {}
  
  print(foo is void Function());
  print(bar is void Function());
  print(biz is void Function());
  print(baz is void Function());
}
複製程式碼

列印結果是

true
true
true
false
複製程式碼

這是因為 Dart 型別系統比較靈活,只要函式採用相同位置的引數,並具有相容的返回型別,它就是有效的函式子型別,所以除了 baz 列印 false 之外所有的結果都是 true

換個方式,如下程式碼所示:

void main() {
  int foo({int a}) {}
  int bar({int a, int b}) {}
  
  print(foo is int Function());
  print(foo is int Function({int a}));
  print(bar is int Function({int a}));
  print(bar is int Function({int b}));
  print(bar is int Function({int b, int c}));
}
複製程式碼

輸出的結果會是:

true
true
true
true
false
複製程式碼

因為當函式具有命名引數的子集時,程式碼檢查函式是否具有有效的子型別,所以除最後一個之外的所有函式都列印 true

如果最後一個修改為 print(bar is int Function({int b, int a})); ,也會列印出 true

可呼叫物件

類是可以被呼叫的,例如:

class Foo {
  void call() => print('hi');
}

void main() {
  Foo()(); // prints "hi"
}

複製程式碼

這實際上是一種欺騙,Foo 例項本身實際上是不可呼叫的,出現這樣的結果是因為 call 隱式提取了該方法。

例如:

void callFoo(void Function() x) {
  print(x is Foo); // false
  print(x is Function); // true
  x();
}

void main() {
  var x = Foo();
  print(x is Foo); // true
  print(x is Function); // false
  callFoo(x);
}
複製程式碼

在這裡 x 似乎是在 FooFunction 之間轉變,這是因為 x 被傳遞到 callFoo 之前,被隱式轉換成一個 Closure

相關文章