Dart 初體驗
我們在Android Studio中建立一個main.dart文件,右鍵Run main.dart
void sayHello(String name) {
print('Hello world, I am $name');
}
main() {
String myName = 'xiaoming';
sayHello(myName);
}
可以看到控制臺會輸出
Hello world, I am xiaoming
和絕大多數編譯型語言一樣,Dart 要求以 main 函數作為執行的入口。
Dart 的變量與類型
在 Dart 中,我們可以用 var 或者具體的類型來聲明一個變量。當使用 var 定義變量時,表示類型是交由編譯器推斷決定的,這樣的話,類型推斷會耗費一些性能,可以用靜態類型去定義變量,更清楚地跟編譯器表達你的意圖,這樣編輯器和編譯器就能使用這些靜態類型,向你提供代碼補全或編譯警告的提示。
- 在默認情況下,未初始化的變量值都是 null,不用擔心JavaScript中未定義的值是undefined的情況。
- Dart 是類型安全的語言,并且所有類型都是對象類型,都繼承自頂層類型 Object,因此一切變量的值都是類的實例(即對象),甚至數字、布爾值、函數和 null 也都是繼承自 Object 的對象。
- Dart 內置了一些基本類型,如 num、bool、String、List 和 Map,在不引入其他庫的情況下可以使用它們去聲明變量。
dynamic,var,object的區別
var a = 1;
object b = 1;
dynamic c = 1;
上面的例子中,看起來三者非常相似,但是背后的原理卻是非常不同。
var本身是一個語法,并不能說是一種類型,其它的兩者object和dynamic是類型。
var聲明的變量在賦值的那一刻,就已經決定了它是什么類型。
所以如果你這樣使用,就會有編譯錯誤:
var a = 1;
a = "Test";
object之所以能夠被賦值為任意類型的原因,是因為所有的類型都派生自object. 所以它可以賦值為任何類型:
object a = 1;
a = "Test";
dynamic不是在編譯時候確定實際類型的, 而是在運行時。
所以下面的代碼是能夠通過編譯的,但是會在運行時報錯:
dynamic a = "test";
a++;
dynamic類型具有所有可能的屬性和方法。Dart語言中函數方法都有dynamic類型作為函數的返回類型,函數的參數也都有dynamic類型。
final和const
如果從未打算更改一個變量,那么使用 final 或 const,不是var,也不是一個類型。 一個 final 變量只能被設置一次,兩者區別在于:const 變量是一個編譯時常量,final變量在第一次使用時被初始化。被final或者const修飾的變量,變量類型可以省略,如:
//可以省略String這個類型聲明
final str1 = "hello world";
//final String str1 = "hello world";
const str2 = "hello world";
//const String str2 = "hello world";
函數
Dart是一門真正的面向對象的語言,所以即使是函數也是對象,并且有一個類型Function。這意味著函數可以賦值給變量或作為參數傳遞給其他函數,這是函數式編程的典型特征。
函數聲明
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
Dart函數聲明如果沒有顯式聲明返回值類型時會默認當做dynamic處理。
typedef bool CALLBACK();
//不指定返回類型,此時默認為dynamic,不是bool
isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != null;
}
void test(CALLBACK cb){
print(cb());
}
//報錯,isNoble不是bool類型
test(isNoble);
函數作為變量
var say = (str){
print(str);
};
say("hi world");
函數作為參數傳遞
void execute(var callback) {
callback();
}
execute(() => print("hello world"))
可選的位置參數
String say(String name, String msg, [String device]) {
var result = '$name says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
下面是一個不帶可選參數調用這個函數的例子:
say('xiaoming', 'hello'); //結果是: xiaoming says hello
下面是用第三個參數調用這個函數的例子:
say('xiaoming', 'hello', 'android'); //結果是:xiaoming says hello with a android
可選的命名參數
定義函數時,使用{param1, param2, …},放在參數列表的最后面,用于指定命名參數。例如:
//設置[bold]和[hidden]標志
void enableFlags({bool bold, bool hidden}) {
// ...
}
調用函數時,可以使用指定命名參數。例如:paramName: value
enableFlags(bold: true, hidden: false);
Flutter 中會大量用到可選命名參數的方式,一定要記住它的用法。
注意,不能同時使用可選的位置參數和可選的命名參數
類
Dart 是面向對象的語言,每個對象都是一個類的實例,都繼承自頂層類型 Object。在 Dart 中,實例變量與實例方法、類變量與類方法的聲明與 Java 類似,值得一提的是,Dart 中并沒有 public、protected、private 這些關鍵字,我們只要在聲明變量與方法時,在前面加上“”即可作為 private 方法使用。如果不加“”,則默認為 public。不過,“_”的限制范圍并不是類訪問級別的,而是庫訪問級別。
接下來,我們以一個具體的案例看看Dart 是如何定義和使用類的。建立一個test.dart文件。在 Calc 類中,定義了兩個成員變量 x 和 y,通過構造函數語法糖進行初始化,成員函數 printInfo 的作用是打印它們的信息;而類變量 z,則在聲明時就已經賦好了默認值 0,類函數 sum 會打印出它的信息。
class Calc {
int x, y;
static num z = 0;
Calc(this.x,this.y); // 語法糖,等同于在函數體內:this.x = x;this.y = y;
void printInfo() => print('($x, $y)');
static void sum(int a, int b) => print('result ${a + b }');
}
void main() {
Calc c = new Calc(100,200); // new 關鍵字可以省略
c.printInfo(); // 輸出 (100, 200);
Calc.z = 10;
Calc.sum(10, 20); // 輸出 result 40
}
有時候類的實例化需要根據參數提供多種初始化方式。除了可選命名參數和可選參數之外,Dart 還提供了命名構造函數的方式,使得類的實例化過程語義更清晰。
此外,與 C++ 類似,Dart 支持初始化列表。在構造函數的函數體真正執行之前,你還有機會給實例變量賦值,甚至重定向至另一個構造函數。
如下面實例所示,Calc 類中有兩個構造函數 Calc.reDirct 與 Calc,其中:Calc.reDirct將其成員變量的初始化重定向到了 Calc 中,而 Calc 則在初始化列表中為 z 賦上了默認值 0。
class Calc {
num x, y, z;
Calc(this.x, this.y) : z = 0; // 初始化變量 z
Calc.reDirct(num x) : this(x, 0); // 重定向構造函數
void printInfo() => print('($x,$y,$z)');
}
void main() {
Calc c1 = Calc(100,200); // new 關鍵字可以省略
c1.printInfo(); // 輸出 (100,200,0)
Calc c2 = Calc.reDirct(300);
c2.printInfo(); // 輸出 (300,0,0)
}
復用
在面向對象的編程語言中,將其他類的變量與方法納入本類中進行復用的方式一般有兩種:繼承父類和接口實現。當然,在 Dart 也不例外。
在 Dart 中,你可以對同一個父類進行繼承或接口實現:
- 繼承父類意味著,子類由父類派生,會自動獲取父類的成員變量和方法實現,子類可以根據需要覆寫構造函數及父類方法;
- 接口實現則意味著,子類獲取到的僅僅是接口的成員變量符號和方法符號,需要重新實現成員變量,以及方法的聲明和初始化,否則編譯器會報錯
class Parent {
num x = 0, y = 0;
void printInfo() => print('($x,$y)');
}
//Children1 繼承自 Parent
class Children1 extends Parent {
num z = 0;
@override
void printInfo() => print('($x,$y,$z)'); // 覆寫了 printInfo 實現
}
//Children2 是對 Parent 的接口實現
class Children2 implements Parent {
num x = 0, y = 0; // 成員變量需要重新聲明
void printInfo() => print('($x,$y)'); // 成員函數需要重新聲明實現
}
void main() {
var xxx = Children1();
xxx
..x = 1
..y = 2
..z = 3; // 級聯運算符,等同于 xxx.x=1; xxx.y=2;xxx.z=3;
xxx.printInfo(); // 輸出 (1,2,3)
var yyy = Children2();
yyy
..x = 1
..y = 2; // 級聯運算符,等同于 yyy.x=1; yyy.y=2;
yyy.printInfo(); // 輸出 (1,2)
print(yyy is Parent); //true
print(yyy is Children2); //true
}
可以看出,子類 Children2采用接口實現的方式,僅僅是獲取到了父類Parent 的一個“空殼子”,只能從語義層面當成接口 Parent 來用,但并不能復用 Parent 的原有實現。那么,我們是否能夠找到方法去復用 Parent 的對應方法實現呢?也許你很快就想到了,可以讓 Children2 繼承 Point,來復用其對應的方法。但如果 Children2 還有其他的父類,我們又該如何處理呢?
除了繼承和接口實現之外,Dart 還提供了另一種機制來實現類的復用,即“混入”(Mixin)。混入鼓勵代碼重用,可以被視為具有實現方法的接口。這樣一來,不僅可以解決 Dart 缺少對多重繼承的支持問題,還能夠避免由于多重繼承可能導致的歧義(菱形問題)。
菱形問題,是支持多繼承的編程語言中一個相當棘手的問題。當 B 類和 C 類繼承自 A 類,而 D 類繼承自 B 類和 C 類時會產生歧義。如果 A 中有一個方法在 B 和 C 中已經覆寫,而 D 沒有覆寫它,那么 D 繼承的方法的版本是 B 類,還是 C 類的呢?
要使用混入,只需要 with 關鍵字即可
class Children2 with Parent {
}
var yyy = Children2();
print (yyy is Parent); //true
print(yyy is Children2); //true
運算符
Dart 和絕大部分編程語言的運算符一樣,你可以用熟悉的方式去執行程序代碼運算。不過,Dart 多了幾個額外的運算符,用于簡化處理變量實例缺失(即 null)的情況。
- ?.運算符:p?.printInfo()表示 p 為 null 的時候跳過,避免拋出異常。
- ??= 運算符:a ??= value 表示,如果 a 為 null,則給 a 賦值 value,否則跳過。
- ??運算符:a ?? b表示如果 a 不為 null,返回 a 的值,否則返回 b。在 Java 中,我們需要通過三元表達式 (a != null)? a : b 來實現這種情況。