个性化阅读
专注于IT技术分析

Dart语言:Java和C#不够用时

点击下载

本文概述

早在2013年, Dart的官方1.0版本(与大多数Google产品一样)受到了广泛的关注, 但并不是每个人都像Google内部团队一样渴望使用Dart语言创建关键业务应用。五年后, 经过深思熟虑的Dart 2重建, Google似乎已经证明了对这种语言的承诺。的确, 今天, 它继续在开发人员(尤其是Java和C#资深人士)中赢得关注。

Dart编程语言之所以重要, 有几个原因:

  • 它具有两全其美的优点:它同时是一种编译的类型安全语言(如C#和Java)和一种脚本语言(如Python和JavaScript)。
  • 它可以转换为JavaScript以用作Web前端。
  • 它可以在所有内容上运行, 并可以编译为本地移动应用程序, 因此你几乎可以将其用于所有内容。
  • Dart在语法上类似于C#和Java, 因此学习起来很快。

我们那些来自C#或Java大型企业系统领域的人已经知道为什么类型安全性, 编译时错误和linter重要。我们中的许多人都不愿采用”脚本”语言, 以免失去习惯的所有结构, 速度, 准确性和可调试性。

但是随着Dart的开发, 我们不必放弃任何这些。我们可以用相同的语言编写一个移动应用程序, Web客户端和后端, 并获得我们仍然喜欢的有关Java和C#的所有东西!

为此, 让我们看一下C#或Java开发人员不熟悉的一些重要Dart语言示例, 最后将它们汇总为Dart语言PDF。

注意:本文仅介绍Dart2.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的软件包的布局示例:

  • 苹果/
    • pubspec.yaml-定义程序包名称, 依赖项和其他一些内容
    • lib /
      • apples.dart-进出口;这是包的任何使用者导入的文件
      • src /
        • seed.dart-其他所有代码
    • 是/
      • runapples.dart-包含主要功能, 即入口点(如果这是可运行的程序包或包括可运行的工具)

然后, 你可以导入整个程序包而不是单个文件:

import 'package:apples';

无关紧要的应用程序应始终作为软件包进行组织。这减轻了很多在每个引用文件中重复文件系统路径的麻烦。另外, 它们运行得更快。它还使在pub.dev上共享软件包变得容易, 其他开发人员可以在其中轻松获取自己的软件包。应用程序使用的软件包将导致源代码复制到文件系统, 因此你可以根据需要对这些软件包进行深度调试。

2.数据类型

在Dart的类型系统中, 需要注意的主要区别在于null, 数字类型, 集合和动态类型。

无处不在

来自C#或Java, 我们习惯于将基本类型或值类型与引用或对象类型区分开。实际上, 值类型是在堆栈或寄存器中分配的, 值的副本作为函数参数发送。而是在堆上分配引用类型, 并且仅将指向对象的指针作为函数参数发送。由于值类型始终占用内存, 因此, 值类型变量不能为null, 并且所有值类型成员都必须具有初始化值。

Dart消除了这种区别, 因为一切都是对象。所有类型最终都源自对象类型。因此, 这是合法的:

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#数组或列表。在将要使用Java / C#HashSet的地方使用Set。在使用Java HashMap或C#词典的地方使用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将是未定义的, 但无论如何仍将运行。

同样, 你可以在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具有伪类型动态特性, 可导致在运行时处理所有类型逻辑。尝试调用a.foo()不会打扰静态分析器, 并且代码将运行, 但是由于没有这种方法, 因此它将在运行时失败。

C#最初类似于Java, 后来又添加了动态支持, 因此Dart和C#在这方面大致相同。

4.功能

函数声明语法

与C#或Java中相比, Dart中的函数语法更轻松, 更有趣。语法是以下任何一种:

// 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

你不能在同一函数声明中同时使用这两种样式。

异步关键字排名

C#的async关键字的位置令人困惑:

Task<int> Foo() {...}
async Task<int> Foo() {...}

这意味着功能签名是异步的, 但实际上只有功能实现是异步的。以上任一签名将是此接口的有效实现:

interface ICanFoo {
    Task<int> Foo();
}

在Dart语言中, 异步处于更逻辑的位置, 表示实现是异步的:

Future<int> foo() async {...} 

范围和关闭

像C#和Java一样, Dart具有词法作用域。这意味着在块中声明的变量超出了块末尾的范围。因此Dart以相同的方式处理闭包。

属性语法

Java普及了属性get / set模式, 但该语言没有任何特殊语法:

// 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.构造函数

与C#或Java相比, Dart构造函数具有更大的灵活性。一个不错的功能是可以在同一类中命名不同的构造函数:

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语言支持接口, 类和一种多重继承。但是, 没有接口关键字。相反, 所有类也是接口, 因此你可以定义一个抽象类, 然后实现它:

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语言备忘单PDF

本文中显示的差异与你现有的知识相结合, 将帮助你在Dart的第一天或第二天内提高工作效率。编码愉快!

赞(0)
未经允许不得转载:srcmini » Dart语言:Java和C#不够用时

评论 抢沙发

评论前必须登录!