本文是【从零开始学习,开发个Flutter App】路上的第 1 篇文章。
这篇文章介绍了 Dart 的基础特性,目的在于让大家建立对 Dart 语言的总体认知,初步掌握 Dart 的语法。
我们假定读者已经有一定的编程基础,如果你了解 JAVAScript 或者 Java 等面向对象语言,那 Dart 学习起来应该很有亲切感。
Dart 是一门采取众家之长的编程语言。尽管 Dart 很多语法和 JavaScript 很相似,但 Dart 语言同时是一门强类型的语言,它同时结合了像 Java 这样强类型面向对象语言的特性,这使得它能胜任大型应用开发,同时它没有 Java 的臃肿,Dart 语言在设计上非常简洁、灵活和高效。
JavaScript 从简单的浏览器脚本到服务端(nodejs),慢慢延伸到PC客户端(electron)、App (React Native)甚至小程序开发,它已然成为一门真正意义上的全栈开发语言。
如果说 JavaScript 是在漫长的时光里野蛮生长,那 Dart 从一开始就是精心设计出来的。如果说有一门语言取代JavaScript的位置,那很可能就是Dart。
Talk is cheep,下面就让我们来亲自感受一下这门语言的吧。
你可以像 JavaScript 那样声明一个变量:
var name = 'Bob';
编译器会推导出 name 的类型是String 类型,等价于:
String name = 'Bob';
我们可以从下面代码窥见 Dart 是强类型语言的特性:
var name = 'Bob';
// 调用 String 的方法
print(name.toLowerCase());
// 编译错误
// name = 1;
前面我们说过,Dart 除了具备简洁的特点,而且也可以是非常灵活的,如果你想变换一个变量的类型,你也可以使用dynamic 来声明变量,这就跟 JavaScript 一样了:
dynamic name = 'Bob'; //String 类型
name = 1;// int 类型
print(name);
上面的代码可以正常编译和运行,但除非你有足够的理由,请不要轻易使用。
final 的语义和 Java 的一样,表示该变量是不可变的:
// String 可以省略
final String name = 'Bob';
// 编译错误
// name = 'Mary';
其中 String 可以省略,Dart 编译器足够聪明地知道变量name 的类型。
如果要声明常量,可以使用const 关键词:
const PI = '3.14';
class Person{
static const name = 'KK';
}
如果类变量,则需要声明为static const 。
不像Java把类型分的特别细,比如整数类型,就有byte、short、int 、long 。Dart 的类型设计相当简洁,这也是 Dart 容易上手的原因之一,可以理解为通过牺牲空间来换取效率吧。
Dart 内置支持两种数值类型,分别是int 和double ,它们的大小都是64位。
var x = 1;
// 0x开头为16进制整数
var hex = 0xDEADBEEF;
var y = 1.1;
// 指数形式
var exponents = 1.42e5;
需要注意的是,在Dart中,所有变量值都是一个对象,int和double类型也不例外,它们都是num类型的子类,这点和Java和JavaScript都不太一样:
// 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');
Dart 字符串使用的是UTF-16编码。
var s = '中';
s.codeUnits.forEach((ch) => print(ch));
// 输出为UNICODE值
20013
Dart 采用了 JavaScript 中类似模板字符串的概念,可以在字符串通过${expression}语法插入变量:
var s = "hello";
print('${s}, world!');
//可以简化成:
print('$s, world!');
//调用方法
print('${s.toUpperCase()}, world!');
Dart 可以直接通过==来比较字符串:
var s1 = "hello";
var s2 = "HELLO";
assert(s1.toUpperCase() == s2);
Dart 布尔类型对应为bool关键词,它有true和false两个值,这点和其他语言区别不大。值得一提的是,在Dart的条件语句if和assert表达式里面,它们的值必须是bool类型,这点和 JavaScript 不同。
var s = '';
assert(s.isEmpty);
if(s.isNotEmpty){
// do something
}
//编译错误,在JavaScript常用来判断undefined
if(s){
}
你可以把Dart中的List对应到 JavaScript 的数组或者 Java 中的ArrayList,但 Dart 的设计更为精巧。
你可以通过类似 JavaScript 一样声明一个数组对象:
var list = [];
list.add('Hello');
list.add(1);
这里List容器接受的类型是dynamic,你可以往里面添加任何类型的对象,但如果像这样声明:
var iList = [1,2,3];
iList.add(4);
//编译错误 The argument type 'String' can't be assigned to the parameter type 'int'
//iList.add('Hello');
那么Dart就会推导出这个List是个List<int>,从此这个List就只能接受int类型数据了,你也可以显式声明List的类型:
var sList = List<String>();
//在Flutter类库中,有许多这样的变量声明:
List<Widget> children = const <Widget>[];
上面右边那个 const 的意思表示常量数组,在这里你可以理解为一个给children赋值了一个编译期常量空数组,这样的做法可以很好的节省内存,下面的例子可以让大家更好的理解常量数组的概念:
var constList = const <int>[1,2];
constList[0] = 2; //编译通过, 运行错误
constList.add(3); //编译通过, 运行错误
Dart2.3 增加了扩展运算符 (spread operator) ... 和...?,通过下面的例子你很容易就明白它们的用法:
var list = [1, 2, 3];
var list2 = [0, ...list];
assert(list2.length == 4);
如果扩展对象可能是null,可以使用...?:
var list;
var list2 = [0, ...?list];
assert(list2.length == 1);
你可以直接在元素内进行判断,决定是否需要某个元素:
var promoActive = true;
var nav = [
'Home',
'Furniture',
'Plants',
promoActive? 'About':'Outlet'
];
甚至使用for来动态添加多个元素:
var listOfInts = [1, 2, 3];
var listOfStrings = [
'#0',
for (var i in listOfInts) '#$i'
];
assert(listOfStrings[1] == '#1');
这种动态的能力使得 Flutter 在构建 Widget 树的时候非常方便。
Set的语意和其他语言的是一样的,都是表示在容器中对象唯一。在Dart中,Set默认是LinkedHashSet实现,表示元素按添加先后顺序排序。
声明Set对象:
var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};
遍历Set,遍历除了上面提到的for...in,你还可以使用类似 Java 的 lambada 中的 forEach 形式:
halogens.add('bromine');
halogens.add('astatine');
halogens.forEach((el) => print(el));
输出结果:
fluorine
chlorine
bromine
iodine
astatine
除了容器的对象唯一特性之外,其他基本和List是差不多的。
// 添加类型声明:
var elements = <String>{};
var promoActive = true;
// 动态添加元素
final navSet = {'Home', 'Furniture', promoActive? 'About':'Outlet'};
Map对象的声明方式保持了 JavaScript 的习惯,Dart 中Map的默认实现是LinkedHashMap,表示元素按添加先后顺序排序。
var gifts = {
// Key: Value
'first': 'partridge',
'second': 'turtledoves',
'fifth': 'golden rings'
};
assert(gifts['first'] == 'partridge');
添加一个键值对:
gifts['fourth'] = 'calling birds';
遍历Map:
gifts.forEach((key,value) => print('key: $key, value: $value'));
在 Dart 中,函数本身也是个对象,它对应的类型是Function,这意味着函数可以当做变量的值或者作为一个方法入传参数值。
void sayHello(var name){
print('hello, $name');
}
void callHello(Function func, var name){
func(name);
}
void main(){
// 函数变量
var helloFuc = sayHello;
// 调用函数
helloFuc('Girl');
// 函数参数
callHello(helloFuc,'Boy');
}
输出:
hello, Girl
hello, Boy
对于只有一个表达式的简单函数,你还可以通过=>让函数变得更加简洁,=> expr在这里相当于{ return expr; } ,我们来看一下下面的语句:
String hello(var name ) => 'hello, $name';
相当于:
String hello(var name ){
return 'hello, $name';
}
在Flutter UI库里面,命名参数随处可见,下面是一个使用了命名参数(Named parameters)的例子:
void enableFlags({bool bold, bool hidden}) {...}
调用这个函数:
enableFlags(bold: false);
enableFlags(hidden: false);
enableFlags(bold: true, hidden: false);
命名参数默认是可选的,如果你需要表达该参数必传,可以使用@required:
void enableFlags({bool bold, @required bool hidden}) {}
当然,Dart 对于一般的函数形式也是支持的:
void enableFlags(bool bold, bool hidden) {}
和命名参数不一样,这种形式的函数的参数默认是都是要传的:
enableFlags(false, true);
你可以使用[]来增加非必填参数:
void enableFlags(bool bold, bool hidden, [bool option]) {}
另外,Dart 的函数还支持设置参数默认值:
void enableFlags({bool bold = false, bool hidden = false}) {...}
String say(String from, [String device = 'carrier pigeon', String mood]) {}
顾名思意,匿名函数的意思就是指没有定义函数名的函数。你应该对此不陌生了,我们在遍历List和Map的时候已经使用过了,通过匿名函数可以进一步精简代码:
var list = ['apples', 'bananas', 'oranges'];
list.forEach((item) {
print('${list.indexOf(item)}: $item');
});
Dart支持闭包。没有接触过JavaScript的同学可能对闭包(closure)比较陌生,这里给大家简单解释一下闭包。
闭包的定义比较拗口,我们不去纠结它的具体定义,而是打算通过一个具体的例子去理解它:
Function closureFunc() {
var name = "Flutter"; // name 是一个被 init 创建的局部变量
void displayName() { // displayName() 是内部函数,一个闭包
print(name); // 使用了父函数中声明的变量
}
return displayName;
}
void main(){
//myFunc是一个displayName函数
var myFunc = closureFunc(); //(1)
// 执行displayName函数
myFunc(); // (2)
}
结果如我们所料的那样打印了Flutter。
在(1)执行完之后,name作为一个函数的局部变量,引用的对象不是应该被回收掉了吗?但是当我们在内函数调用外部的name时,它依然可以神奇地被调用,这是为什么呢?
这是因为Dart在运行内部函数时会形成闭包,闭包是由函数以及创建该函数的词法环境组合而成,这个环境包含了这个闭包创建时所能访问的所有局部变量 。
我们简单变一下代码:
Function closureFunc() {
var name = "Flutter"; // name 是一个被 init 创建的局部变量
void displayName() { // displayName() 是内部函数,一个闭包
print(name); // 使用了父函数中声明的变量
}
name = 'Dart'; //重新赋值
return displayName;
}
结果输出是Dart,可以看到内部函数访问外部函数的变量时,是在同一个词法环境中的。
在Dart中,所有的函数都必须有返回值,如果没有的话,那将自动返回null:
foo() {}
assert(foo() == null);
这部分和大部分语言都一样,在这里简单过一下就行。
if(hasHause && hasCar){
marry();
}else if(isHandsome){
date();
}else{
pass();
}
各种for:
var list = [1,2,3];
for(var i = 0; i != list.length; i++){}
for(var i in list){}
while和循环中断(中断也是在for中适用的):
var i = 0;
while(i != list.length){
if(i % 2 == 0){
continue;
}
print(list[i]);
}
i = 0;
do{
print(list[i]);
if(i == 5){
break;
}
}while(i != list.length);
如果对象是Iterable类型,你还可以像Java的 lambada 表达式一样:
list.forEach((i) => print(i));
list.where((i) =>i % 2 == 0).forEach((i) => print(i));
switch可以用于int、double、String 和enum等类型,switch 只能在同类型对象中进行比较,进行比较的类不要覆盖==运算符。
var color = '';
switch(color){
case "RED":
break;
case "BLUE":
break;
default:
}
在Dart中,assert语句经常用来检查参数,它的完整表示是:assert(condition, optionalMessage),如果condition为false,那么将会抛出[AssertionError]异常,停止执行程序。
assert(text != null);
assert(urlString.startsWith('https'), 'URL ($urlString) should start with "https".');
assert 通常只用于开发阶段,它在产品运行环境中通常会被忽略。在下面的场景中会打开assert:
Dart 的异常处理和Java很像,但是Dart中所有的异常都是非检查型异常(unchecked exception),也就是说,你不必像 Java 一样,被强制需要处理异常。
Dart 提供了Exception 和 Error 两种类型的异常。 一般情况下,你不应该对Error类型错误进行捕获处理,而是尽量避免出现这类错误。
比如OutOfMemoryError、StackOverflowError、NoSuchMethodError等都属于Error类型错误。
前面提到,因为 Dart 不像 Java 那样可以声明编译期异常,这种做法可以让代码变得更简洁,但是容易忽略掉异常的处理,所以我们在编码的时候,在可能会有异常的地方要注意阅读API文档,另外自己写的方法,如果有异常抛出,要在注释处进行声明。比如类库中的File类其中一个方法注释:
/**
* Synchronously read the entire file contents as a list of bytes.
*
* Throws a [FileSystemException] if the operation fails.
*/
Uint8List readAsBytesSync();
throw FormatException('Expected at least 1 section');
throw除了可以抛出异常对象,它还可以抛出任意类型对象,但建议还是使用标准的异常类作为最佳实践。
throw 'Out of llamas!';
可以通过on 关键词来指定异常类型:
var file = File("1.txt");
try{
file.readAsStringSync();
} on FileSystemException {
//do something
}
使用catch关键词获取异常对象,catch有两个参数,第一个是异常对象,第二个是错误堆栈。
try{
file.readAsStringSync();
} on FileSystemException catch (e){
print('exception: $e');
} catch(e, s){ //其余类型
print('Exception details:n $e');
print('Stack trace:n $s');
}
使用rethrow 抛给上一级处理:
try{
file.readAsStringSync();
} on FileSystemException catch (e){
print('exception: $e');
} catch(e){
rethrow;
}
finally一般用于释放资源等一些操作,它表示最后一定会执行的意思,即便try...catch中有return,它里面的代码也会承诺执行。
try{
print('hello');
return;
} catch(e){
rethrow;
} finally{
print('finally');
}
输出:
hello
finally
Dart 是一门面向对象的编程语言,所有对象都是某个类的实例,所有类继承了Object类。
一个简单的类:
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);
}
}
Dart 通过. 来调用类成员变量和方法的。
//创建对象,new 关键字可以省略
var p = Point(2, 2);
// Set the value of the instance variable y.
p.y = 3;
// Get the value of y.
assert(p.y == 3);
// Invoke distanceTo() on p.
num distance = p.distanceTo(Point(4, 4));
你还可以通过.?来避免null对象。在Java 里面,经常需要大量的空判断来避免NullPonterException,这是让人诟病Java的其中一个地方。而在Dart中,可以很方便地避免这个问题:
// If p is non-null, set its y value to 4.
p?.y = 4;
在 Dart 中,没有private、protected、public这些关键词,如果要声明一个变量是私有的,则在变量名前添加下划线_,声明了私有的变量,只在本类库中可见。
class Point{
num _x;
num _y;
}
如果没有声明构造器,Dart 会给类生成一个默认的无参构造器,声明一个带参数的构造器,你可以像 Java这样:
class Person{
String name;
int sex;
Person(String name, int sex){
this.name = name;
this.sex = sex;
}
}
也可以使用简化版:
Person(this.name, this.sex);
或者命名式构造器:
Person.badGirl(){
this.name = 'Bad Girl';
this.sex = 1;
}
你还可以通过factory关键词来创建实例:
Person.goodGirl(){
this.name = 'good Girl';
this.sex = 1;
}
factory Person(int type){
return type == 1 ? Person.badGirl(): Person.goodGirl();
}
factory对应到设计模式中工厂模式的语言级实现,在 Flutter 的类库中有大量的应用,比如Map:
// 部分代码
abstract class Map<K, V> {
factory Map.from(Map other) = LinkedHashMap<K, V>.from;
}
如果一个对象的创建过程比较复杂,比如需要选择不同的子类实现或则需要缓存实例等,你就可以考虑通过这种方法。在上面Map例子中,通过声明 factory来选择了创建子类LinkedHashMap(LinkedHashMap.from也是一个factory,里面是具体的创建过程)。
如果你想在对象创建之前的时候还想做点什么,比如参数校验,你可以通过下面的方法:
Person(this.name, this.sex): assert(sex == 1)
在构造器后面添加的一些简单操作叫做initializer list。
在Dart中,初始化的顺序如下:
class Person{
String name;
int sex;
Person(this.sex): name = 'a', assert(sex == 1){
this.name = 'b';
print('Person');
}
}
class Man extends Person{
Man(): super(1){
this.name = 'c';
print('Man');
}
}
void main(){
Person person = Man();
print('name : ${person.name}');
}
上面的代码输出为:
Person
Man
name : c
如果子类构造器没有显式调用父类构造器,那么默认会调用父类的默认无参构造器。显式调用父类的构造器:
Man(height): this.height = height, super(1);
重定向构造器:
Man(this.height, this.age): assert(height > 0), assert(age > 0);
Man.old(): this(12, 60); //调用上面的构造器
在 Dart 中,对 Getter 和 Setter 方法有专门的优化。即便没有声明,每个类变量也会默认有一个get方法,在隐含接口章节会有体现。
class Rectangle {
num left, top, width, 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;
}
void main() {
var rect = Rectangle(3, 4, 20, 15);
assert(rect.left == 3);
rect.right = 12;
assert(rect.left == -8);
}
Dart 的抽象类和Java差不多,除了不可以实例化,可以声明抽象方法之外,和一般类没有区别。
abstract class AbstractContainer {
num _width;
void updateChildren(); // 抽象方法,强制继承子类实现该方法。
get width => this._width;
int sqrt(){
return _width * _width;
}
}
Dart 中的每个类都隐含了定义了一个接口,这个接口包含了这个类的所有成员变量和方法,你可以通过implements关键词来重新实现相关的接口方法:
class Person {
//隐含了 get 方法
final _name;
Person(this._name);
String greet(String who) => 'Hello, $who. I am $_name.';
}
class Impostor implements Person {
// 需要重新实现
get _name => '';
// 需要重新实现
String greet(String who) => 'Hi $who. Do you know who I am?';
}
实现多个接口:
class Point implements Comparable, Location {...}
和Java基本一致,继承使用extends关键词:
class Television {
void turnOn() {
doSomthing();
}
}
class SmartTelevision extends Television {
@override
void turnOn() {
super.turnOn(); //调用父类方法
doMore();
}
}
比较特别的是,Dart 还允许重载操作符,比如List类支持的下标访问元素,就定义了相关的接口:
E operator [](int index);
我们通过下面的实例来进一步说明重载操作符:
class MyList{
var list = [1,2,3];
operator [](int index){
return list[index];
}
}
void main() {
var list = MyList();
print(list[1]); //输出 2
}
这个特性也是Dart让人眼前一亮的地方(Dart2.7之后才支持),可以对标到 JavaScript 中的 prototype。通过这个特性,你甚至可以给类库添加新的方法:
//通过关键词 extension 给 String 类添加新方法
extension NumberParsing on String {
int parseInt() {
return int.parse(this);
}
}
后面String对象就可以调用该方法了:
print('42'.parseInt());
枚举类型和保持和Java的关键词一致:
enum Color { red, green, blue }
在switch中使用:
// color 是 enmu Color 类型
switch(color){
case Color.red:
break;
case Color.blue:
break;
case Color.green:
break;
default:
break;
}
枚举类型还有一个index的getter,它是个连续的数字序列,从0开始:
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);
这个特性进一步增强了代码复用的能力,如果你有写过Android的布局XML代码或者Freemaker模板的话,那这个特性就可以理解为其中inlclude 的功能。
声明一个mixin类:
mixin 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');
}
}
}
通过with关键词进行复用:
class Musician extends Performer with Musical {
// ···
}
class Maestro extends Person
with Musical, Aggressive, Demented {
Maestro(String maestroName) {
name = maestroName;
canConduct = true;
}
}
mixin类甚至可以通过on关键词实现继承的功能:
mixin MusicalPerformer on Musician {
// ···
}
class Queue {
//类变量
static int maxLength = 1000;
// 类常量
static const initialCapacity = 16;
// 类方法
static void modifyMax(int max){
_maxLength = max;
}
}
void main() {
print(Queue.initialCapacity);
Queue.modifyMax(2);
print(Queue._maxLength);
}
在面向对象的语言中,泛型主要的作用有两点:
1、类型安全检查,把错误扼杀在编译期:
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
//编译错误
names.add(42);
2、增强代码复用,比如下面的代码:
abstract class ObjectCache {
Object getByKey(String key);
void setByKey(String key, Object value);
}
abstract class StringCache {
String getByKey(String key);
void setByKey(String key, String value);
}
你可以通过泛型把它们合并成一个类:
abstract class Cache<T> {
T getByKey(String key);
void setByKey(String key, T value);
}
在Java中,泛型是通过类型擦除来实现的,但在Dart中实打实的泛型:
var names = <String>[];
names.addAll(['Tom',"Cat"]);
// is 可以用于类型判断
print(names is List<String>); // true
print(names is List); // true
print(names is List<int>); //false
你可以通过extends关键词来限制泛型类型,这点和Java一样:
abstract class Animal{}
class Cat extends Animal{}
class Ext<T extends Animal>{
T data;
}
void main() {
var e = Ext(); // ok
var e1 = Ext<Animal>(); // ok
var e2 = Ext<Cat>(); // ok
var e3 = Ext<int>(); // compile error
}
有生命力的编程语言,它背后都有一个强大的类库,它们可以让我们站在巨人的肩膀上,又免于重新造轮子。
在Dart里面,通过import关键词来导入类库。
内置的类库使用dart:开头引入:
import 'dart:io';
了解更多内置的类库可以查看这里。
第三方类库或者本地的dart文件用package:开头:
比如导入用于网络请求的dio库:
import 'package:dio/dio.dart';
Dart 应用本身就是一个库,比如我的应用名是ccsys,导入其他文件夹的类:
import 'package:ccsys/common/net_utils.dart';
import 'package:ccsys/model/user.dart';
如果你使用IDE来开发,一般这个事情不用你来操心,它会自动帮你导入的。
Dart 通过pub.dev来管理类库,类似Java世界的Maven 或者Node.js的npm一样,你可以在里面找到非常多实用的库。
如果导入的类库有类名冲突,可以通过as使用别名来避免这个问题:
import 'package:lib1/lib1.dart';
import 'package:lib2/lib2.dart' as lib2;
// 使用来自 lib1 的 Element
Element element1 = Element();
// 使用来自 lib2 的 Element
lib2.Element element2 = lib2.Element();
在一个dart文件中,可能会存在很多个类,如果你只想引用其中几个,你可以增加show或者hide来处理:
//文件:my_lib.dart
class One {}
class Two{}
class Three{}
使用show导入One和Two类:
//文件:test.dart
import 'my_lib.dart' show One, Two;
void main() {
var one = One();
var two = Two();
//compile error
var three = Three();
}
也可以使用hide排除Three,和上面是等价的:
//文件:test.dart
import 'my_lib.dart' hide Three;
void main() {
var one = One();
var two = Two();
}
目前只有在web app(dart2js)中才支持延迟加载,Flutter、Dart VM是不支持的,我们这里仅做一下简单介绍。
你需要通过deferred as来声明延迟加载该类库:
import 'package:greetings/hello.dart' deferred as hello;
当你需要使用的时候,通过loadLibrary()加载:
Future greet() async {
await hello.loadLibrary();
hello.printGreeting();
}
你可以多次调用loadLibrary,它不会被重复加载。
这个话题稍微复杂,我们将用另外一篇文章独立讨论这个问题,请留意下一篇内容。
我们是一支由资深独立开发者和设计师组成的团队,成员均有扎实的技术实力和多年的产品设计开发经验,提供可信赖的软件定制服务。