Flutter 学习笔记 2:Dart 常用语法特性

一、重要的概念

  • 任何保存在变量中的都是一个对象,并且所有的对象都是对应一个的实例。 无论是数字,函数和 null 都是对象。所有对象继承自Object类。

  • 尽管 Dart 是强类型的,但是 Dart 可以推断类型,所以可以不用写类型。如果要明确说明不需要任何类型,需要使用特殊类型dynamic

  • Dart 支持泛型,如 List <int> (整数列表)或 List <dynamic> (任何类型的对象列表)。

  • Dart 支持顶级函数(例如 main() ),同样函数绑定在类或对象上(分别是 静态函数 和 实例函数 )。 以及支持函数内创建函数 ( 嵌套 或 局部函数 ) 。

  • 类似地, Dart 支持顶级变量,同样变量绑定在类或对象上(静态变量和实例变量)。 实例变量有时称为字段或属性。

  • 与 Java 不同,Dart 没有关键字 “public” , “protected” 和 “private” 。 如果标识符以下划线(_)开头,则它相对于库是私有的。

  • 标识符以字母或下划线(_)开头,后跟任意字母和数字组合。

  • Dart 语法中包含表达式( expressions )(有运行时值)和 语句( statements )(没有运行时值)。 例如,条件表达式 condition ? expr1 : expr2 的值可能是 expr1 或 expr2 。 将其与 if-else 语句相比较,if-else 语句没有值。 一条语句通常包含一个或多个表达式,相反表达式不能直接包含语句。

  • Dart 工具提示两种类型问题:警告错误。 警告只是表明代码可能无法正常工作,但不会阻止程序的执行。 错误可能是编译时错误或者运行时错误。 编译时错误会阻止代码的执行; 运行时错误会导致代码在执行过程中引发 [异常](#exception)。

关于强类型和弱类型,可以参考:

编程语言傻傻分不清:弱类型、强类型、动态类型、静态类型

二、变量

1.声明关键字:var

类似于 JavaScript 中的 var,它可以接收任何类型的变量,但最大的不同是 Dart 中 var 变量一旦赋值,类型便会确定,则不能再改变其类型,如:

void main() {
  var name = 1000;
  //下面的代码会报错
  name = "hello world";
}

错误信息:

Error compiling to JavaScript:
main.dart:3:10:
Error: A value of type 'String' can't be assigned to a variable of type 'int'.
  name = "hello world";
         ^
Error: Compilation failed.

2.dynamic 和 Object

Object是 Dart 所有对象的根基类,也就是说所有类型都是 Object 的子类(包括 Function 和 Null),所以任何类型的数据都可以赋值给 Object 声明的对象。dynamic 与var 一样都是关键词,声明的变量可以赋值任意对象。而dynamic 与 Object 相同之处在于,他们声明的变量可以在后期改变赋值类型。

void main() {
  dynamic name;
  Object age;
  name = "hello world";
  age = 'hi Object';
  //下面代码没有问题
  name = 1000;
  age = 1000;
}

dynamic 与 Object 不同的是,dynamic 声明的对象编译器会提供所有可能的组合, 而 Object 声明的对象只能使用Object 的属性与方法, 否则编译器会报错。如:

dynamic a;
Object b;
main() {
  a = "";
  b = "";
  printLengths();
}

printLengths() {
  // no warning
  print(a.length);
  // warning:
  // The getter 'length' is not defined for the class 'Object'
  print(b.length);
}

3.final和const

如果从未打算更改一个变量,那么使用finalconst,不是 var,也不是一个类型。 一个 final 变量只能被设置一次,两者区别在于:const 变量是一个编译时常量,final 变量在第一次使用时被初始化。被final或者const修饰的变量,变量类型可以省略,如:

  //可以省略String这个类型声明
  final str = "hi world";
  //final String str = "hi world";
  const str1 = "hi world";
  //const String str1 = "hi world";

三、内建类型

Dart 语言支持以下内建类型:

  • Number
  • String
  • Boolean
  • List (也被称为 Array)
  • Map
  • Set
  • Rune (用于在字符串中表示 Unicode 字符)
  • Symbol

1.Number
Number 包含两种类型 :int 和 float

int: 整数值不大于64位, 具体取决于平台。 在 Dart VM 上, 值的范围从 -263 到 263 - 1。

double: 64位(双精度)浮点数,依据 IEEE 754 标准。

字符串转为数字的方法:

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

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

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

// double -> String
String piAsString = 3.14159.toStringAsFixed(2);
//保留小数点后两位

2.String

Dart 字符串是一组 UTF-16 单元序列。 字符串通过单引号或者双引号创建。

var s1 = 'Single quotes work well for string literals.';
var s2 = "Double quotes work just as well.";
var s3 = 'It\'s easy to escape the string delimiter.';
var s4 = "It's even easier to use the other delimiter.";

字符串可以通过 ${expression} 的方式内嵌表达式。 如果表达式是一个标识符,则 {} 可以省略。 在 Dart 中通过调用就对象的 toString() 方法来得到对象相应的字符串。

void main() {
  var s = 'string interpolation';

  print('Dart has $s, which is very handy.' ==
      'Dart has string interpolation, ' + 'which is very handy.');
  print('That deserves all caps. ' + '${s.toUpperCase()} is very handy!' ==
      'That deserves all caps. ' + 'STRING INTERPOLATION is very handy!');
}
//console
true
true

== 运算符用来测试两个对象是否相等。 在字符串中,如果两个字符串包含了相同的编码序列,那么这两个字符串相等。

字符串拼接:

可以使用 + 运算符来把多个字符串连接为一个,也可以把多个字面量字符串写在一起来实现字符串连接:

void main() {
  var s1 = 'String '
      'concatenation'
      " works even over line breaks.";
  print(s1 ==
      'String concatenation works even over '
          'line breaks.');

  var s2 = 'The + operator ' + 'works, as well.';
  print(s2 == 'The + operator works, as well.');
}
//console
true
true

使用连续三个单引号或者三个双引号实现多行字符串对象的创建:

void main() {
  var s1 = '''
You can create
multi-line strings like this one.
''';

  var s2 = """This is also a
multi-line string.""";
  
  print(s1);
  print(s2);
}
//console
You can create
multi-line strings like this one.

This is also a
multi-line string.

3.Boolean

Dart 使用 bool 类型表示布尔值。 Dart 只有字面量 true and false 是布尔类型, 这两个对象都是编译时常量。

4.List

有序的对象集合。

var list = [1, 2, 3];

访问 List 的元素:

void main() {
  var list = [1, 2, 3];
  print(list[0]);
}
1

5.set

在 Dart 中 Set 是一个元素唯一且无序的集合。 Dart 为 Set 提供了 Set 字面量和 Set 类型。

下面是通过字面量创建 Set 的一个简单示例:

var halogens = {'fluorine', 'chlorine', 'bromine', 'iodine', 'astatine'};

要创建一个空集,使用前面带有类型参数的 {} ,或者将 {} 赋值给 Set 类型的变量:

var names = <String>{};
// Set<String> names = {}; // 这样也是可以的。
// var names = {}; // 这样会创建一个 Map ,而不是 Set 。

6.Map

通常来说, Map 是用来关联 keys 和 values 的对象。 keys 和 values 可以是任何类型的对象。在一个 Map 对象中一个 key 只能出现一次。 但是 value 可以出现多次。 Dart 中 Map 通过 Map 字面量 和 Map 类型来实现。

下面是使用 Map 字面量的两个简单例子:

var gifts = {
  // Key:    Value
  'first': 'partridge',
  'second': 'turtledoves',
  'fifth': 'golden rings'
};

var nobleGases = {
  2: 'helium',
  10: 'neon',
  18: 'argon',
};

以上 Map 对象也可以使用 Map 构造函数创建:

var gifts = Map();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';
gifts['fifth'] = 'golden rings';

var nobleGases = Map();
nobleGases[2] = 'helium';
nobleGases[10] = 'neon';
nobleGases[18] = 'argon';

添加 key-value 对到已有的 Map 中:

var gifts = {'first': 'partridge'};
gifts['fourth'] = 'calling birds'; // Add a key-value pair

从一个 Map 中获取一个 value:

var gifts = {'first': 'partridge'};

如果 Map 中不包含所要查找的 key,那么 Map 返回 null:

void main() {
  var gifts = {'first': 'partridge'};
  print(gifts['fifth']);
}
//console
null

7.Rune

在 Dart 中, Rune 用来表示字符串中的 UTF-32 编码字符。

四、函数

Dart 是一门真正面向对象的语言, 甚至其中的函数也是对象,并且有它的类型 Function 。 这也意味着函数可以被赋值给变量或者作为参数传递给其他函数。 也可以把 Dart 类的实例当做方法来调用。

(一)可选参数

可选参数可以是命名参数或者位置参数,但一个参数只能选择其中一种方式修饰。

  • 命名可选参数

定义函数时,使用 {param1, param2, …} 来指定命名参数:

/// Sets the [bold] and [hidden] flags ...
void enableFlags({bool bold, bool hidden}) {...}

调用函数时,可以使用指定命名参数 paramName: value。 例如:

enableFlags(bold: true, hidden: false);
  • 位置可选参数

将参数放到 [] 中来标记参数是可选的:

String say(String from, String msg, [String device]) {
  var result = '$from says $msg';
  if (device != null) {
    result = '$result with a $device';
  }
  return result;
}

不使用可选参数:

print(say("Bob","hello"));
//console
Bob says hello

使用可选参数

print(say("Bob","hello","smoke signal"));
//console
Bob says hello with a smoke signal

(二)默认参数

在定义方法的时候,可以使用 = 来定义可选参数的默认值。 默认值只能是编译时常量。 如果没有提供默认值,则默认值为 null。

下面是设置可选参数默认值示例:

/// 设置 [bold] 和 [hidden] 标志 ...
void enableFlags({bool bold = false, bool hidden = false}) {...}

// bold 值为 true; hidden 值为 false.
enableFlags(bold: true);

下面示例演示了如何为位置参数设置默认值:

void main() {
  print(say('Bob', 'Howdy') == 'Bob says Howdy with a carrier pigeon');
}

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;
}

list 或 map 可以作为默认值传递。 下面的示例定义了一个方法 doStuff(), 并分别指定参数 list 和 gifts 的默认值。

void main() {
  doStuff();
}

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');
}

(三)函数是一等对象

一个函数可以作为另一个函数的参数。 例如:

void printElement(int element) {
  print(element);
}

var list = [1, 2, 3];

// 将 printElement 函数作为参数传递。
list.forEach(printElement);

同样可以将一个函数赋值给一个变量,例如:

var loudify = (msg) => '!!! ${msg.toUpperCase()} !!!';
assert(loudify('hello') == '!!! HELLO !!!');

五、异步支持

(一)Future

Future 与 JavaScript 中的 Promise 非常相似,表示一个异步操作的最终完成(或失败)及其结果值的表示。简单来说,它就是用于处理异步操作的,异步处理成功了就执行成功的操作,异步处理失败了就捕获错误或者停止后续操作。一个 Future 只会对应一个结果,要么成功,要么失败。

由于本身功能较多,这里我们只介绍其常用的 API 及特性。还有,请记住,Future 的所有 API 的返回值仍然是一个 Future 对象,所以可以很方便的进行链式调用。

  • Future.then

为了方便示例,在本例中我们使用 Future.delayed 创建了一个延时任务(实际场景会是一个真正的耗时任务,比如一次网络请求),即 2 秒后返回结果字符串"hi world!",然后我们在 then 中接收异步结果并打印结果,代码如下:

void main() {
  Future.delayed(new Duration(seconds: 2), () {
    return "hello world";
  }).then((data) {
    print(data);
  });
}
  • Future.catchError

如果异步任务发生错误,我们可以在catchError中捕获错误,我们将上面示例改为:

void main() {
  Future.delayed(new Duration(seconds: 2), () {
    //return "hi world!";
    throw AssertionError("Error");
  }).then((data) {
    //执行成功会走到这里
    print("success");
  }).catchError((e) {
    //执行失败会走到这里
    print(e);
  });
}

在本示例中,我们在异步任务中抛出了一个异常,then的回调函数将不会被执行,取而代之的是 catchError 回调函数将被调用;但是,并不是只有 catchError 回调才能捕获错误,then 方法还有一个可选参数 onError,我们也可以它来捕获异常:

Future.delayed(new Duration(seconds: 2), () {
    //return "hi world!";
    throw AssertionError("Error");
}).then((data) {
    print("success");
}, onError: (e) {
    print(e);
});
  • Future.whenComplete

有些时候,我们会遇到无论异步任务执行成功或失败都需要做一些事的场景,比如在网络请求前弹出加载对话框,在请求结束后关闭对话框。这种场景,有两种方法,第一种是分别在 then 或 catch 中关闭一下对话框,第二种就是使用 Future 的 whenComplete 回调,我们将上面示例改一下:

void main() {
  Future.delayed(new Duration(seconds: 2), () {
    //return "hi world!";
    throw AssertionError("Error");
  }).then((data) {
    //执行成功会走到这里
    print(data);
  }).catchError((e) {
    //执行失败会走到这里
    print(e);
  }).whenComplete(() {
    //无论成功或失败都会走到这里
    print("end");
  });
}
  • Future.wait

有些时候,我们需要等待多个异步任务都执行结束后才进行一些操作,比如我们有一个界面,需要先分别从两个网络接口获取数据,获取成功后,我们需要将两个接口数据进行特定的处理后再显示到 UI 界面上,应该怎么做?答案是 Future.wait,它接受一个 Future 数组参数,只有数组中所有 Future 都执行成功后,才会触发 then 的成功回调,只要有一个 Future 执行失败,就会触发错误回调。下面,我们通过模拟 Future.delayed 来模拟两个数据获取的异步任务,等两个异步任务都执行成功时,将两个异步任务的结果拼接打印出来,代码如下:

void main() {
  Future.wait([
    // 2秒后返回结果
    Future.delayed(new Duration(seconds: 2), () {
      return "hello";
    }),
    // 4秒后返回结果
    Future.delayed(new Duration(seconds: 4), () {
      return " world";
    })
  ]).then((results) {
    print(results[0] + results[1]);
  }).catchError((e) {
    print(e);
  });
}

(二)Async/await

Dart 中的 async/await 和 JavaScript 中的 async/await 功能和用法是一模一样的。

回调地狱(Callback Hell)

如果代码中有大量异步逻辑,并且出现大量异步任务依赖其它异步任务的结果时,必然会出现 Future.then 回调中套回调情况。举个例子,比如现在有个需求场景是用户先登录,登录成功后会获得用户 ID ,然后通过用户 ID ,再去请求用户个人信息,获取到用户个人信息后,为了使用方便,我们需要将其缓存在本地文件系统,代码如下:

//先分别定义各个异步任务
Future<String> login(String userName, String pwd){
    ...
    //用户登录
};
Future<String> getUserInfo(String id){
    ...
    //获取用户信息 
};
Future saveUserInfo(String userInfo){
    ...
    // 保存用户信息 
};

接下来,执行整个任务流:

login("alice","******").then((id){
 //登录成功后通过,id获取用户信息    
 getUserInfo(id).then((userInfo){
    //获取用户信息后保存 
    saveUserInfo(userInfo).then((){
       //保存用户信息,接下来执行其它操作
        ...
    });
  });
})

可以感受一下,如果业务逻辑中有大量异步依赖的情况,将会出现上面这种在回调里面套回调的情况,过多的嵌套会导致的代码可读性下降以及出错率提高,并且非常难维护,这个问题被形象的称为回调地狱(Callback Hell)。回调地狱问题在之前JavaScript 中非常突出,也是 JavaScript 被吐槽最多的点,但随着 ECMAScript6 和 ECMAScript7 标准发布后,这个问题得到了非常好的解决,而解决回调地狱的两大神器正是ECMAScript6引入了 Promise,以及 ECMAScript7 中引入的 async/await。 而在 Dart 中几乎是完全平移了JavaScript 中的这两者:Future 相当于 Promise,而async/await 连名字都没改。接下来我们看看通过 Future 和 async/await 如何消除上面示例中的嵌套问题。

  • 使用Future消除Callback Hell
login("alice","******").then((id){
      return getUserInfo(id);
}).then((userInfo){
    return saveUserInfo(userInfo);
}).then((e){
   //执行接下来的操作 
}).catchError((e){
  //错误处理  
  print(e);
});

正如上文所述, “Future 的所有 API 的返回值仍然是一个Future 对象,所以可以很方便的进行链式调用” ,如果在then中返回的是一个 Future 的话,该 future 会执行,执行结束后会触发后面的 then 回调,这样依次向下,就避免了层层嵌套。

  • 使用async/await消除callback hell

通过 Future 回调中再返回 Future 的方式虽然能避免层层嵌套,但是还是有一层回调,有没有一种方式能够让我们可以像写同步代码那样来执行异步任务而不使用回调的方式?答案是肯定的,这就要使用 async/await 了,下面我们先直接看代码,然后再解释,代码如下:

task() async {
   try{
    String id = await login("alice","******");
    String userInfo = await getUserInfo(id);
    await saveUserInfo(userInfo);
    //执行接下来的操作   
   } catch(e){
    //错误处理   
    print(e);   
   }  
}
  • async用来表示函数是异步的,定义的函数会返回一个Future 对象,可以使用then方法添加回调函数。
  • await后面是一个 Future,表示等待该异步任务完成,异步完成后才会往下走;await 必须出现在 async 函数内部。

可以看到,我们通过 async/await 将一个异步流用同步的代码表示出来了。

其实,无论是在 JavaScript 还是 Dart 中,async/await 都只是一个语法糖,编译器或解释器最终都会将其转化为一个 Promise(Future)的调用链。

六、Stream

Stream 也是用于接收异步事件数据,和 Future 不同的是,它可以接收多个异步操作的结果(成功或失败)。 也就是说,在执行异步任务时,可以通过多次触发成功或失败事件来传递结果数据或错误异常。 Stream 常用于会多次读取数据的异步任务场景,如网络内容下载、文件读写等。举个例子:

void main() {
  Stream.fromFutures([
    // 1秒后返回结果
    Future.delayed(new Duration(seconds: 1), () {
      return "hello 1";
    }),
    // 抛出一个异常
    Future.delayed(new Duration(seconds: 2), () {
      throw AssertionError("Error");
    }),
    // 3秒后返回结果
    Future.delayed(new Duration(seconds: 3), () {
      return "hello 3";
    })
  ]).listen((data) {
    print(data);
  }, onError: (e) {
    print(e.message);
  }, onDone: () {
    print("done");
  });
}

参考文章:

  1. Dart 编程语言中文网
  2. 《Flutter 实战》
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 193,968评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,682评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,254评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,074评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,964评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,055评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,484评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,170评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,433评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,512评论 2 308
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,296评论 1 325
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,184评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,545评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,150评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,437评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,630评论 2 335

推荐阅读更多精彩内容