Dart语法篇之类型系统与泛型(七)

5,895 阅读8分钟

简述:

下面开始Dart语法篇的第七篇类型系统和泛型,上一篇我们用了一篇Dart中可空和非空类型译文做了铺垫。实际上,Dart中的类型系统是不够严格,这当然和它的历史原因有关。在dart最开始诞生之初,它的定位是一门像javascript一样的动态语言,动态语言的类型系统是比较松散的,所以在Dart类型也是可选的。然后动态语言类型系统松散对开发者并不是一件好事,程序逻辑一旦复杂,松散的类型可能就变得混乱,分析起来非常痛苦,但是有静态类型检查可以在编译的时候就快速定位问题所在。

其实,dart类型系统不够严格,这一点不仅仅体现在可选类型上和还没有划分可空与非空类型上,甚至还体现dart中的泛型类型安全上,这一点我会通过对比Kotlin和Dart中泛型实现。你会发现Dart和Kotlin泛型安全完全走不是一个路子,而且dart泛型安全是不可靠的,但是也会发现dart2.0之后对这块做很大的改进。

一、可选类型

在Dart中的类型实际上是可选的,也就是在Dart中函数类型,参数类型,变量类型是可以直接省略的。

sum(a, b, c, d) {//函数参数类型和返回值类型可以省略
  return a + b + c + d;
}

main() {
  print('${sum(10, 12, 14, 12)}');//正常运行
}

上述的sum函数既没有返回值类型也没有参数类型,可能有的人会疑惑如果sum函数最后一个形参传入一个String类型会是怎么样。

答案是: 静态类型检查分析正常但是编译运行异常。

sum(a, b, c, d) {
  return a + b + c + d;
}

main() {
  print('${sum(10, 12, 14, "12312")}');//静态检查类型检查正常,运行异常
}

//运行结果
Unhandled exception:
type 'String' is not a subtype of type 'num' of 'other' //请先记住这个子类型不匹配异常问题,因为在后面会详细分析子类型的含义,而且Dart、Flutter开发中会经常看到这个异常。

Process finished with exit code 255

虽然,可选类型从一方面使得整个代码变得简洁以及具有动态性,但是从另一方面它会使得静态检查类型难以分析。但是这也使得dart中失去了基于类型函数重载特性。我们都知道函数重载是静态语言中比较常见的语法特性,可是在dart中是不支持的。比如在其他语言我们一般使用构造器重载解决多种方式构造对象的场景,但是dart不支持构造器重载,所以为了解决这个问题,Dart推出了命名构造器的概念。那可选类型语法特性为什么会和函数重载特性冲突呢?

我们可以使用反证法,假设dart支持函数重载,那么可能就会有以下这段代码:

class IllegalCode {
  overloaded(num data) {

  }
  overloaded(List data){//假设支持函数重载,实际上这是非法的

  }
}

main() {
    var data1 = 100; 
    var data2 = ["100"];
    //由于dart中的类型是可选的,以下函数调用,根本就无法分辨下面代码实际上调用哪个overloaded函数。
    overloaded(data1);
    overloaded(data2);
}

个人一些想法,如果仅从可选类型角度去考虑的话,实际上dart现在是可以支持基于类型的函数重载的,因为Dart有类型推导功能。如果dart能够推导出上述data1和data2类型,那么就可以根据推导出的类型去匹配重载的函数。Kotlin就是这样做的,以Kotlin为例:

fun overloaded(data: Int) {
    //....
}

fun overloaded(data: List<String>) {
   //....
}

fun main(args: Array<String>) {
    val data1 = 100 //这里Kotlin也是采用类型推导为Int
    val data2 = listOf("100")//这里Kotlin也是采用类型推导为List<String>
    //所以以下重载函数的调用在Kotlin中是合理的
    overloaded(data1)
    overloaded(data2)
}

实际上,Dart官方在Github提到过Dart迁移到新的类型系统中,Dart是有能力支持函数重载的 。具体可以参考这个dartlang的issue: github.com/dart-lang/s…

但是,dart为什么不支持函数重载呢? 其实,不是没有能力支持,而是没有必要的。其实在很多的现代语言比如GO,Rust中的都是没有函数重载。Kotlin中也推荐使用默认值参数替代函数重载,感兴趣的可以查看我之前的一篇文章juejin.cn/post/684490…。然而在dart中函数也是支持默认值参数的,其实函数重载更容易让人困惑,就比如Java中的Thread类中7,8个构造函数重载放在一起,让人就感到困惑。具体参考这个讨论: groups.google.com/a/dartlang.…

二、接口类型

在Dart中没有直接显示声明接口的方法,没有类似interface的关键字来声明接口,而是隐性地通过类声明引入。所以每个类都存在一个对应名称隐性的接口,dart中的类型也就是接口类型。

//定义一个抽象类Person,同时它也是一个隐性的Person接口
abstract class Person {
  final String name;
  final int age;

  Person(this.name, this.age);

  get description => "My name is $name, age is $age";
}

//定义一个Student类,使用implements关键字实现Person接口
class Student implements Person {
  @override
  // TODO: implement age
  int get age => null;//重写age getter函数,由于在Person接口中是final修饰,所以它只有getter访问器函数,作为接口实现就是需要重写它所有的函数,包括它的getter或setter方法。

  @override
  // TODO: implement description
  get description => null;//重写定义description方法

  @override
  // TODO: implement name
  String get name => null;//重写name getter函数,由于在Person接口中是final修饰,所以它只有getter访问器函数,作为接口实现就是需要重写它所有的函数,包括它的getter或setter方法。
}

//定义一个Student2类,使用extends关键字继承Person抽象类
class Student2 extends Person {
  Student2(String name, int age) : super(name, age);//调用父类中的构造函数

  @override
  get description => "Student: ${super.description}";//重写父类中的description方法
}

三、泛型

1、泛型的基本介绍

Dart中的泛型和其他语言差不多,但是Dart中的类型是可选的,使用泛型可以限定类型;使用泛型可以减少很多模板代码。

一起来看个例子:

//这是一个打印int类型msg的PrintMsg
class PrintMsg {
  int _msg;

  set msg(int msg) {
    this._msg = msg;
  }

  void printMsg() {
    print(_msg);
  }
}

//现在又需要支持String,double甚至其他自定义类的Msg,我们可能这么加
class Msg {
  @override
  String toString() {
    return "This is Msg";
  }
}

class PrintMsg {
  int _intMsg;
  String _stringMsg;
  double _doubleMsg;
  Msg _msg;

  set intMsg(int msg) {
    this._intMsg = msg;
  }

  set stringMsg(String msg) {
    this._stringMsg = msg;
  }

  set doubleMsg(double msg) {
    this._doubleMsg = msg;
  }

  set msg(Msg msg) {
    this._msg = msg;
  }

  void printIntMsg() {
    print(_intMsg);
  }

  void printStringMsg() {
    print(_stringMsg);
  }

  void printDoubleMsg() {
    print(_doubleMsg);
  }

  void printMsg() {
    print(_msg);
  }
}

//但是有了泛型以后,我们可以把上述代码简化很多:
class PrintMsg<T> {
  T _msg;

  set msg(T msg) {
    this._msg = msg;
  }
  
  void printMsg() {
    print(_msg);
  }
}

补充一点Dart中可以指定实际的泛型参数类型,也可以省略。省略实际上就相当于指定了泛型参数类型为dynamic类型。

class Test {
  List<int> nums = [1, 2, 3, 4];
  Map<String, int> maps = {'a': 1, 'b': 2, 'c': 3, 'd': 4};

//上述定义可简写成如下形式,但是不太建议使用这种形式,仅在必要且适当的时候使用
  List nums = [1, 2, 3, 4];
  Map maps = {'a': 1, 'b': 2, 'c': 3, 'd': 4};

//上述定义相当于如下形式
  List<dynamic> nums = [1, 2, 3, 4];
  Map<dynamic, dynamic> maps = {'a': 1, 'b': 2, 'c': 3, 'd': 4};
}

2、泛型的使用

  • 类泛型的使用

    //定义类的泛型很简单,只需要在类名后加: <T>;如果需要多个泛型类型参数,可以在尖括号中追加,用逗号分隔
    class List<T> {
      T element;
    
      void add(T element) {
        //...
      }
    }
    
  • 函数泛型的使用

    //定义函数的泛型
    void add(T elememt) {//函数参数类型为泛型类型
        //...
    }
    
    T elementAt(int index) {//函数参数返回值类型为泛型类型
        //...
    }
    
    E transform(R data) {//函数参数类型和函数参数返回值类型均为泛型类型
       //... 
    }
    
  • 集合泛型的使用

    var list = <int> [1, 2, 3];
    //相当于如下形式
    List<int> list = [1, 2, 3];
    
    var map = <String, int>{'a':1, 'b':2, 'c':3};
    //相当于如下形式
    Map<String, int> map = {'a':1, 'b':2, 'c':3};
    
  • 泛型的上界限定

    //和Java一样泛型上界限定可以使用extends关键字来实现
    class List<T extends num> {
     T element;
     void add(T element) {
     //...
     }
    }
    

3、子类、子类型和子类型化关系

  • 泛型类与非泛型类

我们可以把Dart中的类可分为两大类: 泛型类非泛型类

先说非泛型类也就是开发中接触最多的一般类,一般的类去定义一个变量的时候,它的实际就是这个变量的类型. 例如定义一个Student类,我们会得到一个Student类型

泛型类比非泛型类要更加复杂,实际上一个泛型类可以对应无限种类型。为什么这么说,其实很容易理解。我们从前面文章知道,在定义泛型类的时候会定义泛型形参,要想拿到一个合法的泛型类型就需要在外部使用地方传入具体的类型实参替换定义中的类型形参。我们知道在Dart中List是一个类,它不是一个类型。由它可以衍生成无限种泛型类型。例如List<String>、List<int>、List<List<num>>、List<Map<String,int>>

  • 何为子类型

我们可能会经常在Flutter开发中遇到subtype子类型的错误: type 'String' is not a subtype of type 'num' of 'other'. 到底啥是子类型呢? 它和子类是一个概念吗?

首先给出一个数学归纳公式:

如果G是一个有n个类型参数的泛型类,而A[i]是B[i]的子类型且属于 1..n的范围,那么可表示为G<A[1],...,A[n]> * G<B[1],...,B[n]>的子类型,其中 A * B 可表示A是B的子类型。

上述是不是很抽象,其实Dart中的子类型概念和Kotlin中子类型概念极其相似。

我们一般说子类就是派生类,该类一般会继承它的父类(也叫基类)。例如: class Student extends Person{//...},这里的Student一般称为Person的子类

子类型则不一样,我们从上面就知道一个类可以有很多类型,那么子类型不仅仅是想子类那样继承关系那么严格。子类型定义的规则一般是这样的: 任何时候如果需要的是A类型值的任何地方,都可以使用B类型的值来替换的,那么就可以说B类型是A类型的子类型或者称A类型是B类型的超类型。可以明显看出子类型的规则会比子类规则更为宽松。那么我们可以一起分析下面几个例子:

注意: 某个类型也是它自己本身的子类型,很明显String类型的值任意出现地方,String肯定都是可以替换的。属于子类关系的一般也是子类型关系。像double类型值肯定不能替代int类型值出现的地方,所以它们不存在子类型关系.

  • 子类型化关系:

如果A类型的值在任何时候任何地方出现都能被B类型的值替换,B类型就是A类型的子类型,那么B类型到A类型之间这种映射替换关系就是子类型化关系

4、协变(covariant)

一提到协变,可能我们还会对应另外一个词那就是逆变,实际上在Dart1.x的版本中是既支持协变又支持逆变的,但是在Dart2.x版本仅仅支持协变的。有了子类型化关系的概念,那么协变就更好理解了,协变实际上就是保留子类型化关系,首先,我们需要去明确一下这里所说的保留子类型化关系是针对谁而言的呢?

比如说intnum的子类型,因为在Dart中所有泛型类都默认是协变的,所以List<int>就是List<num>的子类型,这就是保留了子类型化关系,保留的是泛型参数(intnum)类型的子类型化关系.

一起看个例子:

class Fruit {
  final String color;

  Fruit(this.color);
}

class Apple extends Fruit {
  Apple() : super("red");
}

class Orange extends Fruit {
  Orange() : super("orange");
}

void printColors(List<Fruit> fruits) {
  for (var fruit in fruits) {
    print('${fruit.color}');
  }
}

main() {
  List<Apple> apples = <Apple>[];
  apples.add(Apple());
  printColors(apples);//Apple是Fruit的子类型,所以List<Apple>是List<Fruit>子类型。
  // 所以printColors函数接收一个List<Fruit>类型,可以使用List<Apple>类型替代
  List<Orange> oranges = <Orange>[];
  oranges.add(Orange());
  printColors(oranges);//同理

  List<Fruit> fruits = <Fruit>[];
  fruits.add(Fruit('purple'));
  printColors(fruits);//Fruit本身也是Fruit的子类型,所以List<Fruit>肯定是List<Fruit>子类型
}

5、协变在Dart中的应用

实际上,在Dart中协变默认用于泛型类型实际上还有用于另一种场景协变方法参数类型. 可能对专业术语有点懵逼,先通过一个例子来看下:

//定义动物基类
class Animal {
  final String color;

  Animal(this.color);
}

//定义Cat类继承Animal
class Cat extends Animal {
  Cat() : super('black cat');
}

//定义Dog类继承Animal
class Dog extends Animal {
  Dog() : super('white dog');
}

//定义一个装动物的笼子类
class AnimalCage {
  void putAnimal(Animal animal) {
    print('putAnimal: ${animal.color}');
  }
}

//定义一个猫笼类
class CatCage extends AnimalCage {
  @override
  void putAnimal(Animal animal) {//注意: 这里方法参数是Animal类型
    super.putAnimal(animal);
  }
}

//定义一个狗笼类
class DogCage extends AnimalCage {
    @override
    void putAnimal(Animal animal) {//注意: 这里方法参数是Animal类型
      super.putAnimal(animal);
    }
}

我们需要去重写putAnimal方法,由于是继承自AnimalCage类,所以方法参数类型是Animal.这会造成什么问题呢? 一起来看下:

main() {
  //创建一个猫笼对象
  var catCage = CatCage();
  //然后却可以把一条狗放进去,如果按照设计原理应该猫笼子只能put猫。
  catCage.putAnimal(Dog());//这行静态检查以及运行都可以通过。
  
  //创建一个狗笼对象
  var dogCage = DogCage();
  //然后却可以把一条猫放进去,如果按照设计原理应该狗笼子只能put狗。
  dogCage.putAnimal(Cat());//这行静态检查以及运行都可以通过。
}

其实对于上述的出现问题,我们更希望putAnimal的参数更具体些,为了解决上述问题你需要使用 covariant 协变关键字。

//定义一个猫笼类
class CatCage extends AnimalCage {
  @override
  void putAnimal(covariant Cat animal) {//注意: 这里使用covariant协变关键字 表示CatCage对象中的putAnimal方法只接收Cat对象
    super.putAnimal(animal);
  }
}

//定义一个狗笼类
class DogCage extends AnimalCage {
    @override
    void putAnimal(covariant Dog animal) {//注意: 这里使用covariant协变关键字 表示DogCage对象中的putAnimal方法只接收Dog对象
      super.putAnimal(animal);
    }
}
//调用
main() {
  //创建一个猫笼对象
  var catCage = CatCage();
  catCage.putAnimal(Dog());//这时候这样调用就会报错, 报错信息: Error: The argument type 'Dog' can't be assigned to the parameter type 'Cat'.
}

为了进一步验证结论,可以看下这个例子:

typedef void PutAnimal(Animal animal);

class TestFunction {
  void putCat(covariant Cat animal) {}//使用covariant协变关键字

  void putDog(Dog animal) {}

  void putAnimal(Animal animal) {}
}

main() {
  var function = TestFunction()
  print(function.putCat is PutAnimal);//true 因为使用协变关键字
  print(function.putDog is PutAnimal);//false
  print(function.putAnimal is PutAnimal);//true 本身就是其子类型
}

6、为什么Kotlin比Dart的泛型型变更安全

实际上Dart和Java一样,泛型型变都存在安全问题。以及List集合为例,List在Dart中既是可变的,又是协变的,这样就会存在安全问题。然而Kotlin却不一样,在Kotlin把集合分为可变集合MutableList<E>和只读集合List<E>,其中List<E>在Kotlin中就是不可变的,协变的,这样就不会存在安全问题。下面这个例子将对比Dart和Kotlin的实现:

  • Dart中的实现
class Fruit {
  final String color;

  Fruit(this.color);
}

class Apple extends Fruit {
  Apple() : super("red");
}

class Orange extends Fruit {
  Orange() : super("orange");
}

void printColors(List<Fruit> fruits) {//实际上这里List是不安全的。
  for (var fruit in fruits) {
    print('${fruit.color}');
  }
}

main() {
  List<Apple> apples = <Apple>[];
  apples.add(Apple());
  printColors(apples);//printColors传入是一个List<Apple>,因为是协变的
}

为什么说printColors函数中的List<Fruit>是不安全的呢,外部main函数中传入的是一个List<Apple>.所以printColors函数中的fruits实际上是一个List<Apple>.可是printColors这样改动呢?

void printColors(List<Fruit> fruits) {//实际上这里List是不安全的。
  fruits.add(Orange());//静态检查都是通过的,Dart1.x版本中运行也是可以通过的,但是好在Dart2.x版本进行了优化,
  // 在2.x版本中运行是会报错的:type 'Orange' is not a subtype of type 'Apple' of 'value'
  // 由于在Dart中List都是可变的,在fruits中添加Orange(),实际上是在List<Apple>中添加Orange对象,这里就会出现安全问题。
  for (var fruit in fruits) {
    print('${fruit.color}');
  }
}
  • Kotlin中的实现

然而在Kotlin中的不会存在上面那种问题,Kotlin对集合做了很细致的划分,分为可变与只读。只读且协变的泛型类型更具安全性。一起看下Kotlin怎么做到的。

open class Fruit(val color: String)

class Apple : Fruit("red")

class Orange : Fruit("orange")

fun printColors(fruits: List<Fruit>) {
    fruits.add(Orange())//此处编译不通过,因为在Kotlin中只读集合List<E>,没有add, remove之类修改集合的方法只有读的方法,
    //所以它不会存在List<Apple>中还添加一个Orange的情况出现。
    for (fruit in fruits) {
        println(fruit.color)
    }
}

fun main() {
    val apples = listOf(Apple())
    printColors(apples)
}

四、类型具体化

1、类型检测

在Dart中一般使用 is 关键字做类型检测,这一点和Kotlin中是一致的,如果判断不是某个类型dart中使用is!, 而在Kotlin中正好相反则用 !is 表示。类型检测就是对表达式结果值的动态类型与目标类型做对比测试。

main() {
  var apples = [Apple()];
  print(apples is List<Apple>);
}

2、强制类型转化

强制类型转换在Dart中一般使用 as 关键字,这一点也和Kotlin中是一致的。强制类型转换是对一个表达式的值转化目标类型,如果转化失败就会抛出CastError异常。

Object o = [1, 2, 3];
o as List;
o as Map;//抛出异常

五、总结

到这里我们就把Dart中的类型系统和泛型介绍完毕了,相信这篇文章将会使你对Dart中的类型系统有一个更全面的认识。其实通过Dart中泛型,就会发现Dart2.x真的优化很多东西,比如泛型安全的问题,虽然静态检查能通过但是运行无法通过,换做Dart1.x运行也是可以通过的。Dart2.x将会越来越严谨越来越完善,说明Dart在改变这是一件好事,一起期待它的更多特性。下一篇我们将进入Dart中更为核心的部分异步编程系列,感谢关注~~~.

我的公众号

欢迎关注: 熊喵先生