文章目录
前言
通过前面的学习,我们已经了解了在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>
类型的,结果如下:
怎么避免这个错误呢,这个得看写代码的人是怎么想的了,如果你就是想在一个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一样,它们也拥有安全并且健全的类型系统。
了解了什么是健全性,我们再来看看健全性给我们带来的好处。官方给出了如下四点好处:
- 代码可以在编译时发现类型相关的bug:因为一个健全的类型系统,强制在代码中的变量类型是明确、清晰的,所以类型相关的bug在编译时很容易发现。
- 代码可以有更好的可读性:因为每个类型都有明确的类型,我们可以明确的知道那个值是什么类型,而不是像汇编那样去猜只有上帝和我才看得懂的代码,在Dart中类型不会撒谎。
- 代码可以有更好的维护性:有了健全的类型系统,当你更改一段代码时,类型系统会提醒你,你刚刚更改的代码对其他哪些代码有所破坏,让你及时修改。
- 更好的支持AOT编译。
通过上面两段讲解,我们对Dart中类型系统的两大块也有一个粗略的了解了,至于再具体的,作者也懂得不多,读者可以参考官方的语言设计文档来学习。接下来我们聊聊,如何使用类型系统的特性来帮助我们写出更好的代码以及排除一些Bug。
通过静态类型检查的小提示
在Dart中,给出了我们三点关于如何通过静态类型检查的小提示:
- 当重载一个方法时,使用完整的类型作为返回值。
- 当重载一个方法时,使用完整的类型作为参数。
- 不要将一个
dynamic
类型的list转换为一个具体类型的list。
根据这三点我们来给出三个例子,首先,假设我们有如下的类继承关系:
大致代码如下:
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的子类型,其实是可以通过编译的,只有当返回值是一个不相关的类型时,才会报错)
示例如下:
可以看到,当我们返回值为一个不相关类型时就会报错了。
2.当重载一个方法时,使用完整的类型作为参数。
这个的原则和返回值的原则一致,但是有一点要注意就是关于类型收缩(tighten)的概念,这个概念说起来其实也很简单,就是如果方法参数的类型为父类,那么你在重载该方法的时候,如果要修改方法参数的类型,那么方法参数的类型只能为父类的父类,意思就是,修改后的方法参数能接收的类型的数量,必须大于等于修改前方法参数能接收的数量,下面给出一个具体的例子:
class Cat extends Animal {
void chase(Object a) {}
void otherMethod(Cat a) {}
}
我们可以看到,父类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!。