【Dart】プログラミング言語 Dart のざっくりとしたまとめ

Dart/Flutter

Dart は Google によって開発されたプログラミング言語です。2011年末に発表された、かなり新しい言語と言えます。
2018年2月に Dart 2 が発表され(すごく最近ですね!)、null安全など新しい概念を取り入れるといった機能強化が行われました。

最近は Flutter の登場により急速に注目度が高まっています。
Flutter はクロスプラットフォームの GUI アプリケーションフレームワークで、1つのコードで多くのプラットフォーム(iOS / Android / Web / Windows / Mac / Linux)に対応する GUI アプリケーションを作成することができます
この Flutter で採用されているプログラミング言語が Dart というわけです。

この記事では、Flutter についての説明はここまでとして、プログラミング言語 Dart についてのざっくりとしたまとめを目的とします。
主に、他のプログラミング言語を習得済みの人どういったポイントで Dart を理解すれば良いのか、ということをまとめます。

筆者自身も最近 Dart を学び始めたため、何か間違いなどあればコメントで指摘くださると幸いです。

Dart はどんな雰囲気の言語か

  • 見た目 Java っぽい
    • クラスの継承に extends キーワードを用いる
    • final というキーワードが存在する
  • Java ほど「固く」ない
    • 必ずクラスを用意する仕様ではなく、メソッドや変数をトップレベルに宣言できる
    • そもそもエントリポイントがトップレベルの main() メソッドという仕様になっている

公式ドキュメントが分かりやすい

こんな記事を書いていますが、実は Dart 公式に「他のプログラミング言語を習得済みの人向け」ドキュメントがあります
普通に分かりやすいので、英語に抵抗感のない人はこれを読むのが確実です。
(蛇足ですが、自動翻訳で読むのは間違った知識を習得する可能性があるため、オススメしません)

A tour of the Dart language
A tour of all the major Dart language features.

特徴的な機能をざっくりとまとめ

では、本題のざっくりとしたまとめです。
他の言語と比べて特徴的な機能のみピックアップしているので、言語仕様のすべてを説明しているわけではない点に注意してください。

基本コンセプト

  • 変数に代入して扱えるすべてのものが Object を継承しています。
    • 数・関数・null も例外ではありません。
  • Dart 2.12.0 からnull安全に対応しました。
    • ざっくり言うとnull参照例外をコンパイル時に完全検出できる仕組みです。
    • 筆者の感覚で言うと、C# など他の言語でアプリが落ちる原因の大半はnull参照です。
  • public / protected / private のキーワードは存在しません
    • アンダースコア(_)から始まる名称が、「そのファイル内でのみ参照できる」ことを意味します。

Dart には public / protected / private という概念は存在しません。

たとえば、日本語サイトでよく見かける「アンダースコアから始まるメンバ変数は private を意味する」という説明は、間違いです
正しくは、「同一ファイルでのみ参照可能になる」です。

変数

  • var で型推論できます。
  • Dart 2.12.0 から late キーワードが追加されました。
    • 遅延評価(参照時に評価)を行わせたい変数に対して付加できます。
    • メンバ変数の初期値として他のメンバを参照することができます。
    • 例えばメンバ変数として late String temperature = _readThermometer(); のように書くと、temperature が参照されたときに初めて _readThermometer() が実行されます。
  • null許容型でないメンバ変数は必ず初期化される必要があります
    • ただし、late キーワードを付けた変数については初期化しないことを許容されます。
    • すなわち、実行時エラーが起きうるということなので、late キーワードの付加は慎重に行う必要があります。
  • 実行時定数として final、コンパイル時定数として const キーワードを付加できます。
    これら2つの違いについて詳しくは過去記事をご覧ください。

配列

  • 配列を扱いたい場合、List([])・Set({})・Map({})が選択肢になります。
    • List は配列的に扱いたい場合の最初の選択肢
    • Set は順序付けがなく、重複要素の無い List という認識で良さそうです。C# でいう HashSet。
    • Map は連想配列。C# でいう Dictionary。

関数の引数(パラメータ)

  • 名前付き引数
    • void enableFlags({bool? hoge, bool? fuga}) { } という形式で名前付き引数を持つ関数を宣言できる
    • 呼び出し時に enableFlags(hoge: true, fuga: false); のように名前付きで引数を渡す
    • 必須にしたい名前付き引数は required キーワードを付加する。
  • オプション引数
    • void say(String from, String msg, [String? device]) { } という形式でオプション引数を持つ関数を宣言できる(この例だと device がオプション引数)

無名関数(ラムダ式・クロージャ)

  • var loudify = (msg) => ‘!!! ${msg.toUpperCase()} !!!’;
    のような形式
  • さらっと書きましたが、文字列はシングルクォーテーション・ダブルクォーテーションどちらも利用可能で、${} で変数を文字列形式に展開します

型テストオペレータ

  • as / is / is! が利用可能。意味は C# と同様。
  • is! は is の否定

Cascade

非常に珍しい言語機能として、Cascade を紹介します。

cascade の意味は「滝状の流れ」で、日本語では「カスケード」と書かれることが多いようです。
ただし、発音は「キャスケード」の方が近いように聞こえます。

..」というダブルドットによって Cascade を表現します。

var paint = Paint()
  ..color = Colors.black
  ..strokeCap = StrokeCap.round
  ..strokeWidth = 5.0;

上記の例でやりたいことは分かると思いますが、直前の式の戻り値にアクセスする記法です。
この例は以下のように書いたものと同じです。

var paint = Paint();
paint.color = Colors.black;
paint.strokeCap = StrokeCap.round;
paint.strokeWidth = 5.0;

Dart 2.12.0 から?.. というnullチェック記法も追加されたようです。

Cascade の戻り値は最初の式の戻り値と等しいです。
たとえば、「インスタンスを生成してから初期化メソッドを呼び、インスタンスを返したい」といったケースで有用です。

class Db {
  final String name;
  
  Db(this.name);
  
  void init() {
    // ...
  }
}

void main() {
  Db instance = Db('test')..init();
  // 以下と等しい
  // Db instance = Db('test');
  // instance.init();
}

例外ハンドル

catch の第2引数にスタックトレースを取れます。

try {
  // ...
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}

また、catch 内で外部に例外を伝播させたい場合、rethrow; という記法が使えます。

インスタンスの生成(コンストラクタ)

Dart 2 から new キーワードは不要になりました。
Dart のコンストラクタは非常に珍しい機能が多く搭載されています。

初期化子リスト

C++ のような初期化子リストを利用できて、初期化子リストによってのみ final メンバ変数を初期化できます

class Point {
  final double x;
  final double y;
  final double distanceFromOrigin;

  Point(double x, double y)
      : x = x,
        y = y,
        distanceFromOrigin = sqrt(x * x + y * y);
}

名前付きコンストラクタ(named constructor)

Cascade と同じく珍しい言語機能だと思います。
Dart ではコンストラクタに名前を付けることができます。

class Point {
  final double x, y;
  
  Point.origin()
    : x = 0,
      y = 0;
}

void main() {
  final origin = Point.origin();
  print('${origin.x}, ${origin.y}');
}

たとえば、よく使われるデフォルトインスタンスを名前付きコンストラクタで宣言する、といった利用方法が考えられそうです。
ただし、インスタンスを使い回しているのではなく新しくインスタンスを生成しているという点に注意が必要です。
生成処理が重い場合や、大きなクラスに対しては不適切だと思います。

const コンストラクタ(constant constructor)

こちらも珍しい言語機能だと思います。

const コンストラクタを宣言することで、インスタンスをコンパイル時定数として生成することができます。
もちろん利用には厳し目の条件があり、すべてのメンバ変数が final である必要があります。

class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);

  final double x, y;

  const ImmutablePoint(this.x, this.y);
}

この例では ImmutablePoint.origin がコンパイル時定数になるということですね。

factory コンストラクタ(factory constructor)

こちらも珍しい言語機能です。概念としては以前からありましたが、言語機能として組み込まれているのは初めて見ました。

factory コンストラクタでは、新しいインスタンスを生成しません
そのため、自分でインスタンスを生成する処理を書く必要があります。

主な使い方は2通りで、シングルトンとして扱うファクトリパターンとして扱う(派生クラスを返す)というものが考えられます。
これ以外にも色々考えてみましたが、特別この機能を使う利点が見当たりませんでした。

class Logger {
  static final Map<String, Logger> _cache = <String, Logger>{};
  final String name;

  factory Logger(String name) {
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }

  Logger._internal(this.name);
}

これはシングルトンの亜種みたいな例ですが、コンストラクタで解決するのはなかなか新鮮です。

factory コンストラクタは使い所が非常に難しく、また、誤った(分かりにくい)使い方を招きやすい、正直言って微妙な機能かと考えています…。
名前が factory constructor なのに必ずしも construct しませんし。

ちなみに、2021年5月現在、Dart にはデストラクタは存在しないようです。
factory コンストラクタと仕様が噛み合わないので仕方なさそうですが…。

getter / setter

  • C# でいうプロパティ
  • double get right => left + width;
    set right(double value) => left = value – width;
    のように宣言する

インターフェース

Dart には「インターフェースを宣言する」というキーワードは存在せず、(抽象)クラスとして宣言します

インターフェースを実装するクラスは implements キーワードを利用します。
また、インターフェースなので多重実装も可能です。

class Person {
  // インターフェース(プロパティ)として扱われるが、同一ライブラリ(ざっくり言うと同一ファイル)でのみ参照可能
  final String _name;

  // コンストラクタなのでインターフェースとして扱われない
  Person(this._name);

  // インターフェースとして扱われる
  String greet(String who) => 'Hello, $who. I am $_name.';
}

class Impostor implements Person {
  get _name => '';
  String greet(String who) => 'Hi $who. Do you know who I am?';
}

クラスの継承

  • extends キーワードを使う
  • 多重継承は不可
  • オーバーライドするメソッドには @override キーワードを付加する

拡張メソッド

extension という特別なキーワードを使います。

拡張メソッド内では this によってインスタンスにアクセスできます。
たとえば、String クラスに対する拡張メソッドは以下のように宣言します。

extension NumberParsing on String {
  int parseInt() {
    return int.parse(this);
  }

  double parseDouble() {
    return double.parse(this);
  }
}

mixin

多重継承のようなものを実現する機能です。
イメージとしては、クラスの内容が他の mixin で拡張される、といったところでしょうか。

mixin 対象になるクラスは class の代わりに mixin というキーワードを使って宣言します

mixin Musical {
  // ...
}

mixin を実装するためには with というキーワードを使います。
必ず Object を継承したクラスに対して実装する必要があるようなので、継承先がない場合は extends Object を付加します。
implements (インターフェース)と同様に、多数の mixin を取り入れることができます。

class Maestro extends Person
    with Musical, Aggressive, Demented {
  // ...
}

特定のクラスに対する mixin として制約を掛けたい場合、mixin に対して on キーワードを利用できます。

class Musician {
  // ...
}
mixin MusicalPerformer on Musician {
  // ...
}
class SingerDancer extends Musician with MusicalPerformer {
  // ...
}

mixin は多重継承のようなことができるので、使い方に要注意といったところでしょうか。

ジェネリクス

  • class Foo<T> のように宣言
  • ジェネリクス型に制約を掛けたい場合、class Foo<T extends Bar> のように宣言

ライブラリの利用

  • Dart の組み込みライブラリを利用したい場合、import ‘dart:html’; のように書く
  • その他は import ‘package:test/test.dart’ のように書く
  • ライブラリ内の識別子にプレフィックスをつけたい場合、as キーワードを利用する。
    import ‘package:hoge/hoge.dart’ as hoge; のようにすると、hoge.dart 内のメソッドなどにアクセスする際、hoge.foo(); のように書かなければならなくなる。
  • ライブラリ内の一部を利用したい場合、show キーワードを利用する。
    import ‘package:hoge/hoge.dart’ show foo; のようにすると、hoge.dart 内の foo のみ利用可能になる。
  • 逆に、ライブラリ内の一部を利用したくない場合、hide キーワードを利用する。

Webアプリを考慮した、遅延ロードのための deferred as というキーワードもあるようですが、dart2js のみがサポートしており現状 Flutter はサポートされていないようです。今後に期待ですね。

非同期処理

  • async / await が基本
  • ループ対象が Stream を実装している場合、await for による非同期ループ処理を書ける

ジェネレータ

シーケンシャルな値(C# でいう IEnumerable)のジェネレータを実装する場合、ジェネレータが同期・非同期のどちらかで宣言方法が変わります。
同期ジェネレータの場合は Iterable を実装し、非同期ジェネレータの場合は Stream を実装します。

Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

ジェネレータ内で再帰する場合、yield* というキーワードを利用します。

Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

typedef(関数型エイリアス)

  • 関数型に対してエイリアスを設定できる
  • typedef Compare = int Function(Object a, Object b); のように宣言
  • 今のところ関数型に対してのみ typedef を利用できるが、将来的に拡張されるらしいです

ドキュメンテーションコメント

  • /// または /** から始まるコメントはドキュメンテーションコメントとして認識されます。

まとめ

ざっと見てみましたが、基本コンセプト変数の late 宣言コンストラクタCascademixin 辺りが注目すべきポイントかなぁ、と思います。

コメント

タイトルとURLをコピーしました