Flutter筑基——学好 Dart,才能玩转 Flutter

目录

1. 前言

我们知道 Flutter 这个 UI 框架是使用 Dart 语言开发的,这说明要玩转 Flutter,就要先学好 Dart

那么,怎么学好 Dart 呢?

有的同学抱着“不就是一门语言嘛”的心态,直接开始写 Flutter,然后遇到问题了,再去查看 Dart 的文档。这也是一种学习 Dart 的方式,但这种方式可能不适合大多数同学。

庆幸地是,可以去查看 Dart 官网上的示例,比如Language-tour,就讲解了 Dart 的语法。

但是,官网上的讲法,不太符合我们学习新东西的方法。我们学习新的事物,习惯于通过类比的方法,即通过对相似事物进行比较所进行的一种学习方法。

本文主要针对于 Android 开发者,从 Java 和 Kotlin 出发,通过类比的方法来学习 Dart 语言。

本文没有特别标明的都是 Dart 代码。标明☕️的是 Java 代码,标明🏝️的是 Kotlin 代码。

2. 正文

2.1 Dart 开发环境的搭建

比较便捷地是使用 Dart-pad,是在网页里打开的一个 Dart 开发环境。

不过,笔者不太建议使用这个环境来学习 Dart 语言。因为,在这里写的代码不能保存。我们写的代码示例,随着学习的过程会越来越多,而我们又要回头复习巩固。

推荐的方式是下载 IDEA-community 版本(这个版本不需要去激活),这是 IDEA-community 下载地址

下载 Dart 的 SDK,这里是下载地址,选择稳定版本下载:
在这里插入图片描述

解压 Dart 的 SDK,并在 IDEA 中配置 Dart SDK 的路径:
在这里插入图片描述
如果需要在命令行中使用 dart,需要配置 dart 的环境变量:D:\Program Files\dartsdk-windows-x64-release\dart-sdk\bin 目录到 Path 变量。

新键 Dart 工程:
在这里插入图片描述

编写一个名字为 hello_world.dart 的文件,内容为:

------
======
××××××

打开命令行窗口,输入:dart hello_world.dart,可以得到输出结果:
在这里插入图片描述

2.2 关于 Dart 的重要概念

  • 所有可以放置在变量中的都是一个对象,对象都是类的实例。甚至数字,函数以及 null 都是对象。所有的对象都继承自 Object 类。
  • Dart 是强类型的,但是类型说明是可选的,这是因为 Dart 可以推断类型。当需要明确说不需要类型,使用特殊类型 dynamic。
  • Dart 支持泛型类型,如 List(整型的列表)或者List (任何类型对象的列表)。
  • Dart 支持顶层函数(例如 main()),也支持绑定于类或对象的函数(分别是静态方法和实例方法)。也可以在函数内部创建函数(嵌套函数或局部函数)。
  • Dart 支持顶层变量,也支持绑定于类或对象的变量(静态变量和实例变量)。实例变量有时被成为字段或属性。
  • 和 Java 不一样,Dart 没有 public,protected 和 private 关键字。如果一个标识符以下划线(_)开始,那么这个标识符对所在的库来说是私有的。
    标识符以字母或下划线开头,后面跟着字符及数字的任意组合。
  • Dart 有 expressions (它有运行的值)和 statements (它没有运行的值)。例如,条件表达式 condition ? expr1 : expr2 有 expr1 或 expr2 的值,而 if-else 语句没有值。一个语句经常包含一个或多个表达式,而一个表达式不能直接包含一个声明。
  • Dart 工具可以报告两类问题:警告和错误。

2.3 变量

Dart 中的变量声明方式比 Java 中更加多样。

可以明确地指出类型,这和 Java 是相同的:

String name4 = 'Bob';

可以使用 var 关键字来声明,Dart 会进行类型推断:

var name1 = 'Bob'; // Dart 会推断 name1 是 String 类型

需要说明的是使用了 var 就必须使用类型推断,不能再显式地指明类型:

// 错误的写法:Variables can't be declared using both 'var' and a type name.
var String name2 = 'Bob';

相比较而言,Kotlin 中也有 var 这种声明变量的方式,但是 Kotlin 允许变量类型与 var 共存。Dart 建议我们在声明局部变量时使用 var 关键字,达到简洁的目的。也就是说在多数情况下,都应该避免使用 var 关键字来声明变量。

可以使用 dynamic 关键字来声明,变量不限定为单一类型:

dynamic name2 = 'Bob'; // 把 String 类型的 'Bob' 赋值给 name2 变量
name2 = true; // 把 bool 类型的 true 赋值给 name2 变量

可以使用 Object 来声明,变量不限定为单一类型:

Object name3 = 'Bob'; // 把 String 类型的 'Bob' 赋值给 name3 变量
name3 = 1; // 把 int 类型的 1 赋值给 name3 变量

思考:

  1. 使用 var 关键字声明的变量,它的类型是什么时候推断出来的?

    var z; // 鼠标放在 z 上,按快捷键 Ctrl + Q 后,可以知道 z 是 dynamic 类型
    z = "wzc";
    z = 18;
    

    实际上,上面这种 var 关键字的用法和使用 dynamic 关键字的写法是一样的效果:

    dynamic xxx;
    xxx = 1;
    xxx = true;
    

    再看下边的代码片段:

    var y = "wzc"; // 推断为 String 类型
    // y = 18; // 编译报错:类型不对。
    

    所以,使用 var 关键字声明的变量,它的类型就是在声明的那一行代码确定的:如果对其进行初始化,就推断为初始化的类型;如果不对其进行初始化,那么就是 dynamic 类型。

  2. 使用 dynamic 关键字和使用 Object 类声明变量,有什么区别?

    dynamic t;
    Object x;
    t = 'Hello World';
    x = 'Hello World';
    print(t.length); // ok
    // print(x.length); // 编译错误: The getter 'length' isn't defined for the type 'Object'.
    
    // 调用一个不存在的 foo 方法,编译时不会报错,运行时会报错:NoSuchMethodError: Class 'String' has no instance method 'foo'.
    print(t.foo()); 
    

    所以,dynamic 声明的变量可以调用推断类型上存在的方法以及根本不存在的方法,编译器都不会报错,只会在运行时去检查;而Object 声明的对象只能使用 Object 的属性与方法,否则编译器会报错。

Dart 中的变量默认初始化为 null

这里的变量包括顶层变量,类变量,成员变量,以及局部变量。

// 顶层变量
int topLevelVar;

class Clazz {
  // 类变量
  static int classVar;
  // 成员变量
  int memberVar;

  void method() {
    // 局部变量
    int localVar;
    print('topLevelVar=$topLevelVar');
    print('classVar=$classVar');
    print('memberVar=$memberVar');
    print('localVar=$localVar');
  }
}
void main() {
  Clazz clazz = Clazz();
  clazz.method();
}

运行结果:

topLevelVar=null
classVar=null
memberVar=null
localVar=null

这里的例子也说明了在 Dart 中,int 类型的变量也是对象。实际上,在 Dart 中,一切皆对象。

相比之下,在 Java 中,局部变量是不会被初始化的,基本数据类型也不会被初始化为 null;在 Kotlin 中,也是一切皆对象,但是所有的变量都需要显式初始化。

Dart 中使用 finalconst 声明不会被修改的变量

使用 final 的示例,类似于 Java 中的 final 关键字修饰变量:

final name = 'Bob'; // 类型推断为 String 类型
final String nickname = 'Bobby';
// name = 'Alice'; // 编译报错:'name', a final variable, can only be set once.

使用 const 的示例,类似于 Java 中的 static final 修饰的常量:

const age = 10; // 类型推断为 int 类型
const double salary = 20.5;
// age = 11; // Constant variables can't be assigned a value.

可以看到,使用 finalconst 声明的变量可以通过类型推断得出变量的类型,也可以通过显式的方式来指明类型。

思考:

  1. 使用 final 声明的变量和使用 const 声明的变量有什么区别?

    首先,使用 final 声明的变量在运行时才确定它的值;使用 const 声明的变量在编译时就已经确定它的值。

    final yy = 6;
    // const zz = yy; // 编译报错:Const variables must be initialized with a constant value.
    

    yy 变量虽然使用 final 声明了,是不可变的,但没有在编译时固定它的值,不是常量,所以不能赋值给 zz

    其次,final 修饰的变量不可以改变引用的对象,但是可以改变对象的内容;const 修饰的变量不仅不可以改变引用,而且不可以改变对象的内容。

    final array1 = [1];
    array1[0] = 1; // ok,改变了对象的内容。
    const array2 = [1];
    array2[0] = 1; // 报错,Unsupported operation: Cannot modify an unmodifiable list
    

    实际上,const array2 = [1]; 等价于 const array2 = const [1];

Dart 中使用 const 创建常量值

我们以列表或者说数组来举例说明:

使用 final 声明的变量:

final fo = [];
fo.add(1); // ok

在 Java 中,我们知道使用 final 或者 static final 声明的列表,仍然可以向里面添加元素:

☕️
public class Test {
    static final List<Integer> LIST = new ArrayList<>();
    
    public static void main(String[] args) {
        final List<Integer> list = new ArrayList<>();
        list.add(1); // ok
        LIST.add(1); // ok
    }
}

在 Dart 中,使用 const 可以创建真正的列表常量,不可以向里面添加元素。

var foo = const [];
foo.add(1); // 运行报错:Unsupported operation: Cannot add to an unmodifiable list
final bar = const [];
bar.add(1); // 运行报错:Unsupported operation: Cannot add to an unmodifiable list
const baz = const [];
baz.add(1); // 运行报错:Unsupported operation: Cannot add to an unmodifiable list

思考:

  1. 以下三种写法有什么区别?

    var foo = const [];
    final bar = const [];
    const baz = const [];
    

    var foo = const[]; 表示 foo 变量指向了空的列表常量,但是 foo 还可以指向其他值,这是因为 foo 并不是 final 的,它当然可以指向其他变量:

    var foo = const [];
    foo = [1, 2, 3]; // foo 指向另一个列表
    

    final bar = const [];const baz = const []; 都不可以再指向另一个列表了,但是前者是一个不可变的变量,而后者是一个编译时就确定的常量。

    final bar = const [];
    bar = [1]; // 编译报错:'bar', a final variable, can only be set once.
    
    const baz = const [];
    baz = [42]; // 编译报错:Constant variables can't be assigned a value.
    

Dart 中使用 const 创建常量值的构造函数

这个用法在 Java 中是没有的。

void main() {
  // 使用 const 构造方法,有利于提升性能。
  var p1 = const ImmutablePoint(1, 1);
  var p2 = const ImmutablePoint(1, 1);
  var p3 = const ImmutablePoint(1, 2);
  print('p1 == p2: ${p1 == p2}'); // true
  print('p2 == p3: ${p2 == p3}'); // false
}
class ImmutablePoint {
  final num x; // 如果不加final,则编译报错:Can't define a const constructor for a class with non-final fields.
  final num y;

  const ImmutablePoint(this.x, this.y);
}

在构造函数名前面加上 const 关键字就声明了常量构造函数。

查看上面的例子,p1p2 指向了两次声明的常量值,但是因为使用了常量构造函数,并且传递的参数是一模一样的,所以 p1 == p2 成立,这不会创建多余的对象,有利于提升代码性能。

2.4 内置类型

Dart 中不存在基本数据类型的概念,这里有内置类型

内置类型就是 Dart 定义好的类型,用来表示数字,字符串,数组,列表,集合,布尔值等。这些内置类型,就都属于 class,都可以通过构造方法来创建。

Dart 中的数值类型分类比 Java 要简单

Dart 支持 intdoublenumintdoublenum 的子类。而 Java 有 byteshortintlongfloatdouble 这么多数值类型。

// 定义整型
var x = 1;
// 定义小数型
var y = 1.1;

数值类型与字符串类型互转:

// 字符串与数字的相互转换
// String -> int
var one = int.parse('1');
assert(one == 1);
// String -> double
var onePointOne = double.parse('1.1');
assert(onePointOne == 1.1); // pass
// int -> String
String oneAsString = 1.toString();
assert(oneAsString == '1'); // pass
// double -> String
String piAsString = 3.14159.toStringAsFixed(2); // 保留 2 位小数
assert(piAsString == '3.14'); // pass

Dart 中可以使用单引号或双引号来创建字符串

var s1 = 'Single quotes work well for string literals.';
print(s1.runtimeType); // 打印:String
var s2 = "Double quotes work just as well.";
print(s2.runtimeType); // 打印:String

Dart 中可以使用 ${}+ 以及相邻字符串三种方式来拼接字符串

前两种在 Kotlin 里也有,最后一种使字符串相邻可以达到拼接,是 Dart 中才有的。

var congratulations = "Happy New Year";
var name = "willwaywang6";
assert("Hello $name, $congratulations" == "Hello willwaywang6, Happy New Year"); // pass
assert("Hello $name, ${congratulations.toUpperCase()}!" == "Hello willwaywang6, HAPPY NEW YEAR!");

什么时候使用$name,什么时候使用${name}?
总是正确的办法是加上 {},如果$ 后面跟着一个表达式,如 congratulations.toUpperCase(),那么必须包裹 {};如果$ 后面跟着一个变量,如果变量后面跟着的内容会影响变量名的识别,就必须加上 {}

// name 必须包裹在 {} 里面,否则识别为 name1,而 name1 根本没有定义
assert("Hello ${name}1, $congratulations" == "Hello willwaywang61, Happy New Year"); // pass

使用 + 来拼接字符串:

var slogan = 'Faster ' + 'Higher ' + 'Stronger';
assert(slogan == 'Faster Higher Stronger');

使用相邻字符串来拼接字符串:

var words = 'Study '
    'Exercise'
    ' Conclusion';
assert(words == 'Study Exercise Conclusion'); // pass

需要说明的一点是== 用于字符串的比较时,比较的是字符序列是否一样。这点和 Kotlin 的用法一致。而在 Java 中,== 用于字符串的比较,比较的是二者的地址是否一致。

Dart 中可以输出多行字符串和不转义内容的字符串

使用两对三个单引号或两对三个双引号包裹输出多行字符串:

void main() {
  // 多行字符串
  var m1 = '''
  Line1
  Line2
  Line3
  ''';
  var m2 = """
  ------
  ======
  ××××××
  """;
  print(m1);
  print(m2);
}

输出结果:

  Line1
  Line2
  Line3
  
  ------
  ======
  ××××××

注意的一点是三个单引号('),不要写成三个 ` (Tab 键上面那个键对应的字符)。

在字符串前面加一个 r 可以输出“原始”的字符串:

var  path = r'C:\Users\wzc\kolin';
print(path);

输出结果:

C:\Users\wzc\kolin

在 Kotlin 中,只要使用两对三双引号(""")就可以同时输出多行字符串以及“原始”的字符串了。

🏝️ // Kotlin 代码
fun main(args: Array<String>) {
    val m1 = """
    Line1
    Line2
    Line3
    """
    print(m1)
}

输出结果:

    Line1
    Line2
    Line3

观察它们输出的结果,Dart 的输出结果左边空了两个空格,Kotlin 的输出结果左边空了四个空格。但是,我们希望的是左边不要有空格。

在 Dart 中还没有找到解决办法。

在 Kotlin 中有解决办法:

🏝️ // Kotlin 代码
fun main() {
    val m2 = """
    .------
    .======
    .××××××
    """
    print(m2.trimMargin("."))
}

输出结果:

------
======
××××××

Dart 的集合类型允许字面量初始化以及允许在初始化时使用操作符

集合类型包括 ListSetMap。这里以 Map 为例来进行说明。

允许字面量初始化

在 Java 中,想要在初始化时就给 Map 添加名值对,是不可以的,只能先声明一个空的 Map 对象,再往里面一个一个 put 元素,代码如下:

☕️ // Java 代码
public class Test {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();
        map.put("apple", 8);
        map.put("banana", 3);
        map.put("pear", 4);
        map.put("orange", 4);
        System.out.println(map);
    }
}

而 Kotlin 中,就好一些,可以这样写:

🏝️ // Kotlin 代码
fun main() {
    val map = mapOf("apple" to 8, "banana" to 3, "pear" to 4, "orange" to 4)
    println(map)
}

不过,to 这个中缀调用的写法,不是非常直观。

Dart 中的写法是比较好的:

void main() {
  Map<String, int> map = {"apple": 8, 'banana': 3, 'pear': 4, 'orange': 4};
  print(map);
}

从 Dart 2.3 开始,支持展开运算符(...),空检查展开运算符(...?),集合 if,集合 for

这种用法在 Kotlin 和 Java 中没有,可以说是比较新的用法。

展开运算符(...),是一种将多个元素插入集合的简洁的方式

void main() {
  var map1 = {"apple": 8, "orange": 2};
  var map2 = {"banana": 3, ...map1};
  assert(map2.length == 3);
}

空检查展开运算符(...?),在展开运算符作用的基础上,如果运算符右边的表达式是 null,那么使用这个运算符可以避免抛出异常,实际上,当是 null 时,什么都不会做。

void main() {
  var map1; // 默认初始化为 null
  var map2 = {"banana": 3, ...map1};
  assert(map2.length == 3);
}

运行后,抛出异常:

Unhandled exception:
NoSuchMethodError: The getter 'entries' was called on null.

使用 ...? 运算符,不会抛出异常:

void main() {
  var map1; // 默认初始化为 null
  var map2 = {"banana": 3, ...?map1};
  assert(map2.length == 1); // pass
}

集合 if 的使用

void main() {
  var isNeed = false;
  var map5 = {"car": 200000, "bike": 100, if (isNeed) "plane": 1};
  assert(map5.length == 2); // pass
}

集合 for 的使用

void main() {
  var map6 = {for (var i = 0; i < 5; i++) "key$i": "value$i"};
  assert(map6.length == 5); // pass
}

2.5 函数

Dart 中的函数是一个对象,它的类型是 Function 类型

定义一个判断 int 型数字是否是奇数的函数:

bool isOdd(int number) {
  return number % 2 != 0;
}

Function 是所有函数类型的基类。

void main() {
  Function isOddFunction = isOdd; // 函数的类型是 Function。
  print(isOddFunction.runtimeType); // (int) => bool
}

既然函数是一种类型,那么它就可以赋值给一个变量,可以作为参数传递给其他函数,可以作为函数的返回值。请看下边的例子:

void main() {
  receiveIsOddFunction(isOdd, 6);
  Function myIsOdd = returnIsOddFunction();
  print('myIsdOdd(5)=${myIsOdd(5)}');
}

bool isOdd(int number) {
  return number % 2 != 0;
}
// 函数作为返回值
Function returnIsOddFunction() {
  return isOdd;
}
// 函数作为参数传递给另一个函数
void receiveIsOddFunction(Function f, int number) {
  bool result = f(number);
  print('result = $result');
}

运行结果:

result = false
myIsOdd(5)=true

Dart 中的函数可以省略参数类型和返回值类型

在 Java 中,这是绝对不可以的。
在 Kotlin 中,参数类型必须要有,返回值类型只是对表达式体函数才允许省略。
而 Dart,都可以省略。
下面的 Dart 函数,用来判断一个 int 类型的数字是否是偶数:

bool isEven(int number) {
  return number % 2 == 0;
}

省略参数类型和返回值类型后,就写成了:

isEven(number) {
  return number % 2 == 0;
}

注意,这在 Dart 中是合法的。

下面调用一下省略了参数类型和返回值类型的 isEven 函数:

void main() {
  assert(isEven(2)); // pass
  assert(!isEven(3)); // pass
}

但是,不要滥用类型省略。在容易造成类型不清晰时,一定不要省略类型。 实际上,Dart 建议在你感到类型明显时,才进行类型省略。

还是看 isEven 函数的例子,调用方看到这样的函数:

isEven(number) {
  return number % 2 == 0;
}

调用方只知道 numberdynamic 类型,返回值类型是 dynamic 类型。那么,假如此时他传入了一个 String 类型,会有什么效果:

void main() {
  isEven('xxxx');
}

运行后报错:

Unhandled exception:
NoSuchMethodError: Class 'String' has no instance method '%'.
Receiver: "xxxx"
Tried calling: %(2)

Dart 中的函数支持箭头语法(=>

还以 isEven 函数为例:

isEven(number) {
  return number % 2 == 0;
}

使用箭头语法(=>),可以改为:

bool isEven(int number) => number % 2 == 0;

在 IDEA 中,可以把鼠标停留在函数声明上,按下 ALT + Enter 快捷键,呼出菜单,选择 ‘Convert to expression body’,转为箭头语法;之后再选择 ‘Convert to block body’,再转回来。

箭头语法只适用于函数体是一个表达式的情况。比如,下面这样就不能使用箭头语法了:

bool isEven(int number) {
  print('number = $number'); // 这是一个语句
  return number % 2 == 0; // 这是一个表达式
}

Dart 中的箭头语法对应于 Kotlin 中的表达式体函数。

代码块体的写法:

🏝️ // Kotlin 代码
fun isEven(number: Int): Boolean {
    return number % 2 == 0
}

表达式体的写法:

🏝️ // Kotlin 代码
fun isEven(number: Int): Boolean = number % 2 == 0

Dart 中的函数支持可选参数:命名参数以及可选的位置参数

第一次听这样的概念,不知道是不是有点摸不着头脑,不就是函数的参数嘛,还有分类?

对应于 Java 中那种必须传的函数参数,就是必要的位置参数。

在 Dart 中,除了必要的位置参数之外,还有可选的参数。可选的参数有分为两类:命名参数和可选的位置参数。用图来说明如下:
在这里插入图片描述
接着,我们看一下具体的实例。

命名参数

当定义函数时,使用 {param1, param2, ...} 来指定命名参数

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

使用时必须要传入 参数名字: 实参 这样的格式:

void main() {
  // 使用 paramName: value 来确定命名参数,注意 paraName 不可以省略。
  enableFlags(bold: true, hidden: false); // 正确的写法
  // enableFlags(true, hidden: false); // 错误的写法,提示:Too many positional arguments: 0 expected, but 1 found.
  // enableFlags(bold: true, false); // 错误的写法,提示:Too many positional arguments: 0 expected, but 1 found.
  // enableFlags(true, false); // 错误的写法,提示:Too many positional arguments: 0 expected, but 2 found.
}

在使用声明了命名参数的函数,在传入实参的时候,不能只传入实参,而是要写入参数名字: 实参。这是因为直接传入实参,会被当做是必要的位置参数。而实际上,在 enableFlags 函数上,只有命名参数,没有任何必要的位置参数。

命名参数是可选的,如果不传入的话,那么那个参数的值就是 null

void main() {
  // 使用 paramName: value 来确定命名参数,注意 paraName 不可以省略。
  enableFlags(bold: true);
  enableFlags(hidden: false);
}

打印结果:

bold=true, hidden=null
bold=null, hidden=false

使用 @required 注解标明某个命名参数是强制的,使用者需要给这个参数提供一个值。

void enableFlags({@required bool bold, bool hidden}) {
  print('bold=$bold, hidden=$hidden');
}

在调用的地方:

void main() {
  enableFlags(hidden: false); // enableFlags 下波浪线提示:The parameter 'bold' is required.
}

文档中说,标注 @required 的参数强制要传入值,但是不传的话,只是给了一个提示。

在参数比较少时,使用命名参数获取显得有点多余;但是当参数很多时(例如有十几个),这时命名参数就会显示出它的优势来:调用者以及代码的阅读者都可以清楚地知道每一个参数的含义。

实际上,在 Flutter 里面,特别是控件的构造函数,只使用命名参数,即便是对于强制要传入的参数。这里截一下 Flutter 中的 Text 控件来看一下:

可选的位置参数

当定义函数时,使用 [param1, param2, ...] 来指定可选的位置参数

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

使用时

assert(say('John', 'Hello', 'smiling') == 'John says Hello with a smiling'); // pass
assert(say('Peter', 'Hi') == 'Peter says Hi'); // pass

需要注意的细节:

  • 必要的位置参数和可选参数都声明时,必要的位置参数在前,可选参数在后;
  • 函数声明的可选参数要么是命名参数,要么是可选的位置参数,不能同时声明两种;
  • 命名参数指定默认值,就可以隔着输入命名实参了; 而可选的位置参数只有最后一个为默认值时,才允许不输入;
  • 命名参数传入实参必须带上名字,而位置参数不允许带名字;
  • 命名参数可以选择任何的参数传入还是不传入,而可选的位置参数只能选择前面的传入后面的不传入,不允许前面的不传入而后面的传入。

Dart 中完全支持 lambda 表达式

没有名字的函数即匿名函数,也叫 lambda 表达式,也叫闭包。

在 Java 8 以后,才开始支持 lambda;Kotlin 支持 lambda。

它们的写法有些不同,但是含义基本相同:参数的类型都是可以省略的;参数的个数都可以是 0 个或者多个,都不需要写出返回值类型。

我们先看 Dart 的写法,再使用不同的语言展示同一个比较器的写法。

// Dart
void main() {
  var f = () {
    print('Hello, lambda');
  }; // 是个对象,所以才可以分配给变量。
  f(); // 执行匿名函数
}

打印信息:

Hello, lambda

不同的语言展示同一个比较器的写法:

Java 的写法:

public class Test {
    public static void main(String[] args) {
        Comparator<Integer> comparator = (Integer o1, Integer o2) -> Integer.compare(o1, o2);
        comparator.compare(1, 2);

        Comparator<Integer> comparator1 = (o1, o2) -> {
            System.out.println("xxxxx");
            return Integer.compare(o1, o2);
        };
    }
}

Kotlin 的写法:

fun main() {
    val comparator: Comparator<Int> = Comparator { o1: Int, o2: Int -> Integer.compare(o1, o2) }
    comparator.compare(1, 2)

    val comparator2: Comparator<Int> = Comparator { o1, o2 ->
        println("xxxxx")
        Integer.compare(o1, o2)
    }
}

Dart 的写法:

void main() {
  Comparator<int> comparator = (o1, o2) => o1.compareTo(o2);

  Comparator<int> comparator2 = (o1, o2) {
    print('xxxxxx');
    return o1.compareTo(o2);
  };

}

ps:详细学习 Kotlin 的 lambda 可以参考扔物线大佬的视频: Kotlin 的 Lambda,大部分人学得连皮毛都不算

Dart 中可以比较两个函数是否相等

在 Kotlin 和 Java 中都是不可以的。为什么在 Dart 中可以呢?这是因为在 Dart 中,函数也是对象,它的类型是 Function 类型。

对于顶层函数,只有一份,总是相等的;
对于静态函数,它属于类,只有一份,总是相等的;
对于成员函数,它属于对象,不同的对象,它们对应的成员函数不相等,相同的对象,它们对应的成员函数是相等的。

void foo() {} // 顶层函数

class A {
  static void bar() {} // 静态方法
  void baz() {} // 实例方法
}

void main() {
  var x1 = foo;
  assert(foo == x1); // pass

  var x2 = A.bar;
  assert(A.bar == x2); // pass

  var v = A();
  var w = A();
  var y = w;
  var x3 = w.baz;

  assert(y.baz == x3); // pass

  assert(v.baz != w.baz); // pass
}

Dart 的函数不指定返回值,那么语句 return null; 被隐含地追加到函数体内

这点和 Kotlin,Java 是有区别的。

在 Java 中,返回值要么是 void,要么是明确的类型。如果返回值类型是 void,可以不写 return; 语句。

在 Kotlin 中,返回值不存在时,可以显示写出 Unit 类型(这种情况下对应于 Java 中的 void ),也可以省略,return 语句可以不写;存在返回值时,对于代码块体函数,必须显式地声明;对于表达式体函数,可以显式地声明,也可以不声明(由类型推断出来)。

// Kotlin 代码
fun f(): Unit {

}

fun g() {

}

Dart 代码示例:

// Dart 代码
void main() {
  assert(foo() == null); // pass
  assert(boo() == null); // pass
  // assert(goo() == null); // 编译报错:This expression has a type of 'void' so its value can't be used.
  assert(hoo() == null); // pass
}

foo() {}
//boo 等价于 foo
boo() {
  return null;
}
// goo 不等价于 foo
void goo() {

}

int hoo() {

}

2.6 操作符

Dart 中判断相等需要区分一下

语言比较内容比较地址
Javaequals==
Kotlin=====
Dart==identical(Object? a, Object? b)

Dart 中的类型判断运算符

Dart 中的类型判断运算符代码演示:

// Dart 代码
void main() {
  dynamic emp = "hello";
  emp = Person();
  if (emp is Person) {
    emp.name = "wang";
  }

  (emp as Person).name = "wang";

  if (emp is! num) { // 在 kotlin 中是 !is
    print('emp is not a num');
  }

  try {
    dynamic person1;
    (person1 as Person).name = 'wang'; // person1 是 null, 直接调用 as 报错。
  } catch (e) {
    print(e);
    /*
    报错:NoSuchMethodError: The setter 'name=' was called on null.
          Receiver: null
          Tried calling: name="wang"
     */
  }

  dynamic person2;
  if(person2 is Person) { // person2 是 null,但是调用 is 不会报错。
    person2.name = "wang";
  }

}

class Person {
  String name;
  int age;
}

在 Kotlin 中,另外有一个安全转换运算符:as?,尝试把值转换成指定的类型,如果类型不合适就返回 null,这样可以实现安全转换。

// Kotlin 代码
fun boo1(x: Any?) {
    val number = x as? Number
    println(number)
}

fun main(args: Array<String>) {
    boo1(5)
    boo1("b")
    boo1(null)
}

Dart 中的 ?? 运算符对应于 Kotlin 中的 ?: 运算符

Dart 代码示例:

void main() {
  assert(playerName(null) == 'Guest'); // pass
  assert(playerName('Peter') == 'Peter'); // pass
}

String playerName(String name) => name ?? "Guest";

Kotlin 代码示例:

// Kotlin 代码
fun main(args: Array<String>) {
    assert(playerName(null) == "Guest")
    assert(playerName("Peter") == "Peter")
}

fun playerName(name: String?): String {
    return name ?: "Guest"
}

但是,它们有些不同,在 Kotlin 中,?: 运算符可以和 throw 表达式,return 表达式连用,而 Dart 中的 ?? 运算符不可以。

fun playerName2(name: String?): String {
    return name ?: throw Exception("Cannot be null")
}

fun playerName3(name: String?): String {
    return name ?: return ""
}

在 Kotlin 中,?: 叫做 null 合并运算符;在 Dart 中,?? 是条件表达式中使用的运算符,可以认为是三元运算符 condition ? expr1 : expr2 的一种简写。

Dart 中的级联符号(..)有点类似 Kotlin 中的 with 函数

级联符号的作用是允许在同一个对象上做一系列的操作。免去了临时变量的创建,代码更加流畅。

// Dart 代码
void main() {
  Man man = (ManBuilder()
        ..name("zhichao") // 忽略了 name 方法的返回值。
        ..age(30)
        ..gender(true)
        ..salary(1)
        ..address('china'))
      .build();
}

class Man {
  String _name;
  int _age;
  double _salary;
  bool _gender;
  String _address;
}

class ManBuilder {
  String _name;
  int _age;
  double _salary;
  bool _gender;
  String _address;

  void name(String name) {
    this._name = name;
  }

  void age(int age) {
    this._age = age;
  }

  void salary(double salary) {
    this._salary = salary;
  }

  void gender(bool gender) {
    this._gender = gender;
  }

  void address(String address) {
    this._address = address;
  }

  Man build() {
    var man = Man();
    man._name = this._name;
    man._age = this._age;
    man._gender = this._gender;
    man._salary = this._salary;
    man._address = this._address;
    return man;
  }
}

但是,觉得 Kotlin 的 with 函数更加强大,可以看下边的例子:

// Kotlin 代码
fun alphabet() = with(StringBuilder()) {
    for (letter in 'A'..'Z') {
        append(letter)
    }
    append("\nNow I know the alphabet!")
    toString()
}
fun main(args: Array<String>) {
    println(alphabet())
}

这个例子使用 Dart 的级联符号就不行。

2.7 流程控制

Dart 中的控制结构都是语句

在 Dart 中,控制结构都是语句。
在 Kotlin 中,除了循环以外大多数控制结构都是表达式。

Dart 中的 switch 条件控制结构 case 分支可以使用 break, continue, rethrow, returnthrow 结束

在 Java 中,case 分支只可以使用 breakreturn 来结束。

void main() {
  doByTime('dawn');
  print("-" * 20);
  doByTime('evening');
}

void doByTime(String time) {
  switch (time) {
    case 'dawn': // 空语句,允许穿透; 不写 break 等不会报错。
    case 'morning':
      print('Work');
      break; // 若注释掉 break,编译报错:The last statement of the 'case' should be 'break', 'continue', 'rethrow', 'return' or 'throw'.
    case 'noon':
      print('Have lunch');
      break;
    case 'afternoon':
      print('Work');
      break;
    case 'evening':
      print('Work overtime');
      continue snack;
    case 'night':
      print('Sleep');
      break;
    snack:
    case 'snack':
      print('Have a snack');
      break;
    default:
      print('Unknown time');
  }
}

在 Kotlin 中,没有了 switch 的影子,因为在 Kotlin 中使用 when 表达式来替代了,这个更加强大。

2.8 异常

Dart 的异常都是非检查异常

在 Dart 中,方法不会声明它们可能抛出的异常,开发者不需要捕获任何异常。
在 Java 中,异常是区分位检查异常和非检查异常的。
在 Kotlin 中,不区分检查异常和非检查异常。不用指定函数抛出的异常 而且可以处理也可以不处理异常。
因此,在 Dart 和 Kotlin 里面,都没有 Java 中的 throws 这个关键字。

Dart 的异常类型分为 ExceptionError 两类,但二者没有共同的父类

在 Java 中,异常的超类是 Throwable 类,两个直接子类是 ExceptionError 类。

Dart 可以 throw 任何非空对象作为一个异常,而不仅仅是 ExceptionError 对象

// Dart 代码
void main() {
  var isThrow = false;
  if (isThrow) {
    throw FormatException('Expected at least 1 section');
  }
  if (isThrow) {
    throw 'Out of llamas!'; // throw 一个字符串作为异常
  }
}

Dart 中 throw 一个异常是一个表达式

// Dart 代码
void distanceTo(num other) => throw UnimplementedError();

在 Kotlin 中,throw 一个异常也是一个表达式。在 Java 中,throw 一个异常是一个语句。

Dart 中 catch 只可以用于获取异常对象,需要再使用 on 指定异常类型

catch() 可以指定一个或者两个参数。第一个参数是抛出的异常,它的类型要么是 dynamic 类型,要么是指定的异常类型,第二个参数是堆栈信息对象,它的类型是 StackTrace

// Dart 代码
void main() {
  try {
    throwExceptionInSwitchCase("Tuesday");
  } on TimeoutException catch(e) { // on 用于指定异常
    print(e);
  } on FormatException catch(e) { // catch 用于获取异常对象
    print(e);
    rethrow; // rethrow 重新抛出异常
  } catch(e, s) { // catch 可以指定两个参数,第二个是堆栈信息
    print(e);
    print(s);
  } finally { // If no catch clause matches the exception, the exception is propagated after the finally clause runs:
    print('Finally clause');
  }
}

单独使用 catch() 时,第一个参数的类型是 dynamic 类型,这表示它可以处理任何类型的异常对象。
当使用 on 指定异常类型时,catch() 的第一个参数类型就是指定的异常类型了。

在 Dart 中,使用 rethrow 重新抛出异常。

2.9 类

Dart 2 开始,创建对象可以不使用 new 关键字

void main() {
  var p1 = Point(2, 2);
  // 不省略 new 关键字的写法 The new keyword became optional in Dart 2.
  var p3 = new Point(2, 3);
}

在 Java 中,必须使用 new 关键字来创建对象;在 Kotlin 里,不存在 new 关键字。

Dart 中可以使用 . 以及 ?. 来引用成员变量或成员函数

// Dart 代码
void main() {
  var person = Person("wzc", 32);
  person.gender = true;
  print(person.toString());

  Person nullPerson;
  nullPerson?.gender = true; // ?. 安全调用, 和 kotlin 中的 ?. 是一样的。
}

class Person {
  String name;
  int age;
  bool gender;

  Person(this.name, this.age);

  @override
  String toString() {
    return 'Person{name: $name, age: $age, gender: $gender}';
  }
}

在 Kotlin 中,也有 ?.,被叫做安全调用运算符。

Dart 中构造器的名字可以是类名或者类名.标识符

这点,在 Kotlin 以及 Java 中,构造器的名字都只能是类名。

类名.标识符这种形式的构造器是命名构造器。

// Dart 代码
void main() {
  var p1 = Point(2, 2); 
  print(p1);
  var p2 = Point.fromJson({'x': 1, 'y': 2});
  print(p2);
}

class Point {
  int x;
  int y;

  Point(this.x, this.y);

  Point.fromJson(Map<String, int> map) {
    // return Point(map['x'], map['y']); // Constructors can't return values.
    this.x = map['x'];
    this.y = map['y'];
  }

  @override
  String toString() {
    return 'Point{x: $x, y: $y}';
  }
}

Dart 中的类可以提供常量构造器

这点在 Java 和 Kotlin 中都是没有的。

// Dart 代码
void main() {
  // 常量构造器
  const immutablePoint1 = const ImmutablePoint(1, 1);
  const immutablePoint2 = const ImmutablePoint(1, 1);
  print('immutablePoint1.hashCode = ' + immutablePoint1.hashCode.toString());
  print('immutablePoint2.hashCode = ' + immutablePoint2.hashCode.toString()); // hashCode 是一样的
  print('immutablePoint1 = $immutablePoint1, immutablePoint2 = $immutablePoint2');
  assert(identical(immutablePoint1, immutablePoint2)); // 一样的。

  // 在常量上下文里,构造器前的 const 可以省略
  const pointAndLine1 = {
    'point': const [const ImmutablePoint(0, 0)],
    'line' : const [const ImmutablePoint(1, 1), const ImmutablePoint(2, 2)]
  };
  // 省略 const 的写法:
  const pointAndLine2 = {
    'point': [ImmutablePoint(0, 0)],
    'line': [ImmutablePoint(1, 1), ImmutablePoint(2, 2)]
  };
  // 但是如果没有常量上下文,省略了 const,就不能创建编译器常量了
  var pointAndLine3 = {
    'point': [ImmutablePoint(0, 0)],
    'line': [ImmutablePoint(1, 1), ImmutablePoint(2, 2)]
  };
  assert(identical(pointAndLine1, pointAndLine2)); // pass
  assert(!identical(pointAndLine2, pointAndLine3)); // pass
}
class ImmutablePoint {
  final int x; // 如果不加final,那么编译报错:Can't define a const constructor for a class with non-final fields.
  final int y;

  const ImmutablePoint(this.x, this.y);
}

定义常量构造器需要:

  • 在构造器的名字前面加上 const 关键字;
  • 类的所有字段必须是 final 的,并且要在构造函数里被初始化。

使用常量构造器创建的对象不一定就是常量。必须在常量上下文中,使用常量构造器创建的对象才是常量。

Dart 中成员变量的 getter/setter 函数是隐式生成的

在 Dart 中,所有的成员变量都会生成一个隐含的 getter 方法;非 final 的成员变量另外还会生成一个隐含的 setter 方法。

// Dart 代码
void main() {
  var point = Point();
  point.x = 4;
  assert(point.x == 4);
  assert(point.y == null);

  assert(point.q == 1);

  // point.q = 2; // 编译报错:'q' can't be used as a setter because it is final.
}

class Point {
  num x; // 没有初始化,值就是 null
  num y;
  num z = 0; // 初始化为 0
  final num q = 1;
}

这点和 Kotlin 还是有些类似的。

Dart 中可以使用初始化列表

这点在 Kotlin 和 Java 中都是没有的。

父类构造器是无参构造器的代码演示:

// Dart 代码
class Point3 {
  final num x;
  final num y;
  final num distanceFromOrigin;

  // The right-hand side of an initializer does not have access to this.
  Point3(x, y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

class Point1 {
  num x;
  num y;

  Point1(num x, num y) {
    this.x = x;
    this.y = y;
  }

  // 在初始化列表中使用 assert 来验证输入
  Point1.withAssert(this.x, this.y) : assert(x >= 0) {
    print('In Point.withAssert(): ($x, $y)');
  }
}

父类构造器是有参构造器的代码演示:

void main() {
  var employee1 = Employee(1, {});
}

class Person {
  String firstName;

  Person.fromJson(Map data) {
    print('In Person');
  }
}

class Employee extends Person {
  double salary;

  Employee(double salary, Map data) : salary = salary, super.fromJson(data) {
    print('In Employee');
  }
}

打印信息:

In Person
In Employee fromJson

需要注意的是,初始化列表是在构造函数之前执行的;父类构造器是在子类构造器之前执行的。

Dart 中有工厂构造器

在普通的构造器前面加上 factory 关键字,就声明了工厂构造器了。使用工厂构造器并不总是产生一个新的实例。另外,工厂构造器不能获取到 this

// Dart 代码
class Logger {
  final String name;
  bool mute = false;
  static final Map<String, Logger> _cache = <String, Logger>{};

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

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

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

Dart 中不支持方法重载

方法重载的定义是在同一个类中,可以定义多个这样的函数:函数名相同,参数列表不同,不关心函数的返回值类型。

可以使用可选参数,来解决这个问题;或者,可以使用不同的函数名字。

// Dart 代码
class Point {
  // 不允许方法重载
//  num operate(num value) {
//
//  }
//
//  num operate(num value, String string) { // 编译报错:The name 'operate' is already defined.
//
//  }
  // 使用可选参数来解决
  num operate(num value, {String string}) {

  }
}

作为对比的是,Java 和 Kotlin 都支持方法重载,Kotlin 还支持使用默认参数的办法来消除方法重载。

Dart 使用 runtimeType 属性在运行期获取一个对象的类型

runtimeType 属性是声明在 Object 类里面的,如下源码所示:

class Object {
  external Type get runtimeType;
}

和 Kotlin,Java 做一下对比:

语言获取对象运行期类型的方式返回值
Dart使用 Object 类中的 runtimeType 属性Type 类型
Kotlin使用 ::class 语法,或者使用 ::class.java,返回 Class 类型KClass 类型
Java使用 Object 类中的 getClass() 方法Class 类型

Dart 中的实例变量隐含一个 getter 方法和一个 setter 方法

对比 Java,实例变量仅仅用于存放数据,必须另外写出 getter/setter 方法。

对比 Kotlin,Dart 和 Kotlin 一样,都隐含了 getter/setter 方法,但是还有一些不同,表现在 Dart 中的实例变量所隐含的 getter/setter 方法不允许进行修改了,而 Kotlin 的是可以的。具体可以看代码演示。

比如,我们需要在 setter 方法里做一些数据校验工作:

在 Dart 中,不得不另外定义一个函数,来做数据校验工作。这多么不优雅。

// Dart 代码
class Point {
  num x;
  num y;

  Point(this.x, this.y);

  set checkX(num value) {
    if (value < 0) {
      throw "error";
    }
    x = value;
  }
}

在 Kotlin 中,可以修改 setter 方法来做数据校验的工作。

// Kotlin 代码
class Point {
    var x: Int = 0
        get() = field
        set(value) {
            if (value < 0) throw IllegalArgumentException("error")
            field = value
        }
    var y: Int = 0
}

Dart 中 abstract 关键字可以修饰类,不可以修饰方法

作为对比,在 Java 和 Kotlin 里面,abstract 关键字可以修饰类和方法。

// Dart 代码
abstract class Doer {
  // 抽象的实例方法
  // 不可以加 abstract, 因为 Members of classes can't be declared to be 'abstract'.
  /*abstract*/ void doSomething(); 
  // 抽象的 getter 方法
  num get count;
  // 抽象的 setter 方法
  set count(num value);
}

Dart 中没有 interface 关键字

在 Dart 中,不可以去使用 interface 关键字声明一个类似在 Java 中那样定义的接口。如果要在 Dart 中找出一个和 Java 中接口最接近的概念,那就是所有的方法都是抽象方法的抽象类了。

Dart 中使用 implements 关键字来实现一个隐式的接口

在 Java 中,只能使用 implements 关键字去实现一个使用 interface 关键字声明的接口;在 Dart 中,根本不存在 interface 关键字,但是在 Dart 中,每个类都隐式地定义了一个接口,这个接口包含本类及其所实现接口的所有实例成员。也就是说,在 Dart 中,implements 后面跟的是一个类。

那么,这里要区分一下,何时去 extends 一个类,何时去 implements 一个类呢?

若打算创建一个类 A,支持类 B 的 API 但又不继承类 B 的实现,那么类 A 应该实现 (implements)类 B 接口。
若打算创建一个类 A,支持类 B 的 API 又要继承类 B 的实现,那么类 A 应该继承 (extends)类 B。

Dart2.7 开始支持扩展函数

扩展函数用于给现有的类库添加功能。

在 Kotlin 中,有扩展函数。

这里说明一下 Dart 中扩展函数的特点:

不能在 dynamic 类型的变量上调用扩展函数:

// Dart 代码
void main() {
dynamic d = '42';
// parseInt() 函数是定义在 String 类型上的一个扩展函数
// d.parseInt(); // 运行报错:NoSuchMethodError: Class 'String' has no instance method 'parseInt'.
}

不能在 dynamic 类型的变量上调用扩展函数的原因是扩展函数需要依据接收者的静态类型来解析。

Dart 实现扩展函数的语法如下:

extension <extension name> on <type> {
 (<member definition>)
}

其中 extension 关键字,用于声明一个扩展类库;extension name 是扩展名字,用于解决可能出现的 API 冲突;on 关键字,表示扩展函数定义在哪个类型上面的。

扩展函数可以有类型参数:

extension MyFancyList<T> on List<T> {
  int get doubleLength => length * 2;

  List<T> operator -() => reversed.toList();

  List<List<T>> split(int at) => <List<T>>[sublist(0, at), sublist(at)];
}

Dart 支持混入(mixin)

什么是 Mixin?在面向对象的语言中,mixins 类是一个可以把自己的方法提供给其他类使用,但却不需要成为其他类的父类。

2.10 泛型

Dart 中的泛型是实化的

Dart 中的泛型是实化的,意思就是说在运行期泛型的类型参数依然存在。

作为对比,Java 中的泛型是使用擦除来实现的,在运行期泛型的类型参数都会移除;Kotlin 允许实化类型参数,需要使用到 reified 关键字。

// Dart 代码
void main() {
  var names = List<String>.empty(growable: true);
  names.addAll(['Seth', 'Kathy', 'Lars']);
  print(names is List<String>);
  // 在 Java 中,只能写成下面这行的样子。
  print(names is List);
}

2.11 异步支持

Dart 支持异步编程,允许我们写看起来像同步代码的异步代码。

最后

Dart 目前对于空安全的支持处于 beta 阶段了,本文没有包括这方面的内容。

参考

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值