什麼是型別?
型別是用於描述例項介面的節點,如下示例和註釋所示:
// 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
包含了一堆具有特殊型別屬性的類,下面將介紹這些類。
例項
在物件的整個生命週期中,它只有一個型別,該型別在構造時確定並且永遠不能更改:
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 裡的所有欄位訪問,都是通過對 setter
和 getter
方法的呼叫來完成,當在類中宣告一個欄位時,它隱式宣告瞭讀取和寫入內部變數的 setter
和 getter
方法。
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:hashCode
,get:runtimeType
和 operator==
的方法被呼叫,它丟擲一個格式為 NoSuchMethodError
的異常。
dynamic
和 void
型別都是 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
似乎是在 Foo
和 Function
之間轉變,這是因為 x
被傳遞到 callFoo
之前,被隱式轉換成一個 Closure
。