Dart语言学习——基本语法

298 阅读5分钟

配置环境和"hello world" demo

  前面已经配置过flutter的环境了,能够在控制台直接使用flutter来创建和编译运行一个flutter项目。其实flutter sdk已经包含了dart sdk,其在fluuter的bin/cache目前下,名叫dart-sdk,在dart-sdk目录下也有一个bin目录,这个bin目录中就有一个dart命令命令,将dart-sdk/bin配置到环境变量的path就能在任意位置使用dart命令。
  最佳配置应该是跟flutter的配置方法一样,先在环境变量配置文件中配置一个DART_HOME,然后再将dart-sdk的bin给连接到path上去。

export DART_HOME=$FLUTTER_HOME/bin/cache/dart-sdk
export path=$path:$DART_HOME/bin

这样的好处再明显不过了,如果flutter的位置变了,只需要将FLUTTER_HOME修改就行,DART_HOME和path所引用的都只是相对路径,不需要修改。将dart-sdk的bin配置到path后重启一下控制台程序,就可以发现dart命令了,执行一下dart --version,可以如果没有报找不到命令,并且正确输出了dart的版本号,就说明配置好了。
  还是用世界性的编程题目——"hello world"来测一下,创建一个目录,在这个目录下创建一个test.dart文件,在其中写入hello world的dart程序。

// demo dart

void main() {
	print('hello world!');
}

回到控制台,执行命令dart test.dart,可见控制台输出了"hello world!"

Dart的变量数据类型

  看了dart的代码,发现它其实也是一门类C的语言,所有的代码都是由变量与方法组成,程序控制结构也包括了顺序结构,判断分支结构与循环结构。
  Dart用var来声明一个变量,这跟JS很像,并且还可以自动类型推断,比如var number = 20;就是声明了一个int的变量,并且赋值了20,当然也可以给变量指定类型,比如String name = "Benson"。Dart跟Java和C/C++一样,每行的最后以一个分号结尾,不加分号会编译失败,同样,注释也是使用双左斜杠"//"。
  Dart还支持动态类型,一般来说当一个变量声明时就确定了它的类型了,除了像Java这种面向对象语言可以将变量声明为Object类型的,那这个类型就可以用来赋值为任何对象了,包括基本数据类型,因为Java对所有基本数据类型都给也了其包装类型,如double的包装类型是Double。而Dart直接支持动态类型,只需要使用dynamic来声明一个变量即可。如下demo

// dynamic type demo

main() {
	dynamic t = "Benson";
	print(t);
	t = 20;
	print(t);
}

这段代码是可以编译并正常运行的,执行dart命令来编译并运行,会输出

Benson
20


  Dart跟python一样,是门完全面向对象语言,任何变量或常量都是对象,因为所有的变量的默认值都是null
  几乎所有的语言都支持了常量,那Dart有什么理由不支持呢?Dart同时支持final和const,其中final表示只可赋值一次,而const表示引用的是个字面量,在编译期就决定了它的真实值。
  Dart支持的内置类型也非常丰富,主要包括以下各类 数值型:int和double 字符串:String,字符串可以用单引号'',也可以用双引号"",和其它语言一样,双引号可以内嵌表达式,表达式以$开头,而单引号的字符串不支持。 逻辑值:bool,只有true和false两个取值。 数组类型:List,Dart用范型List来表示数组,同时也可以当成列表来使用。 集合类型:Set,跟其他语言一样,其中的元素值只能有一个,不可重复。 Map:key-value映射,使用方式跟JS中的JSON类型类型 字符类型:Runes

函数

  Dart语言的函数跟Java的特别相似,只是Dart的函数可以不需要声明函数的类型和参数的类型,当然也是可以声明的。Dart也跟Kotlin一样,当函数的实现只有一行代码时,可直接用表达式来写,例如

bool isNoble(int atomicNumber) => _nobleGases[atomicNumber] != null;

Dart语言的函数还支持命名可选参数,定义时如下

void enableFlags({bool bold, bool hidden}) {...}

而使用时,则是

enableFlags(bold: true, hidden: false);

当某些命名参数必填时,可用@required注解来修饰参数定义 Dart函数的位置可选参数使用[]来定义,如

String say(String from, String msg, [String device]) {...}

对于默认参数,在定义可选参数时,可直接在定义函数时,使用=来给出默认值,例如

void enableFlags({bool bold = false, bool hidden = true}) {...}

或者位置可选参数的默认值

String say(String from, String msg, [String device = "none"]) {...}

Dart语言作为比Java更面向对象的语言,和JS一样,将函数作为了一等对象,也就是所有的函数都是对象类型,且可以赋值给变量,并且Dart还支持匿名函数或者说是Lambda

main() {
	var list = ["test1", "test2", "test3"];
	list.forEach((item) {
		print("item is $item");		
	});
}

这几行代码就演示了list.forEarch函数的参数就是一个函数,而这个参数在赋值时传入了一个lambda表达式。
  Dart的逻辑控制跟Java几乎一模一样,都有if-else,for循环,while,do-while循环。Dart的异常处理跟Java也非常类似,只是写法不太一样,抛异常使用throw指令,可以抛一个任意对象,而catch指令则需要用on来指定异常的类型,如下 try { doSomeThing(); } on Exception catch(e) { print("...$e"); } 同样的,Dart可以使用finally来处理try的任意情况,这跟Java是一样的,不论是否有抛出异常,都会执行finally块中的内容。

类与接口

  Dart中的类的概念跟Java也几乎一样,每个类都继承自Object类,且每个类都有且只有一个父类,每个类可以有n个子类。Dart对象的创建跟方法一样,可以省略new关键词,并且传构造参数时,不仅可以像Java那样按位置传,还可以使用命名参数来传,如下两种构造方法是等价的。

class Point {
    num x, y;
    Point(this.x, this.y);
}
var p1 = Point(1, 2);
var p2 = Point({'x':1, 'y': 2});

Dart可以使用对象的runtimeType属性来返回这个对象的Type对象,也就是它的对象类型。
  关于Dart中如何来创建一个类,这跟Java也是非常相似的,

class Point {
    num x;
    num y;
    num z = 0;
    Point(num x, num y) {
        this.x = x;
        this.y = y;
    }
}

由于Dart的所有变量默认值都是null,因此类中的属性默认值也都是null,当构造器传入参数对应其属性的话,则构造器可省略赋值语句,变成

class Point {
    num x, y;
    Point(this.x, this.y);
}

Dart还有一个叫作命名构造函数,其作用类似于Java的create生成方法,如下

class Point {
    num x;
    num y;
    Point(this.x, this.y);
    Point.fromJson(Map<String, num> map) {
        this.x = map["x"];
        this.y = map["y"];
    }

Dart的继承也使用extends关键词,子类也默认不继承父类的构造函数,如要调用父类的构造函数,则需要在子类的构造函数中显式地调用父类的构造函数

class Point {
	num x;
	num y;
	Point(this.x, this.y);
	Point.fromJson(Map<String, num> map) {
		this.x = map["x"];
		this.y = map["y"];
	}
}

class PointV3 extends Point {
	num z;
	PointV3(num x, num y, num z): super(x, y) {
		this.z = z;
	}
}

main() {
	var p1 = Point(1, 2);
	print("x=${p1.x},y=${p1.y}");
	var p2 = Point.fromJson({"x":3, "y":4});
	print("x=${p2.x},y=${p2.y}");
	var p3 = PointV3(1, 2, 3);
	print("x=${p3.x},y=${p3.y},z=${p3.z}");
}


  Dart也支持抽象类和抽象方法,只不过抽象类中的抽象方法不再需要在方法前面声明abstract关键词了,只需要在类名前加abstract关键词,

abstract class Person {
	say(String str);
}

class DXB extends Person {
	say(String str) {
		print("xiaobing say:$str");
	}
}

main() {
	var dxb = DXB();
	dxb.say("Hello");
}


  Dart同样支持接口,但支持方式比较奇葩,Dart让每个类都隐匿定义了一个接口,这个接口中包含了这个类所有的成员及其实现的接口。简单点理解就是如果类A要实现B的接口,而不是继承B的实现,就可以把B当成接口来implements,这样所有类都可以成为接口了,但前提是如果使用了implements,就不是使用的继承,也就是说需要在实现类中实现接口类中定义的所有方法

// person 类。 隐式接口里面包含了 greet() 方法声明。
class Person {
  // 包含在接口里,但只在当前库中可见。
  final _name;

  // 不包含在接口里,因为这是一个构造函数。
  Person(this._name);

  // 包含在接口里。
  String greet(String who) => 'Hello, $who. I am $_name.';
}

// person 接口的实现。
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()));
}


  和其他语言一样,Dart也使用enum关键词来支持枚举类型,其实我一直都觉得所有的面向对象语言的枚举设计都很鸡肋,要说枚举支持switch,那int也是支持的,所有地方的枚举都可以用int来代替,只能说编译期检查值范围是枚举唯一的用处了,但其实Java在Android中也是支持用注解来定义值范围的,比如用注解来标注传入的值必须是资源id类型或者特定字符串范围。   Dart同样也支持静态成员,静态成员属于类的,也用static来修饰。

Dart的范型

  前面有提到Dart的集合类型,如Set、List和Map,这些集合类型都是带有范型参数的,与Java的范型有区别的是,Dart的范型是"固化的",简单点理解就是Dart的范型参数能够在支持期检查到,而Java的范型参数在编译期就会被擦除,最典型的现象就是Dart可以使用names is List来判断集合的具体类型,而Java这样写直接就会报语法错误,只能判断name instanceOf List
  说到范型就不得不说说协变和逆变了,所谓协变就是使用泛型集合get到的泛型值需要使用协变的父类型来引用,这样才能类型安全,而逆变则相反,是指add集合的值必须是逆变的子类

static class A {
        
    }
    
    static class B extends A {
        
    }
    
    static class C extends A {
        
    }
    
    static class D extends B {
        
    }
    
    static void test() {
        List<? extends A> list1 = new ArrayList<>();
        List<? super B> list2 = new ArrayList<>();
        A a = new A();
        B b = new B();
        C c = new C();
        D d = new D();
        list1.add(a); // 语法错误
        list1.add(b); // 语法错误
        list1.add(c); // 语法错误
        list1.add(d); // 语法错误
        
        A a1 = list1.get(0);
        B b1 = (B) list1.get(0);
        C c1 = (C) list1.get(0);
        D d1 = (D) list1.get(0);


        list2.add(a); // 语法错误
        list2.add(b);
        list2.add(c); // 语法错误
        list2.add(d);

        A a2 = (A) list2.get(0);
        B b2 = (B) list2.get(0);
        C c2 = (C) list2.get(0);
        D d2 = (D) list2.get(0);
    }

在这段代码中,有一处是非常出乎人意料的,就是list1在add时,居然四种类型的值都会语法错误,这就是因为list1的泛型参数是协变的,协变是用来适配get的值使用父类引用的,因此A a1 = list1.get(0)就不需要强转。而list2.add(b)和list2.add(d)没有语法错误就是逆变在起作用,由于逆变可以add任意子类,因为在get时全部都要强转,而这些都是有可能会强转异常的。

Dart的导库

  Dart跟Java一样,使用import关键词来导库,但库的定位不再是package指定的包名,而是一个URI,同时,导的库也可以使用as关键词来重命令,以此来避免库名重复导致的歧义。
  Dart的导库还支持部分导入,用show关键词来选择性导入,用hide关键词来屏蔽导入。

Dart异步编程

  Dart使用await、async和Future类型来提供异步支持。其中的Future比较类似于JS中的Promise,创建一个Promise时需要传入一个闭包,这个闭包中要执行耗时任务。同样Future创建时也传入一个匿名函数,这个匿名函数中可以执行耗时操作,同时Future还提供了then和catchError函数,也是要传入匿名函数,分别处理正常执行结果和异常结果,但执行then和catchError函数后,并不会马上执行耗时任务的匿名函数,这就是Dart提供的异步编译框架。

import 'dart:async';

Future<String> sayHello(String name) async {
	return Future(() {
		Future.delayed(const Duration(seconds: 1));
		print("int call...");
		return "hello $name";
	});
}

main() async {
	print("before call...");
	sayHello("xiaobing")
		.then((res) {
				print(res);
			})
		.catchError((error) {
				print(error);
			});
	print("after call...");
}

在上面这个例子中,打印结果是

before call...
after call...
int call...
hello xiaobing

分析可知先执行了main函数中的before call和after call,然后才执行耗时匿名函数,成功返回后将结果打印出来。
  对于await和async的使用,可以想象这样一种场景,用网络请求登录,再获取用户信息,最后将用户信息保存到本地。这个场景中三个任务都是异步的,但三个任务是有先后关系的,如果只用Future,我们会写出如下嵌套层的代码

Future<String> login() {
	return Future(() => "token");
}
Future<String> getUserInfo(String token) {
	return Future(() => "xiaobing-$token");
}
Future<String> saveUserInfo(String name) {
	return Future(() {
		print("save $name");
		return name;
	});
}
main() {
	login().then((token) => getUserInfo(token)
		.then((name) => saveUserInfo(name).then((name) => print("saveUserInfo $name success..."))
		));
}

但若使用async + await就可以避免这种嵌套写法

Future<String> login() {
	return Future(() => "token");
}
Future<String> getUserInfo(String token) {
	return Future(() => "xiaobing-$token");
}
Future<String> saveUserInfo(String name) {
	return Future(() {
		print("save $name");
		return name;
	});
}
main() async {
	var token = await login();
	var name = await getUserInfo(token);
	var saveResult = await saveUserInfo(name);
	print("saveUserInfo $saveResult success");
}

await指令是等待Future的耗时任务执行完,并将结果解包成普通类型,而aysnc是在使用await的函数上必须加的,因为await表示要等待,等待则必须异步,async则表示这个函数是个异步函数。