Dart言語:JavaとC#が十分にシャープでない場合

公開: 2022-03-11

2013年にさかのぼると、Dartの公式1.0リリースは、ほとんどのGoogle製品と同様に、ある程度の報道を受けましたが、誰もがDart言語でビジネスクリティカルなアプリを作成することにGoogleの内部チームほど熱心だったわけではありません。 5年後のDart2のよく考えられた再構築により、Googleは言語へのコミットメントを証明したようです。 実際、今日では、開発者、特にJavaとC#のベテランの間で注目を集め続けています。

Dartプログラミング言語は、いくつかの理由で重要です。

  • コンパイルされたタイプセーフな言語(C#やJavaなど)とスクリプト言語(PythonやJavaScriptなど)の両方の長所があります。
  • これは、Webフロントエンドとして使用するためにJavaScriptに変換されます。
  • すべてで実行され、ネイティブモバイルアプリにコンパイルされるため、ほぼすべての用途に使用できます。
  • Dartは構文がC#やJavaに似ているため、すぐに習得できます。

大規模なエンタープライズシステムのC#またはJavaの世界の人々は、型の安全性、コンパイル時のエラー、およびリンターが重要である理由をすでに知っています。 私たちの多くは、私たちが慣れているすべての構造、速度、正確さ、およびデバッグ可能性を失うことを恐れて、「スクリプト」言語を採用することを躊躇しています。

しかし、ダートの開発では、それをあきらめる必要はありません。 モバイルアプリ、Webクライアント、バックエンドを同じ言語で記述でき、JavaとC#について今でも気に入っているものをすべて手に入れることができます。

そのために、C#またはJava開発者にとって新しいDart言語の主要な例をいくつか見ていきましょう。これらは、最後にDart言語のPDFにまとめられています。

注:この記事では、Dart2.xについてのみ説明します。 バージョン1.xは「完全に調理」されていませんでした。特に、型システムは必須(C#やJavaなど)ではなく、助言(TypeScriptなど)でした。

1.コード編成

まず、最も重要な違いの1つである、コードファイルの編成方法と参照方法について説明します。

ソースファイル、スコープ、名前空間、およびインポート

C#では、クラスのコレクションがアセンブリにコンパイルされます。 各クラスには名前空間があり、多くの場合、名前空間はファイルシステム内のソースコードの編成を反映していますが、最終的に、アセンブリはソースコードファイルの場所に関する情報を保持しません。

Javaでは、ソースファイルはパッケージの一部であり、名前空間は通常ファイルシステムの場所に準拠していますが、最終的には、パッケージは単なるクラスのコレクションです。

したがって、どちらの言語にも、ソースコードをファイルシステムからある程度独立させる方法があります。

対照的に、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から来て、参照型またはオブジェクト型とは異なるプリミティブ型または型に慣れています。 実際には、値の型はスタックまたはレジスタに割り当てられ、値のコピーが関数パラメータとして送信されます。 代わりに、参照型がヒープに割り当てられ、オブジェクトへのポインターのみが関数パラメーターとして送信されます。 値型は常にメモリを占有するため、値型変数をnullにすることはできず、すべての値型メンバーは初期化された値を持っている必要があります。

すべてがオブジェクトであるため、ダートはその区別を排除します。 すべての型は、最終的にObject型から派生します。 したがって、これは合法です。

 int i = null;

実際、すべてのプリミティブは暗黙的にnullに初期化されます。 これは、C#またはJavaで慣れているように、整数のデフォルト値がゼロであると想定できないことを意味し、nullチェックを追加する必要がある場合があります。

興味深いことに、 Nullでさえ型であり、 nullという単語はNullのインスタンスを指します。

 print(null.runtimeType); // prints Null

数値タイプはそれほど多くありません

符号付きおよび符号なしのフレーバーを持つ8〜64ビットの整数型のよく知られた品揃えとは異なり、Dartの主な整数型はちょうどint 、64ビット値です。 (非常に大きな数のBigIntもあります。)

言語構文の一部としてバイト配列がないため、バイナリファイルの内容は整数のリスト、つまりList<Int>として処理できます。

これがひどく非効率的であるに違いないとあなたが考えているなら、デザイナーはすでにそれを考えました。 実際には、実行時に使用される実際の整数値に応じて、さまざまな内部表現があります。 ランタイムは、それを最適化し、ボックス化されていないモードでCPUレジスタを使用できる場合、 intオブジェクトにヒープメモリを割り当てません。 また、ライブラリbyte_dataは、 UInt8Listおよびその他の最適化された表現を提供します。

コレクション

コレクションとジェネリックは、私たちが慣れているものとよく似ています。 注意すべき主な点は、固定サイズの配列がないことです。配列を使用する場合は、 Listデータ型を使用するだけです。

また、3つのコレクションタイプを初期化するための構文サポートがあります。

 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.ageundefinedになりますが、とにかく実行されます。

同様に、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#では、呼び出しサイトでオプションの引数に名前を付ける必要がないため、オプションのパラメーターを使用したメソッドのリファクタリングは危険な場合があります。 リファクタリング後に一部の呼び出しサイトが合法である場合、コンパイラはそれらをキャッチしません。

ダートには、より安全で非常に柔軟な方法があります。 まず第一に、オーバーロードされたメソッドはサポートされていません。 代わりに、オプションのパラメータを処理する2つの方法があります。

 // 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は字句スコープです。 これは、ブロックで宣言された変数がブロックの最後でスコープ外になることを意味します。 したがって、ダートはクロージャーを同じように処理します。

プロパティ構文

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.コンストラクター

Dartコンストラクターは、C#やJavaよりもかなり柔軟性があります。 1つの優れた機能は、同じクラス内の異なるコンストラクターに名前を付ける機能です。

 class Point { Point(double x, double y) {...} // default ctor Point.asPolar(double angle, double r) {...} // named ctor }

クラス名だけでデフォルトのコンストラクターを呼び出すことができますvar c = Client();

コンストラクター本体が呼び出される前にインスタンスメンバーを初期化するための2種類の省略形があります。

 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はミックスイン(セカンダリスーパークラス)です。

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の利点の1つと見なされています。 分離株間で通信するには、分離株間でデータをストリーミングする必要があります。 受信したオブジェクトは、受信側の分離のメモリ空間にコピーされます。

Dart言語で開発する:これを行うことができます!

C#またはJava開発者の場合、Dart言語は使い慣れているように設計されているため、すでに知っていることはDart言語をすばやく習得するのに役立ちます。 そのために、参考のためにDartのチートシートPDFをまとめました。特に、C#やJavaの同等のものとの重要な違いに焦点を当てています。

Dart言語のチートシートPDF

この記事に示されている違いと既存の知識を組み合わせると、ダーツの最初の1日か2日で生産性を高めることができます。 ハッピーコーディング!

関連:ハイブリッドパワー:Flutterの利点と利点