Flutter學習之Dart語法特性

真丶深紅騎士發表於2019-03-13

一、前言

第一天把Flutter環境搭建了,並簡單實現第執行第一個Flutter專案,感覺很不錯,一些基本操作和原生體驗差不多。用Flutter框架寫過App專案的開發者都知道,Flutter是一個使用Dart語言開發的跨平臺移動UI框架,通過自建繪製引擎,能高效能、高保真地進行Android和iOS開發。可能很多開發者都會像我一樣,會有個疑問?為什麼Flutter會選擇Dart呢?我特意查了下網上資料,總結以下下面六點:

  • Dart在release下是AOT(Ahead Of Time, 執行前,比如普通的靜態編譯)編譯的,編譯成快速、可預測的原生程式碼,使Flutter幾乎都可以使用Dart編寫,在Debug下,Dart是JIT(jUST In Time 執行時編譯,邊執行邊編譯,java虛擬機器就用到)編譯,開發週期快,就好像Flutter亞熱級的熱過載。
  • Dart可以更輕鬆地建立以60fps執行的流暢動畫和轉場,60fps是一個什麼概念呢?我相信王者榮耀大家都知道,王者榮耀的遊戲時幀率評測報告是在40fps-50fps中,越高的fps會提供更快速的操控流暢度,體驗感更舒適。
  • Dart可以在沒有鎖的情況下進行物件分配和垃圾回收。像JavaScript一樣,Dart避免了搶佔式排程和共享記憶體,大多數支援併發執行執行緒的計算機語言(Java、Kotlin、Swift)都使用搶佔式來切換執行緒,搶佔就會產生競態條件,競態條件很有可能導致嚴重錯誤,如應用程式崩潰導致資料丟失等等,一般解決競態條件使用來保護,但是鎖本身可能導致卡頓,也有可能產生死鎖,而Dart針對這個問題,採取了isolate方法來解決,Dart中的執行緒稱為isolate,不共享記憶體。那麼Dart是單執行緒的,意味根本不允許搶佔。單執行緒有助於開發者確保關鍵功能(動畫和轉場)完成而無需搶佔。 Flutter應用程式被編譯為原生程式碼,因此它們不需要再領域之間建立緩慢的橋樑,所以啟動速度快得多。
  • Dart使Flutter不需要單獨的宣告式佈局語言,如JSX或XML,或者單獨的視覺化介面構建器。其實針對這點我覺得Android的XML介面構建器是很直觀閱讀的。
  • Dart容易學習,具有靜態和動態語言使用者都熟悉的特性。
  • 易於移植,Dart可編譯成ARM和X86程式碼,這樣Dart移動應用程式可以在iOS、Android和其他地方執行。

那知道Flutter為什麼選擇Dart之後,作為開發Flutter專案來講,把Dar學好就很有必要了,下面就從基本語法開始對Dart入門。

二、Dart環境

1.本地環境

我這邊本地是用IntelliJ IDEA來編寫Dart專案,我這邊簡單說一下環境配置流程:

  1. 下載Flutter sdk 之前下載過,在第一天配置Android Stdio環境有說。
  2. Dart外掛的安裝,Windows下選擇File--Setting--Plugins搜尋Dart下載,而Mac下選擇螢幕左上角IntelliJ IDEA--Perferences--Plugins,如下圖:

Dart下載
下載完重啟即可。

  1. 配置Dart SDK位置,Windows下選擇File--Setting--Languages & Frameworks--Dart--Dart SDK Path配置,而Mac下選擇螢幕左上角IntelliJ IDEA--Perferences--Languages & Frameworks,如下圖:

配置Dart sdk
4.在File--New--Project--Dart,選擇Dart語言,如下圖:

選擇dart語言

  1. 建立專案名字,設定專案路徑,如下圖:

Flutter學習之Dart語法特性
6. 最後點選Finish按鈕,會看到專案已經被建立了,如下圖:

Dart執行結果示意

2.網頁環境

除了上面通過IntelliJ IDEA本地配置環境外,如果覺得上面配置Dart環境太麻煩就走網頁環境,就是下面這個連結直接線上編寫Dart小語法練習專案dartpad.dartlang.org/,下面例子是迴圈輸出5個hello,示意圖如下:

線上學習Dart
看還是很方便快捷的。

三、Dart一些概念

  • 在Dart中,一切都是物件,所有的物件都是繼承Object,也就是所有能夠使用變數引用的都是物件,每個物件都是一個了類的例項。在Dart中甚至數字、方法和null都是物件。
  • 沒有賦初值的變數都會有預設值null
  • 識別符號可以以字母或者_下劃線開頭,後面可以是其他字元和數字的組合。
  • Dart支援頂級方法,如main方法,同時還支援在類中定義函式(靜態函式和例項函式),還可以在方法中定義方法,Dart支援頂層變數,也支援類變數或物件變數。
  • Dart沒有publicprotectedprivate關鍵字。如果某個變數以下劃線_開頭,代表這個變數是在庫中是私有的。
  • Dart中的類和介面都是統一的,類即是介面,你可以繼承一個類,也可以實現一個類,自然也包含了良好的物件導向和併發程式設計的支援。
  • final的值只能被設定一次。const是一個編譯時的常量,可以通過const來建立常量值,var n = const[],這裡n還是一個變數,只是被賦值了一個常量值,它還是可以符其他值。例項變數可以是final,但不能是const。
  • Dart是強型別語言,但可以用var或者dynamic來宣告一個變數,Dart會自動推斷其資料型別,dynamic類似C#
  • 使用靜態型別可以更清晰表面你的意圖,並且可以讓靜態分析工具來分析你的程式碼。
  • Dart在執行之前會先解析你的程式碼。你可以通過使用型別或者編譯時常量來幫助Dart去捕獲異常以及讓程式碼執行的更高效。
  • Dart工具可以指出兩種問題:警告和錯誤。警告只是說你的程式碼可能有問題,但是並不會阻止你的程式碼執行。錯誤可以是編譯時錯誤也可以是執行時錯誤。遇到編譯時錯時,程式碼將無法執行;執行時錯誤將會在執行程式碼的時候導致一個異常。

四、Dart語法

1.關鍵字

下表是Dart語言的關鍵字

const null class new this
as default final continue throw
assert deferred finally operator true
async do for part try
async* dynamic get rethrow typedef
await else if return var
break enum implements set void
case export import static while
catch external in super with
false extends is switch yield
abstract factory library sync* yield*

2.一個最基本的Dart程式

//定義列印數字方法
printNumber(num Number){
  print('The number is $Number');
}

//執行程式入口
void main(){
  //定義初始化一個變數
  var number = 6.76;
  //呼叫列印數字方法
  printNumber(number);
}
複製程式碼

上面是一個簡單基本的Dart程式,雖然簡單但是用到了Dart很多特性:

  • //這是註釋符號,還可以用/*...*/這是很多語言的註釋符號
  • num這是數值型,Stringintbool是另外其他幾種型別。注意:數值型num包含整形int和浮點型double
  • 6.76是一個數字常量,數字常量是編譯時常量。
  • print()列印內容的方法
  • "..."或者是'...'表示字串常量
  • $variableName或者是${expression}是字串插值:在字串常量中引用變數或者表示式
  • var一種不指定型別宣告變數的方式
  • main()是Dart程式的入口方法,每個程式都需要一個這樣得分方法

3.Variables變數

var name = 'knight'; 上面是宣告變數並賦值的示例,變數是一個引用,上面的名字為name的變數引用一個內容為"knight"的String物件。

4.Default value(預設值)

沒有初始化的變數自動獲取一個預設值為null。型別為數字的變數如果沒有初始化那麼預設的值也是null,因為數字型別也是物件,上面直接上程式碼:

//定義列印數字方法
printNumber(num Number){
  print("The number is $Number");
}

//執行程式入口
void main(){
  //定義初始化一個變數
  var number;

  //呼叫列印數字方法
  printNumber(number);
}
複製程式碼

上面列印的結果是The number is null

5.Optional types(可選的型別)

宣告變數的時候,可以選擇加上具體型別,如下面:

  //定義初始化一個變數
  double number = 6.666;
複製程式碼

新增型別可以更加清晰表達你的意圖。IDE編譯器等工具有可以使用型別來更好的幫助,提供提示程式碼補全,提前發現bug等功能。

6.Final and const

如果以後不打算修改一個變數,使用final或者const。一個final變數只能賦值一次;一個const變數是編譯時常量。注意:const變數同時也是final變數,例項變數可以為final但不能是const。直接上例子:

//定義初始化一個變數
  final double number = 6.666;
  number = 6.667;
  //呼叫列印數字方法
  printNumber(number);
複製程式碼

上面例子用final修飾number並賦值,但number = 6.67的時候,想重新給number再賦值的時候,編譯錯報錯:number,a final variable,can only be set once.,意思和上面所說的一樣就是final變數只能賦值一次!下面改為定義為const來修飾number:

  //定義初始化一個變數
  const double number = 6.666;
  number = 6.667;
  //呼叫列印數字方法
  printNumber(number);
複製程式碼

同樣number = 6.667編譯器會報錯Constant variables can not be assigned a value意思是常量值不能賦值,上面也說了,因為const變數同時也是final變數。如果const變數在類中,請定義為static const。可以直接定義const和旗初始值,也可以定義一個const變數使用其他const變數的值來初始化其值,如下面:

  //定義初始化一個變數
  const double number = 6.66;
  const double number1 = 2 * number;
複製程式碼

上面例子的number1就是用了number來將自己初始化值,const關鍵字不僅僅只用來定義常量。有可以用來建立不變的值,還能定義建構函式為const型別 ,這中型別的建構函式建立的物件是不可改變的,任何變數都可以有一個不變的值。

7.Built-in types(內建的型別)

在Dart有幾種內建的資料型別:數值型-Number、布林型-boolean、鍵值對-Map、字串-String、列表-List、其他型別-Runes、Symbols

7.1數值型-Number

Dart中提供了兩種型別:

數值型別

//執行程式入口
void main(){


  //整形,其取值通常位於-2的53次方到2的53之間。
  num x = 777;
  //浮點數 64位
  x = 777.7;

  int y = 777;
  y = 777.7;       //這一行編譯器會報錯,因為將int型的資料轉為double型

  double n = 77,7;
  d = 77;          //這個地方會報錯,因為將double型的資料轉為int型

  int x1 = 7;
  int x2 = 77;
  int x3 = 777;
  int x4 = 7777;

  print('${x1.bitLength}'); //佔了3個bit 相當於00000000 00000111
  print('${x2.bitLength}'); //佔了7個bit 相當於00000000 01001101
  print('${x3.bitLength}'); //佔了10個bit 相當於00000011 00001001
  print('${x4.bitLength}'); //佔了13個bit 相當於00011110 01100001


}
複製程式碼

上面例子可以看到三個點:

  • 使用num宣告的變數,可以隨意的轉換型別,如果使用int或者double明確的宣告,那就不能轉換了
  • 判斷一個int值需要多少位時,可以使用bitLength

8.數值型的操作

運算子:+、-、*、/、~/、% 常用屬性:isNaN、isEven、isOdd 常用方法:abs()、round()、floor()、ceil()、toInt()、toDouble()

//執行程式入口
void main(){


  int i =7;
  double d = 10.1;

  print(i / d);               //0.6930693069306931
  print(i ~/ d);              //0   這個操作是取整 就是得出商

  print(i.isOdd);             // 判斷是奇數
  print(i.isEven);            // 判斷是偶數


  //String -> int
  var x1 = int.parse("7");
  print(x1 == 7);              //輸出true

  //Sting -> double
  var x2 = double.parse("7.7");
  print(x2 == 7.7);             //輸出true

  //int -> String
  var x3 = 7.toString();
  print(x3 == '7');             //輸出true

  //double -> String
  var x4 = 7.1234.toStringAsFixed(2);
  print(x4 == '7.12');          //輸出true

  //求絕對值
  var x5 = (-7).abs();
  print(x5 == 7);

  //四捨五入1
  var x6 = (7.7).round();
  print(x6);                   //輸出8

  //四捨五入2
  var x7 = (7.3).round();
  print(x7);                   //輸出7

  //求小於它的最大整數
  var x8 = (7.7).floor();
  print(x8);                   //輸出7

  //求大於它的最小整數
  var x9 = (7.7).ceil();
  print(x9);                   //輸出8

  double num1 = 7.77;
  print(num1);                //結果是7.77
  double num2 = 7;
  print(num2);                //結果是7.0
  int num3 = 7;
  print(num3.toDouble());     //int 轉 double 結果是7.0
  double num4 = 7.77;
  print(num4.toInt());        //double 轉 int 結果是7
  
}
複製程式碼

上面列出了一些平時遇到最多的操作,如求餘,求整,型別轉換等。

9.Strings(字串)

Dart字串是UTF-16編碼的字元序列,可以使用單引號或者雙引號來建立字串:

//執行程式入口
void main(){

  String m_str1 = '單引號字串';
  String m_str2 = "雙引號字串";

  print(m_str1);        //輸出:單引號字串
  print(m_str2);        //輸出:雙引號字串

}
複製程式碼

String中單、雙引號互相巢狀情況

//執行程式入口
void main(){

  String m_str1 = '單引號中的"雙引號"字串';
  String m_str2 = "雙引號中的'單引號'字串";

  print(m_str1);        //輸出:單引號中的"雙引號"字串
  print(m_str2);        //輸出:雙引號中的'單引號'字串

  //單引號裡面有單引號,必須在前面加反斜槓
  String m_str3 = '單引號中的\'單引號\'';
  String m_str4 = "雙引號裡面有雙引號,\"雙引號\"";
  print(m_str3);        //輸出:單引號中的'單引號'
  print(m_str4);        //輸出:雙引號裡面有雙引號,"雙引號"

}
複製程式碼

單引號巢狀單引號之間不允許出現空串(不是空格),雙引號巢狀雙引號之間不允許出現空串:

 //String m_str5 = '單引號''''單引號'; //報錯
  String m_str6 = '單引號'' ''單引號';
  print(m_str6);        //輸出: 單引號 單引號

  String m_str7 = '單引號''*''單引號';
  print(m_str7);        //輸出: 單引號*單引號

  //String m_str8 = "雙引號""""雙引號";   //報錯

  String m_str9 = "雙引號"" ""雙引號";
  print(m_str9);        //輸出: 雙引號 雙引號

  String m_str10 = "雙引號""*""雙引號";
  print(m_str10);       //輸出: 雙引號*雙引號
複製程式碼

單雙引號混合巢狀空串是可以的,如下:

 String m_str11 = '單引號""""單引號';
  print(m_str11);       //輸出: 單引號""""單引號

  String m_str12 = '單引號"" ""單引號';
  print(m_str12);       //輸出: 單引號"" ""單引號

  String m_str13 = '單引號""*"""單引號';
  print(m_str13);       //輸出: 單引號""*"""單引號

  String m_str14 = "雙引號''''雙引號";
  print(m_str14);       //輸出:  雙引號''''雙引號

  String m_str15 = "雙引號'' ''雙引號";
  print(m_str15);       //輸出:  雙引號'' ''雙引號

  String m_str16 = "雙引號''*''雙引號";
  print(m_str16);       //輸出:  雙引號''*''雙引號
複製程式碼

字串拼接方式,如下:

//使用空格拼接,多個空格也是可以地
  String m_str1 = '單引號字串' '拼接'     '---';
  print(m_str1);       //輸出:單引號字串拼接---

  //使用換行符和空格
  String m_str2 = '單引號字串'
    '換行''加空格' '';
  print(m_str2);       //輸出: 單引號字串換行加空格

  //單雙引號 空格拼接
  String m_str3 = "單雙引號字串加空格" '拼接'      "----";
  print(m_str3);      //輸出: 雙引號字串加空格拼接----

  //單雙引號 換行 空格
  String m_str4 = "單雙引號字串"
    '換行' '加空格' '***';
  print(m_str4);      //輸出: 單雙引號字串換行加空格***

  //使用三個單引號建立多行字串
  String m_str5 = '''
     三個單引號+
     拼接
     ''';
  print(m_str5);      /*輸出    三個單引號+
                                拼接
                                */

  //使用三個雙引號建立多行字串
  String m_str6 = """
    三個雙引號+
    拼接
    """;
  print(m_str6);      /*輸出    三個雙引號+
                                拼接
                                */

  String m_str7 = "正常拼接"+",用加號了來拼接";
  print(m_str7);      //輸出: 正常拼接,用加號了來拼接
複製程式碼

通過提供一個r字首可以建立"原始raw"字串,在字串加字元,或者在\前面再加一個\,可以避免\的轉義作用,如下:

  String m_str1 = r"sdsdsds";
  print(m_str1);  //輸出  sdsdsds

  print("換行:\n"); //輸出:換行

  print(r"換行:\n"); //輸出:換行:\n

  print("換行:\\n"); //輸出:換行:\n
複製程式碼

${表示式的使用},類似JS中ES6的表示式使用,使用$可以獲取字串的內容,用${表示式}可以將表示式的值放入字串中,使用${表示式}也可以使用字串拼接。下面也是直接上例子:

bool flag = true;
  String m_str1 = "字串";
  print("看看這個值:${m_str1} ""看看這個值flag:${flag}"); //輸出:字串:字串 看看這個值flag:true

  //使用$+字串
  String name = "knight";
  print("$name" + "CTO");     //輸出:knightCTO;

  //使用字串拼接,運用了String類中的toUpperCase函式,把字母變成大寫 
  String m_str = "Android";
  assert('${m_str.toUpperCase()} is very good' ==       
        'ANDROID is very good');
  
複製程式碼

==操作符判斷兩個物件的內容是否一樣,如果了;兩個字串包含一樣的字元編碼序列,則他們是相等的。在生產模式 assert() 語句被忽略了。在檢查模式assert(condition) 會執行,如果條件不為 true 則會丟擲一個異常。

10.Boolean(布林值)

Dart中以bool代表布林值,只有兩個物件是布林型別的,那就是truefalse所建立的物件,這兩個物件都是編譯時常量。當Dart需要一個布林值,只有true物件才被認為是true,所有其他值都是false。看下下面的例子:

  String name ="knight";
  //報錯 因為name不是bool型別
  if(name){
    print(name);

  }
複製程式碼

上面程式碼會丟擲異常,提示name不是布林值,Dart使用的是先式的檢查值,如下圖:

  // 檢查是否為空字串
  var fullName = '';
  assert(fullName.isEmpty);

  // 檢查是否小於等於0
  var hitPoints = 0;
  assert(hitPoints <= 0);

  // 檢查是否為 null.
  var unicorn;
  assert(unicorn == null);

  // 檢查是否為 NaN.
  var iMeantToDoThis = 0 / 0;
  assert(iMeantToDoThis.isNaN);
複製程式碼

assert是語言內建的斷言的函式,僅在檢查模式有效,在開發過程中,除非條件為真,否則會引發異常。(斷言失敗則程式立刻終止)

11.Lists列表

array是程式語言中最常見的集合型別,我相信身為開發者,都用過。在Dart中陣列就是List物件,所以一般稱為lists,下面直接上Dart list的示例:

//建立一個int型別的list 並賦值為0,1,2,3,4
  List list =  [0,1,2,3,4];

  //使用構建的方式建立list
  List list1 = new List();

  //建立一個常量的List,不可以改變的List
  List list2 = const[0,1,2,3];

  //增加泛型
  List list3 = new List<String>();

  //建立固定的長度的陣列列表,不能移除或者增加
  List list4 = new List(5);

  //建立包含所有以下元素的可改變的長度列表
  List list5 = new List.from([0,1,2,3]);

  //建立在固定範圍內改變長度的列表
  List list6 = new List()..length = 10;

  //建立包含所有元素的固定長度列表
  List list7 = new List.unmodifiable([0,1,2]);

  //用生成器給所有元素賦初始值
  List list8 = new List<int>.generate(5, (int i){
    return i + i;

  });

複製程式碼

List常用的一些api:

 //在列表中存放不同型別的物件
  List list = [1,2,3,false,"Kinght"];
  print(list);          //輸出:[1, 2, 3, false, Kinght]

  //在列表中新增元素
  list.add(7);
  print(list);          //輸出:[1, 2, 3, false, Kinght, 7]

  //修改列表下標為1的值
  list[1] = "paul";
  print(list);          //輸出:[1, paul, 3, false, Kinght, 7]

  //移除列表的指定值得的元素
  list.remove("paul");
  print(list);          //輸出:[1, 3, false, Kinght, 7]

  //移除列表指定下標下的元素
  list.removeAt(0);
  print(list);          //輸出:[3, false, Kinght, 7]

  //獲取列表的長度
  print(list.length);   //輸出:4

  //向列表中的指定位置新增元素 在第0的位置上插入Android
  list.insert(0, "Android");
  print(list);          //輸出:[Android, 3, false, Kinght, 7]

  //判斷陣列中是否有某元素
  print(list.indexOf("Android")); //這裡存在,輸出對應的下標,如果沒有則輸出-1

  //排序
  List list1 = [3,1,2,6,7];
  // 根據語法提示: List.sort([(int, int) → int compare]) → void
  list1.sort((a,b) => a.compareTo(b));
  print(list1);           //輸出:[1, 2, 3, 6, 7]
複製程式碼

簡單總結:

  1. 下標索引從0開始。
  2. 可以直接列印List的元素,List也是一個物件。

12.Maps

通常來講,Map是一個鍵值對相關的物件,鍵和值可以是任何型別的物件。每個鍵只出現一次,而一個值則可以出現多次。上面直接上Map集合的建立方式:

  //1.通過構建器來建立Map
  Map map1 = new Map();
  //新增值 賦值
  map1["one"] = 'Android';
  map1["two"] = 'IOS';
  map1["three"] = 'Flutter';
  print(map1);              //輸出:{one: Android, two: IOS, three: Flutter}

  //2.通過複製的形式
  Map map2 = Map.of(map1);
  print(map2);              //輸出:{one: Android, two: IOS, three: Flutter}

  //3.跟上面形式一樣  Object.fromEntries() 函式傳入一個鍵值對的列表,並返回一個帶有這些鍵值對的新物件。
  // 這個迭代引數應該是一個能夠實現@iterator方法的的物件,返回一個迭代器物件。它
  // 生成一個具有兩個元素的類似陣列的物件,第一個元素是將用作屬性鍵的值,第二個元素是與該屬性鍵關聯的值。
  Map map3 = Map.fromEntries(map1.entries);
  print(map3);

  //4.直接宣告,直接賦值key為String型別的map
  Map map4 = {'one':'Android',
    'two':'IOS',
    'three':'Flutter'};
  print(map4);              //輸出:{one: Android, two: IOS, three: Flutter}

  //5.建立一個空的Map
  Map map5 = Map.identity();
  print(map5);              //輸出:{}


  //6.建立不可變的Map
  Map map6 = const {'one':'Android','two':'IOS','three':'flutter'};
  print(map6);              //輸出:{one: Android, two: IOS, three: flutter}

  //7.在目標的map6建立(複製)新的不可修改map7
  Map map7 = Map.unmodifiable(map6);
  print(map7);              //輸出:{one: Android, two: IOS, three: flutter}

  //8.建立key為int值得map
  Map map8 = {1:'Android',
    2:'IOS',
    3:'Flutter'};
  print(map8);              //輸出:{1: Android, 2: IOS, 3: Flutter}

  //9.根據list所提供的key value來建立map
  List<String> keys = ['one','two'];
  List<String> values = ['Android','IOS'];
  Map map9 = Map.fromIterables(keys, values);
  print(map9);               //輸出:{one: Android, two: IOS}
  
   //通過構建器來建立Map
   Map map10 = new Map();
   //新增值 賦值 賦值不同型別的Map
   map10["one"] = 'Android';
   map10["two"] = 'IOS';
   map10["three"] = 'Flutter';
   map10[4] = 'RN';
   print(map10);              //輸出:{one: Android, two: IOS, three: Flutter, 4: RN}
複製程式碼

Map常用的一些api:

 //建立Map key是int型別,value是String型別
   var  map1 = new Map<int,String>();

   //對Map第一個位置賦值,中括號是key
   map1[0] = 'Android';
   //對Map第二個位置賦值
   map1[1] = 'IOS';
   //對Map第三個值賦值
   map1[2] = 'flutter';
   //對Map賦空值
   map1[3] = null;
   //因為Map中的鍵值是唯一的,當第二次輸入的key如果存在,Value會覆蓋之前
   map1[2] = 'RN';
   print(map1);                //{0: Android, 1: IOS, 2: RN, 3: null}

   //獲取Map的長度
   print(map1.length);         //輸出:4

   //判斷Map是否為空
   print(map1.isNotEmpty);     //輸出結果:true

   //判斷Map是否不為空
   print(map1.isEmpty);        //輸出結果:false

   //檢索Map是否含有某個Key
   print(map1.containsKey(1)); //輸出:true

   //檢索Map是否包含某個Value
   print(map1.containsValue('Android'));  //輸出:true

   //刪除某個鍵值對
   map1.remove(0);
   print(map1);                //輸出:{1: IOS, 2: RN, 3: null}

   //獲取所有的key
   print(map1.keys);           //輸出:(1, 2, 3)

   //獲取所有的values
   print(map1.values);         //輸出:(IOS, RN, null)

   //迴圈列印
   /*
     key:1, value:IOS
     key:2, value:RN
     key:3, value:null

    */
     map1.forEach((key,value) {
     print("key:${key}, value:${value}");
   });

複製程式碼

簡單總結:

  1. 當Map的Key沒有指定型別時,Key型別不一致也不會報錯。
  2. Map裡面的key不能相同。但是value可以相同,value可以為空字串或者為null。
  3. 建立Map有兩種方式:通過構造器(new)和直接賦值。

13.Runes

在Dart中,runes代表字串的UTF-32 code points。Unicode為每一個字元、標點符號、表情符號等都定義了一個唯一的數值。什麼意思呢?也就是在書寫系統中,每一個字母,數字都是有唯一的數值。由於在Dart字串是UTF-16 code units字元序列,所以在字串表達32-bit Unicode值就需要新的語法,通常用\uXXXX的方式表示Unicode code point,這裡的XXXX是4個16進位制的數。如:心形符號?是\u2665。對於非4個數值的情況,把編號放到大括號即可。例如,笑臉(?)是\u{1f600}String類有一些屬性可以提取rune資訊。codeUnitAtcodeUnit屬性返回16-bit code units。使用runes屬性來獲取字串的runes資訊。下面示例演示了runes16-bit code units、和32-bit code points之間的關係:

//執行程式入口
void main(){

  var clapp = '\u{1f44f}';
  print(clapp);                  //輸出:?

  print(clapp.codeUnits);        //輸出: [55357, 56399]
  print(clapp.runes.toList());   //輸出:  [128079]

  //使用String. fromCharCodes方法顯示字元圖形
  Runes input = new Runes(
      '\u2665  \u{1f605}  \u{1f60e}  \u{1f47b}  \u{1f596}  \u{1f44d}');
  print(new String.fromCharCodes(input));   //輸出:♥  ?  ?  ?  ?  ?

}
複製程式碼

14.Symbols

一個Symbolobject代表Dart程式宣告的操作符或者識別符號。你也許從來不會用到Symbol,但是該功能對於通過名字來引用識別符號的情況是非常有價值的,特別是混淆後的程式碼,識別符號的名字被混淆了,但是Symbol的名字不會改變,下面列下例子:

  //使用 Symbol 字面量來獲取識別符號的 symbol 物件,也就是在識別符號 前面新增一個 # 符號:
  //#radix
  //#bar

  print(#n == new Symbol('n'));  //輸出:true

  var name = 'knight';
  Symbol symbol = #name;
  print(symbol);                //輸出:Symbol("name")
  print(#name);                 //輸出:Symbol("name")
複製程式碼

15.function方法

Dart是一個真正的面嚮物件語言,方法也是物件並且具有一種型別,Function。這意味著,方法可以賦值給變數,也可以當做其他方法的引數。也可以把Dart類的例項當做方法來呼叫,下面是定義方法的示例:

//定義一個方法 判斷列表對應下標是否為null
bool isNoble(int atomicNumber) {
  return list[atomicNumber] != null;
}
複製程式碼

雖然在Effective Dart推薦在公開的APIs上使用靜態型別,當然也可以選擇忽略型別定義:

//定義一個方法 判斷列表對應下標是否為null 忽略型別定義
isNoble(int atomicNumber) {
  return list[atomicNumber] != null;
}
複製程式碼

對於只有一個表示式的方法,可以選擇使用縮寫語法來定義:

//定義一個方法 判斷列表對應下標是否為null 縮寫寫法
bool isNoble(int atomicNumber) => list[atomicNumber] != null;
複製程式碼

=> expr是語法{return expr;}形式的縮寫。=>形式也稱為胖箭頭語法。注意:在箭頭(=>)和冒號(;)之間只能使用一個表示式,不能使用語句。方法可以有兩種型別的引數:必需和可選的。必需的引數在引數列表前面,後面是可選引數。

15.1.Optional parameters(可選引數)

什麼是可選引數麼?定義一個函式時,形參可以定義為可選引數,當呼叫這個方法時,可以不傳這個可選引數。可選引數可以是命名引數或者基於位置的引數,但是這兩種引數不能同時當做可選引數

15.1.1.Optional named parameters(可選命名引數)

呼叫方法的時候,可以使用這種形式paramName:value來指定命名引數。例如:

enableFlags(bold: true, hidden: false);
複製程式碼

定義方法時,使用{param1,param2,...}的形式來指定可選命名引數:

enableFlags({bool bold, bool hidden}) {
  // ...
}
複製程式碼
15.1.2.Optional positional parameters(可選位置引數)

把一些方法的引數放到[]中就變成可選位置引數了,例子如下:

//定義一個方法 [String device]是可選位置引數 也就是呼叫這個方法可以不傳這個引數
String say(String from, String msg, [String device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

//不使用可選引數呼叫方法
assert(say('Bob', 'Howdy') == 'Bob says Howdy');

//使用可選引數呼叫方法
assert(say('Bob', 'Howdy', 'smoke signal') ==
'Bob says Howdy with a smoke signal');
複製程式碼
15.1.3.Default parameter value(預設引數值)

在定義方法的時候,可以使用=來定義可選引數的預設值。預設值只能是編譯時常量。如果沒有提供預設值,則預設值為null。下面是設定可選引數預設值的示例:

//定義一個返回型別為空的方法 方法中hidden預設值為false
void enableFlags({bool bold = false,bool hidden = false}){

}

//呼叫方法 沒有傳hidden的值,那預設值就是false
enableFlags(bold:true);
複製程式碼

下面的示例顯示瞭如何設定位置引數的預設值:

//定義一個方法 這個方法位置可選位置引數device的預設引數是carrier pigon
//也就是當呼叫這個方法,沒有傳這個引數時,這個引數會取預設值
String say(String from, String msg,
    [String device = 'carrier pigeon', String mood]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  if (mood != null) {
    result = '$result (in a $mood mood)';
  }
  return result;
}

//呼叫上面的方法
assert(say('Bob', 'Howdy') ==
'Bob says Howdy with a carrier pigeon');
複製程式碼

還可以使用list或者map作為預設值,下面的示例定義一個方法doStuff(),並分別為listgifts引數指定預設值:

//執行程式入口
void main(){

  //呼叫判斷對應小標的值是否為空
 // print(isNoble(1));    //輸出:true


  doStuff();    //輸出:list:  [1, 2, 3]
                //輸出:gifts: {first: paper, second: cotton, third: leather}


}

//List和Map都取了預設值
void doStuff(
    {List<int> list = const [1, 2, 3],
      Map<String, String> gifts = const {
        'first': 'paper',
        'second': 'cotton',
        'third': 'leather'
      }}) {
  print('list:  $list');
  print('gifts: $gifts');
}
複製程式碼

15.2.The main() function(入口函式)

每個應用都需要有個頂級的main()入口方法才能執行。main()方法的返回值為void並且有個可選的List<String>引數,下面是一個web應用的main()方法:

void main() {
  querySelector("#sample_text_id")
    ..text = "Click me!"
    ..onClick.listen(reverseText);
}
複製程式碼

在上面程式碼中..y語法是級聯呼叫。使用級聯呼叫,可以在一個物件上執行多個操作。下面是一個命令列應用的main()方法,並且使用方法引數作為輸入引數:

// Run the app like this: dart args.dart 1 test
void main(List<String> arguments) {
  print(arguments);

  assert(arguments.length == 2);
  assert(int.parse(arguments[0]) == 1);
  assert(arguments[1] == 'test');
}
複製程式碼

15.3.Functions as first-class objects(一等方法物件)

可以把方法當做引數呼叫另外一個方法。如:

printElement(element) {
  print(element);
}

var list = [1, 2, 3];

// 遍歷集合
list.forEach(printElement);
複製程式碼

方法也可以賦值給一個變數:

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');
複製程式碼

15.4.Anonymous functions(匿名方法)

大部分方法都帶有名字,例如main()或者printElement()。也可以建立沒有名字的方法,稱為匿名方法,有時候也被稱為lambda或者clourse閉包。可以把匿名方法賦值給一個變數,然後可以使用這個方法,比如新增集合或者從集合中刪除。匿名函式和命名函式類似,在括號之間可以定義一些引數·,引數使用逗號分割,也可以是可選引數。後面大括號中的程式碼為函式體:

([[Type] param1[, …]]) { 
  codeBlock; 
}; 
複製程式碼

下面的程式碼定義了一個引數為i的匿名函式。list中的每個元素都會呼叫這個函式來列印出來,同時來計算了每個元素在list中的索引位置。

var list = ['apples', 'oranges', 'grapes', 'bananas', 'plums'];
list.forEach((i) {
  print(list.indexOf(i).toString() + ': ' + i);
});
//輸出:

0: apples
1: oranges
2: grapes
3: bananas
4: plums
複製程式碼

上面說到如果方法只包含一個語句,可以使用胖箭頭語法縮寫,把上面的程式碼改成下面同樣的意思:

list.forEach((i) => print(list.indexOf(i).toString() + ': ' + i));
複製程式碼

15.5.靜態作用域

Dart是靜態作用域語言,變數的作用域在寫程式碼的時候就缺的過了。基本上大括號裡面定義的變數就只能在大括號裡面訪問,和java作用域類似。

var topLevel = true;

main() {
  var insideMain = true;

  myFunction() {
    var insideFunction = true;

    nestedFunction() {
      var insideNestedFunction = true;

      assert(topLevel);
      assert(insideMain);
      assert(insideFunction);
      assert(insideNestedFunction);
    }
  }
}
複製程式碼

注意nestedFunction可以訪問所有的變數,包含頂級變數。

15.6.Lexical closures(詞法閉包)

一個閉包是一個方法物件,不管該物件在何處被呼叫,該物件都可以訪問其作用域內的變數,方法可以封閉定義到其作用域內的變數。方法可以封閉定義到其作用域內的變數。下面示例中,makeAdder()捕獲到了變數addBy。不管在哪裡執行makeAdder()所返回的函式,都可以使用addBy引數:

Function makeAdder(num addBy) {
  return (num i) => addBy + i;
}

main() {
  // Create a function that adds 2.
  var add2 = makeAdder(2);

  // Create a function that adds 4.
  var add4 = makeAdder(4);

  assert(add2(3) == 5);
  assert(add4(3) == 7);
}
複製程式碼

15.7.Testing functions for equality(測試函式是否相等)

下面是測試頂級方法、靜態函式和例項函式相等的示例:

foo() {}               // 頂級方法

class A {
  static void bar() {} // 靜態方法
  void baz() {}        // 例項方法
}

void main() {
  var x;

  // 比較頂級函式
  x = foo;
  print(foo == x);    //輸出:true

  // 比較靜態方法
  x = A.bar;
  print(A.bar == x);  //輸出:true

  // 比較例項方法
  var v = new A(); // 例項1
  var w = new A(); // 例項2
  var y = w;
  x = w.baz;

  //這些閉包引用相同的例項
  //所有相等
  print(y.baz == x);   //輸出:true

  // 閉包引用不同的例項,所以不相等`
  print(v.baz != w.baz);   //輸出:true
}
複製程式碼

15.8.Return values (返回值)

所有的函式都返回一個值。如果沒有指定返回值,則預設把語句return null;作為函式的最後☝一個語句執行。

16.Operators 操作符

操作符
上面列了操作符和一些例子,在操作符表格所列的操作符都是按照優先順序順序從左到右,從上倒下的方式來排列,上面和左邊的操作符優先順序要高於下面和右邊的。例如%操作符優先高於==,而等號高於&&,下面的程式碼結果是一樣的:

void main() {
   var n = 2;
   var i = 2;
   var d = 7;
   if((n % i == 0) && (d % i == 0)){
     print('符合條件');
   }else{
     print('不符合條件');       //進入這裡
   }

   if(n % i == 0 && d % i == 0){
     print('符合條件');
   }else{
     print('不符合條件');       //進入這裡
   }
}
複製程式碼

16.1.算術操作符

算術操作符

16.2.相等相關的操作符

相等相關的操作符

16.3.型別判定操作符

型別判定操作符
只有obj實現了T的介面,obj is T才是true。例如obj is Object總是true。使用as操作符吧物件轉換為特定的型別。一般情況下,可以把它當做is判定型別然後呼叫所判定物件的函式縮寫形式,如下面的示例:

if (emp is Person) { // Type check
  emp.firstName = 'Bob';
}
複製程式碼

使用as操作符可以簡化上面的程式碼:

(emp as Person).firstName = 'Bob';
複製程式碼

注意:上面的這兩個程式碼效果是有區別的。如果emp是null或者不是person型別,則第一個示例使用is則不會執行條件裡面的程式碼,而第二個情況使用as則會丟擲一個異常。

16.4.賦值操作符

使用=操作符賦值。但是還有一個??=操作符來指定值為null的變數值。

a = value;   // 給 a 變數賦值
b ??= value; // 如果 b 是 null,則賦值給 b;
             // 如果不是 null,則 b 的值保持不變
複製程式碼

還有複合賦值符+=等可以賦值:

複合賦值操作符
下面的程式碼使用賦值操作符符合複合賦值操作符:

var a = 2;           // Assign using =
a *= 3;              // Assign and multiply: a = a * 3
assert(a == 6);
複製程式碼

下面是複合賦值操作符工作原理解釋:

複合賦值操作符解釋

16.5.邏輯操作符

邏輯操作符

16.6.位和移位操作符

在Dart中可以單獨運算元字的某一位,下面操作符同樣應用於整數:

位與移位操作符

16.7.條件表示式

Dart有兩個特殊的操作符可以用來替代if-else語句: condition ? expr1 : expr2 如果condition是true,執行expr1(並返回執行的結果);否則執行expr2並返回結果。 expr1 ?? expr2如果expr1是non-null,返回其值;否則執行expr2並返回其結果。如果是基於布林表示式的值來賦值,考慮使用?:

var finalStatus = m.isFinal ? 'final' : 'not final';
複製程式碼

如果是基於布林表示式是測試值是否為null,考慮使用??

String toString() => msg ?? super.toString();

//上面程式碼可以用下面程式碼來表示,意思效果是一樣的,程式碼易懂但是不簡潔
String toString() => msg == null ? super.toString() : msg;

String toString() {
  if (msg == null) {
    return super.toString();
  } else {
    return msg;
  }
}
複製程式碼

16.8.級聯操作符

級聯操作符(..)可以在同一物件上連續呼叫多個函式以及訪問成員變數。使用級聯操作符可以避免建立臨時變數,並且寫出來的程式碼看起來更加流暢,如:

querySelector('#button') // Get an object.
  ..text = 'Confirm'   // Use its members.
  ..classes.add('important')
  ..onClick.listen((e) => window.alert('Confirmed!'));
複製程式碼

第一個方法quertSelector返回一個selector物件。後面的級聯操作符都是呼叫這個物件的成員,並忽略每個操作所返回的值。上面程式碼和下面的程式碼功能一樣:

var button = querySelector('#button');
button.text = 'Confirm';
button.classes.add('important');
button.onClick.listen((e) => window.alert('Confirmed!'));
複製程式碼

級聯呼叫也可以巢狀:

final addressBook = (new AddressBookBuilder()
      ..name = 'jenny'
      ..email = 'jenny@example.com'
      ..phone = (new PhoneNumberBuilder()
            ..number = '415-555-0100'
            ..label = 'home')
          .build())
    .build();
複製程式碼

在方法上使用級聯操作符需要非常小心,例如下面程式碼是不合法的:

var sb = new StringBuffer();
sb.write('foo')..write('bar');
複製程式碼

sb.write函式返回一個void,無法再void上使用級聯操作符。注意級聯語法不是操作符,只是語法!

17.Control flow statements(流程控制語句)

Dart中的控制流程語句和java語言很像,可以說是差不多的:

17.1.if else

if (isRaining()) {//條件語句
  you.bringRainCoat();//內容體
} else if (isSnowing()) {//條件語句
  you.wearJacket();//內容體
} else {
  car.putTopDown();//內容體
}
複製程式碼

17.2.for迴圈

可以使用標準的for迴圈:

var message = new StringBuffer("Dart is fun");
for (var i = 0; i < 5; i++) {
  message.write('!');
}

//使用foreach迴圈 list 和 Set都可以用這種方式
List numbers = [1,2,3,4,5,6,7,8,910];
numbers.foreach((number)=> print(number));

//使用for in迴圈,一般List和Set都是用這種方式
List numbers = [1,2,3,4,5,6,7,8,910];
for(var number in numbers){
     print(number);
}
複製程式碼

17.3.While and do-while

while迴圈在執行迴圈之前先判斷條件是否滿足:

//判斷條件
while (!isDone()) {
  //內容
  doSomething();
}

//例子
var i = 0;
while(i > 5){
    i++;
}
複製程式碼

do-while迴圈是先執行迴圈程式碼再判斷條件:

do {
  printLine();//內容體
} while (!atEndOfPage());//條件判斷
//例子
var i = 0;
do{
    i++;
}while(i > 7);
複製程式碼

17.4.Break and continue

使用break來終止迴圈:

while (true) {
  if (shutDownRequested()) break;
  processIncomingRequests();
}
複製程式碼

使用continue來開始下一次迴圈:

for (int i = 0; i < candidates.length; i++) {
  var candidate = candidates[i];
  if (candidate.yearsExperience < 5) {
    continue;
  }
  candidate.interview();
}
複製程式碼

上面程式碼在實現Iterable介面物件(List和Map)可以使用下面寫法:

candidates.where((c) => c.yearsExperience >= 5)
          .forEach((c) => c.interview());
複製程式碼

17.5.Switch and case

Dart中的Switch語句使用==比較integer、String、或者編譯時常量。比較的兌現必須都是同一個類的例項(並且不是其之類),calss必須沒有覆寫==操作符,每個非空的case語句都必須有一個break語句。另外還可以通過continuethrowreturn來介紹非空case語句。當沒有case語句匹配時,可以使用default語句來匹配這種預設情況。

var command = 'OPEN';
switch (command) {
  case 'CLOSED':
    executeClosed();
    break;
  case 'PENDING':
    executePending();
    break;
  case 'APPROVED':
    executeApproved();
    break;
  case 'DENIED':
    executeDenied();
    break;
  case 'OPEN':
    executeOpen();
    break;
  default:
    executeUnknown();
}
複製程式碼

下面的示例程式碼再case省略了break語句,編譯的時候會出現一個錯誤:

void main() {

  var number = 1;
  var i = 0;
  switch(number){
    case 1;
    i++;
    case 2:
      i--;
      break;

  }

}
複製程式碼

在Dart中的空case語句中可以不要break語句:

void main() {

  var number = 1;
  var i = 0;
  switch(number){
    case 1;
    i++;
    case 2:
      i--;
      break;

  }

}
複製程式碼

如果需要實現這種繼續到下一個case語句中繼續執行,則可以使用continue語句跳轉對應的標籤處繼續執行:

void main() {

  var number = 1;
  var i = 0;
  switch(number){
    case 1;
    i++;
    case 2:
      i--;
      break;

  }

}
複製程式碼

每個case語句可以有區域性變數,區域性變數只有在這個語句內可見。

17.6.Assert(斷言)

如果條件表示式結果不滿足需要,則可以使用assert語句倆打斷程式碼的執行。示例如下:

// 確保變數是非空值 
assert(text != null);
// 確保值是小於100
assert(number < 100);
// 確保這是一個 https 地址
assert(urlString.startsWith('https'));
複製程式碼

assert上面也說過了,assert方法引數可以為任何布林值的表示式或者方法。如果返回的值為true,斷言執行通過,執行結束。如果返回值為false,斷言執行失敗,會丟擲異常,斷言只有在檢查模式執行有效,如果生產模式執行,則斷言不會執行。

18.Exceptions(異常)

程式碼中可以出現異常和捕獲異常。異常表示一些未知的錯誤情況。如果異常沒有捕獲,則異常會丟擲,導致丟擲異常的程式碼終止執行。和java不同的是,所有的Dart異常是非檢查異常。方法不一定宣告·來看他們所丟擲的異常,並且你不要求捕獲異常,Dart提供了ExceptionError型別,以及一些子型別。還可以自己定義異常型別。但是,Dart程式碼可以丟擲任何非null物件為異常,不僅僅是實現了ExceptionError物件。

18.1.Throw

下面是丟擲或者扔出一個異常的示例:

thow new FormatException('Expected at least 1 section');
複製程式碼

還可以丟擲任意物件:

throw 'Out of llamas!';
複製程式碼

由於丟擲異常時一個表示式,所以可以在=>語句中使用,也可以在其他能使用表示式的地方丟擲異常。

distanceTo(Point other) =>
    throw new UnimplementedError();
複製程式碼

18.2.Catch

捕獲異常可以避免異常繼續傳遞(重新丟擲rethrow)異常除外。捕獲異常給你一個處理該異常的機會:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  buyMoreLlamas();
}
複製程式碼

對於可以丟擲多種型別異常的程式碼,你可以指定多個捕獲語句。每個語句分別對應一個異常型別,如果捕獲語句沒有指定異常型別,則該可以捕獲任何異常型別:

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // A specific exception
  buyMoreLlamas();
} on Exception catch (e) {
  // Anything else that is an exception
  print('Unknown exception: $e');
} catch (e) {
  // No specified type, handles all
  print('Something really unknown: $e');
}
複製程式碼

如之前程式碼所示,可以使用on或者catch來宣告捕獲語句,也可以同時使用。使用on來指定異常型別,使用catch來捕獲異常物件。函式catch()可以帶有一個或者兩個引數,第一個引數為丟擲的異常物件,第二個為堆疊資訊。

  ...
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}
複製程式碼

使用rethrow關鍵字可以把捕獲的異常重新丟擲:

final foo = '';

void misbehave() {
  try {
    foo = "You can't change a final variable's value.";
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}

void main() {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e.runtimeType}.');
  }
}
複製程式碼

18.3.Finally

無論是否丟擲異常,要確保某些程式碼都要執行,可以使用finally語句來實現。如果沒有catch語句來捕獲異常,則在執行完finally語句後,異常被丟擲了:

try {
  breedMoreLlamas();
} finally {
  // 即使丟擲異常也會執行
  cleanLlamaStalls();
}
複製程式碼

定義的finally語句在任何匹配的catch語句之後執行:

try {
  breedMoreLlamas();
} catch(e) {
  print('Error: $e');  // 優先處理異常
} finally {
  cleanLlamaStalls();  // 然後再執行
}
複製程式碼

19.Classes

Dart是一個物件導向程式語言,同時支援基於mixin的繼承機制。每個物件都是一個類的例項,所有的類都繼承於object。基於Mixin的繼承意味著每個類(Object除外)都只有一個超類,一個類的程式碼可以在其他多個類繼承中重複使用·。使用new關鍵字和建構函式來建立新的物件。建構函式名字可以為ClassName或者ClassName.identifier。例如:

var jsonData = JSON.decode('{"x":1, "y":2}');

// 建立Point型別的物件
var p1 = new Point(2, 2);

// 根據json來建立Point物件
var p2 = new Point.fromJson(jsonData);
複製程式碼

物件的成員包括方法和資料(函式和示例變數)。當你呼叫一個函式的時候,你是在一個物件上呼叫:函式需要訪問物件的方法和資料,使用(.)來引用物件的變數或者方法:

var p = new Point(2, 2);

// 對例項物件的變數y賦值
p.y = 3;

// 從成員變數得到值
assert(p.y == 3);

// 從p複製例項物件
num distance = p.distanceTo(new Point(4, 4));
複製程式碼

使用?.來替代,可以避免當左邊物件為null時候丟擲異常:

//如果P不是空物件,那麼對其變數y賦值
p?.y = 4;
複製程式碼

有些類提供了常量建構函式。使用常量建構函式可以建立編譯時常量,要使用常量建構函式只需要用const替代new即可:

var p = const ImmutablePoint(2, 2);
複製程式碼

兩個一樣的編譯時常量其實是同一個物件:

var a = const ImmutablePoint(1, 1);
var b = const ImmutablePoint(1, 1);

print(identical(a, b)); // 它們是相同的例項,輸出true
複製程式碼

可以使用Object的runtimeType屬性來判斷例項的型別,該屬性返回一個Type物件。

print('The type of a is ${a.runtimeType}');
複製程式碼

19.1.Instance variables

下面是如何定義例項變數的示例:


class Point{
  num x;//宣告例項變數x,初始化為空
  num y;//宣告例項變數y,舒適化為空
  num z = 0;//宣告例項變數z,初始化為0
}
複製程式碼

所有沒有初始化時變數值都是null。每個例項變數都會自動生成一個getter方法。Non-final例項變數還會自定生成一個setter方法:

class Point{
  num x;//宣告例項變數x,初始化為空
  num y;//宣告例項變數y,舒適化為空
  num z = 0;//宣告例項變數z,初始化為0
}
void main() {
  var point = new Point();
  point.x = 4;              //使用setter方法對x變數賦值
  print(point.x == 4);      //輸出true 使用getter獲取x變數的值
  print(point.y == null);   //輸出true

}
複製程式碼

如果在例項變數定義的時候初始化該變數(不是在建構函式或者其他方法中初始化),改值是在例項物件的時候初始化的,也就是在建構函式和初始化引數列表執行之前。

19.2.Constructors

定義一個和類名字一樣的方法就定義一個建構函式還可以帶有其他可選的識別符號。常見的建構函式生一個物件的新例項:

class Point {
  num x;
  num y;

  Point(num x, num y) {
    this.x = x;
    this.y = y;
  }
}
複製程式碼

this關鍵字指當前的例項。只有當名字衝突的時候才使用this。由於建構函式引數賦值給例項變數的場景太常見了,Dart提供一個語法糖來簡化這個操作:

class Point {
  num x;
  num y;

  // 設定x和y的語法糖
  // 在建構函式主體執行之前
  Point(this.x, this.y);
}
複製程式碼
19.2.1.Default copnstructors(預設建構函式)

如果沒有定義建構函式,則會有個預設建構函式。預設建構函式沒有引數,並且會呼叫超類的沒有引數的建構函式

19.2.2.Constructors aren’t inherited(建構函式不會繼承)

子類不會繼承超類的建構函式。子類如果沒有定義建構函式,則只有一個預設建構函式。

19.2.3.Named constructors(命名建構函式)

使用命名建構函式可以為一個類實現多個建構函式,或者使用命名建構函式來更清晰自己的意圖:

class Point {
  num x;
  num y;

  Point(this.x, this.y);

  // 命名建構函式
  Point.fromJson(Map json) {
    x = json['x'];
    y = json['y'];
  }
}
複製程式碼

建構函式不能繼承,所以超類的命名建構函式也不會被繼承,如果子類也有超類一樣命名建構函式,就必須在子類中自己實現該建構函式。

19.2.4.Invoking a non-default superclass constructor(呼叫超類建構函式)

預設情況下,子類的建構函式會自動呼叫超類的無名無引數的預設建構函式。超類的建構函式在子類建構函式體開始執行的位置呼叫。如果提供了一個 initializer list(初始化引數列表) ,則初始化引數列表在超類建構函式執行之前執行。 下面是建構函式執行順序:

  1. initializer list(初始化引數列表)
  2. superclass’s no-arg constructor(超類的無名建構函式)
  3. main class’s no-arg constructor(主類的無名建構函式)

如果超類沒有無名無參建構函式,則需要手動去呼叫超類的其他建構函式。在建構函式引數後使用冒號:可以呼叫超類建構函式,下面中,Employee類的建構函式呼叫超類Person的命名建構函式:

//定義Person類
class Person {
  String firstName;

  Person.fromJson(Map data) {
    print('in Person');
  }
}

class Employee extends Person {
  // Person 沒有預設建構函式
  // you must call super.fromJson(data).
  Employee.fromJson(Map data) : super.fromJson(data) {
    print('in Employee');
  }
}

main() {
  var emp = new Employee.fromJson({});

  // 列印輸出
  // in Person
  // in Employee

}
複製程式碼

由於超類建構函式的引數在建構函式執行之前執行,所以擦拭可以是一個表示式或者一個方法呼叫:

class Employee extends Person {
  // ...
  Employee() : super.fromJson(findDefaultData());
}
複製程式碼

如果在建構函式的初始化列表中使用 super(),需要把它放到最後。呼叫超類建構函式的引數無法訪問 this。 例如,引數可以為靜態函式但是不能是例項函式。

19.3.Initializer list(初始化列表)

在建構函式體執行之前除了可以呼叫超類建構函式之外,還可以 初始化例項引數。 使用逗號分隔初始化表示式:

class Point {
  num x;
  num y;

  Point(this.x, this.y);

  // 初始化列表在建構函式體執行之前設定例項變數。
  Point.fromJson(Map jsonMap)
      : x = jsonMap['x'],
        y = jsonMap['y'] {
    print('In Point.fromJson(): ($x, $y)');
  }
}
複製程式碼

初始化表示式等號右邊的部分不能訪問 this。初始化列表非常適合用來設定 final 變數的值。 下面示例程式碼中初始化列表設定了三個 final 變數的值:

import 'dart:math';

export 'src/DartProject_base.dart';

// TODO: Export any libraries intended for clients of this package.


class Point {
 final num x;
 final num y;
 final num distanceFromOrigin;

 Point(x, y)
     : x = x,
       y = y,
       distanceFromOrigin = sqrt(x * x + y * y);
}

main() {
 var p = new Point(2, 3);
 print(p.distanceFromOrigin);//輸出:3.605551275463989
}
複製程式碼

19.4.Redirecting constructors(重定向建構函式)

有時候一個建構函式會調動類中的其他建構函式。一個重定向建構函式是沒有程式碼的,在建構函式宣告後,使用冒號呼叫其他建構函式。

class Point {
  num x;
  num y;

  // 主建構函式
  Point(this.x, this.y);

  // 呼叫主建構函式
  Point.alongXAxis(num x) : this(x, 0);
}
複製程式碼

19.5.Constant constructors(常量建構函式)

如果你的類提供一個狀態不變的物件,你可以把這些物件定義為編譯時常量。要實現這個功能,需要定義一個const建構函式, 並且宣告所有類的變數為final

class ImmutablePoint {
  final num x;
  final num y;
  const ImmutablePoint(this.x, this.y);
  static final ImmutablePoint origin =
      const ImmutablePoint(0, 0);
}
複製程式碼

19.6.Factory constructors(工廠方法建構函式)

如果一個建構函式並不總是返回一個新的物件,則使用factory來定義這個建構函式。例如,一個工廠建構函式可能從快取中獲取一個例項並返回,或者返回一個子型別的例項。例子:

class Logger {
  final String name;
  bool mute = false;

  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    if (_cache.containsKey(name)) {
      return _cache[name];
    } else {
      final logger = new Logger._internal(name);
      _cache[name] = logger;
      return logger;
    }
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) {
      print(msg);
    }
  }
}
複製程式碼

使用new關鍵字來呼叫工廠建構函式

var logger = new Logger('UI');
logger.log('Button clicked');
複製程式碼

19.7.函式

函式是類中定義的方法,是類物件的行為。

19.7.1.Instance methods(例項函式)

物件的例項函式可以訪問this,下面例子中distanceTo函式就是例項函式:

import 'dart:math';

class Point {
  num x;
  num 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);
  }
}
複製程式碼

Getterssetters是用來設定和訪問物件屬性的特殊函式。每個例項變數都隱含的具有一個getter, 如果變數不是final的則還有一個setter。可以通過實行gettersetter來建立新的屬性, 使用getset關鍵字定義gettersetter

class Rectangle {
  num left;
  num top;
  num width;
  num height;

  Rectangle(this.left, this.top, this.width, this.height);

  // 定義兩個計算屬性:右和下。
  num get right             => left + width;
      set right(num value)  => left = value - width;
  num get bottom            => top + height;
      set bottom(num value) => top = value - height;
}

main() {
  var rect = new Rectangle(3, 4, 20, 15);
  assert(rect.left == 3);
  rect.right = 12;
  assert(rect.left == -8);
}
複製程式碼

gettersetter的好處是,可以開始使用例項變數,後來可以把例項變數用函式包裹起來,而呼叫程式碼的地方不需要修改。

19.7.2.Abstract methods(抽象函式)

例項函式、 getter、和setter函式可以為抽象函式,抽象函式是隻定義函式介面但是沒有實現的函式,由子類來實現該函式。如果用分號來替代函式體則這個函式就是抽象函式。

abstract class Doer {
  // ...定義例項變數和方法...

  void doSomething(); // 定義一個抽象方法.
}

class EffectiveDoer extends Doer {
  void doSomething() {
    // ...提供實現,因此此處的方法不是抽象的...
  }
}
複製程式碼

呼叫一個沒實現的抽象函式會導致執行時異常。

19.8.Overridable operators(可覆寫的操作符)

下表中的操作符可以被覆寫。 例如,如果你定義了一個 Vector 類, 你可以定義一個 + 函式來實現兩個向量相加。

可覆寫的操作符
下面舉了覆寫+-操作符的示例:

class Vector {
  final int x;
  final int y;
  const Vector(this.x, this.y);

  /// 覆寫 + (a + b).
  Vector operator +(Vector v) {
    return new Vector(x + v.x, y + v.y);
  }

  /// 覆寫 - (a - b).
  Vector operator -(Vector v) {
    return new Vector(x - v.x, y - v.y);
  }
}

main() {
  final v = new Vector(2, 3);
  final w = new Vector(2, 2);

  // v == (2, 3)
  assert(v.x == 2 && v.y == 3);

  // v + w == (4, 5)
  assert((v + w).x == 4 && (v + w).y == 5);

  // v - w == (0, 1)
  assert((v - w).x == 0 && (v - w).y == 1);
}
複製程式碼

19.9.Abstract classer(抽象類)

使用abstract修飾符定義一個抽象類,一個不能被例項化的類。抽象類通常用來定義介面,以及部分實現,如果抽象類是可例項化的,則定義一個工廠建構函式 抽象類通常具有抽象函式,下面是定義具有抽象函式的抽象類:

// 這個類是抽象類,不能例項化
abstract class AbstractContainer {
  // ...定義建構函式, 變數, 方法...

  void updateChildren(); // 抽象方法.
}
複製程式碼

下面的類不是抽象的,但是定義了一個抽象函式,這樣的列是可以被例項化:

class SpecializedContainer extends AbstractContainer {
  // ...定義更多的構造方法, 方法...

  void updateChildren() {
    // ...實現 updateChildren()...
  }

  //Abstract method causes a warning but doesn't prevent instantiation.
抽象方法導致警告,但不阻止例項化。
  void doSomething();
}
複製程式碼

19.10.Implicit interfaces(隱式介面)

每個類都隱式的定義了一個包含所有例項成員的介面,並且這個類實現了這個介面。如果你想建立類A來支援類B的api,而不想繼承 B 的實現, 則類A應該實現B的介面:

// A person. 隱式介面包含greet().
class Person {
  // 在介面中,但僅在此庫中可見
  final _name;

  // 不在介面中,因為這是建構函式
  Person(this._name);

  // 在介面中
  String greet(who) => 'Hello, $who. I am $_name.';
}

// 實現Person 介面
class Imposter implements Person {
  // 我們必須定義這個,但我們不使用它。
  final _name = "";

  String greet(who) => 'Hi $who. Do you know who I am?';
}

greetBob(Person person) => person.greet('bob');

main() {
  print(greetBob(new Person('kathy')));
  print(greetBob(new Imposter()));
}
複製程式碼

下面是實現多個介面的示例:

class Point implements Comparable, Location {
  // ...
}
複製程式碼

19.11.Extending a class(擴充套件類)

使用extends定義子類,supper引用超類:

class Television {
  void turnOn() {
    _illuminateDisplay();
    _activateIrSensor();
  }
  // ...
}

class SmartTelevision extends Television {
  void turnOn() {
    super.turnOn();
    _bootNetworkInterface();
    _initializeMemory();
    _upgradeApps();
  }
  // ...
}
複製程式碼

子類可以覆寫例項函式,gettersetter。下面是覆寫Object類的noSuchMethod()函式的例子, 如果呼叫了物件上不存在的函式,則就會觸發noSuchMethod()函 數。

class A {
  //除非重寫NoSuchMethod,否則使用不存在的函式將導致NoSuchMethodError。
  void noSuchMethod(Invocation mirror) {
    print('You tried to use a non-existent member:' +
          '${mirror.memberName}');
  }
}
複製程式碼

還可以使用@override註解來表明函式是想覆寫超類的一個函式:

class A {
  @override
  void noSuchMethod(Invocation mirror) {
    // ...
  }
}
複製程式碼

如果使用noSuchMethod函式來實現每個可能的getter、setter、以及其他型別的函式,可以使用@proxy註解來避免警告資訊:

@proxy
class A {
  void noSuchMethod(Invocation mirror) {
    // ...
  }
}
複製程式碼

如果要知道編譯時的具體型別,可以實現這些類來避免警告,和使用@proxy效果一樣:

class A implements SomeClass, SomeOtherClass {
  void noSuchMethod(Invocation mirror) {
    // ...
  }
}
複製程式碼

19.12.Enumerated types(列舉型別)

列舉型別通常稱為enumerations或者enums是一種特殊的類,用來表現一個固定數目的常量。使用enum關鍵字來定義列舉型別:

enum Color {
  red,
  green,
  blue
}
複製程式碼

列舉型別中的每個值都有一個indexgetter函式,該函式返回該值在列舉型別定義中的位置(從0開始),例如,第一個列舉值的位置為0,第二個為1:

print(Color.red.index == 0); //輸出:true
print(Color.green.index == 1);//輸出:true
print(Color.blue.index == 2);//輸出:true
複製程式碼

列舉values常量可以返回所有的列舉值

List<Color> colors = Color.values;
print(colors[2] == Color.blue);//輸出:true
複製程式碼

可以在switch語句中使用列舉。如果在switch(e)中的e的型別為列舉類,如果沒有處理所有該列舉型別的值的話,則會丟擲一個警告:

enum Color {
  red,
  green,
  blue
}
// ...
Color aColor = Color.blue;
switch (aColor) {
  case Color.red:
    print('Red as roses!');
    break;
  case Color.green:
    print('Green as grass!');
    break;
  default: // 如果沒有這個,你會看到一個警告
    print(aColor);  // 'Color.blue'
}
複製程式碼

列舉型別具有以下限制:

  1. 無法繼承列舉型別,無法使用mixin、無法實現一個列舉型別
  2. 無法顯示的初始化一個列舉型別

19.13.Adding features to a class:mixins(為類新增新的功能)

Mixins是一種多類繼承中重用一個類程式碼的方法,使用with關鍵字後面為一個或者多個mixin名字來使用mixin,上例子如何使用mixin:

class Musician extends Performer with Musical {
  // ...
}

class Maestro extends Person
    with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}
複製程式碼

定義一個類繼承Object,該類沒有建構函式,不能呼叫super,則該類就是一個mixin。下面例子:

abstract class Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;

  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}
複製程式碼

從Dart1.13開始,Mixins可以繼承其他類,不再限制為繼承Object,Mixins可以呼叫super()

19.14.Class variables and methods(類變數和函式)

使用static關鍵字來實現級別的變數和函式。

19.14.1.Static variables(靜態變數)

靜態變數對於類別的狀態是非常有用的:

class Color {
  static const red =
      const Color('red'); // 靜態構造變數.
  final String name;      // 靜態例項變數.
  const Color(this.name); // 構造方法.
}

main() {
  print(Color.red.name == 'red'); //輸出:true
}
複製程式碼

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

19.14.2.Static methods(靜態函式)

靜態函式不再例項是執行,所以無法訪問this:

import 'dart:math';

class Point {
  num x;
  num 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);
  }
}

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

對於通用的或者經常使用的靜態函式,考慮使用頂級方法而不是靜態函式,靜態函式還可以當做編譯時常量使用,如:把靜態函式當做常量建構函式的引數來使用。

20.泛型

在檢視List型別的API文件,可以看到實際的型別定義為List<E>。這個<..>宣告list是一個泛型(或者引數化)型別。通常情況下,使用一個字母來代表型別引數,例如E,T,S,K和V等。

20.1.Why use generics?(為何使用泛型)

在Dart中型別是可選的,你可以選擇不用泛型。有很多情況都要使用型別表明自己的意圖,不管是使用泛型還是具體型別。如,如果自己希望List只包含字串物件。則可以定義為List<String>代表("list of string")。這樣開發工具或者自己的同事可以幫助檢查自己的程式碼是否把非字串型別物件給放到這個list中,如下面:

main() {
   List Tech = new List<String>();
   Tech.addAll(['Android','IOS','Flutter']);
   Tech.add(42);//執行時報錯
}
複製程式碼

另外使用泛型的原因是減少重複程式碼。泛型可以在多種型別之間定義同一個實現,同時還可以繼續使用檢查模式和靜態分析工具提供的程式碼分析功能。例如:建立一個儲存快取物件的介面:

abstract class ObjectCache {
  Object getByKey(String key);
  setByKey(String key, Object value);
}
複製程式碼

後來發現需要一個用來快取字串的實現,那又要定義一個介面:

abstract class StringCache {
  String getByKey(String key);
  setByKey(String key, String value);
}
複製程式碼

然而,又需要用一個用來快取數字的實現,在後來,又需要另外一個型別的快取實現,等等。。。這時候,泛型的另一個作用體現出來了,泛型可以避免這種重複程式碼,例子上:

abstract class Cache<T> {
  T getByKey(String key);
  setByKey(String key, T value);
}
複製程式碼

在上面的程式碼中,T是一個備用型別,這是型別佔位符,自己呼叫該介面的時候會指定具體型別。

20.2.Using collection literals(使用集合字面量)

ListMap字面量也是可以引數化的。引數化定義list需要在中括號之間新增<type>,定義map需要在大括號之前新增<KeyType,valueType>。如果你需要更加安全的型別檢查,則可以使用引數化定義。例子如下:

var names = <String>['Seth', 'Kathy', 'Lars'];
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};
複製程式碼

20.3.Using parameterized types with constructors(建構函式中使用泛型)

在呼叫建構函式的時候,在類名字後面使用尖括號(<...>)來指定泛型型別。例如:

var names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = new Set<String>.from(names);
複製程式碼

下面程式碼建立了一個keyinteger,valueView型別的map:

var views = new Map<int, View>();
複製程式碼

20.4.Generic collections and the types they contain(泛型集合和包含的型別)

Dart的泛型型別是固化的,在執行時有也可以判斷具體的型別,例如在執行時也可以檢測集合裡面的物件型別:

var names = new List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // 輸出:true
複製程式碼

is表示式只是判斷集合的型別,而不是集合裡面具體物件的型別。在成產模式,List<String>變數可以包含非字串型別物件。對於這種情況,可以選擇分包判斷每個物件的型別或者處理型別轉換異常,java中的泛型資訊是編譯時的,泛型資訊在執行時是不純在。在java中可以測試一個物件是否為List,但是無法測試一個物件是否為List<String>

20.5.Restricting the parameterrized type(限制泛型型別)

當使用泛型型別的時候,可能想限制泛型的具體型別,使用extends可以實現這個功能:

// T必須是SomebaseClass或其後代之一。
class Foo<T extends SomeBaseClass> {...}

class Extender extends SomeBaseClass {...}

void main() {
  // 可以在<>中使用somebaseclass或它的任何子類。
  var someBaseClassFoo = new Foo<SomeBaseClass>();
  var extenderFoo = new Foo<Extender>();

  //也可以使用no<>
  var foo = new Foo();



   //指定任何非SomeBaseClass型別將導致警告,在Debug檢查模式,執行時錯誤。
  // var objectFoo = new Foo<Object>();
}
複製程式碼

20.6.Using generic method(使用泛型函式)

一開始,泛型只能在Dart類中使用。新的語法也支援在函式和方法上使用泛型。

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

這裡first(<T>)泛型可以在如下地方使用引數T

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

在Dart1.21開始使用泛型函式,如果需要使用泛型函式,需要設定SDK版本為1.21或者以上。

21.Libraries and visibility(庫和可見性)

使用importlibrary指令可以幫助建立模組化的可分享程式碼。庫不僅僅提供API,還是一個私有單元:以下劃線(_)開頭的識別符號只有在庫內部可見。每個Dart app都是一個庫,即使沒有使用library命令也是一個庫。這裡的庫和Android所說的庫有相似的地方,簡單來講就是用人家寫好庫中的API。例如:拍照庫,網路請求庫等。

21.1.Using libraries(使用庫)

使用import來指定一個庫如何使用另外一個庫,例如:Dart web應用通常使用dart:html庫,然後可以這樣匯入庫:

import 'dart:html';
複製程式碼

import必須引數為庫的URL。對於內建的庫,URI使用特殊的dart:scheme。對於其他的庫,可以使用檔案系統路徑或者package:schemepackage:scheme指定的庫通過包管理器來提供,如pub工具,如:

import 'dart:io';
import 'package:mylib/mylib.dart';
import 'package:utils/utils.dart';
複製程式碼

21.2.Specifying a library prefix(指定庫字首)

如果匯入的庫具有衝突的識別符號,這個經常遇到,則可以使用庫的字首來區分。例如:如果library1library2都有一個名字為Element的類,可以這樣使用:

import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// ...
Element element1 = new Element();           // 使用 lib1 中的 Element .
lib2.Element element2 = new lib2.Element(); // 使用 lib2 中的 Element.
複製程式碼

21.3.Importing only part of a library(匯入庫的一部分)

如果只使用庫的一部分功能,可以選擇需要匯入的內容,例如:

// 僅匯入foo
import 'package:lib1/lib1.dart' show foo;

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

21.4.Lazily loading a library(延遲載入庫)

Deferred loading可以讓應用在需要的時候再載入庫。這我就想到了App的開啟時間,下面是一些使用延遲載入庫的場景:

  • 減少APP的啟動時間
  • 執行A/B測試,例如嘗試各種演算法的不同實現
  • 載入很少使用的功能,例如可選的螢幕和對話方塊

延遲載入一個庫,需要先使用deferred as來匯入:

import 'package:deferred/hello.dart' deferred as hello;
複製程式碼

當需要使用的時候,使用庫識別符號呼叫loadLibrary()函式來載入庫:

greet() async {
  await hello.loadLibrary();
  hello.printGreeting();
}
複製程式碼

在上面程式碼,使用await關鍵字暫停程式碼執行一直到庫載入完成。在一個庫上可以多次呼叫loadLibrary()函式。但是該庫只是載入一次。在使用延遲載入庫的時候,要注意:

  • 延遲載入庫的常量在匯入的時候是不可用的。只有當庫載入完畢的時候,庫中常量才可以使用。
  • 在匯入檔案的時候無法使用延遲庫中的型別。如果需要使用型別,則考慮把介面型別移動到另外一個庫中, 讓兩個庫都分別匯入這個介面庫。
  • Dart隱含的把loadLibrary()函式匯入到使用deferred as 名稱空間loadLibrary()方法返回一個Future

22.Asynchrony support(非同步支援)

Dart有一些語言特性來支援非同步程式設計。最常見的特性是async方法和await表示式。Dart庫中有很多返回Future或者Stream物件的方法。這些方法是非同步:這些函式在設定完基本的操作後就返回了,而無需等待執行完成。例如讀取一個檔案,在開啟檔案後就返回了。有兩種方式可以使用Future物件中的資料:

  • 使用asyncawait
  • 使用Future API

同樣,從Stream中獲取資料也有兩種方式:

  • 使用async和一個非同步for迴圈(await for)
  • 使用Stream API

使用asyncawait的程式碼是非同步的,但是看起來有點像同步程式碼。如:下面是使用await來等待非同步方法返回的示例:

await lookUpVersion()
複製程式碼

要使用await,其方法必須帶有async關鍵字:

checkVersion() async {
  var version = await lookUpVersion();
  if (version == expectedVersion) {
    // 主體內容.
  } else {
    // 主體內容.
  }
}
複製程式碼

可以使用try,catchfinally來處理使用await的異常:

try {
  server = await HttpServer.bind(InternetAddress.LOOPBACK_IP_V4, 4044);
} catch (e) {
  // 對無法繫結到埠作出反應...
}
複製程式碼

22.1.Declaring async functions(宣告非同步方法)

一個async方法是函式體被標記為async的方法。雖然非同步方法的執行可能需要一定時間,但是非同步方法立刻返回-在方法體還沒執行之前就返回了。

checkVersion() async {
  // ...
}

lookUpVersion() async => /* ... */;
複製程式碼

在一個方法上新增async關鍵字,則這個方法返回值為Future。例如,下面是一個返回字串的同步方法:

String lookUpVersionSync() => '1.0.0';
複製程式碼

如果使用async關鍵字,則該方法返回一個Future,並且認為該函式是一個耗時操作。

Future<String> lookUpVersion() async => '1.0.0';
複製程式碼

注意,方法的函式體並並不需要使用FutureAPI。Dart自動在需要的時候建立Future物件。

22.2.Using await expressions with Futures(使用await表示式)

await表示式具有如下的形式:

await expression
複製程式碼

在一個非同步方法內可以使用多次await表達水。例如,下面的示例使用三次await表示式來執行相關的功能:

var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
複製程式碼

await expression中,expression的返回值通常是一個Future;如果返回的值不是Future,則Dart會自動把該值放到Future中返回。Future物件代表返回一個物件的承諾。await expression執行的結果為這個返回的物件。await expression會阻塞主,直到需要的物件返回為止。如果await無法正常使用,請確保是在一個async方法中。例如要在main()方法中使用await,則main()方法的函式體必須標記為async:

main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}
複製程式碼

22.3.Using asynchronous for loops with Stream(在迴圈中使用非同步)

非同步for迴圈具有以下形式:

await for (variable declaration in expression) {
  // 每次流發出時會執行.
}
複製程式碼

上面的expression返回的值必須是Stream型別。執行流程如下:

  1. 等待直到stream返回一個資料
  2. 使用stream返回的引數執行for迴圈程式碼
  3. 重複執行1和2直到stream資料返回完畢

使用break或者return語句可以停止接收stream資料,這樣就挑出了for迴圈並且從stream上取消註冊了。如果**非同步for迴圈不能正常工作,確保是在一個async方法中使用。**如:要想在main()方法中使用非同步for迴圈,則需要把main()方法的函式體標記為async

main() async {
  ...
  await for (var request in requestServer) {
    handleRequest(request);
  }
  ...
}
複製程式碼

23.Callable classes(可呼叫的類)

如果Dart類實現call()函式則可以當做方法來呼叫,下面例子中,wannabeFunction類定義了一個call()方法,該方法有三個字串引數,並且返回三個字串串聯起來的結果:


class WannabeFunction {
  call(String a, String b, String c) => '$a $b $c!';
}

main() {
  var wf = new WannabeFunction();
  var out = wf("Hi","there,","gang");
  print('$out');   //輸出:Hi there, gang!
}
複製程式碼

24.Isolates

現代的瀏覽器和移動瀏覽器執行在多核CPU系統上。要充分利用這些CPU,開發者一般使用共享記憶體資料來爆炸多執行緒的正確執行。然而,多執行緒共享資料通常會導致很多潛在的問題,並導致程式碼執行出錯,結果不是預期。所有的Dart程式碼在isolates中執行而不是執行緒,每個isolate都有自己的堆記憶體,並且確保每個isolate的狀態都不能被其他isolate訪問。

25.Typedefs

在Dart語言中,方法也是物件。使用typedef或者function-type-alias來為方法型別命名,然後可使用命名的方法,當把方法型別賦值給一個變數的時候,typedef保留型別資訊。下面程式碼沒有使用typedef:

class SortedCollection {
  Function compare;

  SortedCollection(int f(Object a, Object b)) {
    compare = f;
  }
}

 // Initial, broken implementation.
 int sort(Object a, Object b) => 0;

main() {
  SortedCollection coll = new SortedCollection(sort);

  // 我們只知道 compare 是一個 Function 型別,
  // 但是不知道具體是何種 Function 型別?
  assert(coll.compare is Function);
}
複製程式碼

當把f賦值給compare的時候,型別資訊丟失了,f的型別是(object,object)->int當然該型別是一個Function。如果使用顯式的名字並保留型別資訊,開發者和工具可以使用這些資訊:

typedef int Compare(Object a, Object b);

class SortedCollection {
  Compare compare;

  SortedCollection(this.compare);
}

 // Initial, broken implementation.
 int sort(Object a, Object b) => 0;

main() {
  SortedCollection coll = new SortedCollection(sort);
  assert(coll.compare is Function);
  assert(coll.compare is Compare);
}
複製程式碼

26.Metadata(後設資料)

使用後設資料給程式碼新增額外資訊,後設資料註解是以@字元開頭,後面是一個編譯時常量或者呼叫一個常量建構函式。有三個註解所有的Dart程式碼都可使用:@deprecated@override@proxy,下面直接上@deprecated的示例:

class Television {
  /// 已經棄用,請改用[開啟]
  @deprecated
  void activate() {
    turnOn();
  }

  /// 開啟電視.
  void turnOn() {
    print('on!');
  }
}
複製程式碼

可以定義自己的後設資料註解。下面的示例定義一個帶有兩個引數的@todo註解:

library todo;

class todo {
  final String who;
  final String what;

  const todo(this.who, this.what);
}

複製程式碼

使用@todo註解的示例:

import 'todo.dart';

@todo('seth', 'make this do something')
void doSomething() {
  print('do something');
}
複製程式碼

後設資料可以在librarytypedeftype parameterconstructorfactoryfunctionfieldparameter、或者variable宣告之前使用,也可以在import或者export指令之前使用,使用反射可以再執行時獲取後設資料資訊。

五、總結

Dart語法和Java語法很像,很容易上手,理解很簡單,用一天就把語法整理了一遍,我為什麼要學習Dart語法呢?一開始解釋很清楚了,無非就是把根基打穩。學什麼一定要帶有目的性去學,下面直接上一張圖:

帶有目的性去學

學習資源:

  1. 起步-Dart
  2. 為java開發人員準備的Dart教程

六、額外知識

Flutter有四種執行模式:Debug、Release、Profile和test。

  1. Debug:Debug模式可以在真機和模擬器上同時執行:會開啟所有的斷言,包括debugging資訊、debugger aids(比如observatory)和服務擴充套件。
  2. Release:Release模式只能在真機上執行,不能在模擬器上執行:會關閉所有斷言和debugging資訊,關閉所有debugger工具。
  3. Profile:Profile模式只能在真機上執行,不能在模擬器上執行:基本和Release模式一致。
  4. test: headless test模式只能在桌面上執行:基本和Debug模式一致。

Flutter四種執行方式

相關文章