Dart 3を発表@Google I/O 2023

訳者 前書き

Michael, thank you for your willingness to allow me to translate!

このページは、Michael Thomsen(Product Manager working on Dart and Flutter.)さんによる「Announcing Dart 3」の日本語翻訳です。コンタクトを取りましたら、ありがたくも翻訳の許可をいただきました。

誤訳、誤字脱字等ありましたら、TwitterのDMにて、御連絡ください。より良い翻訳の提案も大歓迎です!

Dart 3での新機能、使いこなせるようになりたいですね!その前に、古いコード、直さなくっちゃ。

Dart 3を発表

100%健全なnullセーフティ。レコード、パターン、クラスモディファイア。そして、未来を覗き見る。

Google I/O 2023からこんにちは。本日(訳注: 2023/5/11)、マウンテンビューから生中継で、これまでで最大のDartのリリースとなるDart 3を発表します!Dart 3には、3つの大きな進化があります。1つ目は、100%健全なNULLの安全性を達成したことです。2つ目は、レコード、パターン、クラス修飾子に関する新しい言語機能を追加したことです。3つ目は、WasmコンパイルによるWeb用ネイティブコードでプラットフォームサポートを拡大する、未来のプレビューです。では、その詳細を見ていきましょう。

(訳者追記) 「sound null safety」を「健全なnullセーフティ」と訳してます。Flutterアプリのプログラム内でnullを使わない、もしくは管理下であることを、パッケージを含めて強制する、という意味合いです。要するに、今後Flutterアプリはnullでプログラムが落ちることはない、はず。

100%健全な nullセーフティ

この4年間で、私たちはDartを高速でポータブル、かつモダンな言語へと進化させてきました。そして今、Dart 3では、100%健全なnullセーフティとなりました!以前にも説明したように、既存の言語に健全な nullセーフティを追加したプログラミング言語は他にないと考えています。つまり、それは非常に長い道のりだったのです。

Dartの100%のnullセーフティにより、私たちは健全な型システムを手に入れることができました。ある型が、ある値がnullでないと言っている場合、その値は決してnullになり得ないと信じることができます。これにより、nullポインタ例外のような特定のクラスのコーディングエラーを回避することができます。また、コンパイラやランタイムは、nullの安全性がなければできない方法でコードを最適化できるようになります。この設計上の選択にはトレードオフがあります。移行は少し難しくなりましたが、私たちはDartにとって正しい選択をしたと信じています。

Dart 3への移行

pub.devのトップ1000パッケージの99%がnullセーフティをサポートしています!

このことから、ヌルサクに移行したパッケージやアプリの大部分は、Dart 3 で動作することが期待されます。ごく一部のケースでは、Dart 3 の関連する少量のクリーンアップが、一部のコードに影響を与える可能性があります。一部のレガシーコアライブラリ API が削除され (#34233#49529)、一部のツールが調整されました (#50707)。Dart 3 SDK への移行に問題がある場合は、Dart 3 への移行ガイドを参照してください。その他、合理化された新しいコアライブラリとツールをお楽しみください。

主要な言語機能 – レコード、パターン、クラス修飾子

Dart 3は、既存の言語を変更するだけではありません。重要な新機能が追加されているのです!レコード、パターン、そしてクラス修飾子です。

レコードによる構造化データの構築

従来、Dartの関数は1つの値しか返すことができませんでした。そのため、複数の値を返す必要がある関数は、マップやリストなどの他のデータ型にパッケージ化するか、値を保持できる新しいクラスを定義する必要がありました。型付きのないデータ構造を使うことで、型安全性が損なわれる。データを運ぶために新しいクラスを定義しなければならないのは、コーディングの過程で摩擦が生じます。複数の戻り値に対する言語リクエストは、私たちの4番目に高い評価を受けている課題です。

レコードを使えば、構造化されたデータを、すっきりとした構文で構築することができます。この関数を考えてみましょう。これは、JSON blobの名前と年齢を読み出し、その両方をレコードで返します:

(String, int) userInfo(Map<String, dynamic> json) { (json['name'])
  return (json['name'] as String, json['height'] as int);
}

これは、すべてのDart開発者にとって見慣れたものであるはずです。レコードは [‘Michael’, ‘Product Manager’] のようなリストリテラルのように見えますが、括弧の代わりに角括弧を使用しています。Dartでは、レコードは一般的な機能です。レコードは、関数の戻り値以外にも使用することができます。また、変数に格納したり、リストに入れたり、マップのキーとして使用したり、他のレコードを含むレコードを作成したりすることもできます。先の例でやったような無名フィールドも、(42, description: ‘Meaning of life’) のような名前付きフィールドも追加することができます。

レコードは値型であり、IDを持ちません。このため、コンパイラは場合によってはレコード・オブジェクトを完全に消去することができます。レコードには、自動的に定義される == 演算子と hashCode 関数も付属しています。詳しくは、レコードのドキュメントをご覧ください。

パターンとパターン・マッチによる構造化データの処理

レコードは、構造化データを構築する方法を簡略化します。これは、より正式な型階層を構築するためにクラスを使用することに取って代わるものではありません。ただ、別の選択肢を提供するだけです。いずれの場合も、構造化データを個々の要素に分解して扱いたいと思うかもしれません。そこで、パターン・マッチの出番となる。

パターンの基本形について考えてみましょう。次のレコードパターンは、レコードを2つの新しい変数nameとheightに分解する。これらの変数は、printの呼び出しなど、他の変数と同様に使用することができます:

var (String name, int height) = userInfo({'name': 'Michael', 'height': 180});
print('User $name is $height cm tall.');

リストやマップにも同様のパターンがあります。いずれも、アンダースコア・パターンで個々の要素をスキップすることができます:

var (String name, _) = userInfo(...);

パターンは、switch文の中で使用すると効果的です。Dartでは、当初からswitchのサポートが限定的でした。Dart 3では、switch文のパワーと表現力の幅を広げました。このような場合、パターン・マッチをサポートするようになりました。各ケースの末尾にbreakを追加する必要がなくなりました。また、ケースを組み合わせるための論理演算子もサポートしています。次の例では、文字コードを解析するswitch文が、すっきりしています:

switch (charCode) {
  case slash when nextCharCode == slash:
    skipComment();
  case slash || star || plus || minus:
    operator(charCode);
  case >= digit0 && <= digit9:
    number();
  default:
    invalid();
}

switch文は、それぞれのケースに対して1つ以上の文が必要な場合に、大きな助けとなります。場合によっては、ある値を計算したいだけということもあります。そのようなユースケースのために、非常に簡潔なswitch式を提供しています。これはswitch文に似ていますが、式用に細かく調整された別の構文を使っています。次のサンプル関数は、今日の平日の説明を計算するために、switch式の値を返します:

String describeDate(DateTime dt) =>
  switch (dt.weekday) {
      1 => '月曜日の憂鬱を感じているか'、
      6 || 7 => '週末を楽しもう!'、
      _ => '頑張れ'.
  };

パターンの強力な特徴は、「網羅性」をチェックする機能です。この機能は、スイッチがすべての可能なケースを処理することを保証します。先ほどの例では、weekdayというint型の可能な値をすべて処理しています。特定の値167に対するマッチステートメントを組み合わせ、残りのケースにはデフォルトのケース_を使用することで、可能な値をすべて使い切っています。クラス階層のようなユーザー定義のデータ階層でこのチェックを有効にするには、次の例のようにクラス階層の最上位で新しいsealed修飾子を使用します:

sealed class Animal { ... }.
class Cow extends Animal { ... }
class Sheep extends Animal { ... }
class Pig extends Animal { ... }
String whatDoesItSay(Animal a) =>
    switch (a) { Cow c => '$c says moo', Sheep s => '$s says baa' };

これは次のようなエラーを返し、最後のサブタイプであるPigを処理し損ねたことを警告しています:

line 6 • The type 'Animal' is not exhaustively matched by the switch cases
since it doesn't match 'Pig()'.
(訳)
line 6 - タイプ 'Animal' は、スイッチのケースで完全にはマッチしません。
'Pig()'にマッチしないためです。

最後に、if文はパターンも使うことができます。次の例では、JSONマップを再構築するために、マップパターンに対するifケースマッチングを使用しています。その中で、定数値(‘name’‘Michael’ などの文字列)と型検査パターンint h に対してマッチングして、JSON値を読み出しています。パターンマッチが失敗した場合、Dartはelse文を実行する。

final json = {'name': 'Michael', 'height': 180};
// マイケルの身長を検索する。
if (json case {'name': 'Michael', 'height': int h}) { // マイケルの身長を求める。
  print('マイケルの身長は$h cmです。'); 
} else { 
  print('Error: json contains no height info for Michael!');
}

これは、パターンを使ってできることのほんの一例に過ぎません。私たちは、パターンがすべてのDartコードに浸透していくことを信じています。もっと詳しく知りたい方は、パターン・ドキュメントパターン・コードラボをご覧ください。

クラス修飾子によるクラスへのきめ細かなアクセス制御

Dart 3の言語機能の3つ目は、クラス修飾子です。すべてのDart開発者が使用することを想定しているレコードやパターンとは異なり、これはパワーユーザー向けの機能です。これは、大規模なAPIサーフェイスを作成するDart開発者やエンタープライズクラスのアプリケーションを構築する開発者のニーズに対応するものです。

クラス修飾子は、API作成者が特定の機能のセットのみをサポートすることを可能にします。しかし、デフォルトは変更されません。私たちは、Dartをシンプルで親しみやすいものにしたいと考えています。そのため、以前のように、以下の例に示すように、通常のクラスを構築、拡張、実装することができます:

class Vehicle {
  String make; String model;
  void moveForward(int meters) { … }
}
// Construct.
var myCar = Vehicle(make: 'Ford', model: 'T',);
// Extend.
class Car extends Vehicle {
  int passengers;
}
// Implement.
class MockVehicle implements Vehicle {
  @override void moveForward …
}

クラス修飾語句は、これに制限を加えることをサポートします。いくつかの使用例を考えてみましょう:

  • interface class を使うと、他の人が実装するための契約を定義することができます。インターフェイスクラスは拡張できません。

  • base class では、そのクラスのインターフェースを実装する代わりに、そのクラスのすべてのサブタイプを継承することを保証することができます。これにより、すべてのインスタンスでプライベートメソッドを利用できるようになります。

  • final class を使用すると、自分のライブラリ以外のサブクラスが存在しないように、型階層を閉じることができます。サンプルの利点として、APIの所有者は、APIの消費者の変更を壊すリスクを冒すことなく、新しいメンバーを追加することができます。

詳しくは、新しいクラス修飾子のドキュメントを参照してください。

(訳者メモ) 「abstract class」のabstractみたいな感じでinterface、base、final を使用する

将来への展望

Dart 3は、現在使用できる新機能の面で大きく前進しているだけではありません。私たちは、次に何が起こるかを予見することもできます。

Dart言語

レコード、パターン、クラス修飾子は非常に大きな新機能なので、その設計に改善できる部分がある可能性があります。今後も皆様からのフィードバックを監視し、Dart 3以降のマイナーリリースで更新が必要かどうかを確認します。

また、移行コストをかけずに開発者の生産性を向上させることに重点を置いた、まったく壊れない、より小さなインクリメンタルな機能についても検討しています。例えば、既存の型をコストゼロの「ラッパー」で包むinline class や、いくつかのフィールドと1次コンストラクタを持つクラスを定義するための、より簡潔な構文を導入した primary constructorsがあります。

マクロ(メタプログラミングとも呼ばれる)については、以前にも紹介しました。特に、JSON(および類似のもの)のデシリアライゼーションをより良くするためと、データクラスを可能にするために、このことに注目しています。メタプログラミングの規模と固有のリスクを考慮し、私たちは非常に徹底的なアプローチをとっています。したがって、設計の最終決定を行うための具体的なスケジュールはありません。

ネイティブの相互運用

モバイルやデスクトップのアプリは通常、通知、支払い、電話の位置情報の取得など、ネイティブプラットフォームが提供する多くのAPIに依存しています。従来Flutterでは、これらのAPIにアクセスするにはプラグインを作成する必要があり、API用のDartコードと、実装を提供するためのプラットフォーム固有のコードの両方を書く必要がありました。

私たちはすでにdart:ffiでCライブラリにコンパイルされたコードとの相互運用をサポートしています。現在、これを拡張して、AndroidではJavaとKotlinの相互接続を、iOS/macOSではObjective CとSwiftの相互接続をサポートするように取り組んでいます。Androidの相互運用の紹介については、新しいGoogle I/O 23 Android interoperabilityビデオをご覧ください。

WebAssemblyへのコンパイル – ネイティブコードでウェブをターゲットにする

WebAssembly(略称:Wasm)は、すべてのモダンブラウザにおいてプラットフォームニュートラルなバイナリ命令フォーマットとして成熟度を高めています。Flutterフレームワークはしばらく前からWasmを使用しています。C++で書かれたSKIAグラフィックスレンダリングエンジンを、Wasmコンパイルされたモジュールを介してブラウザに提供する方法です。私たちは長い間、Wasmを使ってDartのコードを展開することに興味を持っていましたが、阻まれてきました。Dartは、他の多くのオブジェクト指向言語と同様に、ガベージコレクションを使用します。過去1年間、私たちはWasmエコシステムのいくつかのチームと協力して、新しいWasmGC機能をWebAssembly標準に追加しました。これは現在、ChromiumブラウザとFirefoxブラウザでほぼ安定した状態になっています。

DartをWasmモジュールにコンパイルする私たちの作業には、Webアプリのための2つのハイレベルな目標があります:

ロード時間:ブラウザがより速くロードできるようにWasmでデプロイメントペイロードを配信し、ユーザーがWebアプリと対話できるようになるまでの時間を改善することを期待しています。

パフォーマンス: JavaScriptを使用したWebアプリは、優れたパフォーマンスを実現するためにジャストインタイムコンパイルが必要です。Wasmのモジュールは、より低レベルでマシンコードに近いため、ジャンクの少ない高いパフォーマンスと安定したフレームレートを実現できると考えています。

意味的な一貫性: Dartは、サポートするプラットフォーム間で高い一貫性を持っていることを誇りにしています。しかし、ウェブ上では、これにはいくつかの例外があります。例えば、Dartウェブでは現在、数字の表現方法が異なっています。Wasmモジュールを使用すれば、他のネイティブターゲットと同様のセマンティクスを持つ「ネイティブ」プラットフォームのようにウェブを扱うことができるようになります。

本日、DartからWasmへのコンパイルの最初のプレビューを発表できることを嬉しく思います!私たちの最初の焦点は、Flutterのウェブサポートです。まだ初期段階であり、完成させなければならないことがたくさんありますが、私たちと同じように興奮するかどうか、ぜひ試してみてください。

クロージング

最後までお読みいただき、ありがとうございました。この記事で、今日からスタンドアロンのDart SDKFlutter 3.10 SDKの両方で利用できるDart 3に興味を持っていただけたなら幸いです。

私たちは、健全なnullセーフティ、コアライブラリとツールのクリーンアップなど、Dart言語の大規模なオーバーホールを完了しました。Dartをより表現豊かにし、レコードやパターンをより鮮明にする新しい言語機能が大きく追加されています。大規模なAPIサーフェスでは、クラスモディファイアによって詳細な制御が可能になります。また、WebAssemblyのサポートが開始されるなど、将来のプレビューも含まれています。

これらの機能により、Dart 3は私たちの長期的なビジョンを示していると考えています: あらゆるプラットフォームで高速なアプリケーションを構築するための最も生産性の高いプログラミング言語を構築することです。皆様にもそう思っていただけると幸いです!

元記事の帰属

この記事は Michael Thomsen氏 の「 Announcing Dart 3」 を翻訳したものです。元の記事はクリエイティブ・コモンズ(属性表示)ライセンスの下で公開されています。元の記事へのリンク

This article is a translation of “Announcing Dart 3” by Michael Thomsen. The original article is published under a Creative Commons Attribution License. Link to original article)