Dart语法

1,083 阅读12分钟

本文只是简单总结下 dart 的常用语法,加深下印象。想学习 dart 语法推荐去官网

一、重要概念

  • 任何一个变量都是对象,每个对象都是类的实例,甚至是数字、函数、null都是对象。所有的对象都继承自 Object 类。
  • 虽然 dart 是强类型语言,不过类型声明是可选的,因为 dart 可以推断类型。如 var num = 1;num 会被推断为 int,当你知道没有明确的类型时,可以使用 dynamic 来修饰变量。
  • Dart 支持泛型,如 List<int>List<dynamic>(一系列任意类型的对象)。
  • Dart 支持顶级函数,也可以在类或对象中定义函数,也可以函数嵌套函数。
  • 同样也支持顶级变量,在类或对象中定义变量。
  • Dart 没有访问修饰符,如果想要私有的,则变量名称前缀加上 _
  • Dart 具有表达式,如条件表达式。

二、变量

var

var 修饰的变量可以表示任意类型,当变量被赋值后,它自动会推断出变量是什么类型。

var name = "sun"; // 自动推断为 String 类型

dynamic

dynamic 修饰的变量也可以表示任意类型,它和 var 的区别是不需要推断出类型。这样即使在之后的赋值类型不同也不会报错。

dynamic name = "sun";
name = 1; 	// 赋值int类型值也不会报错

final

final 修饰的变量只能被赋值一次,而且赋值必须发生在构造函数执行之前:

class Test{
  final x = 1;
  Test(){}
}
// 或 
class Test{
  final x;
  Test(this.x){}
}

const

乍一看 const 和 final 很类似,都只能被赋值一次,但 const 其实更复杂些。

  • const 修饰的变量在编译期就已经确定,运行期不会再改变了,因此想上面 final 的第二种用法(构造函数传参赋值)这种运行期才执行的是不行的。

  • const 一般在声明在顶级域,如果想在类中声明,那么必须加上 static 修饰。

    const X = 1;
    const Y = X+1;
    class Test{
      static const Z = 3;
    }
    
  • const 修饰的值只会创建一次,每次使用到时都会复用。如:

    var list = const [1,2,3];
    

    list 列表会被赋值为 [1,2,3],之后也可以再赋值,这些都是一样的。不过由于 const 修饰了 [1,2,3],这个常量值之后不会再去开辟一个新的内存空间,而是每次赋值都会复用。这个特性在开发中特别有用,对于一些需要频繁创建对象、集合但每次创建都是固定不变的地方,就会节省很多内存及性能开销。下面是复用一个对象的例子:

    class Test{
      final x;
      const Test(this.x);
    }
    var test = const Test("hello");
    

    这个时候 Test("hello") 这个对象永远只会创建一次。

三、内置类型

1. Numbers

int: 在 Dart 中 int 占8个字节,取值范围是 -2^63 ~ 2^63-1,如果编译成 JavaScript 则范围变成 -2^53 ~ 2^53-1。

double: 也占8个字节,double 和 int 都是 num 的子类型。以下是一些互转操作。

var one = int.parse('1'); // String to int
var onePointOne = double.parse('1.1'); // String to double
String oneAsStr = 1.toString(); // int to String
String onePointOneAsStr = 1.1.toString(); // double to String

2. String

Dart 字符串是用 UTF-16 编码的(对编码有疑问的可以看 字符编码的概念 )。

3. bool

和其他语言一样,true or false。

4. List

var list = [1,2,3]; // 推断为 List<int> 类型。
List<String> list1 = List<String>(10);	// 泛型参数类型为 String 并且长度为10的列表

Dart 中提供扩展操作符来为集合(List, Set, Map等)插入多个节点,如:

var list = [1,2,3];
var list1 = [0, ...list];

5. Set

和 List 不同之处在于里面的item是唯一的,用花括号包括item。

var set = {1,2,1};	// 事实上set只有 {1,2}
var set1 = Set<String>();

6. Map

和其他语言一样是key-value映射集合。

var map = {
  "key1": "value1",
  "key2": "value2",
}
var map1 = Map();
map1['key1'] = "value1";

7. Rune

Rune 也是字符串,但是它是 UTF-32 编码的,不过UTF-16也是可以通过扩展表示4个字节的。暂时不清楚这个乐行的真实意图。

8. Symbol

Symbol 属于编译时常量,通过名字来引用标识符很有价值。它主要用于,代码被混淆后很多变量名、方法名之类的都被混淆了,此时无法通过反射获得。一般 Android 中会采取添加混淆文件的做法,目前还不知道 dart 是否有这种东西。不过使用 Symbol 可以找到对应的变量、方法名,具体可以看下 Symbol

四、函数

Dart 是面向对象的语言,因此万物皆对象,函数也是一种 Function 类型,可以直接把函数赋值给变量。一个函数也可以只包含有一个表达式:

var fun = getFun(1);
bool getFun(num) => num != 1;

1. 可选参数

// 命名参数
void test({bool x = false, String y}); // 调用时显示指定参数名并给其赋值,如 test(y: "xxx");

// 位置参数
void test([bool x, String y = 'test']); // 调用时需要按顺序传参,如 test(true),但test("hello")就会报错

2. 函数作为一级对象使用

意思就是函数既可以作为参数传递也可以赋值给变量。函数式编程的关键。

3. 匿名函数

不需要声明一个函数,直接在需要函数传递的地方写一个去掉函数名的函数即可,格式如 (context) {} 这么个东西。 4. 闭包

嵌套的函数能访问外部函数的变量。

Function makeAdder(num addBy) {
  return (num i) => addBy + i;
}

void main() {
  var add2 = makeAdder(2);	
  var add4 = makeAdder(4);
  print(add2(3)); // 5
  print(add4(3)); // 7
}

基本上可以认为把外部函数的变量看做自己的变量,但是有一个问题,就是外部函数的变量自己改变值的话,内部函数也会跟着改变,这个是比较危险的一个行为。

五、操作符

as 和 is

as 表示直接强转类型,可能会抛出异常。

(name as String) = 'bb';

is 表示判断类型:

if(name is String){
	// 自动强转
  name = 'bb';
}

? ?? ?.

二元表达式: condition ? expr1 : expr2,如果 condition 为true,则执行 expr1,否则执行 expr2;

expr1 ?? expr2,如果 expr1 为 null,则返回 expr2, 否则返回它自己。

foo?.bar 这里的语法和 kotlin 一样,foo不为 null 才会访问 bar。

级联

一般给对象实例变量或方法赋值时,会类似于 test().x = true 这样的形式,如果采取 test()..x = true,赋值动作是没有不同的,但是返回值为实例对象本身,因此可以链式调用起来了, 看起来非常的干净整洁。

test()
	..x = true
	..y = "hello";

六、异常

try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // 一个指定的异常
  buyMoreLlamas();
} on Exception catch (e) {
  // 任何异常
  print('Unknown exception: $e');
} catch (e) {
  // 任何异常
  print('Something really unknown: $e');
}finally {
  // 无论有没有异常都会走到finally.
}

on 关键字用来指定特定的异常,catch 关键字是捕获异常对象。两者也可以一起使用。

七、类

1. factory

使用 factory 关键字修饰构造函数一般有两种用途:

  • 简单工厂模式,通过传 type 返回对应的子类型。

    abstract class Animal {
      
      Animal._();
    
      factory Animal(String type){
        if("dog" == type){
          return new Dog();
        }else {
          return new Cat();
        }
      }
    }
    
    class Dog extends Animal {
      Dog(): super._(){}
    }
    
    class Cat extends Animal {
      Cat(): super._(){}
    }
    
  • 单例模式:

    class Singleton {
      static Singleton _instance;
      // 禁止外部调用构造函数
      Singleton._internal(){}
    
      factory Singleton() => _getInstance();
    
      static Singleton _getInstance(){
        if(_instance == null){
          _instance = Singleton._internal();
        }
        return _instance;
      }
    }
    

**2. getter setter **

每个定义的变量默认都有 get 和 set 方法,如果想要自定义,则:

class Test{
  int get width => 1;
  set width(int num) => width = num;
}

可以看做是一个函数表达式,只是要加上 get 和 set 关键字。

3. 抽象类

基本和 Java 的抽象类差不多。

4. 接口

Dart 中每个类其实都是个接口,可以使用 implements 实现这个接口,并且必须实现这个接口的变量和函数。

class Test {
  int num = 1;
  void getNum(){}
}

class Test1 implements Test {
  @override
  int num;
  
  @override
  void getNum() {
    // TODO: implement getNum
  }
}

5. 枚举类

也和 Java 一样。

6. mixin

mixin 是Dart的特色,比较实用,它主要的作用就是复用代码。很多时候不想写父类或者想用于更多不同类别的类时,mixin就是非常好的选择。

class Test with MixinTest{
  Test(){
    this.age = 1;
  }
  void doSomeThing(){
    this.getAge();
  }
}

mixin MixinTest {
  int age = 18;
  int getAge(){
    return age;
  }
}

看上去和继承非常像,其实 mixin 的作用也有为了解决 Dart 单继承的特性,类可以混合多个 mixin 类就如同实现接口一般。Test 能用 MixinTest 内所有的变量和方法。不过 mixin 修饰的类不能有构造函数,另外可以省略 MixinTest 修饰的 mixin 关键字,只需要用到 with 关键字即可。mixin 还可以强制使用自己的类必须继承某些类:

// Test 必须继承Super
class Test extends Super with MixinTest{  }
// 使用 on 关键字
mixin MixinTest on Super{}

class Super{}

这种使用场景基本是 mixin 类需要某些类的支撑,才能使用。

7. static

static 修饰的变量只有在被使用到的时候才会被初始化,这一点和 Java 有很大的差异,Java 是在类初始化的时候就会把 static 变量初始化。

八、泛型

Dart 中的泛型和 Java 中完全一样,也是泛型擦除,并且使用方式都是一模一样的。

九、库

导入库的用法:

import 'package:lib2/lib2.dart' as lib2;  // 相当于把库重命名,之后就使用lib2来调用库内的函数、变量
import 'package:lib1/lib1.dart' show foo; // show 展示库中一部分的内容,这里只展示 foo
import 'package:greetings/hello.dart' deferred as hello; // 懒加载库,用到的时候才会加载

十、异步

Future

首先必须要理解一个前提,Dart 本身是一个单线程的模型,所以在 Dart 中执行耗时操作不能叫做并发执行(当然dart中也有类似的操作),一般来说不是非常耗时的操作,如在一秒之内的网络请求、I/O操作之类的,一般都会采取异步的方式。而Future就是实现异步的关键。

print(1); 
Future future = Future(()=>"hello");
future.then((value) => print(value));
print(2);

这段代码的输出结果顺序是 1 2 hello

同步意思就是按顺序执行代码,而异步就是让其他代码先执行,自己在它们执行完了之后再执行。()=>"hello" 这个函数完全可以用耗时操作代替,它会被加入到队列中,等待同步代码执行完后执行。但要知道的是主线程的总耗时是不变的,只是换个顺序罢了,所以非常耗时的最好还是不要用异步。

async await

async await 其实是个语法糖,其本质依然是 Future,只是它们能把异步当做同步代码来看,能让人更容易理解。

void test async {
  print(1); 
	hello();
	print(2);
}
Future<void> hello() async{
   return await Future(() => print("hello"));
}

其实看起来并没有更容易理解。。。

Stream

异步事件流,即响应式编程。概念比较绕,**就我现在的理解就是异步去执行某个操作后最后会回调到监听函数中,然后监听函数可能又会做出相应的操作,然后又回调到流订阅的另一个监听函数中,以链式调用的形式按顺序执行一系列事件却不影响在其之后的代码的执行。**Future 只关心一个异步操作然后回调结果,而Stream就是加强版的Future,它可以接受触摸事件、数据流等任何事件。

var transformer = StreamTransformer<int,String>.fromHandlers(
  handleData: (data, sink){
    sink.add((data + 1).toString());
  }
);
StreamController<int> controller = StreamController();
controller.stream
    .transform(transformer)
    .listen((data) => {
      print("result = " + data.toString())
    });
controller.sink.add(3);
controller.sink.add(1);

以上的示例代码中 controller.stream 就是一个流,每当有数据发送到流中时即 add(),中间可以经过很多层的调用,比如转换了一次数据,然后最后达到监听函数。

也可以用 async, await for 关键字实现stream看起来像同步代码那样执行:

controller.sink.add(3);
controller.sink.add(1);
testStream(controller);

testStream(StreamController controller) async {
  await for(var item in controller.stream){
    print("testStream = " + item.toString());
  }
}

个人感觉还是回调看着舒服些。。推荐一篇 Stream

十一、生成器

生成器顾名思义就是生成了某个东西。其实 async await 也算是一个生成器,即返回的值并不是最终的返回值,async 函数如果你返回了 String 类型的值,那么它的真实返回结果其实是 Future<String>。现在生成器有几个关键字:sync*, async*, yield, yield*。

sync* 表示同步生成器,其返回值是一个迭代器 Iterable, 可以通过迭代器遍历:

void main(){  
  Iterator it = test(5).iterator;
  print(test(5));
  while(it.moveNext()){
    print(it.current);
  }
}

Iterable<int> test(int num) sync*{
  while(num > 0){
    yield num;
    num --;
  }
}

这个貌似挺好理解的。不过 async* 异步生成器返回的是 Stream,这里就要提到Stream生成的几种方式:

  • StreamController() 可以通过生成的 controller.stream 获取流。
  • 生成器 async, yield 关键字生成 Stream。
  • Stream.fromFuture 从 Future 创建新的单订阅流,当 future 完成时将触发一个 data 或者 error,然后使用Down事件关闭这个流。
  • Stream.fromFutures 从一组Future创建一个单订阅流,每个future都有自己的data或者error事件,当整个Futures完成后,流将会关闭。如果Futures为空,流将会立刻关闭。
  • Stream.fromIterable 创建从一个集合中获取其数据的单订阅流。

十二、Isolate

在上文有提到过异步只是调换了执行代码的顺序,不是在Java中理解的并发概念,在 Dart 中如果想做到并发那就要使用 Isolate 了。可以把它理解为微线程或协程,它不是一个线程。我们平时的代码是执行在主Isolate中的,多个 Isolate 是不共享内存的,可以通过特定的方式进行通信。这里不会详细说明它的用法。

十三、元数据

元数据即注解,在 dart 中可以直接把一个类作为注解,如下:

class Todo {
  final String who;
  final String what;

  const Todo(this.who, this.what);
}
// 使用
@Todo("1","2")
var test(){}

然后用反射获取注解即可。