flutter不完全指南系列--(二)dart詳解(前端角度的兩萬字解析)

shotCat發表於2019-10-22

dart語言詳述

這部分內容量很多,文字加程式碼接近三萬了。所以強烈建議收藏儲存。

這篇文章涵蓋了flutter會遇到的絕大部分關於dart的知識點,由於dart是參考了大量java和c++及部分JavaScript語言,所以純前端學起來剛開始還是有一點吃力的,這篇文章就是為了解決這個痛點,站在一個純前端的角度進行詳細解讀。

吐槽:

它和那些你能找到的所有所謂“關於dart詳解”都要詳細。其實它們大多跟風copy的是dart網站論壇好幾年前的帖子(這種copy文章居然在掘金也能拿到幾百贊??),並且絕大多數關於dart文章都會忽視java部分,dart很多地方和java類似,包括很多關鍵字。這些文章都理所當然地認為所有讀者都知道,都有java基礎。雖然這些小地方,不懂都可以百度查出來,甚至猜出來。但既然選擇了寫一篇文章,幹嘛還要在這種小地方偷懶(包括很多書)。要麼不做,要做就要盡力做好。

重要:

  • 1,如果對我的解釋還有疑惑的可以檢視官中的文件:傳送門 官方文件必收藏
  • 2,如果想了解更多關於dart的屬性和方法可以檢視官中的說明·:傳送門 也是必收藏,例如:dart對陣列list有哪些方法?

重要概念

在學習Dart之前,首先得有這些意識:

  • 所有的東西都是物件,無論是變數、數字、函式等都是物件。 所有的物件都是類的例項。 所有的物件都繼承自內建的 Object類。這點類似於 Java語言“一切皆為物件”。
  • dart可以指定資料型別(並不是強制性的),方便進行語法檢查,使得程式可以合理分配記憶體空間。
  • 這條可能是在前期最容易出錯的,就是Dart會自動對變數進行型別推斷,所以不能像JS裡那樣自由的改變或新增其他資料型別! 詳見:傳送門
  • Dart 在執行之前會先解析你的程式碼。 指定資料型別和編譯時的常量, 可以提高執行速度。
  • Dart程式有統一的程式人口: main()。 這一點與 Java、 C++語言相像。
  • Dart 沒有 public、 protected 和 private 的概念 。 私有特性通過變數或函式加上下劃線_來表示 。
  • Dart 的工具可以檢查出警告資訊( warning)和錯誤資訊( errors)。 警告資訊只是表明程式碼可能不工作,但是不會妨礙程式執行 。 錯誤資訊可以是編譯時的錯誤,也可能是執行時的錯誤 。 編譯時的錯誤將阻止程式執行,執行時的錯誤將會以異常 (exception)的方式呈現 。
  • Dart 支援 anync/await 非同步處理 。
  • Dart必須用分號來結束語句, 不加就會報錯。(對於js基本不寫分號的我,真的很容易忘)
  • 在Dart裡,你不能像JS裡通過字面量宣告物件var a = {b:123}。需要通過class產生。
  • dart關鍵字如下: abstract,do, import, super, as, dynamic, in , switch, assert, else, interface, sync*, enum, implements, is, this, async*, export, library, throw, await, external, mixin, true, break ,extends, new, try, case,factory, null, typedef, catch, false, operator, var, class, final, part, void, const, finally, rethrow , while , continue , for , return , with , covariant , get , set , yield*, default, if , static, deferred。

dart使用評率最高的三個庫:

  • dart:core: 核心庫,包括 strings、 numbers、 collections、 errors、 dates、 URis 等 。
  • dart:html: 網頁開發裡 DOM 相關的一些庫 。
  • dart:io: I/O命令列使用的I/O庫。

更詳細的可以檢視這裡:傳送門

PS:

  • dart:core庫是 Dart語言初始已經包含的庫,其他的任何庫在使用前都需要加上 import 語句。例如,使用dart:html可以使用如下的命令: import 'dart:html';
  • 使用官方提供的 pub工具可以安裝豐富的第三方庫。 第三方庫的地址為 : pub.dartlang.org(很慢,建議搭梯子)

變數與基本資料型別

變數

在 Dart語言裡一切皆為物件如果沒有將變數賦值,那麼它的預設值為 null。

以下程式碼是Dart中定義變數的方法:

void main() {
  var a = 110;
  dynamic b = 1.8;
  Object c = 666;
  final d = '只能被賦值一次';
  const e = '只能是常量';
  int f = 120;
  String g = "shotCat";
}
複製程式碼
  • 1, var

var:

dart中的var和JavaScript中的var看起來一樣,但是它們的差別還是很大的。首先dart中的var宣告的變數一旦賦值,則其資料型別也會一併確定(dart比較智慧,可以通過第一次的賦值來確定變數的資料型別)

var name = 'shotCat';
name = 8848; // 此時會報錯,因為name已經被賦值為字串型別
複製程式碼
  • 2, dynamic,和Object(注意O為大寫) dynamic:

dynamic從它的字面意思就可以知道它是動態的,所以dynamic賦值的變數,後面可以賦值為其他資料型別。並且dynamic宣告的物件編譯器會提供所有可能的組合。什麼意思?解釋一下,前面說過var宣告的變數,dart可以智慧地推斷出它的型別。dynamic宣告的變數,dart也會去推斷其資料型別,只不過是dart不會限定變數的資料型別。而是動態地進行推斷,並提供對應資料型別的屬性和方法。

dynamic name;
name = "shotCat";
name = 8848;//可以進行修改宣告型別,不會報錯
print(name);//輸出為:8848
複製程式碼

Object:

Object 是Dart所有物件的根基類,也就是說所有型別都是Object的子類(包括Function和Null),所以任何型別的資料都可以賦值給Object宣告的物件.

Object和dynamic一樣,它們宣告的變數後面可以賦值為其他資料型別。但與dynamic不同的是:Object宣告的變數預設只能使用Object型別的屬性和方法。為什麼?因為使用Object宣告的變數,編譯器是不會推斷其具體資料型別(Object的某個子類),因此預設該變數只能呼叫Object的屬性和方法, 需要後面手動轉換為具體型別來進行操作。

Object name = "shotCat";
print(name.length);//這裡是object型別就不能呼叫length方法,編譯就報錯
複製程式碼
  • 3, final和const

final:

如果你想宣告一個變數後,不再被改變。那麼就可以使用final。final宣告的變數只能被賦值一次!不能再去修改,否則就會報錯!

final name = "shotCat";
name = "吳彥祖比我帥";//這裡很顯然是會報錯的
複製程式碼

const:

和JS中的const類似,在dart中const宣告的變數為一個編譯時常量,並且在宣告時,就必須進行賦值為一個編譯時常量!同樣的const宣告變數以後,如果再去修改,編譯器直接就報錯

使用關鍵字 const 修飾變數表示該變數為 編譯時常量。如果使用 const 修飾類中的變數,則必須加上 static 關鍵字,即 static const(注意:順序不能顛倒)。在宣告 const 變數時可以直接為其賦值,也可以使用其它的 const 變數為其賦值

注意:const不僅可以宣告變數,而且還能修飾變數,修飾之後的變數也會變為固定值。

const name = "shotCat";
name = "彭于晏比我帥";//很顯然這句話是會報錯的!
var list =  const [1, 2, 3];//const還可以修飾變數對應的值,強制讓這個值也變成固定的
複製程式碼

final 與 const的不同點:

  • const是編譯時常量,const修飾的值在編譯(非執行)的時候就需要進行確定,即如果你在編寫的時候,const後面接的是一個變數,則會報錯。
  • final是執行時常量,它是惰性初始化,即在第一次執行時才會進行初始化。即如果final 後面接的是一個變數,執行時也不會報錯。

舉個栗子;

void main() {
var a = 123456;
// const b =  a; 使用const時則會報錯
final b = a; // final則不會
print(b);
複製程式碼
  • 4,明確需要變數的型別 如果事先已經明確某個變數的型別,則可以直接用int,bool,String等進行宣告,當然宣告的變數在後面也是不能改變其資料型別的。也可以用var或 dynamic來宣告一個未明確型別的變數,Dart會自動推斷其資料型別。

基本資料型別

Dart有如下幾種內建的資料型別:numbers,strings,boolean,lists(或者是arrays),maps,runes(UTF-32字符集的字元),symbols

這裡只介紹常用的Number、 String、 Boolean、 List、 Map幾種常見型別。

  • Number型別:
    • int整形。取值範圍: -2^53 到 2^53
    • double浮點型。即雙精度浮點型 ,64 位長度的浮點型資料

int和 double型別都是 Number 型別的子類。 int型別不能包含小數點。 num型別包括的操作有:+,-,*,/以及位移操作>>。 num 型別包括的常用方法有: abs、 ceil 和 floor。

void main() {
  // numbers
  var a = 0;
  int b = 1;
  double c = 0.1;
}
複製程式碼
  • String型別: 字串型別和JS裡的差不多。也可以通過+進行拼接。不同的是dart裡可以使用三個單引號或三個雙引號來表示多行文字。

dart中還支援一種類似於模板字串的寫法。${expression},如果表示式是一個識別符號,可以省略 {}。 如果表示式的結果為一個物件,則 Dart 會呼叫物件的 toString() 函式來獲取一個字串。

void main() {
  // strings
  var name = 'shotCat';
  String name = "shotCat";
  
  var foo ='''
	一個用三個單引號包裹起來的字串 , 
	可以用來表示多行資料。
	''';
	var bar = """同樣一個用三個雙引號包裹起來的字串 , 
	也可以用來表示多行資料。
	""";
	
	// 也可以用${expression}的寫法
	String nickName = "武漢彭于晏";
	print("沒錯,我就是 $nickName");
	
	// 也可以呼叫對應string的方法
	String nickName1 = "wuhan pengyuyan";
	print("沒錯,我就是 ${nickName1.toUpperCase()}");
}
複製程式碼

字串和數字之間的轉換:

// String -> int
var one = int.parse('1');
assert(one == 1);

// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1);

// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1');

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
assert(piAsString == '3.14');
複製程式碼

說明:assert其實就是斷言,後面會有詳解,這裡理解為判斷是否相等,並且dart中相等就是==,沒有===這個說法。

  • Boolean型別: dart是強bool型別檢查,只有bool型別的值才會被判斷為true或false。 這點是和JS有很大不同的,不會把undefined,0等這樣的值判斷為false。
void main() {
  // booleans
  var real = true;
  bool unReal = false;
  
  var name = '吳彥祖';
  // 下面這段是不能正常編譯的,因為name是字串,而不是boolean,不能這樣進行判斷
  if (name) {
  	print('我的名字是' + name);
  }
}
複製程式碼
  • List型別: dart中將具有一系列相同型別的資料稱為 List 物件。 非常類似JS語言的陣列 Array物件。
void main() {
  // list
  var list1 = [1, 2, 3, 4, 5]; //這種寫法會被dart判斷為int型,後面add新增非int型別資料時,就會報錯
  list1.add('shotCat');//這時編譯就會報錯
  
  // 你也可以通過下面這種形式,直接指明你想要的型別
  List<String> list2 = ['shotCat', '吳彥祖', "彭于晏"];
  List<dynamic> list3 = [10086, true, '傻**'];//這種也是可以新增其他型別
  
  //如果你想list可以新增不同型別的值,可以使用下面的形式
  List list4 = new List();
  list4.add("shotCat");
  list4.add(1);
  list4.add(true);
  print(list4); //列印出 [aa, 1, true] ,這時因為new List()dart會預設list可以新增的型別為Object這個基類,所以後面的不同型別都可以新增進去
  
  var list5 = ['shotCat', 10086];
  list5.add(true);
  print(list5);// [shotCat, 10086, true] 這樣也是可以的
}
複製程式碼

並且list還引入了和js類似的擴充運算子...(dart 2.3以上版本)

var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
複製程式碼
  • Map型別: Map其實和JS裡的挺像的,用來關聯 keys 和 values 的物件。 keys 和 values 可以是任何型別的物件。並且在一個 Map 物件中一個 key 只能出現一次。 但是 value 可以出現多次。
void main() {
  // 可以直接使用字面量的形式,就像寫JS物件一樣
  // 需要注意的是當map的key或value為同一種型別時,dart就會型別推斷為Map<String, String>
  var shotCat = {
  'name': 'shotCat',
  'age': '18',
  'sec': 'man',
  'like': 'girl',
 };
 shotCat['luckyNumber'] = 666;
 // 這種情況就會報錯,因為此時dart的型別推斷為Map<String, String>,666很顯然不是string型別
 
 // 也可以用Map生成,然後自己新增
 var girlFriends = new Map();// 這裡的new關鍵字在dart2中可以省略
girlFriends['first'] = 'Taylor Swift';
girlFriends['second'] = '石原里美';
girlFriends['fifth'] = '新垣結衣';
girlFriends[4] = '迪麗熱巴';
girlFriends['我列舉的這些都是'] = true;
// 這種寫法就不會對型別進行限制

// 當然這樣寫之後,dart也不會再進行型別限制
var foo = {
  'name': 'shotCat',
   18: true,
 };
}
複製程式碼

函式

dart中函式也是物件,它的型別是Function。它可以被賦值給變數或者作為其他函式的引數。

dart函式的寫法與es6中的寫法很像

函式寫法

// 常規寫法就是這樣,不需要加function,String為宣告函式返回值型別,其實也可以省略,函式照樣正常執行
String getName() {
  return "shotCat";
}

// 如果函式體只有一個表示式可以進行簡寫
String getName1() => "shotCat"; // 切記!!不要與箭頭函式混淆,給函式體加上{} dart不支援這種寫法!

// 匿名函式寫法
var getName2 = (name) {return 'Hello $name!';};

// 閉包寫法其實也是類似的
  Function getPlus() {
    int num = 0;
    return () {
      num++;
      print(num);
    };
  }

  Function foo = getPlus();
  foo(); // 1
  foo(); // 2
複製程式碼

可選引數與預設引數值

dart支援兩種可選引數的寫法:一種叫命名可選引數,另一種叫位置可選引數。在一個函式中這兩種可選方式不能同時使用。當然不屬於這兩種寫法的就是必選引數。並且沒有賦值的引數,dart都會預設設為null。

  • 命名可選引數使用大括號{}。預設值使用等號=(老版使用的是冒號:,現在不建議繼續使用)。由名字和{}可知,命名可選引數的呼叫與順序無關,需要指明引數名才可以呼叫。
  • 位置可選引數使用中括號[]。預設值使用等號=。由名字和[]可知,位置可選引數與順序有關,呼叫的時候也是按照順序呼叫。

注意: 雖然命名可選引數是可選引數,即我們可以選擇不傳。但dart還是提供了@required標記。foo({int age, @required String name}){...}此時引數name就是必選引數了。這種寫法在flutter中很常見,其實其他語言裡也很常見。

// 這裡使用命名可選引數寫法,其中a很顯然就是必選引數
foo (a, {b, int c=1, bool d=true}){
  print('$a $b $c $d');
}

// 這裡使用位置可選引數寫法,其中a很顯然也是必選引數
bar (a, [b, int c=2, bool d=false]){
  print('$a $b $c $d');
}

void main()
{
  // 命名可選引數的呼叫需要`:`來進行宣告
  foo('shot', c:666, d:false); //結果為 shot null 666 false
  bar('cat', 8848, 6666, true); //結果為 cat 8848 6666 true
}
複製程式碼

函式返回值

所有函式都會返回一個值。 如果沒有明確指定返回值, 函式體會被隱式的新增 return null; 語句。

foo() {}
assert(foo() == null);

//這裡順帶說一句關於void,如果只接觸JS沒接觸其他語言的同學,可能會奇怪void的意思,其實void就是宣告沒有返回值,如果一個函式前面加上void則表明該函式沒有返回值。如果你在一個有指定return的函式前加void則會報錯

// 這樣寫就會報錯
void getName (name) {return'Hello $name!';};
複製程式碼

main()函式

任何應用都必須有一個頂級 main() 函式,作為應用服務的入口。 main() 函式返回值為空,引數為一個可選的 List 。

void main() {
  runApp(MyApp());
}
複製程式碼

運算子

Dart中使用到的運算子如下表格

Description Operator
一元后綴 expr++ expr-- () [] . ?.
一元字首 -expr !expr ~expr ++expr --expr
乘除操作 * / % ~/
加減操作 + -
移位 << >>
按位與 &
按位異或 ^
按位或 |
比較關係和型別判斷 >= > <= < as is is!
等判斷 == !=
邏輯與 &&
邏輯或 ||
是否null ??
條件語句操作 expr1 ? expr2 : expr3
級聯操作 ..
分配賦值操作 = *= /= ~/= %= += -= <<= >>= &= ^= |= ??=

操作符的優先順序由上到下逐個減小!

這裡列舉一些與JS不一樣,需要注意的地方:

  • 1,首先是最重要的,dart判斷兩個物件是否相等,使用的是==沒有=== 。同理,不等於也只有!=,沒有!==
  • 2, 型別判定運算子:as, is, 和 is! 運算子用於在執行時處理型別檢查。
    • as 運算子會將物件強制轉換為特定型別。當然如果不能轉換就會報錯。
    • is 運算子:當物件是相應型別時返回 true。
    • is! 運算子:當物件不是相應型別時返回 true。(注意這裡!在is後面)。
    if (user is User) {
    	// 型別檢測
    	user.name =’Flutter’;
    }
    //如果能確定user是User的例項, 則你可以通過as直接簡化程式碼: 
    (user as User) .name =’Flutter’;
    複製程式碼
  • 3,??=運算子:正常來說,我們的運算子就是=,但是如果我們想在變數不存在為null時才進行賦值的話則可以使用??= 例子:a ??= 666;表示當a變數為null時,它才會被賦值為666
  • 4, 重要: ??與?.運算子: 為什麼會有這兩個運算子?在JS中我們經常會這樣寫a || b 和 c && d但是在dart中,這樣寫是有前提的,雖然dart也有|| &&運算子,但是要求的是兩邊必須為明確的boolean值(true和false)。 即為undefined,null,0這些都不算。我在前面基本資料型別裡也講過這點,這裡再強調下。
    • ??運算子 表示前面的值如果為null或空的話就取??後面的值。正好對應的就是|| 例子:myName = name ?? 'shotCat';如果name變數不存在,則取值'shotCat'
    • ?.運算子表示前面的值如果不為null或空的話就取?.後面的值。正好對應的就是&& 例子:myName = name ?. myName;如果name變數存在,則返回name.myName。如下例:
    void main(){
       var name = new Name();
       var myName = name?.myName;
    //如果name存在則返回name.myName
       print(myName); //得到結果 彭于晏
    } 
    class Name { 
    String myName = '彭于晏'; 
    }
    複製程式碼
  • 5,.. 級聯運算子: 可以實現對同一個對像進行一系列的操作。 除了呼叫函式, 還可以訪問同一物件上的欄位屬性。 這通常可以節省建立臨時變數的步驟, 同時編寫出更流暢的程式碼。
    • 這裡直接引用官網的例子,就不再舉自己是彭于晏的例子了(狗頭)
    final addressBook = (AddressBookBuilder()
      ..name = '彭于晏'
      ..email = 'pengyuyan@shotCat.com'
      ..phone = (PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();
    // AddressBookBuilder() 獲取了一個物件,之後設定該物件name屬性、email屬性並且在設定phone屬性時又**巢狀使用了級聯運算子**,獲取了PhoneNumberBuilder()物件。
    
    // 需要注意一點的是:在使用級聯運算子時,要確保該物件存在,例如:如果後端返回的資料不存在,使用了就會報錯
    複製程式碼

控制流程語句

dart中的流程語句有以下幾個:

  • if and else
  • for loops
  • while and do-while loops
  • break and continue
  • switch and case
  • assert

if和else

基本和JS裡的if else一樣。不過有一點需要注意:也是前面提到過的,dart裡條件判斷必須是布林值來判斷!!! 例如,let a = 0;if(!a) console.log('a')這種條件語句在dart裡是錯誤的!~

for 迴圈

for迴圈也和JS裡的很像,不過有一點不同。既然dart是比JS更好,那當然是會糾正JS裡的陷阱。

舉個栗子:

// 在JS中輸入以下程式碼 得到的是兩個2
var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.push(() => console.log(i));
}
callbacks.forEach((c) => c());

// 在dart中輸入以下等價的程式碼,卻可以得到我們所期望的0和1
var callbacks = [];
for (var i = 0; i < 2; i++) {
  callbacks.add(() => print(i));
}
callbacks.forEach((c) => c());
複製程式碼

從上面的例子可以看出,它修復了JS的作用域的陷阱。即for迴圈中的變數i的作用域就只在for迴圈體內部可以訪問。 當然你也可以把它當成是let

注意: dart還提供了forEach方法和for-in,可以用於遍歷具有迭代屬性的物件,比如List和Set

var lists = [1, 2, 3];//lists是一個可迭代的物件
  for(var item in lists){//可以用for in迴圈進行遍歷
    print(item);//列印'1 2 3'
  }
複製程式碼

while 和 do-while

while是先判斷條件再決定是否執行迴圈體,而do-while則是先執行一次迴圈體,然後結合條件判斷決定是否繼續執行下一次迴圈體

//定義了一個整型變數i,其值為2
  int i = 2;
//while迴圈遍歷,這裡顯然i小於3,所以while迴圈體會執行
  while (i < 3) {
    print(i++);
  }
//do-while迴圈,因為先執行一次while迴圈體,
//然後再判斷i是否小於2,所以只會列印 2 之後就不會執行執行體了,因為不小於2了
  do {
    print(i++);
  } while (i < 2);
複製程式碼

break 和 continue

和JS裡的類似,break用於跳出當前迴圈體,而continue則是跳過迴圈體的當前執行,繼續執行下一次迴圈。

var arr= [0, 1, 2, 3, 4, 5, 6]; 
for (var v in arr) {
	if(v == 2 ){
		//break; //切換為break後就只能列印0,1
		continue;
	}
	print(v);
}
複製程式碼

switch和case

在 Dart 中 switch 語句使用 == 比較整數,字串,或者編譯時常量。 比較的物件必須都是同一個類的例項(並且不可以是子類), 類必須沒有對 == 重寫。 列舉型別 可以用於 switch 語句。每一個 非空的 case 子句最後都必須跟上 break 語句 。

void main() {
  String name = "吳彥祖";
  switch (name) {
    case "吳彥祖":
      print("吳彥祖");
	  break;	//注意這裡!!!,若沒有使用break,dart編譯器會報錯!!!
    case "彭于晏":
      print("彭于晏");
      break;
    default:
      print("shotCat");
  }
}
複製程式碼

assert

assert即斷言,是很多語言都提供的一種檢測機制,它是用來判斷是否符合執行邏輯的語句,當 assert 判斷的 條件為 false 時發生中斷。

assert 判斷的條件是任何可以轉化為 boolean型別的物件,即使是函式也可以。 如果 assert的判斷為 true,則繼續執行下面的語句;反之則會丟擲一個斷言錯誤異常 AssertionError。

// 確認變數值不為空。
assert(text != null);
複製程式碼

注意: 斷言預設只在開發環境中生效,釋出到生產環境中的斷言是會被忽略的。因此,斷言實際上起到了在開發環境除錯程式、測試程式的目的。並且 Flutter 中的 assert 只在 debug 模式 中有效。

物件導向之類

dart是一個物件導向的語言,當然是支援繼承的。但是它的機制和Java,c++相似,卻不一樣。它既不是Java那這種單繼承,也不是c++那樣的多繼承。

Dart 裡的繼承是一種基於類和 mixin 繼承機制。 每個物件都是一個類的例項,所有的類都繼承於 Object. 。 並且支援基於 Mixin 繼承方式(mixin的繼承方式可以簡單理解為 : 一個類可以繼承自多個父類)。

先上栗子:

class Person {
  String name; //例項變數
  Person(String name) {    //構造方法
    this.name = name;//注意this關鍵字
  }
  // 上面的構造方法可以省略為
  // Person(this.name)
}

// 在定義類的時候,如果沒有提供構造方法,同樣會由系統提供一個預設的無參構造方法。
class Girl {
  String name; //例項變數
}

void main() {
  Person shotCat = new Person("彭于晏");//生成一個Person類例項
  print(shotCat.name);
  shotCat = Person("吳彥祖");//dart2 支援省略new關鍵字,這裡新產生了一個Person類例項
  print(shotCat.name);
  shotCat.name = "胡歌";//我們也可以通過例項變數完成賦值
  print(shotCat.name);
  
  var myWife = new Girl();
  myWife.name = "新垣結衣";
  print(myWife.name);
}

複製程式碼

dart定義類的關鍵字是class,而且在定義類的時候,如果沒有提供構造方法,同樣會由系統提供一個預設的無參構造方法。 當我們提供了有參構造方法的時候,無參構造方法將會自動失效,除非我們又顯示定義了其無參構造方法。

類的組成

如果對前面的例子,不太熟悉。這裡我們就先概括的來介紹dart中類的組成以及相關作用。這對後面類的理解會起到很好的幫助作用。

類的組成:

  • 建構函式:首先就是它的建構函式。類的作用是什麼?最直觀的就是例項物件,那類的建構函式就是起到這個作用。
    • 主建構函式:就是宣告一個與類名一樣的函式。例如:一個類class Shotcat。在它裡面主建構函式就是Shotcat () {...}。用它例項一個物件:var cat1 = Shotcat("吳彥祖")。從dart2開始,例項物件可以省略關鍵字 new 。並且主建構函式只能有一個!
    • 命名建構函式: 就是宣告一個“類名.識別符號 形式”的函式。例如:一個類class Shotcat。在它的命名建構函式可以是Shotcat.wife () {...}。用它例項一個物件:var cat2 = Shotcat.wife("新垣結衣")。命名建構函式可以有多個。
  • 例項變數和例項方法:顧名思義就是給例項用的變數和方法,包括其子類的例項也可以訪問!
    • 例項變數:給例項使用的變數。一般僅在類中進行宣告,然後在建構函式或者例項方法中進行賦值初始化。
    • 例項方法:給例項使用的方法。物件的例項方法可以訪問例項變數和 this。
  • 類變數和類方法:同理,既然有了例項的變數方法,當然也有類的變數方法。不過我們一般是稱呼為靜態變數和靜態方法。形式就是使用關鍵字 static 宣告類的變數和方法。記住它們是不能被例項訪問到的!
    • 類變數(靜態變數):常用於宣告類範圍內所屬的狀態變數和常量。並且在其首次被使用的時候才被初始化。
    • 類方法(靜態方法):不能被一個類的例項訪問,同樣地,靜態方法內也不可以使用 this。

補充:class的型別宣告:

感覺這個知識點插這裡其實不太好,但想想也沒其他地方。

假設我們有一個類Father,那麼Father son() {...};的意思就是son返回的值型別必須和Father的返回值型別一樣,或者是Father的子類。

建構函式

前面剛剛講到了建構函式分為兩種:主建構函式和命名建構函式。這裡就詳細展開:

  • 主建構函式:
// 養成好習慣,類的命名 首字母記得大寫
class Shotcat {
  // 宣告例項變數 並且對於多個相同型別的變數,可以像這樣寫在一行
  String name, wife;

  Shotcat(String name, String wife) {
  	// 此時的this指向的是當前例項物件  並不是類
    this.name = name;
    this.wife = wife;
  }
   // 對於上面這樣的建構函式,Dart 提供了一種特殊的語法糖來簡化該步驟
  Shotcat(this.name, this.wife);
  
}
複製程式碼
  • 命名建構函式:可以為一個類宣告多個命名式建構函式來表達更明確的意圖
class Shotcat {
  String name; //例項變數
  Shotcat(String name) {//主構造方法
    this.name = name;
  }
//這個就是命名構造方法
  Shotcat.name(String name){
    this.name = name;
  }
}
複製程式碼
  • 預設建構函式:如果你沒有宣告建構函式,那麼 Dart 會自動生成一個無引數的建構函式(即建構函式名後面沒有加.引數,例如:Shotcat後面沒有加.name)並且該建構函式會呼叫其父類的無引數構造方法。

注意:主建構函式和預設建構函式雖然都是無引數構造方法,但兩者不一樣。只有當你沒有宣告建構函式時,預設建構函式才會起效。

  • 子類呼叫父類的非命名建構函式:注意:這裡是呼叫不是繼承!!!子類必須宣告一個自己的建構函式。
class Father {
  Father() {
    print("I am your father");
  }
}
//子類Son,的例項 會先呼叫Father的建構函式,然後再呼叫Son的建構函式
class Son extends Father {
  Son(){
    print("hello Father");
  }
}

void main() {
  Student you = new Son(); // 狡黠一笑
  // 列印結果是
  // I am your father
  // hello Father
}
複製程式碼

注意: 如果父類沒有匿名無引數建構函式,那麼子類必須呼叫父類的其中一個建構函式,為子類的建構函式指定一個父類的建構函式只需在建構函式體前使用(:)指定,並使用super關鍵字。

class Father {
  Father.name() {
    print("I am shotcat");
  }
}

class Son extends Father {
  Son():super.name(){ //通過super完成了對父類命名構造方法的呼叫
  // 這裡先執行父類的命名建構函式,再執行這裡子類的建構函式
    print("hello Father");
  }
}

void main() {
  Student you = new Son(); // 狡黠一笑
  // 列印結果是
  // I am your father
  // hello Father
}
複製程式碼
  • 初始化列表:上面講到子類會先呼叫父類的非命名建構函式,再呼叫自己的。但是,如果子類建構函式還有一個初始化列表。則順序變為:
  1. 初始化列表

  2. 父類的無引數建構函式

  3. 當前類的建構函式

PS: 如果例項變數在宣告的時候就將其初始化,那麼它的順序將比上面三個都早!

初始化列表可以在呼叫父類建構函式之前初始化例項變數。

class Shotcat {
  String name; 
  int age;
  Shotcat()
      :name = "彭于晏",//這就是初始化列表
        age = 18 {
    print("I am your father");
  }
}
複製程式碼
  • 常量建構函式:如果類生成的物件都是不會變的,那麼可以在生成這些物件時就將其變為編譯時常量。你可以在類的建構函式前加上 const 關鍵字並確保所有例項變數均為 final 來實現該功能。

兩個使用相同建構函式相同引數值構造的例項是同一個物件!

class Shotcat {
  final String name; //name必須宣告為final的
  const Shotcat(this.name); //定義了一個常量構造方法,**注意不允許有body**
}

void main() {
  Shotcat s1 = const Shotcat("彭于晏"); 
  Shotcat s2 = const Shotcat("彭于晏");
  print(identical(s1, s2));//列印true
}
複製程式碼

如果 Const 變數是類級別的,需要標記為 static const。

例項變數和例項方法

  • 例項變數:就是類的例項物件使用的變數
class Point {
  num x; // 宣告例項變數 x 並初始化為 null。
  num y; // 宣告例項變數 y 並初始化為 null。
  num z = 0; // 宣告例項變數 z 並初始化為 0。
}
複製程式碼

所有未初始化的例項變數其值均為 null。

如果你在宣告一個例項變數的時候就將其初始化(而不是在建構函式或其它方法中),那麼該例項變數的值就會在物件例項建立的時候被設定,該過程會在建構函式以及它的初始化器列表執行前。

  • 例項方法 :類的例項物件使用的方法

物件的例項方法可以訪問例項變數和 this。

import 'dart:math';

class Point {
  num x, y;

  Point(this.x, this.y);

  num distanceTo(Point other) {
    var dx = x - other.x;
    var dy = y - other.y;
    return sqrt(dx * dx + dy * dy);
  }
}
複製程式碼

靜態變數和靜態方法

  • 靜態變數:類使用的變數,用static關鍵字進行宣告
class Shotcat {
  static const age = 18;
  static const girlfriend = "新垣結衣";
}
複製程式碼

靜態變數在其首次被使用的時候才被初始化。

  • 靜態方法:類使用的方法,並且方法內不可以使用this。同樣用static關鍵字進行宣告。

這裡以官方的例子進行說明:

import 'dart:math';

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

  static num distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}

void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}
複製程式碼

類的在擴充

其實上面的例子都用到過,就是我們可以使用extends關鍵字來建立一個繼承父類的子類。並可以在子類裡面使用super關鍵字來引用其父類。

這裡我繼續以前面的例子為例:

class Father {
  Father.name() {
    print("I am shotcat");
  }
}

// 子類Son 通過extends建立一個繼承父類Father的子類
class Son extends Father {
  Son():super.name(){ //通過super完成了對父類命名構造方法的呼叫
  // 這裡先執行父類的命名建構函式,再執行這裡子類的建構函式
    print("hello Father");
  }
}

void main() {
  Student you = new Son(); // 狡黠一笑
  // 列印結果是
  // I am your father
  // hello Father
}
複製程式碼
  • 在dart中你可以使用@override 表示重寫某個方法或某個類
class Son extends Father {
	@override
	String getName() {...}
  //...
}
複製程式碼

列舉型別

列舉型別是一種特殊的類,也稱為 enumerations 或 enums。通常用來表示相同型別的一組常量值。 每個列舉型別都用於一個 index 的 getter,用來標記元素的元素位置 。 第一個列舉元素的索引是 0。

使用enum關鍵字來定義列舉型別:

enum website { github, google, juejin }

//使用values 方法獲取一個包含所有列舉值的列表:
List<website> websites = website.values;

void main() {
  print(websites); // [website.github, website.google, website.juejin]
}
複製程式碼

注意:

  • 列舉不能成為子類,也不可以 mix in,你也不可以實現一個列舉。

  • 不能顯式地例項化一個列舉類。

Mixins混入

Mixins (混入功能)相當於多繼承,也就是說可以繼承多個類 。 使用 with關鍵字並在其後跟上 Mixin 類的名字來使用 Mixin 模式:

class C {
	c() {print("c語言");}
}

class Java {
	java() {print("java語言");}
}

class JavaScript {
	javascript() {print("javascript語言");}
}

// 當你需要mixin多個類時,後面只需加上逗號連線
class Dart extends Java with C, JavaScript {
	Dart() {
		print("Dart借鑑了:");
	}
}
void main() {
  Dart dart = new Dart();
  // dart 通過mixin繼承了C,JavaScript類 可以使用其中的方法
  dart.c(); 
  dart.java();  
  dart.javascript();
  // 列印結果為:
  //Dart借鑑了:
  //c語言
  //java語言
  //javascript語言
}
複製程式碼

泛型

在dart中,泛型可以簡單理解為 將資料進行限定。例如:限定陣列中的每一項只能為string;限定引數只能為int等等。就是將資料進行廣泛的限定。譬如:男廁所只要你是帶把的就都可以進,不管你是真男人還是蔡徐坤。

dart中的泛型 使用< ... >表示,尖括號就是你要限定為的資料型別。它可以是String字串型別 (List<String>),或者是你已宣告的Shotcat類 (<Shotcat>),這種明確的。也可以是不明確的,有時候你只知道需要對資料進行限定,但限定為具體那種型別則需要根據具體情況在後面進行設定。這種情況你就可以在<>裡設定一個大寫字母,進行佔位,表示你要限定為的資料型別,只是你現在不知道是什麼型別。例如:<E>

為了更好的可讀性,一般會預設為:

  • E代表Element,元素.
  • T代表Type,型別.
  • K代表Key,鍵.
  • V代表Value,值.
// 我們以官網的例子為例,我們做一個存取資料作為快取的抽象類
// 我們不確定限定存取那種資料型別 就暫時使用<T> 進行佔位
abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
  // ...
}

// 由於<T> 進行佔位,所以我們可以根據需求進行限定
Cache<Object> objectCache; // 限定為物件
Cache<String> stringCache; // 限定為字串
複製程式碼

泛型的好處

在上面解釋泛型時,其實已經明確了。

  • 1,因為它的限定性,所以它可以更規範地程式碼生成。
  • 2,前面講到了泛型的不確定型,可以方便我們後面對不同的型別進行資料限定。從而減少了程式碼重複。

用於集合關鍵字宣告

一般用得最多的就是在List,Set 以及 Map關鍵字 後加上<...> 對宣告的資料進行限定。

var myGirlfriends = List<String>();
myGirlfriends.addAll(['新垣結衣', '石原里美', '暫時沒想到']);
myGirlfriends.add(66); // 報錯
複製程式碼

用於集合字面量

當我們以字面量形式宣告 (即不用關鍵字進行宣告) List,Set 以及 Map時,可以直接在字面量表示式前面加上<...>進行限定。

var myGirlfriends = <String>['新垣結衣', '石原里美', '暫時沒想到'];
var myNames = <String>{'吳彥祖', '彭于晏', '不會打籃球的CXK'};
var pages = <String, String>{
  'github': '1011cat',
  'juejin': 'shotCat',
  'blog': 'shotcat.com'
};
複製程式碼

函式中使用泛型

泛型也可以在函式中進行使用:

T first<T>(List<T> ts) {
  // 處理一些初始化工作或錯誤檢測……
  T tmp = ts[0];
  // 處理一些額外的檢查……
  return tmp;
}
複製程式碼

方法 first<T> 的泛型 T 可以在如下地方使用:

  • 函式的返回值型別 (T)。
  • 引數的型別 (List<T>)。
  • 區域性變數的型別 (T tmp)。

庫的使用

每個 Dart 程式都是一個庫,程式碼庫不僅只是提供 API 而且還起到了封裝的作用:以下劃線(_)開頭的成員僅在程式碼庫中可見。

引用庫

和es6一樣,使用import關鍵字進行引用。

不過不同的是,dart的import具有引數,對不同的庫進行區分:

  • 對於dart內建的庫:使用dart:xxxx的形式。
  • 對於第三方的庫:使用package: xxxx的形式。
import 'dart:io';
import 'package:mylib/mylib.dart'; 
import 'package:utils/utils.dart';
複製程式碼

指定庫的字首

如果你匯入的兩個程式碼庫有衝突的識別符號,你可以為其中一個指定字首,有點像名稱空間。

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;

// 使用 lib1 的 Element 類。
Element element1 = Element();

// 使用 lib2 的 Element 類。
lib2.Element element2 = lib2.Element();
複製程式碼

引用庫的一部分

如果你只想使用程式碼庫中的一部分,你可以有選擇地匯入程式碼庫。

  • show關鍵字:只引入這一個,其餘的都不引入
  • hide關鍵字:除了這一個,其餘的都引入
// 只匯入 lib1 中的 foo。
import 'package:lib1/lib1.dart' show foo;

// 匯入 lib2 中除了 foo 外的所有。
import 'package:lib2/lib2.dart' hide foo;
複製程式碼

**PS:**目前flutter 1.9版本是不支援延遲引入庫,目前僅dart2支援。這裡就不展開了。

非同步支援

dart是一個支援非同步操作的語言,其中的future和stream物件都是非同步的。dart中使用async和await關鍵字用於非同步程式設計。

PS:future和stream都是非同步的,兩者主要區別是:future只能返回一次非同步獲得的資料。而stream則可以返回多次非同步獲得的資料。

處理 Future

可以通過下面兩種方式,獲得 Future 執行完成的結果:

  • 使用 async 和 await
  • 使用 Future API,具體描述,參考庫概覽

其中async 和 await和es6類似。其中await必須是帶有async關鍵字的函式中使用!

async 和 await和future有什麼具體關係嗎?答:因為await表示式返回的就是一個future物件。然後等待 await 表示式執行完畢後才繼續執行。

getDate () async {
	var data = await askData()
	//..
}
複製程式碼

處理 stream

如果想從 Stream 中獲取值,可以有兩種選擇:

  • 使用 async 關鍵字和一個 非同步迴圈(使用 await for 關鍵字標識),正如我前面說的stream是返回多次非同步資料,因此需要非同步迴圈。
  • 使用 Stream API。詳情參考庫概覽
Future main() async {
  // ...
  await for (var request in requestServer) {
    handleRequest(request);
  }
  // ...
}
複製程式碼

後設資料

使用後設資料可以為程式碼增加一些額外的資訊。後設資料註解以 @ 開頭,其後緊跟一個編譯時常量或者呼叫一個常量建構函式。

目前dart提供三個@修飾符:

  • @deprecated 表示被棄用的
  • @override 表示重寫
  • @proxy 表示代理
class Television {
  /// _棄用: 使用 [turnOn] 替代_
  @deprecated
  void activate() {
    turnOn();
  }

  /// 開啟 TV 的電源。
  void turnOn() {...}
}

// @override 可能是用得最多的

class myName extends Father {
	// 這裡我們重寫name
	@override
	void name () {
		print('shotCat')
	}
}
複製程式碼

該系列列表

引用參考

相關文章