这篇继续来学习 Dart 语法。
目录:
- 类和函数
- mixin 入门
- 异步
- 库相关
1. 类和函数
Dart 是一种面向对象的语言,具有类和基于 mixin 的继承。每个对象都是一个类的实例,所有类都来自 Object
。 基于 Mixin
的继承意味着虽然每个类(除了 Object)
只有一个超类,但是类体可以在多个类层次结构中重用。
(一) 类的分类
(1) 普通类
1) Dart 使用 class
关键字表示一个类,对象具有由函数和数据(分别为方法和实例变量)组成的成员。我们通过 new 关键字来创建一个类对象,然后使用(.)调用类里面的变量或者普通(实例,非静态)函数(注:函数和方法意思是一样的。),以便访问该对象的函数和数据。例如类 Test 里面有一个普通函数 tests(),我们可以使用 new Test().tests(); 来调用这个 tests() 函数,在 Dart2 里面创建对象时可以省略 new 关键字。
例如:
class Test{
void tests(){}
}
void main(){
new Test().tests();
}
2) 要在运行时获取对象的类型,可以使用 Object 类的 runtimeType
属性,该属性返回一个 Type 对象。
如果没有指明具体类型,Dart 具有类型推断机制,会自动推断变量类型。示例如下:
var a = 10;
var b = 10.0;
var c = '10';
var d = true;
var e = [12.5,13.1];
var f = {3:'5',5:'11'};
var t = new Test();// 这里就直接使用上面那个Test类 不再去重复写这个类了
print('a 的类型是: ${a.runtimeType}'); // a 的类型是: int
print('b 的类型是: ${b.runtimeType}'); // b 的类型是: double
print('c 的类型是: ${c.runtimeType}'); // c 的类型是: String
print('d 的类型是: ${d.runtimeType}'); // d 的类型是: bool
print('e 的类型是: ${e.runtimeType}'); // e 的类型是: List<double>
print('f 的类型是: ${f.runtimeType}'); // f 的类型是: _InternalLinkedHashMap<int, String>
print('t 的类型是: ${t.runtimeType}'); // t 的类型是: Test
class Test{}
3) Dart 和 Java 一样,使用 extends 关键字,表示一个类继承另一个类。
使用 @override
注解声明你要重写的函数,在这个函数内部可以使用 super 调用重写的这个父类的函数。实例如下:
class Test {
void test() {/*这里省略方法内部的逻辑操作*/}
// 其他逻辑
}
class TestChild extends Test {
@override //@override标注在test()函数上面 表示test()函数是重写父类的。
void test() {
super.test();// 调用父类的test()函数
/*这里省略方法内部的逻辑操作*/
}
// 其他逻辑
}
@override
是元数据。元数据注解以字符开头@,后跟对编译时常量(如 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');
}
4) 静态变量
使用 static
关键字修饰的类范围的变量。静态变量(类变量)对于类范围的状态和常量很有用。静态变量在使用之前不会初始化。
class Test {
static const num = 16;
// ···
}
void main() {
print(Test. num); // 16
}
5) 重写操作符
你可以重写下表中显示的运算符。
操作符名称 | |||
---|---|---|---|
< | + | | | [] |
> | / | ^ | []= |
<= | ~/ | & | ~ |
>= | * | << | == |
- | % | >> |
注意:!=
不是可重写的运算符。表达式e1 != e2
是 !(e1==e2)
的语法糖。以下是一个重写 + 和 - 运算符的类的示例:
void main(){
final a = Testoperator(2, 3);
final b = Testoperator(2, 2);
var num1 = Testoperator(4, 5);
var num2= Testoperator(0,1);
print(a + b == num1); // true
print(a - b == num2); // true
}
class Testoperator {
final int x, y;
Testoperator(this.x, this.y);
Testoperator operator +(Testoperator o) => Testoperator(x + o.x, y + o.y);
Testoperator operator -(Testoperator o) => Testoperator(x - o.x, y - o.y);
// Override hashCode using strategy from Effective Java, Chapter 11.
@override
int get hashCode {
int result = 17;
result = 37 * result + x.hashCode;
result = 37 * result + y.hashCode;
return result;
}
// 如果重写了 hashCode,应该重写==操作符。
@override
bool operator ==(dynamic other) {
if (other is! Testoperator) return false;
Testoperator person = other;
return (person.x == x &&
person.y == y);
}
}
6) noSuchMethod()
要在代码尝试使用不存在的方法或实例变量时检测或做出反应,你可以重写 noSuchMethod()。
void main() {
TestMethod test = new TestMethod();
dynamic f = test.foo;
// Invokes `Object.noSuchMethod`, not `TestMethod.noSuchMethod`, so it throws.
f(42);
}
class TestMethod {
// 除非你重写noSuchMethod,否则使用不存在的成员会导致NoSuchMethodError
// Unless you override noSuchMethod, using a
// non-existent member results in a NoSuchMethodError.
@override
void noSuchMethod(Invocation invocation) {
print('You tried to use a non-existent member: ' +
'${invocation.memberName}');
}
dynamic foo();
}
你不能调用未实现的方法,除非以下的某一条是 true:
- 1. 接收处有静态类型 dynamic。
- 2. 接收处定义了一个未实现的方法(abstract 也是 OK 的)的静态类型 dynamic,接收器的动态类型的实现与类
noSuchMethod()
中的实现不同 Object。
(2) 抽象类
1) 使用 abstract 修饰符定义抽象类(无法实例化的类)。抽象类对于定义接口非常有用,通常还有一些实现。如果希望抽象类看起来是可实例化的,请定义工厂构造函数。
抽象类通常有抽象方法。这是一个声明具有抽象方法的抽象类的示例:
// 此类声明为abstract,因此无法实例化
abstract class Test {
//定义构造函数,字段,方法...
// 抽象方法
void test();
}
2) 隐式接口
每个类都隐式定义一个接口,该接口包含该类的所有实例成员及其实现的任何接口。如果要在不继承 B 实现的情况下创建支持 B类 API 的 A 类,则 A 类应实现 B 接口。一个类通过在 implements 子句中声明它们然后提供接口所需的 API 来实现一个或多个接口。例如:
void main() {
print(sayHello(Person('李四'))); // 你好 张三. 我是 李四.
print(sayHello(PersonImpl())); // 你好 张三 你知道我是谁吗?
}
// Person类 隐式接口包含hello()
class Person {
// 在接口中,但是仅在此库中可见。
final _name;
// 不在接口中,因为这是一个构造函数
Person(this._name);
// 在接口中
String hello(String who) => '你好 $who. 我是 $_name.';
}
// Person接口的实现
class PersonImpl implements Person {
get _name => '';
String hello(String name) => '你好 $name 你知道我是谁吗?';
}
String sayHello(Person person) => person.hello('张三');
一个类也可以实现多个接口,例如:
class ZhangSan implements Run,Life {
//...
}
class Run {}
class Life {}
(3) 可调用的类(Callable Class)
更多详情可以查看 Dart 官网: 在 Dart 中模拟函数。要允许像函数一样调用 Dart 类,请实现该 call()
方法。在下面的示例中,Test 类定义了一个 call()
方法,它接受三个字符串并连接它们,用空格分隔每个字符串,并附加一个感叹号。
void main() {
var test = new Test();
var result = test(166.6665,"Flutter真好玩",672);
print("$result");// 666.666 Flutter真好玩 666
}
class Test {
// 必须是call函数
call(double a, String b, int c) => '${a*4} ${b} ${c-6}';
}
(4) 枚举类型
1) 使用 enum 关键字声明枚举类型:
例如:enum Color { red, green, blue }
2) 枚举中的每个值都有一个 index getter,它返回枚举声明中值的从零开始的位置。例如,第一个值具有索引0,第二个值具有索引1。
assert(Color.red.index == 0);
assert(Color.green.index == 1);
assert(Color.blue.index == 2);
3) 要获取枚举中所有值的列表,请使用枚举 values 常量。
List<Color> colors = Color.values;
assert(colors[2] == Color.blue);
4) 你可以在 switch 语句中使用枚举,如果你不处理所有枚举值,你将收到警告。
var aColor = Color.blue;
switch (aColor) {
case Color.red:
print('Red');
break;
case Color.green:
print('Green');
break;
default: // 你没有这个 你会看到一个警告
print(aColor); // 'Color.blue'
}
5) 枚举类型的限制条件
- 1. 你不能在子类,
mixin
或者实现枚举。 - 2. 你不能显式实例化枚举。
(5) mixin
Mixins 是一种在多个类层次结构中重用类代码的方法。
1) 要实现 mixin,请创建一个扩展 Object 的类,并且不声明构造函数。除非你希望 mixin 可用作常规类,否则请使用 mixin 关键字而不是 class。例如:
// 声明mixin
// 专家
mixin Expert {
// 发现和解决难题
bool solveProblems = false;
// 精通数据结构和算法
bool dataStructureAndAlgorithms = false;
// 会架构设计
bool architectureDesign = false;
// 性能优化
bool performanceOptimization = false;
// 熟练掌握计算机系统
bool computerSystem = false;
void develop() {
// 娱乐节目
if (solveProblems) {
print('发现和解决难题');
}
if (dataStructureAndAlgorithms) {
print('精通数据结构和算法');
}
if (architectureDesign) {
print('会架构设计');
}
if (performanceOptimization) {
print('熟练掌握性能优化');
}
if (computerSystem) {
print('熟练掌握计算机系统');
}
}
}
// 特性
// 有效率的
mixin Efficient {
void getEfficientAttrs() {
print('有效率的');
}
}
// 和蔼的
mixin Kind {
void getKindAttrs() {
print('和蔼的');
}
}
// 软件架构师
class SoftwareArchitect {
SoftwareArchitect() {
print('软件架构师');
}
}
2) 要使用 mixin,请使用 with 关键字后跟一个或多个 mixin 名称。以下示例显示了两个使用 mixins 的类。
void main() {
// 姓名:张三
// 发现和解决难题
// 精通数据结构和算法
// 会架构设计
ACompanySoftwareArchitect architect1 = new ACompanySoftwareArchitect('张三');
architect1.develop();
print('====');
// 姓名:李四
// 发现和解决难题
// 精通数据结构和算法
// 会架构设计
// 熟练掌握性能优化
// 熟练掌握计算机系统
// 有效率的
// 和蔼的
BCompanySoftwareArchitect architect2 = new BCompanySoftwareArchitect('李四');
architect2.develop();
architect2.getEfficientAttrs();
architect2.getKindAttrs();
}
// 使用mixin
// A公司的软件架构师,继承自软件架构师,拥有专家的特性。
class ACompanySoftwareArchitect extends SoftwareArchitect with Expert {
String name;
ACompanySoftwareArchitect(String name) {
this.name = name;
print('姓名:' + name);
solveProblems = true;
dataStructureAndAlgorithms = true;
architectureDesign = true;
}
@override
void develop() {
super.develop();
}
}
// B公司的软件架构师,继承自软件架构师,
class BCompanySoftwareArchitect extends SoftwareArchitect
with Expert, Efficient, Kind {
String name;
BCompanySoftwareArchitect(String name) {
this.name = name;
print('姓名:' + name);
solveProblems = true;
dataStructureAndAlgorithms = true;
architectureDesign = true;
performanceOptimization = true;
computerSystem = true;
}
}
3) 要指定只有某些类型可以使用 mixin。例如,所以你的 mixin 可以调用它没有定义的方法, 用于 on 指定所需的超类。
mixin SoftwareDeveloper on ACompanySoftwareArchitect{}
(二) 泛型
如果你查看基本数组类型的 API 文档 List,你会看到该类型实际上是 List<E>
。<...>
表示法将 List 标记为泛型(或参数化)类型 - 具有正式类型参数的类型。按照惯例,大多数类型变量都有单字母名称,例如 E,
T,
S,
K和
V`。
(1) 为什么使用泛型?
类型安全通常需要泛型,但它们比仅允许代码运行有更多好处:
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);
}
如果还有其他更改,就要写很多接口。
class Test1 extends ObjectCache {
@override
Object getByKey(String key) {
return 'object cache';
}
@override
void setByKey(String key, Object value) {
return null;
}
}
class Test2 extends StringCache {
@override
String getByKey(String key) {
return 'String cache';
}
@override
void setByKey(String key, String value) {
return null;
}
}
泛型可以省去创建所有这些接口的麻烦。你可以创建一个带有类型参数的接口。示例如下:T 是一个占位符,你可以将其视为开发人员稍后定义的类型。
abstract class Cache<T> {
T getByKey(String key);
}
使用泛型以前的调用方式:
print(new Test1().getByKey('123'));
print(new Test2().getByKey('456'));
(2) 使用集合文字
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'
};
(3) 使用带有构造函数的参数化类型
要在使用构造函数时指定一个或多个类型,请将类型放在类名称后面的尖括号 <...> 中。例如:
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
var nameSet = Set<String>.from(names);
以下代码创建一个具有整数的 key 和 View 类型的 value 的 map:
var views = Map<int, View>();
(4) 泛型集合及其包含的类型
Dart 的泛型类型是具体的。也就说,它们在运行时会携带类型信息。
var names = List<String>();
names.addAll(['Seth', 'Kathy', 'Lars']);
print(names is List<String>); // true
print(names.runtimeType); // List<String>
注意:相反,Java 中的泛型使用擦除,这意味着在运行时删除泛型类型参数。在 Java 中,你可以测试对象是否为 List,但你无法测试它是否是 List<String>
。
(5) 限制参数类型
实现泛型类型时,你可能希望限制其参数的类型。你可以在 <> 里面使用 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>;
(6)
使用泛型方法
新版本的 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 Test {
static int f<M, N>(int x) => 3;
int m<M, N>(int y) => 5;
}
泛型方法也适用于函数类型参数,本地函数和函数表达式。
// 作为参数
void functionTypedParameter<T>(T callback){}
// 声明一个本地泛型函数本身
。
void localFunction(){
T itself<T>(T thing) => thing;
}
// 将泛型函数表达式绑定到局部变量。
void functionExpression(){
var lambda = <T>(T thing) => thing;
}
使用泛型方法:
void main() {
List<String> data = ["张三","李四","王五"];
var result = first(data);
print(result);
}
(三) 函数
(1) 普通函数
Dart 是一种真正的面向对象的语言,所以即使是函数也是对象,函数属于 Function
类型。可以通过函数指定变量或者把函数作为其他函数的参数。
1)函数的简写。
1. 对于只有一个表达式的函数,可以简写。
例如 Flutter 新建工程里面的 main.dart
, 找到里面的 runApp
函数,可以使用 =>
这样的箭头函数去操作,如下所示:
操作前:
void main(){
runApp(new MyApp());
}
操作后:(main.dart 文件里面默认使用的是 ==> 箭头函数)
void main() => runApp(new MyApp());
注意:】main
函数是程序的入口,不管是纯 Dart 代码,还是 Flutter 项目,或者其他语言,基本都是 main
函数是入口函数。
2. 返回值为 void
时,可以省略 void
关键字 (开发中不建议这么做)。
函数的返回值可以是 void
, 也可以是 null
,也可以是具体对象
。如果没有指定返回值,则该函数返回的是 null
。例如 Flutter 新建工程里面的 main.dart
, _incrementCounter()
函数,可以省略关键字 void
,如下所示:
操作前:
void _incrementCounter(){
//...
}
操作后:
_incrementCounter(){
//...
}
我们使用 assert(_incrementCounter()==null);
测试一下,发现程序运行正常,可以看出该函数返回值为 null
【注意】函数属于 Function 类型,可以通过断言 assert(XXX is Funtion); 判断出结果,返回值必须是具体类型或者省略,如果返回值写为 void,编译器有错误提示。举例如下:
void testMethod (){
//...
}
例如我们:assert(testMethod () is Function);
//这时候编译器会报错。
2)普通参数与可选参数
Dart 的函数最好玩的就是这个可选参数了,就是可以声明多个参数,使用时可以调用其中的某一个或者多个参数,与参数位置无关。
1. 可选参数的基本使用
可选参数的定义方式:{参数1,参数2,,...}
,使用方式:函数名(paramName1: value1, paramName2: value2, paramName3: value3...);
下面我们来看一个简单的示例对比一下普通函数和可选参数:
操作前:
// 工作:地址、公司名、工资、工作时长、公司人数
void work(
String address,
String cpompany,
double money,
String workTime,
int workerNumbers) {
//TODO:...
}
使用:
void main() {
// 缺一个参数都会报错
work('hangzhou','XXCompany',1000000.00,'9:00-5:00',500);
}
操作后:
void work2({
String address,
String cpompany,
double money,
String workTime,
int workerNumbers}) {
//TODO:...
}
使用:
void main() {
//你随意使用其中的参数都是可以的,例如我使用了其中的参数1,参数4和参数5
work2(address:'hangzhou', workTime:'9:00-5:00', workerNumbers:500);
}
2. 可选参数默认的值
可以使用 =
为任意的可选参数设置默认值,默认值必须是编译时常量,如果没有提供默认值,则默认值为 null。例如下例就是给参数1和参数2设置了默认值:
void work3({
String address = 'hangzhou',
String cpompany = ' XXCompany ',
double money,
String workTime,
int workerNumbers}) {
//TODO:...
}
3. 普通函数参数为 list 或者 map 的默认值
如果普通函数的参数是一个匿名 List 集合 (也叫数组)
,也可以使用 =
设置默认值,数组不能被包含在可选参数里面。例如:
void work4(
String address,
[String cpompany = 'XXCompany',
double money,
String workTime,
int workerNumbers]) {
//TODO:...
}
4. 可变参数为 list 或者 map 的默认值
可变参数可以是显示声明的 List 集合或者 map
,但是 list 或者 map 的值比如是 const
修饰。举例如下:
void work5({
List<int> list = const [10, 20, 30],
Map<String, String> gifts = const {
'cpompany':'XXCompany',
'money':'50000',
'workTime':'9:00-5:00',
}}) {
//TODO:...
}
3)函数作为一个参数传给另一个函数
这个就类似于 Java 里面的回调功能。例如 Flutter 新建工程里面的 main.dart
,我们看看这段代码就知道了:
// 函数作为参数传给另一个函数
void main() {
// 例如main.dart里面FloatingActionButton的onPressed参数引入了一个_incrementCounter()函数
// floatingActionButton: new FloatingActionButton(onPressed: _incrementCounter,),
}
void _incrementCounter() {
// setState(() {
// _counter++;
// });
}
4)匿名函数我们还是以 Flutter 新建工程里面的 main.dart
为例,我们看看这里的 setState
函数,这里面的参数是一个(){}
。小括号里面没有参数,我们去看看 setState
源码你会发现它的参数是一个 Function
,这里没有传入任何参数。这里面其实就是一种匿名函数的用法。
void _incrementCounter() {
setState(() {
_counter++;
});
}
再比如常见的 list.foreach 用法也会用到匿名函数,例如下例中的 forEach() 我们这里写的是无类型参数的匿名函数item,forEach
源码是:forEach (void f(E element))
, 它的参数是一个函数。
List list = [10, 7, 23];
list.forEach((item) {
print('$item');
});
以上语句可以简写成:list.forEach((item) => print('$item'));
5)函数作用域
Dart 是一种具有语法范围的语言,变量的范围是静态确定的,只需通过代码布局来确定。通过花括号向外查看,可以确定变量是否在范围内。以下是一个嵌套函数的例子,每个作用域级别上都有变量,变量作用域为函数内部,外部无法访问。我们可以看看日志就清楚了:
// main函数里面可以输出topLevel和insideMain的值。
// myFunction函数里面可以输出topLevel、insideMain和insideFunction的值。
// nestedFunction函数里面可以输出topLevel、insideMain、insideFunction和insideNestedFunction的值。
bool topLevel = true;
void main() {
var insideMain = true;
void myFunction() {
var insideFunction = true;
void nestedFunction() {
var insideNestedFunction = true;
print('topLevel\r');
print(topLevel);
print('insideMain\r');
print(insideMain);
print('insideFunction\r');
print(insideFunction);
print('insideNestedFunction\r');
print(insideNestedFunction);
}
// print('topLevel\r');
// print(topLevel);
// print('insideMain\r');
// print(insideMain);
// print('insideFunction\r');
// print(insideFunction);
// 调用函数nestedFunction
nestedFunction();
}
// 调用函数myFunction
myFunction();
// print('topLevel\r');
// print(topLevel);
// print('insideMain\r');
// print(insideMain);
}
6)闭包
当函数定义和函数表达式位于另一个函数的函数体内。而且这些内部函数可以访问它们所在的外部函数中声明的所有局部变量、参数和声明的其他内部函数。当其中一个这样的内部函数在包含它们的外部函数之外被调用时,就会形成闭包。
- a. 内部函数为有参数的匿名函数示例:
void main() {
var result = test();
print(result(2.0));//结果为:12.56
}
Function test(){
const PI = 3.14;
return (double r) => r * r * PI;
}
- b. 内部函数为无参数的匿名函数示例:
void main() {
var result2 = test2();
print(result2());//结果为:3.14
}
Function test2() {
const PI = 3.14;
return () => PI;
}
7)等价函数
//函数也是对象
void topMethod() {} // 一个顶级函数
class Demo {
static void staticMethod() {} //一个静态方法
void caseMethod() {} //实例方法
}
void main() {
var compareVar;
// 比较顶级的函数
compareVar = topMethod;
print(topMethod == compareVar);
// 比较静态方法
compareVar = Demo.staticMethod;
print(Demo.staticMethod == compareVar);
// 比较实例方法
var demo1 = Demo(); // Demo类的实例1
var demo2 = Demo(); // Demo类的实例2
var y = demo2;
compareVar = demo2.caseMethod;
//这些闭包指向同一个实例demo2,所以它们相等。
print(y.caseMethod == compareVar);
//这些闭包是指不同的实例,所以他们不平等。
print(demo1.caseMethod != demo2.caseMethod);
}
8)函数别名
在 Dart 中,函数是对象,就像字符串一样,数字是对象。一个类型定义,或功能型的别名,给出了一个函数类型声明字段时,你可以使用和返回类型的名称。当函数类型分配给变量时,typedef
会保留类型信息。以下代码,它不使用 typedef
:我们可以看到 funs 是一个函数,但它是哪一种类型的函数? 不是很清楚。
class Demo {
Function funs;
Demo (int f(Object a, Object b)) {
funs = f;
}
}
int test(Object a, Object b) => 0;
void main() {
Demo demo = Demo(test);
// funs是一个函数,但它是哪一种类型的函数?
print(demo.funs is Function); // true
}
可以使用 typedef
给函数取个别名,这一点让我想起了 C 语言里面的函数指针。接下来使用 typedef
改造一下,我们将代码更改为使用显式名称并保留类型信息,开发人员和工具都可以使用该信息。
- 1. 给 Function 取一个别名叫做
TypedefFuns
typedef TypedefFuns = int Function(Object a, Object b);
- 2. Demo 类里的构造方法使用这个别名
class Demo {
TypedefFuns funs;
Demo(this.funs);
}
- 3. 使用 Demo 类,传入一个函数。这里给 Demo 类传入了一个函数
test
。如果想知道然后判断demo.funs
属于哪一种类型。
int test(Object a, Object b) => 0;
void main() {
Demo demo = Demo(test);
print(demo.funs is Function); // true
print(demo.funs is Demo); // false
}
目前:typedef
仅限于函数类型。
因为 typedef
只是别名,Dart 提供了一种检查任何函数类型的方法。例如:
typedef TypedefFuns2<T> = int Function(T a, T b);
int test2(int a, int b) => a - b;
void main() {
print(test2 is TypedefFuns2<int>); // True
}
9)getter 和 setter
getter
和 setter
是提供对象属性的读写访问权限的特殊方法。所有实例变量都生成一个隐式 getter 方法。非 final 实例变量也会生成隐式 setter 方法。使用 get 和 set 关键字通过实现 getter 和 setter 来创建其他属性。
- 1. 使用 getter 和 setter,可以从实例变量开始。
例如:
void main() {
var rect = Rectangle(3, 4, 20, 15);
print(rect.left == 3); // true
rect.right = 12;
print(rect.left == -8); // true
}
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;
}
注意: 无论是否明确定义了 getter,增量(++)等运算符都以预期的方式工作。为避免任何意外的副作用,只需调用一次getter,将其值保存在临时变量中。
- 2.实例变量的隐式 getter 和 setter 方法
所有实例变量都生成一个隐式 getter 方法。非 final 实例变量也会生成隐式 setter 方法。例如:
class Point {
num x;
num y;
}
void main() {
var point = Point();
point.x = 4; // Use the setter method for x.
assert(point.x == 4); // Use the getter method for x.
assert(point.y == null); // Values default to null.
}
如果初始化声明它的实例变量(而不是构造函数或方法),则在创建实例时设置该值,该实例在构造函数及其初始化列表执行之前。例如:
class Point2 {
num x = 10;
num y = 5;
Point2 p = new Point2();//p在构造函数之前执行
Point2(){}
}
void main() {
var point2 = Point2();
point2.x = 4; //
}
10)抽象方法
实例,getter
和 setter
方法可以是抽象的,定义一个接口,但将其实现留给其他类。抽象方法只能存在于抽象类中。要使方法抽象,请使用分号(;
)而不是方法体。
abstract class Test {
//定义实例变量和方法...
//定义一个抽象方法
void doSomething();
}
class TestImpl extends Test {
// 抽象方法的实现
void doSomething(){
// 具体的实现...
}
}
11)静态方法
静态方法:使用 static
关键字修饰的方法,也叫类方法,它不对实例进行操作,因此无权访问 this
。
void main() {
print(Point.area(5, 4));// 10
}
class Point {
static num area(num x, num y) {
return (x * y)/2;
}
}
注意:对于常用或广泛使用的实用程序和功能,请考虑使用顶级函数而不是静态方法。你可以使用静态方法作为编译时常量。例如,你可以将静态方法作为参数传递给常量构造函数。
(2) 构造函数
通过创建一个与其类同名的函数来声明构造函数(另外,还有一个额外的标识符,如命名构造函数中所述)。
1) 最常见的构造函数形式,即生成构造函数,创建一个类的新实例。
例如:
class Test {
int x, y;
Test(int x, int y) {
this.x = x;//this.x指向的是当前Test类里面的变量int x
this.y = y;
}
}
void main(){
// 使用Test构造函数创建Test对象
var test = new Test(5, 15);
}
2) Dart 具有语法糖,可以将构造函数参数赋值给实例变量。
class Test {
num x, y;
// 构造函数运行之前设置x和y
Test(this.x, this.y) {
print('x:$x, y:$y');
}
}
如果没有内容体,可以使用简写形式,示例如下:
class Test {
num x, y;
// 构造函数运行之前设置x和y
Test(this.x, this.y);
}
3) 默认构造函数(空参构造)
如果你未声明构造函数,则会为你提供默认构造函数。默认构造函数没有参数,并在超类中调用无参数构造函数。注意:如果定义了空参构造,再去写实参构造,会报错(这一点和 Java 不一样)。
class Test{
// 如果不写 默认就是空参构造
Test(){}
}
4) 命名构造函数
Java 可以做到 方法重载(也就是:多个同名不同参数构造函数),但是 Dart 不可以这么做。Dart 提供了命名构造。使用命名构造函数为类实现多个构造函数或提供更多的解释说明。
例如:
class Test{
num x, y;
// 命名构造
Test.help() {
x = 5;
y = 10;
print('x=${x}, y = ${y}');
}
}
void main(){
Test.help();// 调用命名构造函数
}
构造函数不是继承的,也就是说超类的命名构造函数不会被子类继承。如果希望使用超类中定义的命名构造函数创建子类,则必须在子类中实现该构造函数。例如:Test
类里面有一个 Test.help()
命名构造,它的子类是 TestChild
,如果使用 new TestChild.help()
就会报错,因为构造函数不能继承。只有在 TestChild
类里面写一个 TestChild.help()
命名构造函数,才可以使用该命名构造。示例如下:
void main() {
// 先执行Test类的空参构造, 再执行TestChild类的空参构造。
new TestChild();
// 调用时会报错
//new TestChild.help();
}
class Test{
var x, y;
Test(){
print('这是 Test 类的空参构造');
}
// 命名构造不能被继承
Test.help(){
x = 5;
y = 10;
print('Test.help() 命名函数 x=${x}, y = ${y}');
}
}
class TestChild extends Test{
var x, y;
TestChild(){
print('这是 TestChild 类的空参构造');
}
// 加上与父类相同的命名构造就不会错 注释了就会报错
// TestChild.help(){
// x = 3;
// y = 2;
// print('TestChild.help() 命名函数 x=${x}, y = ${y}');
// }
}
5) 构造函数不是继承的
子类不从其超类继承构造函数。声明没有构造函数的子类只有默认(无参数,无名称)构造函数。
6) 构造函数调用流程
默认情况下,子类中的构造函数调用超类的无参构造函数。超类的构造函数在构造函数体的开头被调用。如果还使用初始化列表,则在调用超类之前执行。
执行顺序如下:初始化列表 -> 超类的无参数构造函数 -> 主类的无参数构造函数
,
超类必须要有一个空参构造,如果超类没有未命名的无参数构造函数,则必须手动调用超类中的一个构造函数。在冒号(:
)之后,在构造函数体(如果有)之前指定超类构造函数。例如下面的示例:TestChild
类和其超类 Test
类。运行结果为:
Test 空参构造
TestChild 有参构造
面积为:6.0
示例代码:
void main() {
var result = new TestChild.area(3, 4);
print('面积为:${result.area}');
}
class Test {
num width;
num height;
num area;
// 必须加上空参构造,如果注释掉 它的子类会报错
Test() {
print('Test 空参构造');
}
Test.area(width, height)
: width = width,
height = height,
area = width * height {
print('Test 有参构造');
}
}
class TestChild extends Test {
num width;
num height;
num area;
TestChild() {
print('TestChild 空参构造');
}
TestChild.area(num width, num height)
: area = (width * height)/2 {
print('TestChild 有参构造');
}
}
7) 重定向构造函数
有时构造函数的唯一目的是重定向到同一个类中的另一个构造函数。重定向构造函数的主体是空的,构造函数调用出现在冒号(:)之后。示例如下:
void main() {
var result = new Test(4, true, '数字', 10);
print('abcd分别是:${result.a},${result.b},${result.c},${result.d}');
}
class Test {
num a;
bool b;
String c;
num d;
// 主构造函数
Test(this.a, this.b, this.c, this.d);
// 委托给主构造函数
Test.test1(num x,bool y) : this(x, y,'', 0);
Test.test2(num a,bool b, String c) : this(a, b, c, 0);
Test.test3(num a,bool b, String c,num d) : this(a, b, c, d);
}
结果是:abcd 分别是:4, true, 数字, 10。
8) 常量构造函数
- 1. 如果你的类生成永远不会更改的对象,则可以使这些对象成为编译时常量。为此,请定义
const
构造函数并确保所有实例变量都是final
。要使用 const 构造函数创建编译时常量,请将const
关键字放在构造函数名称之前。
示例代码:
oid main() {
var result = new Test(4, 10);
print('x=${result.x}, y:=${result.y}'); // x=4, y=10
print(Test.origin.x); // 5
print(Test.origin.y); // 1
var a = const Test(1, 1);
var b = const Test(1, 1);
// 它们是相同示实例
print(a == b); // true
class Test {
static final Test origin = const Test(5, 1);
final num x;
final num y;
const Test(this.x, this.y);
}
- 2. 常量上下文中的 const 构造函数
在常量上下文中,你可以省略 const
构造函数或文字之前的内容。常量上下文,可以简单的理解为:const
后面包裹的语句一定是连续的一个整体,例如声明一个 list 或者 map。例如,查看此代码,该代码创建一个 const
的 map:
// 这里有很多const关键字
const pointAndLine = const {
'point': const [const ImmutablePoint(0, 0)],
'line': const [const ImmutablePoint(1, 10), const ImmutablePoint(-2, 11)],
};
class ImmutablePoint{
const ImmutablePoint(int a, int b);
}
你可以省略除 const
关键字的第一次使用之外的所有内容:
// 只有一个const, 它建立了常量上下文
const pointAndLine2 = {
'point': [ImmutablePoint(0, 0)],
'line': [ImmutablePoint(1, 10), ImmutablePoint(-2, 11)],
};
如果一个常量构造函数在常量上下文之外,并且在没有常量的情况下被调用,则会创建一个非常量对象:
var a = const ImmutablePoint(1, 1); //创建一个常量
var b = ImmutablePoint(1, 1); // 不创建常量
assert(!identical(a, b)); // 不是同一个实例
9) 工厂构造函数
factory
是在实现不总是创建其类的新实例的构造函数时使用关键字。例如,工厂构造函数可能从缓存中返回实例,或者它可能返回子类型的实例。
class Test{
final String name;
static Map<String, Test> _cache = new Map<String, Test>();
factory Test(String name) {
if (_cache.containsKey(name)) {
return _cache[name];
} else {
final symbol = new Test._internal(name);
_cache[name] = symbol;
return symbol;
}
}
Test._internal(this.name);
void test(){
print('调用了test()');
}
}
void main(){
var a = new Test('abc');
var b = new Test('abc');
// 检查两个是否引用的相同的对象
print(identical(a, b)); // true
new Test('abc').test();
}
运行结果:
true
调用了test()
说明:
- 1. 工厂构造函数无权访问
this
。 - 2. 可以创建子类的实例(例如:取决于传递的参数)。
- 3. 返回缓存的实例而不是新的实例。
- 4. 可以使用 new 关键字,也可以不使用。(上例中可以这样写:Test('abc').test())
- 5. 工厂构造函数没有初始化列表(没有 :super())
以下是 Dart 的工厂函数实现的单例:
// factory实现的单例
class Singleton {
factory Singleton() => const Singleton._internal_();
const Singleton._internal_();
}
void main() {
print(new Singleton() == new Singleton());
print(identical(new Singleton(), new Singleton()));
}
(3) 初始化列表
1) 可以在构造函数体运行之前初始化实例变量,用逗号分隔初始化。例如:
有参构造的初始化:
class Test1 {
var x, y;
Test1(var x, var y)
: x = x,
y = y {
print('Test1 有参构造初始化');
}
}
命名构造的初始化:
class Test2{
var x,y;
Test2.from(Map<String, num> json)
: x = json['x'],
y = json['y'] {
print('Test2.from(): ($x, $y)');
}
}
2) 在实际应用开发中,可以使用 assert
在初始化列表用来校验输入参数。
例如:
有参构造使用 assert
校验参数:
class Test1 {
var x, y;
Test1(var x, var y) : assert(x >= 0) {
print('Test1(): ($x, $y)');
}
}
命名构造使用 assert
校验参数:
class Test2 {
var x, y;
Test2.withAssert(this.x, this.y) : assert(x >= 0) {
print('Test2.withAssert(): ($x, $y)');
}
}
3) 如果没有更多实际操作内容,可以简写。
有参构造使用 assert
校验参数的简写形式:
class Test1 {
var x, y;
Test1(var x, var y) : assert(x >= 0);
}
命名构造使用 assert
校验参数的简写形式:
class Test2{
var x, y;
Test2.withAssert(this.x, this.y) : assert(x >= 0);
}
4) 如果要把构造的初始化和 assert
校验同时使用,可以采用这种方式:
使用 assert
简写,然后在内容体里面执行初始化操作。
class Test1{
var x, y;
Test1(var x, var y) : assert(x > 0){
this.x = x;
this.y = y;
print('Test1 有参构造初始化');
}
}
5) 设置 final
字段,初始化程序时更方便。
import 'dart:math';
// 设置final字段,初始化程序时更方便
void main() {
var p = new Test1(4, 3);
print('长方形的对角线长度:${p.hypotenuse}');
}
class Test1 {
final num width;
final num height;
final num hypotenuse;
Test1(width, height)
: width = width,
height = height,
hypotenuse = sqrt(width * width + height * height);
}
2. mixin 入门
在面向对象的编程语言中,Mixin 是包含供其他类使用的方法的类,而不必是其他类的父类。其他类如何访问 Mixin
的方法取决于语言。Mixin
有时被描述为“包含的”而不是“继承的”。
Mixins
鼓励代码重用,并且可用于避免多重继承可能导致的继承歧义 (菱形问题)
,或者用于解决语言中缺少对多重继承的支持的问题。Mixin
还可以被视为具有实现方法的接口。这个模式是执行依赖倒置原则的一个例子。
一、继承歧义
(一) 定义
继承歧义
,也叫菱形问题
,也叫做钻石问题
,或者有时被称为致命的死亡钻石
。当两个 B 和 C 类继承自 A,D 类继承自 B 和 C 时产生歧义。如果 A 中有一个方法在 B 和 C 中已经重写,而 D 没有重写它,那么 D 继承的方法的版本是 B,还是 C?
如下图所示:
(二) 继承歧义的缓解
不同的编程语言有不同的方法来处理这些重复继承的问题,这里列举几个用的比较多的语言。
语言 | 解决方案 |
---|---|
C++(底层、硬件、编解码、算法等都用得到) | 默认情况下,每个继承路径都是分开的,因此D对象实际上包含两个独立的a对象,并且必须正确限定a成员的使用。如果从A到B的继承和从A到C的继承都标记为virtual (例如,class B:virtual public A ),那么c++会特别注意只创建一个对象,并正确使用A的成员。如果虚拟继承和非虚拟继承是混合的,那么只有一个虚拟A,对于每个到A的非虚拟继承路径,都有一个非虚拟A。C++需要显式地声明要使用的特性是从哪个父类调用的(例如:Worker::Human.Age )。C++不支持显式的重复继承,因为没有办法限定要使用哪个超类(例如:在一个派生列表[class Dog : public Animal, Animal] 中出现一个类不止一次)。C++还允许通过虚拟继承机制创建多个类的单个实例(例如:Worker::Human 和Musician::Human 将引用相同的对象)。 |
Java8(服务端开发、Android开发) | Java 8在接口上引入默认方法。如果A、B、C 是接口,B、C 可以为A 的抽象方法提供不同的实现,从而导致菱形问题 。D 类必须重新实现该方法(它的主体可以简单地将调用转发给一个超类来实现),否则模糊将被拒绝作为编译错误。(在Java 8之前,Java不受钻石问题风险的影响,因为它不支持多重继承。) |
Go(可以用于区块链有关) | 在编译时防止钻石问题。如果一个结构体D嵌入两种结构体B和C(这两个结构体都有一个方法F() ),从而满足接口A,那么如果调用D.F(),或者如果D的实例被分配给类型A.B和C的变量,则编译器将会提示ambiguous selector(模拟两可的选择) 。 |
Python(可以用于人工智能有关) | Python的继承顺序影响类语义。Python在引入新样式的类时必须处理这个问题,所有这些类都有一个共同的祖先对象。Python使用C3线性化(或方法解析顺序(Method Resolution Order,MRO ))算法创建类列表。该算法强制执行两个约束:子类先于父类,如果一个类从多个类继承,它们将按照基类元组中指定的顺序保存(但是在这种情况下,继承图中较高的一些类可能先于图中较低的类)。因此,方法的分辨率顺序为:D, B, C, A 。 |
Scala(可以用于大数据方面) | Scala允许特性的多个实例化,通过在类层次结构和特征层次结构之间添加区别,可以实现多重继承。类只能从单个类继承,但是可以根据需要混合(mix-in )任意多的特性。Scala使用扩展的traits 的右优先深度优先的搜索来解析方法名,然后除去结果列表中每个模块的最后一次出现。所以,解决的顺序是[D, C, A, B, A] ,被减少到[D, C, B, A] |
只允许单个继承 (类只能从一个基类派生) 的语言没有菱形问题。这样做的原因是,无论方法的重复或位置如何,这些语言在继承链的任何级别上最多只能实现一个方法。通常,这些语言允许类实现多个 protocols
,在 Java 中称为接口
。这些协议定义了方法,但没有提供具体的实现。这个策略已经被 ActionScript、c#、D、Java、Nemerle、Object Pascal (Free Pascal and Delphi)、Objective-C、Smalltalk、Swift、PHP
所使用。所有这些语言都允许类实现多个 protocols
。此外,Ada、Objective-C、c#、Delphi/Free Pascal、Java、Swift、PHP
等语言允许接口的多重继承 (在 Objective-C 和 Swift 中称为 protocols(协议)
)。接口就像抽象基类,它们指定方法签名而不实现任何行为。(“纯”接口,例如版本7之前的Java 接口,不允许接口中的任何实现或实例数据。)然而,即使当多个接口声明相同的方法签名时,只要该方法在继承链中的任何位置实现(定义),它就会覆盖该方法在其上链中的任何实现(在它的超类中)。因此,在继承链的任何给定级别上,任何方法最多只能有一个实现。因此,单继承方法实现即使在接口的多继承中也不存在菱形问题。随着 Java 8 中接口的默认实现的引入,仍然有可能生成菱形问题,尽管这只会作为编译时错误出现。
3. 异步
Dart 库中包含许多返回 Future 或 Stream 对象的函数。这些函数是异步的:它们在设置可能耗时的操作(例如I / O)后返回,而不等待该操作完成。
async
和 await
关键字支持异步编程,让你写异步代码看起来类似于同步代码。
(一) 处理Future
当你需要完成 Future 的结果时,你有两个选择:
- 1) 使用 async 和 await。
- 2) 使用 Future API。
(二) 使用 async 和 await
使用 async
和 await
异步的代码,但它看起来很像同步代码。例如,这里有一些代码 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);
}
// ...
}
4. 库相关
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 表示隶属于某个库的一部分。
注意事项:
不能同时使用 library 和 part of,它们都用于指定属于库的内容。
// library testlib2; 这个不能和part of同时使用 会报错
// part of 表示这个库是testlib库的一部分
part of testlib1;
B 库是 A 库的一部分,在 B 库里面声明:part of A 库名称。
例如:在 testlib2.dart 里面声明 part of testlib1
; 表示 testlib2 这个库是 testlib 库的 yi 部分。
如果 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()函数');
}
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。