@[toc]
Dart是谷歌開發(fā)并在2011年亮相,2015推出了基于Dart語言的移動應用程序的開發(fā)框架 Sky,后更名為 Flutter。經過多年的發(fā)展和完善,Flutter 逐漸成為公司開發(fā)應用程序的新寵兒。
Dart是面向對象的、類定義的、單繼承的語言。它的語法類似C語言,可以轉譯為JavaScript,支持接口(interfaces)、混入(mixins)、抽象類(abstract classes)、具體化泛型(reified generics)、可選類型(optional typing)和sound type system。
體驗 Dart 程序
// 定義一個函數
printInteger(int aNumber) {
print('The number is $aNumber.'); // 打印輸出到控制臺。
}
// Dart 程序從 main() 函數開始執(zhí)行。
main() {
var number = 42; // 聲明并初始化一個變量。
printInteger(number); // 調用一個函數。
}
Dart 應用程序總是會從頂級函數 main() 開始執(zhí)行;
Dart 語言需要 ; 符號作為每一條語句的結束符號;
$variableName (或 ${expression}) 表示字符串插值。
簡介
Dart 在語法上和一些熱門的語言非常相似,例如C、JavaScript、Python、Swift。語法的設計上有著非常多的相似之處,如果你使用或者了解多門語言,你能很輕松的掌握 Dart 語言。
學習不同的編程語言,通過相互之間的類比,是了解和掌握語言之間的差異性必要條件,筆者這里只記錄一些值得玩味的點,讓你能快速的一名 Dart 語言的開發(fā)者。想要更深入的了解 Dart 語言特性,請查閱 官方開發(fā)文檔 以及相關的社區(qū)。
變量
String name = 'Bob'; // 指定類型,不可接收其他類型
var name = 'Bob'; // 推斷出類型,不可接收其他類型
Object name = 'Bob'; // 可接收其他類型,只能使用Object中定義的方法和變量,類似NSObject
dynamic name = 'Bob'; // 可接收其他類型,可以使用所有可能的方法和變量,類似id
final 和 const
如果你從未打算更改一個變量,那么使用 final 或 const ,而不是 var 或者某一指定類型。 final 和 cons 修飾的變量只能被設置一次,區(qū)別在于, final 變量在第一次使用時被初始化,const 變量是一個編譯時期的常量。
//可以省略String這個類型聲明
final str = "hi world";
//final String str = "hi world";
const str1 = "hi world";
//const String str1 = "hi world";
內置類型
Dart 語言內置如下的類型:numbers、strings、booleans、list(數組)、sets(集合)maps(字典)、runes(Unicode字符)、symbols。
Dart 所有變量引用的都是對象類型,每個對象都是一個類型的實例。數字、函數或者 null 都是對象。所有的類都繼承自 Object 類。
-
numbers
Dart 有兩種類型的 number:int 和 double,兩個都是 num 的子類。
var x = 1; var hex = 0xDEADBEEF; var y = 1.1; double z = 1; // double z = 1.0.Dart 盡管是強類型語言,但是在聲明變量的時候,類型是可選的,因此你可以指定變量類型,如 double,也可以使用 var 聲明,由 Dart 自動進行類型推斷。
-
Strings
var s1 = '使用單引號創(chuàng)建字符串字面量。'; var s2 = "雙引號也可以用于創(chuàng)建字符串字面量。"; var s3 = '使用單引號創(chuàng)建字符串時可以使用斜杠來轉義那些與單引號沖突的字符串:\'。'; var s4 = "而在雙引號中則不需要使用轉義與單引號沖突的字符串:'"; var s5 = r'在 raw 字符串中,轉義字符串 \n 會直接輸出 “\n” 而不是轉義為換行。'; var s6 = '可以拼接' '字符串' "即便它們不在同一行。"; var s7 = '使用加號+運算符' + '也可以達到相同的效果。'; var s8 = """ 你可以像這樣創(chuàng)建多行字符串 """; var s9 = '字符串插值'; print ('Dart 有$s9,使用起來非常方便'); print ('使用${s9.substring(3,5)}表達式也非常方便'); -
Booleans
布爾類型只有兩個對象 true 和 false,兩者都是編譯時常量。Dart 需要顯示地檢查布爾值,不允許使用其他類型作為條件,例如 int 類型。
var fullName = ''; assert(fullName.isEmpty); bool hited = false; assert(hited); -
Lists
Dart 中的數組類型的字面量和 JavaScript 中的數組字面量一樣。
var list1 = [1,2,3]; var count = list1.length; // 3 // 替換 list1[0] = 0; var list2 = [4,5,6]; // 拼接 var list3 = list1 + list2; // [0, 2, 3, 4, 5, 6] // 擴展符號 ... 和 ...? var list4 = [1, ...list2]; // [1, 4, 5, 6] var list5; // null var list6 = [1,...?list5]; // [1],數組可能為空,使用 ...? 避免異常 // 2.3版本的 Collection If bool promoActive = true; var list7 = [ 'Home', 'Furniture', 'Plants', if (promoActive) 'OutLet' ]; // 2.3版本的 Collection For var listOfInts = [1,2,3]; var list8 = [ '0', for (var i in listOfInts) '$i' ]; var list9 = [ for (var i =1; i<4; i++) i, for (var i in [4,5,6]) i ]; -
Sets
一組特定元素的無序集合。
// Dart 2.2 版本之后才加入 Set 字面量的創(chuàng)建方式 var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'}; // 創(chuàng)建空 set 類型,添加非指定類型時異常,不指定類型的{}是空Map類型 var names = <String>{}; // 添加一個或一組 names.add('luo'); names.addAll(['liang','guo']);從 Dart2.3版本開始,Set 和 List 一樣支持使用擴展操作符 ... 和 ...? 以及 Collection If 和 Collection For 的方式創(chuàng)建實例。
-
Maps
Maps是一組 key 和 value 的集合對象,在 OC 中,我們稱之為字典類型。Dart 中的 key 和 value 可以是任何類型的對象。
// 字面量 var fruits = { "first":"Apple", "second":"pear" }; print(fruits); // 構造函數 var nobleGases = Map(); nobleGases['1'] = 'helium'; nobleGases['2'] = 'neon'; // 所有的key var keys = nobleGases.keys; // 所有的值 var values = nobleGases.values;從 Dart2.3版本開始,Map 和 List 一樣支持使用擴展操作符 ... 和 ...? 以及 Collection If 和 Collection For 的方式創(chuàng)建實例。
-
Runes
Unicode 編碼為每一個字母、數字和符號都定義了一個唯一的數值。因為 Dart 中的字符串是一個 UTF-16 的字符序列,所以如果想要表示 32 位的 Unicode 數值則需要一種特殊的語法。
通常使用 \uXXXX 來表示 Unicode 字符, XXXX 是一個四位數的 16 進制數字。例如心形字符(?)的 Unicode 為 \u2665。對于不是四位數的 16 進制數字,需要使用大括號將其括起來。例如大笑的 emoji 表情(??)的 Unicode 為 \u{1f600}。
import 'package:characters/characters.dart'; ... var hi = 'Hi ????'; print(hi); print('The last character: ${hi.characters.last}\n');
函數
Dart 中函數也是一種對象類型:Function,這就意味著函數可以作為變量、參數和返回值使用。
// 返回類型可選,可不寫
返回值類型 函數名(參數列表) {
函數體;
};
例如:
bool isNoble(int atomicNumber) {
return _nobleGases[atomicNumber] != nil;
}
當函數體只包含一個表達式,可以簡寫成下面這樣,你可以經??吹竭@種寫法:
bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != nil;
可選參數
函數的參數有兩種:必須要參數和可選參數。必要參數定義在最前面,可選參數接在后面,可選參數又分為兩種:命名參數和位置參數。這兩種選擇其中之一,不能同時出現。
必要參數沒什么可說的,我們來了解一下可選參數。
命名參數
通過名稱來傳遞參數,名稱的位置可變動。
通過 {parm1, param2, ...} 的形式來傳遞參數。
// 定義可選參數
void enableFlags(String str, {bool blod, bool hidden}) {
print("$str, $blod,$hidden");
}
// 調用
void main() {
enableFlags("xiaoming",hidden: false, blod: true);
}
位置參數
使用 [] 將一系列參數包裹起來作為位置參數。
String say(String from, String msg, [String device]) {
var result = '$from says $msg';
if (device != null) {
result = '$result with a $device';
}
return result;
}
提供默認參數值
你可以為可選參數提供默認值,默認值必須為編譯時常量,沒有指定默認值的情況下值為 null。
// 提供默認參數
void enableFlags({bool bold = false, bool hidden = false}) {...}
String say(String from, String msg,
[String device = 'carrier pigeon', String mood]) {...}
函數對象
函數可以看作是 Function 類,可以賦值給變量,也可以作為參數使用。
void main() {
var method = printElement; // 賦值給變量
[1,2,3].forEach(method); // 作為參數
}
printElement(int e) => print(e);
匿名函數
匿名函數是編程語言中關于函數的一種重要的變體:沒有名字的函數,也叫 Lambda 表達式 或者 Closure 閉包。你可以在各種編程語言中找到它們,可以將匿名函數直接賦值給變量去使用,或者直接放在參數列表中使用(通常是最后一個參數,部分語言稱之為尾隨閉包,有其他變體)。
(參數列表) {
函數體;
};
// 使用匿名函數實現上個列子中的效果
[1,2,3].forEach((e) {
print(e);
});
// 或者
[1,2,3].forEach( (e) => print(e) );
注意事項
- 同類的同一個方法在不同實例對象中不相等。
- 所有函數都有返回值,即使沒有顯示的返回語句(返回nil)。
運算符
運算符在編程語言中同樣占據著重要的地位,Dart 中同樣擁有并遵循的絕大部分的運算符效果,例如算術運算符、關系運算符、類型判斷運算符、邏輯運算符等等。我們主要關注一些特別的運算符的定義和使用場景。
-
算術運算符
~/: 除并且取整5 ~/ 2的結果為 2。 -
類型判斷運算符
Dart 作為一門面向對象語言,類型判斷運算符必不可免。常見的
as類型轉換和is類型判斷。Dart 中有一個和is相反的類型判斷運算符is!,當對象是指定類型則返回 false。 -
賦值運算符
=、+=、-=等等這樣的復合賦值運算符在編程語言中非常常見,Dart 中有??=這樣的復合運算符來為 null 的變量進行默認值的賦值。a ??= "xiaoming"; // a = a ?? "xiaoming"; -
級聯運算符
..可以讓你在同一個對象上連續(xù)調用多個對象的變量或者方法。querySelector('#confirm') // 獲取對象 (Get an object). ..text = 'Confirm' // 使用對象的成員 (Use its members). ..classes.add('important') ..onClick.listen((e) => window.alert('Confirmed!')); -
訪問運算符
.和?.,Dart 通過點語法訪問成員變量或者方法,?.則會加上一層判斷,如果操作對象為 null,則停止訪問并返回 null。
異常
Dart中處理異常沒有太多的差別,只是拋出的對象可以是任何非 null 對象而不局限于 Exception 或 Error 類型。
- 拋出異常
throw FormatException('Expected at least 1 section');
- 捕獲異常
try {
breedMoreLlamas();
} on OutOfLlamasException {
// 指定異常
buyMoreLlamas();
} on Exception catch (e) {
// 其它類型的異常
print('Unknown exception: $e');
} catch (e) {
// 不指定類型,處理其它全部
print('Something really unknown: $e');
} finally {
// 總是清理,即便拋出了異常。
cleanLlamaStalls();
}
類
Dart 是支持基于 mixin 繼承機制的面向對象語言,所有對象都是一個類的實例,所有的類都繼承自 Object 類?;?mixin 的繼承意味著除 Object 類之外都只能單繼承。Extension 方法是一種在不更改類型或創(chuàng)建子類的情況下向類添加功能的方法,類似于 Swift 中的 Extension,OC 中的分類。
構造函數
聲明一個與類名一樣的函數即可聲明一個構造函數。
class Point {
double x, y;
Point(double x, double y) {
this.x = x;
this.y = y;
}
// 或者使用 Dart 提供的語法糖
Point(this.x, this.y);
}
- 如果你沒有聲明構造函數,那么Dart 會自動生成一個無參的構造函數并且調用父類的無參構造方法。
- 構造函數不被繼承,子類不繼承父類的構造函數,如果子類不聲明自己的構造函數,那么只會有一個默認無參的構造函數。
調用父類非默認構造函數
默認情況下,本類的構造函數會調用父類的無參構造方法,并且先于本類的構造函數之前,如果本類構造函數存在一個初始化列表,那么調用父類的無參構造方法會在該列表的初始化之后,本類的構造函數之前,即下面的順序。
- 本類的初始化列表
- 父類的無參構造函數
- 本類的構造函數
如果父類沒有匿名無參構造函數,那么子類就必須在函數體前使用 : 指定調用父類其中一個構造函數。
class Person {
String firstName;
Person.fromJson(Map data) {
print('in Person');
}
}
class Employee extends Person {
// 指定構造函數
Employee.fromJson(Map data) : super.fromJson(data) {
print('in Employee');
}
}
main() {
var emp = new Employee.fromJson({});
// in Person
// in Employee
}
初始化列表
在構造函數體之前,我們還可以初始化實例變量。
Point.fromJson(Map<String, double> json)
: x = json['x'],
y = json['y'] {
print('In Point.fromJson(): ($x, $y)');
}
重定向構造函數
類似于便利構造器,我們可以將提供一些默認值后,調用類的其他構造器。
class Point {
double x, y;
// 該類的主構造函數。
Point(this.x, this.y);
// 委托實現給主構造函數。
Point.alongXAxis(double x) : this(x, 0);
}
Getter 和 Setter
Dart 中實例對象的每個屬性都有一個隱式的 Getter 方法,如果非 final 屬性的話還會有一個 Setter 方法,你可以使用 get 和 set 關鍵字為額外的屬性添加 Getter 和 Setter 方法。
class Rectangle {
double left, top, width, height;
Rectangle(this.left, this.top, this.width, this.height);
// 定義兩個計算產生的屬性:right 和 bottom。
double get right => left + width;
set right(double value) => left = value - width;
double get bottom => top + height;
set bottom(double value) => top = value - height;
}
抽象類
使用關鍵字 abstract 標識符可以讓該類成為抽象類,抽象類一般無法被實例化,常用于聲明接口方法,有時也會有具體的方法實現。
abstract class AbstractContainer {
// 定義構造函數、字段、方法等……
void updateChildren(); // 抽象方法。
}
隱式接口
每一個類都隱式地定義并實現了該接口,這個接口包含所有這個類的實例成員以及這個類所有實現的其他接口。如果你想要創(chuàng)建一個 A 類,它支持所有 B 類中的 API,但是不想繼承 B,那么就可以通過關鍵字 implements 實現 B 類中的隱式接口。
class Person {
// _name 變量同樣包含在接口中,但它只是庫內可見的。
final _name;
// 構造函數不在接口中。
Person(this._name);
// greet() 方法在接口中。
String greet(String who) => '你好,$who。我是$_name。';
}
// Person 接口的一個實現。
class Impostor implements Person {
get _name => '';
String greet(String who) => '你好$who。你知道我是誰嗎?';
}
如果需要實現多個類接口,可以使用逗號分割每個接口類:
class Point implements Comparable, Location {...}
創(chuàng)建子類
使用 extends 關鍵字來創(chuàng)建一個子類,并可使用 super 關鍵字引用一個父類:
class Television {
void turnOn() {
...
}
}
class SmartTelevision extends Television {
void turnOn() {
super.turnOn();
...
}
}
重寫
你可以使用 @override 注解來表示你重寫來一個成員。
class SmartTelevision extends Television {
@override
void turnOn() {...}
}
noSuchMethod()
如果調用了對象上不存在的方法或實例變量將會觸發(fā) noSuchMethod 方法,可以通過重寫該方法來追蹤和記錄這一行為。
class A {
// 除非你重寫 noSuchMethod,否則調用一個不存在的成員會導致 NoSuchMethodError。
@override
void noSuchMethod(Invocation invocation) {
print('你嘗試使用一個不存在的成員:' +
'${invocation.memberName}');
}
}
你不能調用一個未實現的方法除非下面其中的一個條件成立:
- 接收方是靜態(tài)的 dynamic 類型
- 接收方具有靜態(tài)類型,定義了未實現的方法/抽象方法,并且實現了 noSuchMethod 方法且實現與 Object 中的不同
Extension 擴展類
extension 是 Dart2.7 引入的,向現有庫添加功能的一種方式。
// 給 String 添加轉換為 Int 的功能
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
main() {
// 使用新的方法
print('123'.parseInt());
}
枚舉類型
一些固定數量的常量值,Dart 的枚舉功能沒有特別之處,我們看下常用的方法。
// 定義枚舉
enum Color { red, green, blue }
// 獲取索引
var idx = Color.red.index;
// 獲取所有的類型
List<Color> colors = Color.values;
Dart 中的枚舉類型還有兩個限制:
- 枚舉不能繼承,也不可以實現一個枚舉。
- 不能顯示地實例化一個枚舉類。
Mixin 模式
Mixin 是一種在多重繼承中復用某個類中代碼的方法模式。Dart 通過關鍵字 with 來使用 Mixin 模式。
mixin Athlete {
run() {
print("run.");
}
}
mixin Singer {
sing() {
print("Sing.");
}
}
class A with Athlete, Singer {
// ...
}
void main() {
var a = A();
a.run();
a.sing();
}
- Minix 類:定一個類,繼承自 Object 但不為該類定義構造函數
- 單純的 Mixin 類:可以使用關鍵字 mixin 代替 class
類變量和類方法
類變量和類方法也成為靜態(tài)變量和靜態(tài)方法,它們屬于類而不是某個實例變量。
在變量和方法前通過關鍵字 static 可以聲明類變量和類方法。
類方法不能被實例訪問,因此內部也不能使用 this。
泛型
泛型存在于很多編程語言中,例如 Python,Swfit,OC 等,Dart 語言同樣不例外,在之前 List<E> 這樣的聲明,使用到的就是泛型。<...> 符號表示一個泛型類,通常使用一個字母來表示類型參數,例如E、T、S、K 和 V 等。
泛型常用于需要類型安全的情況,有了它可以更好地生成代碼,減少代碼量。拿 List 來舉例,List<String> 讓數組中的元素都必須是字符串類型,當你放入非字符串類就會提前告知你錯誤,另外,通過泛型,我們可以只編寫一份通用的 List 操作就可以適用于多種類型的操作。
類似的,Set 和 Map 同樣可以使用泛型來約束元素內容。
var names = <String>['小蕓', '小芳', '小民'];
var uniqueNames = <String>{'小蕓', '小芳', '小民'};
var pages = <String, String>{
'index.html': '主頁',
'robots.txt': '網頁機器人提示',
'humans.txt': '我們是人類,不是機器'
};
var nameSet = Set<String>.from(names);
var views = Map<int, View>();
var names = List<String>();
names.addAll(['小蕓', '小芳', '小民']);
限制參數化類型
有時想要泛型只支持一定的類型時,可以通過關鍵字 extends 來限制泛型的范圍。
// 支持泛型,但是有約束范圍
class Foo<T extends SomeBaseClass> {...}
// 某類的子類
class Extender extends SomeBaseClass {...}
這樣你在使用 Foo 類時,就可以使用 SomeBaseClass 或者其子類作為泛型參數。
var a = Foo<SomeBaseClass>();
var b = Foo<Extender>();
var c = Foo<Object>(); // 非SomeBaseClass類型時,將發(fā)生錯誤
泛型方法
Dart 中的泛型還可以用在方法中,我們稱使用泛型的方法為泛型方法。
T first<T>(List<T> ts) {
// 處理一些初始化工作或錯誤檢測……
T tmp = ts[0];
// 處理一些額外的檢查……
return tmp;
}
- 方法的返回值為 T 類型
- 參數 ts 為 T 類型的數組
- 臨時變量的類型為 T 類型
庫和可見性
代碼庫不僅只是提供 API 而且還起到了封裝的作用,庫中的私有成員通過 _ 開頭聲明。每個 Dart 程序都是一個庫。
導入庫
對于 Dart 內置庫,使用 dart:xxx 的形式。
import 'dart:html';
而對于其他的庫,使用系統(tǒng)路徑或者以 package:xxx 的形式 package:xxx 指定的庫通過包管理器,如pub工具,來提供。
import 'package:test/test.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();
部分導入
// 導入 foo
import 'package:lib1/lib1.dart' show foo;
// 導入 除了 foo 外的所有
import 'package:lib2/lib2.dart' hide foo;
延遲加載庫
允許應用在需要的時候才去加載庫代碼,延遲加載有幾個好處:
- 減少應用的初始化時間
- 處理 A/B 測試,比如測試各種算法的不同實現
- 加載很少會使用到的功能
使用 deferred as 關鍵字來標識需要延時加載的代碼庫。
import 'package:greetings/hello.dart' deferred as hello;
當實際需要使用到庫中 API 時先調用 loadLibrary 函數加載庫。
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
- 延遲加載的代碼庫中的常量需要在代碼庫被加載的時候才會導入,未加載時候是不會導入的。
- 導入文件的時候無法使用延遲加載庫中的類型。
- Dart會隱式地將
loadLibrary方法導入到使用了deferred as命名空間 的類中。loadLibrary函數返回的是一個Future類型。
異步
Dart 通過 async 和 await 關鍵字實現異步編程,使用了這些關鍵字的函數會返回 Future 和 Stream 對象。
- Future 對象
必須在帶有 async 關鍵字的異步函數中使用 await。
// 異步函數
Future checkVersion() async {
var version = await lookUpVersion(); // 耗時操作
// 使用 version 繼續(xù)處理
}
你可以在異步函數中多次使用 await 關鍵字。
var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);
await 表達式的返回值通常是一個 Future 對象,如果不是 Dart 也會自動將其包裹在一個 Future 對象里。Future 對象代表一個“承諾”,await 表達式會阻塞到需要的對象返回。
- Stream 對象
想要從 Stream 中獲取值,需要使用 async 關鍵字或一個異步循環(huán) await for。
Future method() async {
await for (var request in requestList) {
// 返回類型為 Stream
handleRequest(request);
}
}
生成器
Dart 中的生成器和迭代器和 Python 中的類似,當你需要延遲地生成一連串的值時,可以考慮使用生成器函數。Dart 內置支持兩種形式的生成器方法。
- 同步 生成器:返回一個 Iterable 對象
- 異步 生成器:返回一個 Stream 對象
通過在函數上加 sync* 關鍵字并將返回值類型設置為 Iterable 來實現一個 同步 生成器函數,在函數中使用 yield 語句來傳遞值。
Iterable<int> naturalsTo(int n) sync* {
int k = 0;
while (k < n) yield k++;
}
實現 異步 生成器函數與同步類似,只不過關鍵字為 async* 并且返回值為 Stream。
Stream<int> asynchronousNaturalsTo(int n) async* {
int k = 0;
while (k < n) yield k++;
}
總結
- 所有變量引用都是對象,是類的實例,包括數字、函數以及 null。
- Dart 是強類型語言,變量在聲明是可以指定類型,也可以由Dart自行推斷出類型。
- Dart 支持泛型,List<int> 表示一組由 int 對象組成的數組。
- Dart 支持頂級函數,類方法,類變量等,你還可以進行函數嵌套和局部函數。
- Dart 的私有屬性通過
_開頭表示 - Dart 可以顯示警告和錯誤兩種類型問題。