Dart 语言:当 Java 和 C# 不够锋利时
已发表: 2022-03-11早在 2013 年,Dart 的官方 1.0 版本就受到了一些媒体的关注——就像大多数 Google 产品一样——但并不是每个人都像 Google 的内部团队一样渴望使用 Dart 语言创建关键业务应用程序。 五年后,通过对 Dart 2 的深思熟虑的重建,谷歌似乎已经证明了它对这门语言的承诺。 事实上,今天它继续受到开发人员的关注,尤其是 Java 和 C# 的老手。
Dart 编程语言之所以重要有几个原因:
- 它兼具两全其美:它是一种编译的、类型安全的语言(如 C# 和 Java)和脚本语言(如 Python 和 JavaScript)。
- 它转译为 JavaScript 以用作 Web 前端。
- 它可以在所有东西上运行,并编译为原生移动应用程序,因此您几乎可以将它用于任何事情。
- Dart 在语法上与 C# 和 Java 类似,所以它学起来很快。
我们这些来自大型企业系统的 C# 或 Java 世界的人已经知道为什么类型安全、编译时错误和 linter 很重要。 我们中的许多人都不愿采用“脚本”语言,因为害怕失去我们习惯的所有结构、速度、准确性和可调试性。
但是随着 Dart 的发展,我们不必放弃任何这些。 我们可以用同一种语言编写移动应用程序、Web 客户端和后端,并获得我们仍然喜欢 Java 和 C# 的所有东西!
为此,让我们浏览一些关键的 Dart 语言示例,这些示例对于 C# 或 Java 开发人员来说是新的,我们将在最后以 Dart 语言 PDF 进行总结。
注意:本文仅涵盖 Dart 2.x。 1.x 版本并没有“完全成熟”——特别是,类型系统是建议性的(如 TypeScript)而不是必需的(如 C# 或 Java)。
1.代码组织
首先,我们将讨论最重要的区别之一:代码文件的组织和引用方式。
源文件、范围、命名空间和导入
在 C# 中,类的集合被编译为程序集。 每个类都有一个命名空间,而且命名空间通常反映了文件系统中源代码的组织结构——但最终,程序集不会保留任何有关源代码文件位置的信息。
在 Java 中,源文件是包的一部分,命名空间通常与文件系统位置一致,但归根结底,包只是类的集合。
所以这两种语言都有办法让源代码在某种程度上独立于文件系统。
相比之下,在 Dart 语言中,每个源文件都必须导入它所引用的所有内容,包括您的其他源文件和第三方包。 没有相同方式的命名空间,您经常通过文件系统位置引用文件。 变量和函数可以是顶级的,而不仅仅是类。 在这些方面,Dart 更像脚本。
因此,您需要将您的想法从“类的集合”转变为更像“包含的代码文件序列”。
Dart 支持包组织和无包的临时组织。 让我们从一个没有包的例子开始来说明包含文件的顺序:
// file1.dart int alice = 1; // top level variable int barry() => 2; // top level function var student = Charlie(); // top level variable; Charlie is declared below but that's OK class Charlie { ... } // top level class // alice = 2; // top level statement not allowed // file2.dart import 'file1.dart'; // causes all of file1 to be in scope main() { print(alice); // 1 }
您在源文件中引用的所有内容都必须在该文件中声明或导入,因为没有“项目”级别,也没有其他方法可以在范围内包含其他源元素。
Dart 中命名空间的唯一用途是为导入命名,这会影响您从该文件引用导入代码的方式。
// file2.dart import 'file1.dart' as wonderland; main() { print(wonderland.alice); // 1 }
套餐
上面的示例组织了没有包的代码。 为了使用包,代码以更具体的方式组织起来。 这是名为apples
的包的示例包布局:
-
apples/
-
pubspec.yaml
— 定义包名、依赖项和其他一些东西 lib/
-
apples.dart
进口和出口; 这是包的任何消费者导入的文件 src/
-
seeds.dart
— 此处的所有其他代码
-
-
bin/
-
runapples.dart
包含 main 函数,它是入口点(如果这是一个可运行的包或包含可运行的工具)
-
-
然后你可以导入整个包而不是单个文件:
import 'package:apples';
重要的应用程序应始终组织为包。 这减轻了在每个引用文件中重复文件系统路径的许多麻烦; 另外,他们跑得更快。 它还可以轻松地在 pub.dev 上共享您的包,其他开发人员可以很容易地获取它以供自己使用。 您的应用程序使用的包将导致源代码被复制到您的文件系统,因此您可以尽可能深入地调试这些包。
2.数据类型
Dart 的类型系统在空值、数字类型、集合和动态类型方面存在重大差异。
到处都是空值
来自 C# 或 Java,我们习惯于将原始类型或值类型与引用或对象类型区别开来。 实际上,值类型在堆栈或寄存器中分配,并且值的副本作为函数参数发送。 而是在堆上分配引用类型,并且仅将指向对象的指针作为函数参数发送。 由于值类型总是占用内存,所以值类型变量不能为空,所有值类型成员都必须有初始化值。
Dart 消除了这种区别,因为一切都是对象。 所有类型最终都派生自Object
类型。 所以,这是合法的:
int i = null;
事实上,所有原语都被隐式初始化为null
。 这意味着您不能像在 C# 或 Java 中习惯的那样假设整数的默认值为零,并且您可能需要添加空检查。
有趣的是,甚至Null
也是一种类型,而null
一词指的是Null
的一个实例:
print(null.runtimeType); // prints Null
没有那么多数字类型
与熟悉的从 8 位到 64 位的有符号和无符号整数类型的分类不同,Dart 的主要整数类型只是int
,一个 64 位值。 (还有BigInt
用于非常大的数字。)
由于语言语法中没有字节数组,因此二进制文件内容可以作为整数列表处理,即List<Int>
。
如果您认为这一定是非常低效的,那么设计师已经想到了。 实际上,根据运行时使用的实际整数值,存在不同的内部表示。 如果运行时可以优化它并在未装箱模式下使用 CPU 寄存器,则运行时不会为int
对象分配堆内存。 此外,库byte_data
提供UInt8List
和一些其他优化的表示。
收藏品
集合和泛型很像我们习惯的。 需要注意的主要一点是没有固定大小的数组:只要使用数组就可以使用List
数据类型。
此外,还有对初始化三种集合类型的语法支持:
final a = [1, 2, 3]; // inferred type is List<int>, an array-like ordered collection final b = {1, 2, 3}; // inferred type is Set<int>, an unordered collection final c = {'a': 1, 'b': 2}; // inferred type is Map<string, int>, an unordered collection of name-value pairs
因此,在您将使用 Java 数组、 ArrayList
或Vector
的地方使用 Dart List
; 或 C# 数组或List
。 在您将使用 Java/C# HashSet
的地方使用Set
。 在您将使用 Java HashMap
或 C# Dictionary
的地方使用Map
。
3.动态和静态类型
在 JavaScript、Ruby 和 Python 等动态语言中,即使成员不存在,您也可以引用它们。 这是一个 JavaScript 示例:
var person = {}; // create an empty object person.name = 'alice'; // add a member to the object if (person.age < 21) { // refer to a property that is not in the object // ... }
如果你运行它, person.age
将是undefined
,但它仍然运行。
同样,您可以在 JavaScript 中更改变量的类型:
var a = 1; // a is a number a = 'one'; // a is now a string
相比之下,在 Java 中,您不能编写像上面这样的代码,因为编译器需要知道类型,并且它会检查所有操作是否合法——即使您使用 var 关键字:
var b = 1; // a is an int // b = "one"; // not allowed in Java
Java 只允许您使用静态类型进行编码。 (您可以使用自省来执行一些动态行为,但它不是语法的直接部分。)JavaScript 和其他一些纯动态语言只允许您使用动态类型进行编码。
Dart 语言同时允许:
// dart dynamic a = 1; // a is an int - dynamic typing a = 'one'; // a is now a string a.foo(); // we can call a function on a dynamic object, to be resolved at run time var b = 1; // b is an int - static typing // b = 'one'; // not allowed in Dart
Dart 具有伪类型dynamic
,它导致所有类型逻辑在运行时处理。 调用a.foo()
的尝试不会打扰静态分析器并且代码会运行,但它会在运行时失败,因为没有这样的方法。
C#本来就跟Java很像,后来加入了动态支持,所以Dart和C#在这方面差不多。
4. 功能
函数声明语法
Dart 中的函数语法比 C# 或 Java 中的更轻松、更有趣。 语法是以下任何一种:
// functions as declarations return-type name (parameters) {body} return-type name (parameters) => expression; // function expressions (assignable to variables, etc.) (parameters) {body} (parameters) => expression
例如:
void printFoo() { print('foo'); }; String embellish(String s) => s.toUpperCase() + '!!'; var printFoo = () { print('foo'); }; var embellish = (String s) => s.toUpperCase() + '!!';
参数传递
由于一切都是对象,包括int
和String
等原语,参数传递可能会令人困惑。 虽然没有像 C# 中那样传递ref
参数,但一切都是通过引用传递的,并且函数不能更改调用者的引用。 由于对象在传递给函数时不会被克隆,因此函数可能会更改对象的属性。 然而,像 int 和 String 这样的原语的区别实际上是没有实际意义的,因为这些类型是不可变的。

var id = 1; var name = 'alice'; var client = Client(); void foo(int id, String name, Client client) { id = 2; // local var points to different int instance name = 'bob'; // local var points to different String instance client.State = 'AK'; // property of caller's object is changed } foo(id, name, client); // id == 1, name == 'alice', client.State == 'AK'
可选参数
如果您在 C# 或 Java 世界中,您可能已经被这些令人困惑的重载方法所困扰:
// java void foo(string arg1) {...} void foo(int arg1, string arg2) {...} void foo(string arg1, Client arg2) {...} // call site: foo(clientId, input3); // confusing! too easy to misread which overload it is calling
或者使用 C# 可选参数,还有另一种混淆:
// c# void Foo(string arg1, int arg2 = 0) {...} void Foo(string arg1, int arg3 = 0, int arg2 = 0) {...} // call site: Foo("alice", 7); // legal but confusing! too easy to misread which overload it is calling and which parameter binds to argument 7 Foo("alice", arg2: 9); // better
C# 不需要在调用点命名可选参数,因此使用可选参数重构方法可能很危险。 如果重构后某些调用站点恰好是合法的,编译器将不会捕获它们。
Dart 有一种更安全且非常灵活的方式。 首先,不支持重载方法。 相反,有两种方法可以处理可选参数:
// positional optional parameters void foo(string arg1, [int arg2 = 0, int arg3 = 0]) {...} // call site for positional optional parameters foo('alice'); // legal foo('alice', 12); // legal foo('alice', 12, 13); // legal // named optional parameters void bar(string arg1, {int arg2 = 0, int arg3 = 0}) {...} bar('alice'); // legal bar('alice', arg3: 12); // legal bar('alice', arg3: 12, arg2: 13); // legal; sequence can vary and names are required
您不能在同一个函数声明中同时使用这两种样式。
async
关键字位置
C# 的async
关键字有一个令人困惑的位置:
Task<int> Foo() {...} async Task<int> Foo() {...}
这意味着函数签名是异步的,但实际上只有函数实现是异步的。 上述任何一个签名都将是此接口的有效实现:
interface ICanFoo { Task<int> Foo(); }
在 Dart 语言中, async
处于更合乎逻辑的位置,表示实现是异步的:
Future<int> foo() async {...}
范围和闭包
与 C# 和 Java 一样,Dart 是词法范围的。 这意味着在块中声明的变量在块末尾超出范围。 所以 Dart 以同样的方式处理闭包。
属性语法
Java 普及了属性获取/设置模式,但该语言没有任何特殊语法:
// java private String clientName; public String getClientName() { return clientName; } public void setClientName(String value}{ clientName = value; }
C# 有它的语法:
// c# private string clientName; public string ClientName { get { return clientName; } set { clientName = value; } }
Dart 的语法支持属性略有不同:
// dart string _clientName; string get ClientName => _clientName; string set ClientName(string s) { _clientName = s; }
5. 构造函数
Dart 构造函数比 C# 或 Java 具有更多的灵活性。 一个不错的特性是能够在同一个类中命名不同的构造函数:
class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }
您可以仅使用类名调用默认构造函数: var c = Client();
在调用构造函数主体之前初始化实例成员有两种简写:
class Client { String _code; String _name; Client(String this._name) // "this" shorthand for assigning parameter to instance member : _code = _name.toUpper() { // special out-of-body place for initializing // body } }
构造函数可以运行超类构造函数并重定向到同一类中的其他构造函数:
Foo.constructor1(int x) : this(x); // redirect to the default ctor in same class; no body allowed Foo.constructor2(int x) : super.plain(x) {...} // call base class named ctor, then run this body Foo.constructor3(int x) : _b = x + 1 : super.plain(x) {...} // initialize _b, then call base class ctor, then run this body
在 Java 和 C# 中调用同一类中的其他构造函数的构造函数在它们都有实现时会让人感到困惑。 在 Dart 中,重定向构造函数不能有主体的限制迫使程序员使构造函数的层更清晰。
还有一个factory
关键字允许像构造函数一样使用函数,但实现只是一个常规函数。 您可以使用它来返回缓存实例或派生类型的实例:
class Shape { factory Shape(int nsides) { if (nsides == 4) return Square(); // etc. } } var s = Shape(4);
6.修饰符
在 Java 和 C# 中,我们有访问修饰符,例如private
、 protected
和public
。 在 Dart 中,这被大大简化了:如果成员名称以下划线开头,它在包内的任何地方都可见(包括从其他类中)并且对外部调用者隐藏; 否则,它随处可见。 没有像private
这样的关键字来表示可见性。
另一种修饰符控制可变性:关键字final
和const
用于此目的,但它们的含义不同:
var a = 1; // a is variable, and can be reassigned later final b = a + 1; // b is a runtime constant, and can only be assigned once const c = 3; // c is a compile-time constant // const d = a + 2; // not allowed because a+2 cannot be resolved at compile time
7. 类层次结构
Dart 语言支持接口、类和一种多重继承。 但是,没有interface
关键字; 相反,所有的类也是接口,所以你可以定义一个abstract
类然后实现它:
abstract class HasDesk { bool isDeskMessy(); // no implementation here } class Employee implements HasDesk { bool isDeskMessy() { ...} // must be implemented here }
多重继承是通过使用extends
关键字和其他类使用with
关键字来完成的:
class Employee extends Person with Salaried implements HasDesk {...}
在此声明中, Employee
类派生自Person
和Salaried
,但Person
是主要超类, Salaried
是 mixin(次要超类)。
8. 运营商
有一些我们不习惯的有趣且有用的 Dart 运算符。
级联允许您在任何东西上使用链接模式:
emp ..name = 'Alice' ..supervisor = 'Zoltron' ..hire();
扩展运算符允许将集合视为初始值设定项中的元素列表:
var smallList = [1, 2]; var bigList = [0, ...smallList, 3, 4]; // [0, 1, 2, 3, 4]
9. 线程
Dart 没有线程,因此可以转译为 JavaScript。 相反,它有“隔离”,它们更像是单独的进程,因为它们不能共享内存。 由于多线程编程非常容易出错,因此这种安全性被视为 Dart 的优势之一。 要在隔离之间进行通信,您需要在隔离之间传输数据; 接收到的对象被复制到接收隔离的内存空间中。
使用 Dart 语言开发:你可以做到!
如果您是 C# 或 Java 开发人员,那么您已经知道的内容将帮助您快速学习 Dart 语言,因为它的设计目的是让您熟悉。 为此,我们整理了一份 Dart 备忘单 PDF 供您参考,特别关注与 C# 和 Java 等价物的重要区别:
本文中显示的差异与您现有的知识相结合,将帮助您在使用 Dart 的第一天或两天内提高工作效率。 快乐编码!