Dart中的可选类型是如何工作的
Dart 语言是动态类型的。你可以编写、运行没有类型标注的任何程序,就像你使用Javascript的方式。
51CTO推荐专题:Google Dart新结构化编程语言
你可以在程序中添加类型标注:
◆ 添加类型不会阻止你程序的编译和运行——即使标注不完整或错误。
◆ 不论你添加了什么类型标注,你的程序都具有完全相同的语义。
然而,添加类型标注可以使你获益。类型提供了下面这些好处:
◆ 给人看的文档。明智地放置类型标注可以使别人更容易地阅读你的代码。
◆ 给机器看的文档。工具可以有多种方式利用类型标注。特别是,它们可以在 IDE 中帮助提供很好的特性,如名称补全和增强的导航。
◆ 早期的错误检测。Dart 提供了静态检查器,它可以警告你潜在的问题,而不用你自己查。另外,在开发模式中,Dart 自动把类型标注转换为运行时断言检查来辅助调试。
◆ 有时,在编译到 Javascript 时,类型可以帮助改进性能。
静态检查器
静态检查器(static checker)行为很像C中的链接。它在编译时警告你潜在的问题。这些警告中的很多是和类型相关的。静态检查器不会产生错误——不论检查器说什么你总是可以编译和运行你的代码。
检查器不会对每个可能的类型违反都敏感。它不是类型检查器(typechecker),因为Dart并不是按照典型的类型系统那样使用类型。检查器会抱怨那些非常可能是真实问题的地方,而不会强迫你去满足心胸狭隘的类型系统。
例如,考虑这个:
String s1 = '9'; String s2 = '1'; ... int n = s1 + s2; print(n);
这里明显是个问题。这种情况下静态检查器会产生一个警告。注意代码依然可以运行,n 被置为字符串'91'并打印出来。
然而,不像典型的强类型系统,这样的代码:
Object lookup(String key) {...} // a lookup method in a heterogenous table String s = lookup('Frankenstein');
检查器不会抱怨。因为这种情况下代码很有可能是对的,虽然缺少类型信息。你作为程序员通常知道程序的语义,而类型检查器(typechecker)不知道。你知道'Frankenstein'这个key在表中存储的是字符串,即使 lookup 方法声明返回的是Object。
Dynamic类型
没有提供类型的时候,Dart如何避免抱怨呢?这其中的关键就是 Dynamic 类型,这是程序员没有明确给出类型时候的默认类型。使用 Dynamic 类型让检查器闭嘴。
偶尔,你可能想要明确地使用 Dynamic 。
Map<String, Dynamic> m = { 'one': new Partridge(), 'two': new TurtleDove(), ..., 'twelve': new Drummer()};
我们本来也可以给m使用 Map<String, Object> ,但是那样的话,当我们获取内容的时候,它们将是Object的静态类型,而它只有很少的信息。因为map的内容除了Object外没有公共的super接口,我们可能更愿意使用 Dynamic 。如果我们像这样调用map 的值的方法:
pearTree = m['one'].container();
如果内容是Object类型,我们会得到警告,因为Object不支持container方法。如果我们使用Dynamic类型,就不会产生警告。
范型
Dart 支持具体化范型(reified generics)。就是说,范型类型的对象在运行时携带它们的类型参数。传递类型参数给范型类型的构造函数是运行时操作。这如何与可选类型的要求相一致呢?
好吧,如果你不想总是考虑类型,范型并不强迫你。你可以创建范型类的实例,而不需要提供类型参数。例如,这样写没问题:
new List();
当然,如果你想要,也可以这样写:
new List<String>(); new List();
是下面这样的快捷方式:
new List<Dynamic>();
在构造函数中,类型参数起到运行时角色。实际上,它们在运行时被传递,所以你可以做动态类型测试的时候使用它们。
new List<String>() is List<Object> // true: every string is an object new List<Object>() is List<String> // false: not all objects are strings
Dart中的范型符合程序员的直觉。这是一些更有趣的情况:
new List<String>() is List<int> // false new List<String>() is List // true new List<String>() is List<Dynamic> // same as line above new List() is List<Dynamic> // true, these are exactly the same
与此相反,类型标注(例如变量前添加的类型或者函数和方法的返回类型)起到非运行时角色并且不影响程序的语义。最后一个值得学习的情况:
new List() is List<String> // true as well!
你可以不用类型写程序,但是你经常要传递数据到有类型的库中。为了防止类型妨碍你,没有类型参数的范型类型被认为是任何其它范型类型的替代品(子类型)。
检查模式
在开发过程中,Dart 程序可以在检查模式(checked mode)下运行。如果你在检查模式下运行程序,在参数传递、返回结果和执行赋值时,系统将自动执行某些类型的检查。如果检查失败,程序将在该处停止执行,并带有清晰的错误信息。所以,
String s = new Object();
将会停止执行,因为Object不是String的子类型。然而,
Object foo(){return "x";} String s = foo();
工作正常,因为foo在运行时返回的实际对象就是String,尽管其类型签名说foo返回的是Object。当对象赋值给变量时,Dart 检查对象的运行时类型是否为变量(静态)声明类型的子类型。
本质上,检查模式就像是在对每次赋值、返回等进行子类型检查的调试器下运行。一些更复杂的例子:
<int>[0,1, 1][2] = new Object(); // fails in checked mode bar(int n) { return n *2;} ... bar(3.2); // returns 6.4 in production, but fails in checked mode
在检查模式下,每次把参数传递给函数时,都要检查参数的运行时类型是否是形式参数声明类型的子类型。我们可以很容易地纠正这个:
bar(num n) { return n *2;} ... bar(3.2); // works fine int i_bar(num n) { return n *2;} ... i_bar(3.2); // fails in checked mode // because returned value is not an int
注意最后一行。检查发生在返回值上,即使函数的结果并没有进行赋值。
让我们回到之前的Frankenstein例子上。
Object lookup(String key) {...} // a lookup method in a heterogenous table String s = lookup('Frankenstein');
如果我们假设的lookup方法返回一个String是正确的,那么检查模式会平滑地执行。如果不是,那么它将捕获到我们的这个错误。在生产模式(production mode)下,代码都会运行,不会抱怨。假设lookup方法真的返回了一个非String对象,一个Frankenstein类的实例。那么变量 s 将容纳那个实例。Dart 绝不会神奇地强制它为一个字符串。如果Dart那样做就会意味着类型标注正在改变我们程序的行为,类型就不再是可选的了。
当然,如果你根本就不用类型,检查模式不会妨碍你。
my_add(s1, s2) { return s1 + s2;} my_add(3, 4); // 7 my_add("3", "4"); // "34"
所有这些检查会带来很大的性能损失,所以通常不能用在生产环境中。这些检查的好处是它们可以在源头上捕获动态类型的错误,更容易地调试问题。虽然总可以在测试过程中发现大多数这类问题,但是检查模式有利于缩小它们的范围。
使用类型
如何使用类型取决于你。如果你讨厌类型,你不必使用它们。你不会得到任何类型的警告,你可以用你在其它动态语言中感到舒适的方式开发。然而你依然可以从类型中获益,因为Dart的库中有类型签名,它们告诉你它们期望什么和返回什么。如果你在检查模式中运行,传递了错误的参数给类库,检查模式将在你犯错的地方发现它们。
如果你喜欢类型,你可以在任何地方使用它们,很像是静态类型语言。然而,即使那样你也不会获得同样级别的静态检查。Dart的规则比较宽松。我们期望为这些人提供额外的工具来更加严格地解释类型标注。
我们不建议太极端地使用方式。应该在有意义的地方使用类型。你能做的最有价值的事情是添加类型到你类库中公有成员的声明上。接下来,再对私有成员做同样的事。即使没有别人需要维护代码,如果你离开代码几周或几个月后又回来,你会发现它是有帮助的。在这两种情况下,你不一定要在方法体或函数体中添加类型。库的使用者从类型签名中获得价值,即使它们不是100%准确。
在函数体中,并不总是需要标注声明。有时代码足够简单,真的无所谓,类型反而可能会造成混乱。