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 數組、 ArrayListVector的地方使用 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() + '!!';

參數傳遞

由於一切都是對象,包括intString等原語,參數傳遞可能會令人困惑。 雖然沒有像 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# 中,我們有訪問修飾符,例如privateprotectedpublic 。 在 Dart 中,這被大大簡化了:如果成員名稱以下劃線開頭,它在包內的任何地方都可見(包括從其他類中)並且對外部調用者隱藏; 否則,它隨處可見。 沒有像private這樣的關鍵字來表示可見性。

另一種修飾符控制可變性:關鍵字finalconst用於此目的,但它們的含義不同:

 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類派生自PersonSalaried ,但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 的第一天或兩天內提高工作效率。 快樂編碼!

相關:混合動力:Flutter 的優點和好處