Dart类型系统

前言

通过前面的学习,我们已经了解了在Dart中常用的数据类型,对Dart的类型也有了一个基本的认识,那么本篇我们将讨论一个重量级的话题,就是Dart的类型系统。要了解类型系统,首先要知道类型系统是什么,维基百科上对于类型系统的定义如下:

在计算机科学中,类型系统(英语:type system)用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,如何操作这些类型,这些类型如何互相作用。类型可以确认一个值或者一组值具有特定的意义和目的(虽然某些类型,如抽象类型和函数类型,在程序执行中,可能不表示为一个具体的值)。类型系统在各种语言之间有非常大的不同,也许,最主要的差异存在于编译时期的语法,以及执行时期的操作实现方式。

通过维基百科的描述,我们可以知道,类型系统是一种用于定义和管理数据类型的系统。它规定了变量、表达式和值的类型,并定义了这些类型之间的操作和转换规则。

那么为什么需要类型系统呢

我们都知道,计算机只能识别二进制代码,但是二进制代码的编写对于程序员来说难度太高了,并且很容易出错,所以为了简化程序编写,于是就出现了汇编语言,汇编语言是一种二进制代码的别名,通过汇编器将相应的汇编语言的语句转换为二进制。

你可能要问,我们不是要讲类型系统吗,为什么又绕到二进制和汇编上来了,因为类型系统就是为了解决低级语言如汇编语言,机器语言的痛点,就是类型。机器语言与汇编语言都没有真正意义上的类型抽象,对于它们来说所有的代码其实就是一串二进制指令,那么我们在使用低级语言编写程序的时候,就需要自己去控制那些值是什么类型,以及各种值之间的转换,在汇编里你可以轻松的将一串数字转成其它任意类型,如果你想的话。

这样一来,我们要编写出一段还算优秀的代码,就需要耗费大量的时间与精力,在类型转换或者一些涉及到类型概念的操作上,比如字符串拼接之类的,使用汇编你得清楚的知道一串字符有多少个字符,在内存中的顺序是什么,大端存储还是小端存储,两个字符串的起始地址分别是哪里,要往后拼接多少个字符。这些如果都要程序员亲自来计算的话,那么不仅写出优秀代码的概率大大降低,并且可能每一个项目就得消耗几个程序员才能完成。

所以,计算机科学界的大牛们就设计了类型与类型系统的概念,这也是高级语言与低级语言比较本质的区别之一,有了类型,我们就不用再关心这个类型具体是怎么实现的,并且操作该类型的API都是由语言提供,一般的程序员那就不必再担心在项目中被消耗了。

就例如刚刚那个字符串的例子,在高级语言如Dart中,语言的设计者们提供了完善的操作字符串的方式,他们将字符串抽象成了一个类型,我们只需要使用语言的设计者们提供的该类型相关操作的API,就能轻松的完成一次字符串的操作,例如拼接,我们只需要使用+就可以将两个字符串拼接起来,而不必去计算内存地址和字符串的长度。所以,类型其实就是一种抽象,主要就是为了方便程序员们操作,并且能更高概率写出优质的代码,至于底层实现,那是语言设计者们该关心的事情。当然,如果你也想称为一名优秀的语言设计者,这些还是有必要去了解的。

现在我们知道了类型的好处,那么类型系统呢?

我们在前面提到,类型系统是一种定义和管理数据类型的系统。而类型又是一种高级的抽象,那么类型系统就是管理这些抽象的机制。它规定了哪些值的类型是什么,那些类型能进行哪些操作,哪些类型能互相转换,哪些类型只能向下转换,哪些类型只能向上转换,等等等等。

有了类型系统,妈妈再也不用担心我分不清什么值是什么类型啦,因为在语言的实现中都写的清清楚楚、明明白白,也不用担心我会转换错误啦,因为编译器会让你通不过编译!

了解了什么是类型,什么是类型系统,接下来我们就来看看Dart的设计者们,设计的是一个什么样的类型系统吧(由于高级语言的类型系统实在过于庞大,作者对于整体也不太熟悉,所以就只会介绍官方文档上说明的特性,还望见谅)

Dart中的类型系统

在Dart的官方介绍中,它自称Dart是类型安全的,并且是类型健全的。因为它使用了静态类型检查与运行时类型检查相结合的方式,来确保一个变量的类型始终与编译时的静态类型相匹配,在整个程序的生命周期中它的类型都不会发生变化。

由于Dart是支持静态分析的,所以我们在编译时就能发现很多潜在的错误,而让你无法通过编译,这也是Dart为什么敢说是类型完整和类型安全的底气之一。

接下来我们来看看Dart对于我们使用类型系统来避免错误给出的一些建议,首先来看一个例子:

void printInts(List<int> a) => print(a);

void main() {
  final list = [];
  list.add(1);
  list.add('2');
  printInts(list);
}

这个例子在运行的时候会报错,原因很明显,因为printInts要求的是一个List<int>的参数,而list不是该类型的,它是List<dynamic>类型的,结果如下:

image-20231008215612346

image-20231008215706376

怎么避免这个错误呢,这个得看写代码的人是怎么想的了,如果你就是想在一个list中存放不同类型的元素,还是说你只是一时手误,写错了,如果是前者,那么你可以将要调用的函数的签名也更改为List<dynamic>,如果是后者,那么我们可以使用一个静态类型的注释,来让这个list只能传入某种类型的。代码如下:

void printInts(List<int> a) => print(a);

void main() {
  // 静态类型声明,表示list中只能存在int类型的
  final list = <int>[];
  list.add(1);
  list.add(2);
  printInts(list);
}

然后我们再来讨论为什么list会被推断成List<dynmaic>,这是因为,Dart 的类型推断机制,需要你提供足精确和足够多的类型信息,在最上面的例子中,Dart没有足够的多的信息将其推断成一个具体的类型,所以只能泛化该类型为dynamic

以上这段例子,就是Dart中类型安全的作用,规定了哪些类型能够互相转换,不合里的转换在编译期间就会被发现,提高我们程序的健壮性。这也是类型系统的功能之一,接下来我们来看看Dart中的类型系统健全性。

什么是类型系统的健全性

Dart官方给出的解释是,健全性是只程序在运行中不会进入某些无效状态,并且由于一个健全类型系统的存在,表达式和表达式的结果,它们的静态类型不会不匹配,例如:如果表达式中的操作数类型是String,那么在运行时,它们计算的结果也一样定是一个String。

从这我们可以了解到,类型系统的健全性,就是在运行时值和变量的类型是匹配的,不会发生变化,结合刚刚的类型安全性。这两个组成了Dart的类型系统,结合最开始类型系统的概念,我们再来看一看,类型系统,就是规定变量,表达式,值的类型并且定义了类型之间的操作和转换规则。

这也从另一个方面论证了我们最开始对于类型系统的理解是正确的,一个完善的类型系统,主要就是需要具备类型安全性和类型健全性。就像C#、Java一样,它们也拥有安全并且健全的类型系统。

了解了什么是健全性,我们再来看看健全性给我们带来的好处。官方给出了如下四点好处:

  1. 代码可以在编译时发现类型相关的bug:因为一个健全的类型系统,强制在代码中的变量类型是明确、清晰的,所以类型相关的bug在编译时很容易发现。
  2. 代码可以有更好的可读性:因为每个类型都有明确的类型,我们可以明确的知道那个值是什么类型,而不是像汇编那样去猜只有上帝和我才看得懂的代码,在Dart中类型不会撒谎。
  3. 代码可以有更好的维护性:有了健全的类型系统,当你更改一段代码时,类型系统会提醒你,你刚刚更改的代码对其他哪些代码有所破坏,让你及时修改。
  4. 更好的支持AOT编译。

通过上面两段讲解,我们对Dart中类型系统的两大块也有一个粗略的了解了,至于再具体的,作者也懂得不多,读者可以参考官方的语言设计文档来学习。接下来我们聊聊,如何使用类型系统的特性来帮助我们写出更好的代码以及排除一些Bug。

通过静态类型检查的小提示

在Dart中,给出了我们三点关于如何通过静态类型检查的小提示:

  1. 当重载一个方法时,使用完整的类型作为返回值。
  2. 当重载一个方法时,使用完整的类型作为参数。
  3. 不要将一个dynamic类型的list转换为一个具体类型的list。

根据这三点我们来给出三个例子,首先,假设我们有如下的类继承关系:

image-20231008222405277

大致代码如下:

class Animal {
  Animal? _parent;
  void chase(Animal a) {}
  Animal? get parent => _parent;
  Animal? set(Animal parent) => _parent = parent;
}


class Cat extends Animal {}

class Lion extends Cat {}

class MaineCoon extends Cat {}

class HoneyBadger extends Animal {
  HoneyBadger? _honeyBadger;
  Root? root;
  
  void chase(Animal a) {
    super.chase(a);
  }

  
  HoneyBadger? get parent => _honeyBadger;
}

1.当重载一个方法时,使用完整的类型作为返回值。

其实主要意思就是,当我们重载父类的方法时,如果父类中该方法的返回值是父类本身,那么我们在重载该方法的时候,它的返回值类型就必须是父类本身,或者父类的子类型,这样才会通过编译,否则就会出错,代码示例如下:

class HoneyBadger extends Animal {
  
  void chase(Animal a) { ... }

  
  HoneyBadger get parent => ...
}

对于这段代码,get属性的返回值可以为Animal,HoneyBadger,Aligator,Cat,Lion,MaineCoon中的任意一个类型,不能是其他类型,如果你给出一个不相关的类型,那么编译器就会报错(这个地方官方文档的示例好像是错误的,因为官方文档中Root也是Animal的子类型,其实是可以通过编译的,只有当返回值是一个不相关的类型时,才会报错)

示例如下:

image-20231008222929406

可以看到,当我们返回值为一个不相关类型时就会报错了。

2.当重载一个方法时,使用完整的类型作为参数。

这个的原则和返回值的原则一致,但是有一点要注意就是关于类型收缩(tighten)的概念,这个概念说起来其实也很简单,就是如果方法参数的类型为父类,那么你在重载该方法的时候,如果要修改方法参数的类型,那么方法参数的类型只能为父类的父类,意思就是,修改后的方法参数能接收的类型的数量,必须大于等于修改前方法参数能接收的数量,下面给出一个具体的例子:

class Cat extends Animal {
  
  void chase(Object a) {}
  
  void otherMethod(Cat a) {}
}

image-20231008224336087

我们可以看到,父类Animal这两个方法的参数都是Animal,子类Cat在重载时,更改参数为Object重载成功,因为Object类型能接收的类型数量远远大于Animal类型能接收的类型数量,而将参数更改为Cat重载失败,因为Cat仅能接收Lion,Cat,MaineCoon,三种类型,而Animal能够接受,Animal,Aligator,Cat,HoneyBadger,Lion,MaineCoon六种类型,这里发生了类型收缩,所以重载失败,所以我们在重载一个方法,要更改该方法的参数类型时,需要注意这一点。

3.不要将一个dynamic类型的list转换为一个具体类型的list。

这个也很好理解,并且在类型安全性的时候也给出过介绍。不过我们也再次给出一个例子来进行说明:

void main() {
  List<Cat> foo = <dynamic>[Dog()]; // Error
  List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

大概就是如上这种情况,不可以将一个动态类型的列表转换为一个具体类型的列表。

最后要注意关于类型的一点就是运行时类型检查,当我们使用type check运算符的时候,我们就要注意是否能转换成功,因为在使用type check运算符的时候,Dart会放弃静态分析,转而使用运行时检查,当使用type check类型错误的时候,就可能会发生运行时错误。

如果有读者不知道type check运算符是什么,可以去看作者前面的文章,但是这里也给出一些简单的示例:

void main() {
  List<Animal> animals = [Dog()];
  //这里的as就是type check运算符之一
  List<Cat> cats = animals as List<Cat>;
}

总结

以上就是关于Dart中类型系统的概况,相信通过这篇文章,读者能比较清晰的了解到什么是类型系统,类型系统的健全性和安全性分别有什么作用,以及在Dart中使用类型相关的一些知识。

最后,由于作者水平有限,在文章中难免出现错误,如果发现错误,请各位及时告知作者,作者会第一时间修改,以免误导他人,respect!。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值