Dart的语法详解系列篇(四)-- 泛型、异步、库等有关详解

96
AWeiLoveAndroid 11f8cfa8 ec9f 4f82 be92 d6a39f61b5c1
3.2 2018.12.30 19:34* 字数 4933

版权声明:本文为博主原创文章,未经博主允许不得转载。https://www.jianshu.com/p/a4a9c89b85ca

转载请标明出处:
https://www.jianshu.com/p/a4a9c89b85ca
本文出自 AWeiLoveAndroid的博客


Flutter系列博文链接 ↓:

工具安装:

Flutter基础篇:

Flutter进阶篇:

Dart语法系列博文链接 ↓:

本文代码同步发布在Github: https://github.com/AweiLoveAndroid/Flutter-learning/tree/master/projects/dart_demo

上一篇主要讲了Dart的类与函数,由于内容有太多,我就把剩下的内容分开写一篇文章。
这一篇我们讲Dart的泛型、异步、库等有关详解,内容较多,希望大家可以耐心看完。我也是花了很长时间研究的。喜欢的就点个赞,打个赏吧。
感谢大家支持。


九、泛型(Generics)

如果您查看基本数组类型的API文档 List,您会看到该类型实际上是List<E>。<...>表示法将List标记为 泛型(或参数化)类型 - 具有正式类型参数的类型。按照惯例,大多数类型变量都有单字母名称,例如E,T,S,K和V.

(一)为什么使用泛型?

类型安全通常需要泛型,但它们比仅允许代码运行有更多好处:

1).正确指定泛型类型可以生成更好的代码。

如果您希望列表只包含字符串,则可以将其声明为List<String>(将其读作“字符串列表”)。这样一来,工具可以检测到将非字符串分配给列表可能是一个错误。
例子:

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
// 报错 The argument type 'int' can't be assigned to the parameter type 'String'.
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);
}

如果还有其他更改,就要写很多接口。
泛型可以省去创建所有这些接口的麻烦。你可以创建一个带有类型参数的接口。
示例如下:T是一个占位符,您可以将其视为开发人员稍后定义的类型。

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

(二)使用集合文字

list和map文字可以参数化。参数化文字就像你已经看到的文字一样,除了你在开始括号之前添加 <type>(对于list)或 <keyType, valueType>(对于map)。
以下是使用类型文字(typed literals)的示例:

var numbers = <String>['11', '22', '33'];
var pages = <String, String>{
  'index.html': 'Homepage',
  'store.html': 'Store',
  'mine.html': 'Mine'
};

(三)使用带有构造函数的参数化类型

要在使用构造函数时指定一个或多个类型,请将类型放在类名称后面的尖括号<...>中。例如:

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = Set<String>.from(names);

以下代码创建一个具有整数的key和View类型的value的map:

var views = Map<int, View>();

(四)泛型集合及其包含的类型

Dart的泛型类型是具体的。也就说,它们在运行时会会携带类型信息。示例如下:(相反,Java中的泛型使用擦除,这意味着在运行时删除泛型类型参数。在Java中,您可以测试对象是否为List,但您无法测试它是否是List<String>。)

var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
print(names.runtimeType); // List<String>

(五)限制参数类型

实现泛型类型时,您可能希望限制其参数的类型。你可以在<>里面使用extends。
例如:

abstract class SomeBaseClass {
    // 其他操作
}

class Foo<T extends SomeBaseClass> {
  String toString() {
      return "Instance of Foo<$T>";
  }
}

class Extender extends SomeBaseClass {
     //其他操作
}

现在可以使用SomeBaseClass或它的任何子类作为泛型参数。

例如:

void main() {
    var someBaseClassFoo = Foo<SomeBaseClass>();
    var extenderFoo = Foo<Extender>();
    print(someBaseClassFoo.toString());// Instance of Foo<SomeBaseClass>
    print(extenderFoo.toString());// Instance of Foo<SomeBaseClass>
}

也可以不指定泛型参数。
例如:

var foo = Foo();
//等同于print(foo.toString());
print(foo);// Instance of Foo<SomeBaseClass>

如果指定任何非SomeBaseClass类型会导致错误。
例如:var foo = Foo<Object>;

(六)使用泛型方法

新版本的Dart的泛型方法,允许在方法和函数上使用类型参数。(但它同样适用于实例方法,静态方法,顶级函数,本地函数甚至lambda表达式。)
例如:

T first<T>(List<T> data) {
      // 做一些初始工作或错误检查...
      T tmp = data[0];
  // 做一些额外的检查或处理...
  return tmp;
}

在first(<T>)上的的泛型类型参数,允许你在以下几个地方使用类型参数T:

  • 1). 在函数的返回类型(T)中
  • 2). 在参数类型(List<T>)中
  • 3). 在局部变量的类型(T tmp)

泛型方法可以声明类方法(实例和静态)以相同的方式获取泛型参数。

class  C {
   static  int f < S,T > (int x) => 3 ;
  int m < S,T > (int x) => 3 ;
}

泛型方法也适用于函数类型参数,本地函数和函数表达式。

1.将泛型方法作为参数[callback]。

void  functionTypedParameter(T callback <T>(T thing)){}

2.声明一个本地泛型函数本身

void  localFunction(){
   T itself<T>(T thing) => thing;
}

3.将泛型函数表达式绑定到局部变量。

void functionExpression(){
   var lambda =  <T>(T thing) => thing;
}

十、库和可见性

该import和library指令可以帮助您创建一个模块化的,可共享的代码库。库不仅提供API,还是隐私单元(以下划线(_)开头的标识符仅在库内可见)。每个Dart应用程序都是一个库,即使它不使用library指令。可以使用包来分发库。

(一)使用库

使用import指定一个库中的命名空间如何在另一个库汇总使用。
例如,Dart Web应用程序通常使用dart:html 库,它们可以像这样导入:
import 'dart:html';
对于内置库,URI具有特殊dart: 方案(scheme)。对于其他库,您可以使用文件系统路径或package: 方案(scheme),这个是由包管理器(如pub工具)提供的库。
例如:
import 'libs/mylib.dart';

(二)指定库前缀

如果导入两个具有冲突标识符的库,则可以为一个或两个库指定前缀。例如,如果test2.dart和test3.dart都有一个hello()函数,那么直接导入这两个文件会有冲突,这种情况下我们可以使用as关键字给库指定一个前缀:

test2.dart代码如下:

void hello() {
  print('test2.dart : hello()函数');
}

test3.dart代码如下:
void hello(){
  print('test3.dart : hello()函数');
}

现在要在test1.dart中导入这两个文件:
// 这样写会报错
// import 'test2.dart';
// import 'test3.dart';
    
  // 正确写法:
import 'test2.dart';
// 给导入的库指定一个前缀 方便识别
import 'test3.dart' as test3;
  
  调用方式:
  void main(){
    hello();//test2.dart : hello()函数
    test3.hello();//test3.dart : hello()函数
}

导入库也可以使用相对路径。例如:lib/demo1/a.dart, lib/demo2/b.dart这两个文件。现在b.dart这个文件需要引用a.dart,可以使用import '../demo1/a.dart'导入。

(三)仅导入库的一部分

如果只想使用库的一部分,则可以有选择地导入库,可以使用show或者hide关键字。例如:show表示仅导入当前库,hide表示除了当前库之外全部导入。

// 仅导入mylib.dart里面的test2函数
// import 'libs/mylib.dart' show test2;
// 刚好和show相反 除了test2函数之外  其它的都导入
import 'libs/mylib.dart' hide test2;
//我们想导入mylib库,但是不想用里面的otherLib这个库 可以这样写
// import 'libs/mylib.dart' hide otherLib;

(四)懒加载一个库

延迟加载(也称为延迟加载)允许应用程序根据需要加载库,如果需要的话。以下是您可能使用延迟加载的一些情况:

  • 1).减少应用程序的初始启动时间。
  • 2).例如,执行A/B测试 - 尝试算法的替代实现。
  • 3).加载很少使用的功能,例如可选的屏幕和对话框。
    要延迟加载库,必须先使用deferred as它导入一个库。当我们import一个库的时候,如果使用了as 不能同时使用deferred as
    例如:
// import 'libs/mylib.dart'; // 不能同时使用
import 'libs/mylib.dart' deferred as tests;

当您需要库时,使用库的标识符调用loadLibrary()。
例如(注意导包:import 'dart:async';):

Future hello() async {
  await tests.loadLibrary();
  tests.test2();
}
// 然后再去使用:
void main(){
  hello(); // 结果是: mylib.dart:test2()函数
}

在上述的代码中,await关键字暂停执行,直到库被加载。
您可以在一个库上调用loadLibrary()多次,而不会出现问题。该库只加载一次。

使用延迟加载时请记住以下内容:

  • 1).延迟库的常量不是导入文件中的常量。请记住,在加载延迟库之前,这些常量不存在。
  • 2).您不能在导入文件中使用延迟库中的类型。相反,请考虑将接口类型移动到由延迟库和导入文件导入的*库。
  • 3).Dart隐式插入loadLibrary()到你使用deferred as namespace定义的命名空间。loadLibrary()函数返回Future。

(五)库的拆分

【说明】dart官网不推荐使用part ,这个仅作为了解。
使用part指令,可以将库拆分为多个Dart文件。part of表示隶属于某个库的一部分。
注意事项:

  • 1.不能同时使用library和part of,它们都用于指定属于库的内容。
// library testlib2; 这个不能和part of同时使用 会报错
// part of 表示这个库是testlib库的一部分
part of testlib1;
    1. B库是A库的一部分,在B库里面声明:part of A库名称
      例如:在testlib2.dart里面声明 part of testlib1; 表示testlib2这个库是testlib库的yi部分。
    1. 如果B库声明A库的一部分,同时A库也想声明它的一部分是B库,正确写法:B库声明part of A库名称,然后A库声明part 'B库的路径' , 同时,如果B库没有声明,那么在A库里面使用part指令会报错。
      testlib1.dart内容:
// 第1个库:
library testlib1;
// 可以不写
part 'testlib2.dart';
void run() {
  print('testlib1库 : run()函数');
}

testlib2.dart内容:

part of testlib1;
class testLib2 {}
void start() {
  print('testlib2库 : start()函数');
}
    1. B库声明了part of A库名称,A库可以省去声明part 'B库的路径'
// 第1个库:
library testlib1;
// 可以不写
part 'testlib2.dart';

(六)库的自动导入

在A库中使用export关键字引入B库,当我们使用A库的时候,会自动引入B库,也就是说我们导入了A库,就可以使用B库了。
mylib.dart内容为:

// 这是一个库 命名为mylib
library mylib;
// 希望使用mylib的时候 自动使用otherlib.dart  可以使用export关键字引入其他库
export 'otherlib.dart';
// 导入otherlib2.dart
export 'otherlib2.dart';

class MyLib {
  void test() {
    print('mylib.dart: MyLib : test()函数');
  }
}

void test2() {
  print('mylib.dart: test2()函数');
}

otherlib.dart库内容为:

// otherlib库
library otherlib;
class otherLib {}
void test() {
  print('otherLib库 : test()函数');
}

otherlib2.dart库内容为:

// otherlib2库
library otherlib2;
class otherLib2 {}
void test2() {
  print('otherLib2库 : test2()函数');
}

(七)库的组成结构

库的最低要求是:pubspec.yaml文件和lib目录。
库的pubspec.yaml文件与普通应用程序包的文件格式相同。
lib目录:库代码位于lib 目录下,并且对其他包是公共的。您可以根据需要在lib下创建任何层次结构。
声明一个库的关键字是library。
例如在文件test.dart文件首行加上:library mylib; 表示这个库的名称是mylib


十一、异步支持

Dart库中包含许多返回Future或Stream对象的函数。这些函数是异步的:它们在设置可能耗时的操作(例如I / O)后返回,而不等待该操作完成。

Dart官网有关于异步的教学:
使用Future完成异步任务:https://www.dartlang.org/tutorials/language/futures
使用Streams(流)管理序列化数据:https://www.dartlang.org/tutorials/language/streams

asyncawait关键字支持异步编程,让你写异步代码看起来类似于同步代码。

(一)处理Future

当您需要完成Future的结果时,您有两个选择:

  • 1).使用async和await。
  • 2).使用Future API,如 库浏览 中所述。

(二)使用async和await

使用asyncawait异步的代码,但它看起来很像同步代码。例如,这里有一些代码await 用于等待异步函数的结果。例如:await lookUpVersion();

要使用async,代码必须在async函数中(标记为async的函数)。
例如:

Future checkVersion() async {
  var version = await lookUpVersion();
  // 其他操作
}

注意: 虽然async函数可能执行耗时的操作,但它不会等待这些操作。async函数只在遇到第一个await表达式时执行。然后它返回一个Future对象,仅在await表达式完成后才恢复执行。

使用try,catch,finally在使用await的代码中处理错误和清理代码。

try {
  var version = await lookUpVersion();
} catch (e) {
  // 这里可以看到是什么错误。
}finally{
  // 正确的解决方式写在这里
}

您可以在异步功能中多次使用await。例如,以下代码等待三次函数结果:

var entrypoint = await findEntrypoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

在await表达式中,表达式的值通常是Future; 如果不是,那么该值将自动包含在Future中。 这个Future对象表示返回一个对象的promise。 await表达式的值是返回的对象。 await表达式使执行暂停,直到该对象可用。

如果在使用await时遇到编译时错误,请确保await在async函数中。
例如,要在应用程序的main()函数中使用await,main()方法必须标记为async:以下是一个完整的示例代码:

import 'dart:async';

Future main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

Future checkVersion() async {
  print('checkVersion()');
  var version = await lookUpVersion();
}

Future<String> lookUpVersion() async{
  print('lookUpVersion()');
  return '版本号:v1.0';
}

结果:

// checkVersion()
// lookUpVersion()
// lookUpVersion()
// In main: version is 版本号:v1.0

(三)定义一个异步函数

方法被async修饰的函数是异步函数。给一个函数添加async关键字,使得返回值是一个Future。

void main(){
  lookUpVersion(); //输出结果:lookUpVersion()同步方法 返回值是:1.0.0
  lookUpVersion2(); // 输出结果:lookUpVersion2()异步方法 返回值是:1.0.0
  lookUpVersion3(); // 输出结果:lookUpVersion3()异步方法 没有返回值
}

例如,看下面这个返回值是String的同步函数:

String lookUpVersion() {
      print('lookUpVersion()同步方法 返回值是:1.0.0');
      return '1.0.0';
}

如果将其更改为异步函数 - 例如,因为Future的实现将非常耗时 - 返回的值是Future:

Future<String> lookUpVersion2() async{
      print('lookUpVersion2()异步方法 返回值是:1.0.0');
      return '1.0.0';
}

如果您的函数没有返回有用的值,请设置其返回类型Future<void>
例如:

Future<void> lookUpVersion3() async {
  print('lookUpVersion3()异步方法 没有返回值');
}

(四)处理Stream

当您需要完成Future的结果时,您有两个选择:

  • 1).使用async和异步for循环(await for)。
    注意:在使用await for之前,请确保它使代码更清晰,并且您确实希望等待所有Stream的结果。 例如,通常情况,不应该使用await for UI事件侦听器,因为UI框架会发送无穷无尽的事件流(streams of events)。
  • 2).使用Stream API(主要是IO操作。)

异步for循环的格式:await for(var或具体类型 标识符 in 表达式){}
例如:我们读取本地的一个文件内容,实例代码如下:

import 'dart:io';
import 'dart:convert';
void main() {
      test();
}
// await for循环的使用示例
// 这里是读取本地文件的内容
Future test() async {
      var config=File('d:\\test.txt');
      // 打开io流进行文件读取
      Stream<List<int>> inputStream = config.openRead();
      var lines = inputStream
        // 设置编码格式为utf-8
        .transform(utf8.decoder)
        .transform(LineSplitter());
      try {
          await for (var line in lines) {
              print('从Stream中获取到的内容是: ${line} \r文本内容长度为:'+ '${line.length}\r=======');
          }
        print('文件现在没有关闭。。。');
      } catch (e) {
        print(e);
      }
}

表达式的值必须有Stream类型,执行过程如下:

  • 1).等待,知道Stream发出一个数值。
  • 2).执行for循环的主体,讲变量设置为这个发出的数值。
  • 3).重复1和2,知道关闭Stream。

要停止监听Stream,你可以使用break或者return语句跳出for循环B并且从Stream中取消订阅。
如果在实现异步for循环时遇到编译时错误,确保await for在一个async函数中。
例如,要在应用程序的main()函数中使用await for循环,main()方法必须标记为async:以下是一个完整的示例代码:

Future main() async {
   // ...
  await for (var request in requestServer) {
    handleRequest(request);
  }
  // ...
}

有关异步编程的更多信息,请查看库浏览的 Dart库之旅 --- dart:async 部分,另请参阅文章 Dart语言异步支持:阶段2(该页面可能过期了) 和 Dart语言规范。


十二、Isolates

大多数计算机,甚至在移动平台上,都有多核CPU。为了利用所有这些核心,开发人员传统上使用并发运行的共享内存线程。但是,共享状态并发容易出错,并且可能导致代码复杂化。
所有Dart代码都在隔离区内运行,而不是线程。每个隔离区都有自己的内存堆,确保不会从任何其他隔离区访问隔离区的状态。

Dart是单线程模型,但是使用Isolates可以用于多线程。

这个库主要用于服务端的开发。如果你不使用Dart做服务端开发,仅作为了解即可。
源码可以看Github:https://github.com/dart-lang/isolate
官方API文档: https://api.dartlang.org/stable/2.1.0/dart-isolate/dart-isolate-library.html

使用isolate 需要先导入包:import 'dart:isolate';

下面来一个简单的示例代码:
// 在另一个隔离区()中同步读取“D://file.json”
// 结果是{msg: [{title: 你好1, contents: yes}, {title: 你好2, contents: NO}]}
main() async {
// 在其他隔离(isolate)中同步读取文件,然后对其进行解码。
print(await readIsolate());
}

// 同步读取'D//file.json'(在同一个线程中)
Map readSync() {
JsonCodec().decode(new File('D://file.json').readAsStringSync());
}

// 在另一个隔离区()中同步读取“D://file.json”
Future readIsolate() async {
final response = new ReceivePort();
await Isolate.spawn(_isolate, response.sendPort);
return response.first;
}

/// 期望通过[Isolate.spawn]创建
void _isolate(SendPort sendPort) {
sendPort.send(readSync());
}

下面是file.json文件的内容:
{
"msg": [
{
"title": "你好1",
"contents": "yes"
},
{
"title": "你好2",
"contents": "NO"
}
]
}


十三、生成器(Generators)

当您需要懒惰地生成一系列值时,请考虑使用生成器函数。Dart支持两种生成器功能。

(一)同步生成器,返回一个Iterable对象。

要实现同步生成器函数,请将函数体标记为sync*,并使用yield语句来传递值。

Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

如果您的生成器是递归的,您可以使用yield*以下方法来提高其性能:

Iterable<int> naturalsDownFrom(int n) sync* {
    if (n > 0) {
      yield n;
      yield* naturalsDownFrom(n - 1);
    }
}

(二)异步生成器,返回一个Stream对象。

要实现异步生成器函数,请将函数体标记为async*,并使用yield语句来传递值。

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

十四、类型定义

在Dart中,函数是对象,就像字符串一样,数字是对象。一个类型定义,或功能型的别名,给出了一个函数类型声明字段时,您可以使用和返回类型的名称。当函数类型分配给变量时,typedef会保留类型信息。

以下代码,它不使用typedef:我们可以看到compare是一个函数,但它是哪一种类型的函数?不是很清楚。

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

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

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

  // compare是一个函数,但它是哪一种类型的函数?
  assert(coll.compare is Function);
}

接下来使用typedef改造一下:
我们将代码更改为使用显式名称并保留类型信息,开发人员和工具都可以使用该信息。

typedef Compare = int Function(Object a, Object b);

class SortedCollection {
  Compare compare;
  SortedCollection(this.compare);
}

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

void main() {
  SortedCollection coll = SortedCollection(sort);
  assert(coll.compare is Function);
  assert(coll.compare is Compare);
}

目前:typedef仅限于函数类型。

因为typedef只是别名,Dart提供了一种检查任何函数类型的方法。
例如:

typedef Compare<T> = int Function(T a, T b);
int sort(int a, int b) => a - b;
void main() {
  assert(sort is Compare<int>); // True
}

十五、元数据Metadata

使用元数据提供有关代码的其他信息。元数据注解以字符开头@,后跟对编译时常量(如deprecated)的引用或对常量构造函数的调用。
元数据可以出现在库,类,typedef,类型参数,构造函数,工厂,函数,字段,参数或变量声明之前以及导入或导出指令之前。您可以使用反射在运行时检索元数据。
所有Dart代码都有两个注解:@deprecated@override
以下是使用@deprecated 注解的示例:

class Television {
  /// _Deprecated: Use [turnOn] instead._
  @deprecated
  void activate() {
    turnOn();
  }

  // Turns the TV's power on.
  void turnOn() {
//...
}
}

您可以定义自己的元数据注释。

这是一个定义带有两个参数的@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');
}
Flutter&Dart