こんにちは、教育系エンジニアのひらまつ(@hiramatsuu)です。
ひらまつの簡単な自己紹介
書籍「ゼロからわかる Linuxコマンド200本ノック(技術評論社)」の著者。Udemy受講者8万人。
プログラミング教育をメインに活動するエンジニアとして、動画教材の作成・技術書の執筆・学習アプリの開発などを行なっています(詳しくはこちら)。
本記事では、Dart・Flutterにおける、メタデータアノテーション(metadata annotation)という機能について解説していきます。
メタデータアノテーションを曖昧な理解で使っているという方は、ぜひ読んでみてください。
metadata annotationとは
- metadata annotation(メタデータアノテーション)を使うことで、コードに関する追加の情報(metadata)を与えることができるようになる。
- annotation(アノテーション)は、「注釈」の意味。コードにつける注釈がアノテーション。
- コメントと比較すると、アノテーションとは何かが理解しやすい。すなわち、コメントは「人間」への情報を付加する場所であるのに対して、アノテーションはコンパイラや静的解析ツールなど、「ツール」への情報を付加する場所。1
- メタデータアノテーションは「@」から始まり、後にはコンパイル時定数(compile-time constant)やconstantコンストラクタ(constant constructor)が続く。
- 例えば、
@Deprecated
アノテーションを廃止予定・非推奨のクラスにつけることで、それを静的解析ツール(Dart analyzer)2が解釈して、ユーザーがそのクラスを利用した時に警告を出せるようになる。より詳しくは後述。 - Dartにおいてアノテーションは、メタデータアノテーションだけでなく、type annotation(型アノテーション)などもあるが、共通するのはツールへ情報を与える目的で使用されているということ。
- 型アノテーションは、以下のように、変数などの型を指定するもの。型アノテーションをつけることで、静的解析ツールに型を伝えることができるので、静的解析ツールのサポートを受けやすくなる。Dartでは常に、型アノテーションをつける必要がある。3
int foo = 10; // 「var foo = 10」としない
final Bar bar = Bar();
String baz = 'hello';
const int quux = 20;
dart:coreに含まれる4つのメタデータアノテーション
dart:coreライブラリに含まれるメタデータアノテーションとしては、以下の4つがある。(「dart:coreライブラリに含まれる」ということは、言い換えれば「インポート不要で、あらゆるDartコードで使える」ということ。4)
@Deprecated / @deprecated
- どちらも、機能が廃止予定・非推奨(deprecated)であることを示すアノテーション。@Deprecated / @deprecatedのいずれかのアノテーションがついた機能を、ユーザーが使用すると、警告が表示されるようになる。
- 廃止予定の機能を使用している人に、「現在はうまく動いていても、将来的にはうまく動作しなくなるため、使用をやめる必要がある」ことを知らせる役割を担う。
- 「廃止予定の機能」と書いたが「機能」のサイズは、ライブラリ全体でも良いし、1つのパラメータでも良い。APIの任意の部分を、deprecatedとして指定できる。
- @Deprecatedと@deprecatedの違いは、メッセージを付加できるかどうか。@Deprecatedを使えば、いつ廃止されるのか?どのように対応すれば良いのか?などの付加情報を伝えることができるが、@deprecatedでは、機能が次のリリースで廃止されるという情報以上を伝えることは出来ない。
- 言い換えれば、@deprecatedアノテーションは、次のリリースまでの間、移行の指示なしで非推奨にするためのアノテーション。
- なので、追加のメッセージが必要ないなら、@Deprecatedではなく@deprecatedを使う、という方針になるが、基本的には常に@Deprecatedでメッセージをつけることが推奨されている。
- このアノテーションは、アノテーションをつけた機能の「部分」にも遷移的に適用される。つまりは、以下のようになる。
- ライブラリが廃止予定ならば、ライブラリに含まれるすべての機能も廃止予定。
- クラスが廃止予定なら、そのクラスのすべてのメンバーも廃止予定。
- 変数が廃止予定なら、暗黙的なgetterとsetterも廃止予定。
- 注意点としては、スーパークラスのある機能が非推奨でも、サブクラスの該当機能も必ず非推奨になるわけではない。なので、廃止予定のスーパークラスから、サブクラスへメンバーを移すことは妥当な選択。あるサブクラスでは依然として、その機能が重要な機能であるかもしれない。
- また、ユーザーが使用している非推奨の機能の内部で、非推奨の機能が使われている場合は、内部の非推奨の情報を、ユーザーに通知する必要はない。非推奨の機能の内部で、別の非推奨の機能が使われることはよくある。
- 実際のコードでも確認してみる。次のTelevisionクラスのactivateメソッドにアクセスすると、警告が表示される。@Deprecated(‘Use turnOn instead’)と、コンストラクタで指定されている通り、ユーザーは、turnOnメソッドを代わりに利用する必要がある。
class Television {
/// Use [turnOn] to turn the power on instead.
@Deprecated('Use turnOn instead')
void activate() {
turnOn();
}
/// Turns the TV's power on.
void turnOn() {...}
// ···
}
- つまりまとめると、@Deprecatedと@deprecatedの対応は、以下のように表現できる(ソースコードで、以下のように定義されている)。5
- 以下では、constantコンストラクタ(constant constructor)を使って、コンパイル時定数(compile-time constant)を定義している。
const Deprecated deprecated = Deprecated("next release");
@override
- オーバーライドしたメソッドやフィールドには、必ず@overrideアノテーションをつける必要がある。これは、コードの可読性を向上させ、意図しないオーバーライドを防ぐのに役立つ。6
- オーバーライドしていることは、サブクラスの宣言だけからは読み取れない。なので、アノテーションでオーバーライドしていることを、明示する方が良い。明示することによって、Dart analyzerの補助を受けられるようにもなり、意図通りにできていないことを警告してくれるようになる。
class Lucky extends Cat {
final int lives = 14; // Catクラスも見ないと、オーバーライドしているかわからない
}
abstract class Dog {
String get breed;
void bark() {}
}
class Husky extends Dog {
@override // オーバーライドしていることを明示する
final String breed = 'Husky';
@override // オーバーライドしていることを明示する
void bark() {}
}
- @overrideアノテーションは、インスタンスの次の要素に使用できる。
- メソッド
- ゲッター
- セッター
- 変数(フィールド)
- インスタンス変数にアノテーションが付与されると、その変数が持つgetterとsetterにも、暗黙的にアノテーションが付与される。
@pragma
- @pragmaアノテーションは、Dartのプログラムを扱うツールに対して、ツールの振る舞いをガイドするヒントを提供するためのアノテーション。
- 「pragma(プラグマ)」とは、ソフトウェア開発の一般的な用語で、コンパイラ・インタプリタなどのツールに特定の情報を渡すために使用する指令のこと。7
- 例えば、Dartにおいて、@pragma(‘vm:entry-point’)というアノテーションを、トップレベル関数につけると、「vm(Dart VM)に、entry-pointというプラグマを与える」という指示になる。このアノテーションをつけたトップレベル関数を含むファイルが、main関数を含むファイルに変わって、エントリーポイントになる。8
- 一般的に、エントリーポイント(entrypoint)とは「プログラムなどの実行に際して、一番最初に実行される箇所」のこと。9
- Dartにおける、エントリーポイントとは、Dartの実装10によって、直接呼び出されるDartライブラリのこと。例えば、Dartライブラリを
<script>
タグで参照したり、スタンドアロンのDart VMにコマンドライン引数として渡したりする場合、そのライブラリがエントリーポイントになる11。 - 言い換えると、エントリーポイントは通常、main関数を含む.dartファイルのことを指す。だが、@pragma(‘vm:entry-point’)アノテーションを、別のトップレベル関数につければ、main関数のない.dartファイルを、エントリーポイント変更することもできる。
- このように、Dart VMのようなツールに、指令を与える用途で使われるのが、pragmaであり、Dartにおける@pragmaアノテーションの役割。
dart:core以外のメタデータアノテーション
- その他、package:metaなど、パッケージごとに様々なメタデータアノテーションが用意されている。
- 例えば、package:metaには、@immutableアノテーションがある。
- これは、あるクラスCと、クラスCのすべてのサブタイプが、イミュータブルである必要がある場合に、クラスCにつけるアノテーション(すべてのフィールドがfinalなら、そのクラスはイミュータブルなクラスと言える)。
- @immutableアノテーションをつけることで、イミュータブルでない場合に、警告をアナライザーが出すようになる。
メタデータアノテーションを自作する方法
- 自作のメタデータアノテーションを用意することも可能。
- 上述したように、メタデータアノテーションの実体は、コンパイル時定数(compile-time constant)かconstantコンストラクタ(constant constructor)なので、これらを定義すれば、自作のメタデータアノテーションが作れる。
- 例えば、2つの引数を取る@Todoアノテーションを作成するには、次のように書く。
class Todo {
final String who;
final String what;
const Todo(this.who, this.what); // constantコンストラクタ
}
- @Todoアノテーションとして使用するには、次のようにする。
@Todo('Dash', 'Implement this function') // コンストラクタに2つの引数を渡す
void doSomething() {
print('Do something');
}
メタデータアノテーションが使える場所
メタデータアノテーションは、
- ライブラリ
- クラス
- typedef(type alias)
- 型パラメータ
- コンストラクタ
- ファクトリー
- 関数
- フィールド
- パラメータ
- 変数の宣言
- import/exportディレクティブ
の前に置くことができる。
メタデータアノテーションの注意点
- 自作のメタデータアノテーションも、通常のクラスと同様に、UpperCamelCaseで定義する必要がある。12
- 必ず、次のようにメタデータアノテーションの「前」に、ドキュメンテーションコメント(doc comments)を書くようにする。13
/// A button that can be flipped on and off.
@Component(selector: 'toggle')
class ToggleComponent {}
参考文献
Metadata
Metadata and annotations in Dart.
脚注
- https://e-words.jp/w/%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3.html ↩︎
- https://dart.dev/tools/analysis ↩︎
- https://dart.dev/tools/linter-rules/always_specify_types ↩︎
- https://api.dart.dev/stable/3.3.3/dart-core/dart-core-library.html ↩︎
- https://github.com/dart-lang/sdk/blob/main/sdk/lib/core/annotations.dart ↩︎
- https://dart.dev/tools/linter-rules/annotate_overrides ↩︎
- https://docs.oracle.com/cd/E19957-01/806-4838/Pragmas.html ↩︎
- https://dart.dev/tools/linter-rules/unreachable_from_main ↩︎
- https://e-words.jp/w/%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88.html ↩︎
- プログラミング言語の「仕様」と「実装」の違いは、私のUdemyのコース「Python でわかる オブジェクト指向 とはなにか?【Python オブジェクト指向 の「なぜ?」を「徹底的に」解説】」などを参照。 ↩︎
- https://dart.dev/tools/pub/glossary#entrypoint ↩︎
- https://dart.dev/effective-dart/style#do-name-types-using-uppercamelcase ↩︎
- https://dart.dev/effective-dart/documentation#do-put-doc-comments-before-metadata-annotations ↩︎