Flutter入门 Dart语言简介

219 阅读12分钟

参考资料:

  1. Dart 官方文档

Dart 在静态语法方面和 Java 非常相似,如类型定义、函数声明、泛型等,而在动态特性方面又和 JavaScript 很像,如函数式特性、异步支持等。除了融合 Java 和 JavaScript 语言之所长之外,Dart 也具有一些其它具有表现力的语法,如可选命名参数、..(级联运算符)和?.(条件成员访问运算符)以及??(判空赋值运算符)。其实,对编程语言了解比较多的读者会发现,在 Dart 中其实看到的不仅有 Java 和 JavaScript 的影子,它还具有其它编程语言中的身影,如命名参数在 Objective-C 和 Swift 中早就很普遍,而??操作符在PHP 7.0 语法中就已经存在了,因此我们可以看到 Google 对 Dart 语言给予厚望,是想把 Dart 打造成一门集百家之所长的编程语言。

1、变量声明

1. var

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

var t = "aaa";
// 下面代码在dart中会报错,因为变量t的类型已经确定为String,
// 类型一旦确定后则不能再更改其类型。
t = 1000;

上面的代码在 JavaScript 是没有问题的,前端开发者需要注意一下,之所以有此差异是因为 Dart 本身是一个强类型语言,任何变量都是有确定类型的,在 Dart 中,当用var声明一个变量后,Dart 在编译时会根据第一次赋值数据的类型来推断其类型,编译结束后其类型就已经被确定,而 JavaScript 是纯粹的弱类型脚本语言,var 只是变量的声明方式而已。

2. dynamic 和 Object

Object 是 Dart 所有对象的根基类,也就是说在 Dart 中所有类型都是Object的子类(包括Function和Null),所以任何类型的数据都可以赋值给Object声明的对象。 dynamicObject声明的变量都可以赋值任意对象,且后期可以改变赋值的类型,这和 var 是不同的,如:

dynamic t;
Object x;
t = "hello world";
x = 'hello Object';
//下面代码没有问题
t = 1000;
x = 1000;

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

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

 printLengths() {
    // 正常
    print(a.length);
    // 报错 The getter 'length' is not defined for the class 'Object'
    print(b.length);
 }

dynamic 的这个特点使得我们在使用它时需要格外注意,这很容易引入一个运行时错误,比如下面代码在编译时不会报错,而在运行时会报错:

print(a.xx); // a是字符串,没有"xx"属性,编译时不会报错,运行时会报错

3. final 和 const

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

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

空安全(null-safety)

Dart 中一切都是对象,这意味着如果我们定义一个数字,在初始化它之前如果我们使用了它,假如没有某种检查机制,则不会报错,比如:

test() {
  int i; 
  print(i*8);
}

在 Dart 引入空安全之前,上面代码在执行前不会报错,但会触发一个运行时错误,原因是 i 的值为 null 。但现在有了空安全,则定义变量时我们可以指定变量是可空还是不可空。

int i = 8; //默认为不可空,必须在定义时初始化。
int? j; // 定义为可空类型,对于可空变量,我们在使用前必须判空。

// 如果我们预期变量不能为空,但在定义时不能确定其初始值,则可以加上late关键字,
// 表示会稍后初始化,但是在正式使用它之前必须得保证初始化过了,否则会报错
late int k;
k=9;

如果一个变量我们定义为可空类型,在某些情况下即使我们给它赋值过了,但是预处理器仍然有可能识别不出,这时我们就要显式(通过在变量后面加一个”!“符号)告诉预处理器它已经不是null了,比如:

class Test{
  int? i;
  Function? fun;
  say(){
    if(i!=null) {
      print(i! * 8); //因为已经判过空,所以能走到这 i 必不为null,如果没有显式申明,则 IDE 会报错
    }
    if(fun!=null){
      fun!(); // 同上
    }
  }
}

上面中如果函数变量可空时,调用的时候可以用语法糖:

fun?.call() // fun 不为空时则会被调用

2、函数

Dart是一种真正的面向对象的语言,所以即使是函数也是对象,并且有一个类型Function。这意味着函数可以赋值给变量或作为参数传递给其他函数,这是函数式编程的典型特征。

1. 函数声明

bool isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

Dart函数声明如果没有显式声明返回值类型时会默认当做dynamic处理,注意,函数返回值没有类型推断:

typedef bool CALLBACK();

//不指定返回类型,此时默认为dynamic,不是bool
isNoble(int atomicNumber) {
  return _nobleGases[atomicNumber] != null;
}

void test(CALLBACK cb){
   print(cb()); 
}
//报错,isNoble不是bool类型
test(isNoble);

对于只包含一个表达式的函数,可以使用简写语法:

bool isNoble (int atomicNumber)=> true ;   

2. 函数也是对象

在 dart 中,可以用变量来引用函数对象、向函数传递函数参数、创建函数对象 函数作为变量:

var say = (str){
  print(str);
};
say("hello world");

函数作为参数传递:

void execute(var callback) {
    callback();
}
execute(() => print("xxx"))

可选的位置参数

包装一组函数参数,用[]标记为可选的位置参数,并放在参数列表的最后面:

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

下面是一个不带可选参数调用这个函数的例子:

say('Bob', 'Howdy'); //结果是: Bob says Howdy

下面是用第三个参数调用这个函数的例子:

say('Bob', 'Howdy', 'smoke signal'); //结果是:Bob says Howdy with a smoke signal

可选的命名参数

定义函数时,使用{param1, param2, …},放在参数列表的最后面,用于指定命名参数。例如:

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

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

enableFlags(bold: true, hidden: false);
  1. 可选命名参数在Flutter中使用非常多。 注意,不能同时使用可选的位置参数和可选的命名参数

3、运算符

dart 提供了一些比较简便的运算符来简化操作,大部分和 Java 相同,以下介绍下几个不同的运算符

void main() {

// is 用于判断变量是否是指定的数据类型
// is! 取反
var strValue = "hello";
print(strValue is String); //true
print(strValue is int); //false
print(strValue is! String); //false

// ~/ 用于除法运算是取整
// / 则不取整
print(10 / 3); //3.333333333
print(10 ~ 3); //3

// as 用于强制类型转换
num numValue = 10;
int intValue = numValue as int;

// 如果 ??= 左边的变量值为null. 则将其赋值为右边的值
var name = null;
var age = 10;
name ??= "hello";
age ??= 15;
print("name: $name"); //name:hello
print("age: $age"); //age:10

// 如果 ?. 左边的变量值不为null, 则右边的操作生效
// 用于避免发生空指针异常
var area = null;
print(area?.runtimeType); //null

// 级联运算符 .. 用于连续操作某个对象,而无需每次操作时都调用该对象
var list = [1,2,3,4,5];
list
 ..insert(0,6)
 ..removeAt(4)
 ..add(7)
print(list); //[6,1,2,3,5,7]

// 扩展运算符 ... 和 空值感知扩展运算符 ...?
// 提供了一种将多个元素插入集合的简洁方法
var list1 = [1,2,3];
var list2 = [0, ...list1];
print(list2); //[0,1,2,3]

// 如果 list3 可能为null,此时则需要使用空值感知扩展运算符,否则会抛出异常
// 空值感知扩展运算符只有当 list3 不为 null 时才会执行插值操作
var list3 = null;
var list4 = [0, ...?list3];
print(list4); //[0]
}

4、类与对象

Dart 中,所有的对象都是类的实例,并且所有的类都是 Object 的子类。

1. 类的定义和构造函数

类的定义用 class 关键字,如果未显式定义构造函数,会默认一个空的构造函数,这一点与 Java 不谋而合。另外,Dart 还提供 命名构造函数这一功能,格式为:Class.costructorName(var params)。这种用法在某些场景下很实用,比如:

class Person{
  String name;
  int age;
  bool sex;
  String hobby;

  Person(this.name, this.age, this.sex, this.hobby);

  /// 命名构造函数
  Person.fromJson(Map json){
    print("Person constructor...");
    this.name  = json['name'];
    this.age   = json['age'];
    this.sex   = json['sex'];
    this.hobby = json['hobby'];
  }
}

2. 命名构造函数

通过命名构造函数,我就可以直接通过多种方式来生成当前类的实例,这里通过 Json 格式数据生成 Person 对象。另外,如果构造函数只是简单的参数传递,也可以直接在构造函数后加 : 来对参数进行赋值,多个参数可通过 , 相连:

class Point {
    num x;
    num y;
    num z;
    
    Point(this.x, this.y, z) { //第一个值传递给this.x,第二个值传递给this.y
            this.z = z;
    }
    
    Point.fromeList(var list): //命名构造函数,格式为Class.name(var param)
            x = list[0], y = list[1], z = list[2]{//使用冒号初始化变量
    }

    //当然,上面句你也可以简写为:
    //Point.fromeList(var list): this(list[0], list[1], list[2]);

     String toString() => 'x:$x  y:$y  z:$z';
}

如果你要创建一个不可变的对象,你可以定义编译时常量对象 需要在构造函数前加 const:

class ImmutablePoint {
    final num x;
    final num y;
    const ImmutablePoint(this.x, this.y); // 常量构造函数
    static final ImmutablePoint origin = const ImmutablePoint(0, 0); // 创建一个常量对象不能用new,要用const
}

3. 属性访问器

即我们常说的 Setter/Getter,主要是用来读写一个属性的方法。每个字段都对应一个隐式的 Getter 和 Setter,但是调用的时候是 obj.name,而不是 obj.name()。当然,你可以通过 get 和 set 关键字扩展功能 来实现自己的 Setter/Getter , 如果字段为 final 或者 const 的话,那么它只能有一个 getter 方法。如:

class Rectangle {
  num left, top, width, height;

  Rectangle(this.left, this.top, this.width, this.height);

  // Define two calculated properties: right and bottom.
  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);
}

4. 工厂构造函数

并不是任何时候我们都需要创建一个新的对象,如:可以返回一个已经缓存好的对象或者该类的子类对象。我们以返回该类的一个缓存对象为例:

class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache =
      <String, Logger>{};

  factory Logger(String name) {
    return _cache.putIfAbsent(
        name, () => Logger._internal(name));
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

注意:factory 构造函数不能通过 this 关键字来获取类的成员

5. 抽象类

Dart 中并没有 interface 关键字,只有 abstract 来修饰"抽象类",但是,这里的抽象类既可以被继承(extends),也可以被实现(implements)。如:

abstract class Person {  // 可以不用 abstract 修饰,如果加上 abstract 的话 Person 不能被实例化 
    String greet(who);   
}

class Student implements Person { 
    String name; 
    Student(this.name); 
     
    String greet(who) => 'Student: I am $name!'; 
} 

class Teacher implements Person { 
    String name; 
    Teacher(this.name); 
   
    String greet(who) => 'Teacher: I am $name!'; 
} 

void main() { 
    Person student = new Student('Wang'); 
    Person teacher = new Teacher('Lee'); 
    
    print( student.greet('Chen')); 
    print(teacher.greet('Chen')); 
}

同样地,Dart 中的类是单继承,多实现。这里使用的 implements 后,子类 Student 无法去访问父类 Persion 中的变量,但是抽象方法必须显式实现。相反,如果使用 extends 继承,子类就可以直接使用父类的非私有变量,倘若父类不是抽象类,那么子类同样不需要显式实现里面方法。如:

// A person. The implicit interface contains greet().
class Person {
  // In the interface, but visible only in this library.
  final _name;

  // Not in the interface, since this is a constructor.
  Person(this._name);

  // In the interface.
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// An implementation of the Person interface.
class Impostor implements Person {
  get _name => '';

  String greet(String who) => 'Hi $who. Do you know who I am?';
}

String greetBob(Person person) => person.greet('Bob');

void main() {
  print(greetBob(Person('Kathy')));
  print(greetBob(Impostor()));
}

5、mixin

Dart 是不支持多继承的,但是它支持 mixin,简单来讲 mixin 可以 “组合” 多个类,我们通过一个例子来理解。

定义一个 Person 类,实现吃饭、说话、走路和写代码功能,同时定义一个 Dog 类,实现吃饭、和走路功能:

class Person {
  say() {
    print('say');
  }
}

mixin Eat {
  eat() {
    print('eat');
  }
}

mixin Walk {
  walk() {
    print('walk');
  }
}

mixin Code {
  code() {
    print('key');
  }
}

class Dog with Eat, Walk{}
class Man extends Person with Eat, Walk, Code{}

我们定义了几个 mixin,然后通过 with 关键字将它们组合成不同的类。有一点需要注意:如果多个mixin 中有同名方法,with 时,会默认使用最后面的 mixin 的,mixin 方法中可以通过 super 关键字调用之前 mixin 或类中的方法。我们这里只介绍 mixin 最基本的特性,关于 mixin 更详细的内容读者可以自行百度。

6、异步支持

Dart类库有非常多的返回Future或者Stream对象的函数。 这些函数被称为异步函数:它们只会在设置好一些耗时操作之后返回,比如像 IO操作。而不是等到这个操作完成。

asyncawait关键词支持了异步编程,允许您写出和同步代码很像的异步代码。

Future

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

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

1. Future.then

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

Future.delayed(Duration(seconds: 2),(){
   return "hi world!";
}).then((data){
   print(data);
});

2. Future.catchError

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

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

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

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

3. Future.whenComplete

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

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

4. Future.wait

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

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

执行上面代码,4秒后你会在控制台中看到“hello world”。