はじめに
動機
本記事執筆時点で型安全導入から4年、Null安全導入から1年5ヶ月経過しているにもかかわらず、こともあろうかこれからDartを学ぼうとする者向けに、Dart 1.xまでの知識(または、その誤解)をベースとして解説する新規記事が散見されます。その一つへのコメントが元記事ごと消えてしまったので、ここで改めて纏めます。
Dartの型安全 (Null安全を除く)
Dartは2.0(SDK 2.0.0は2018/8/7リリース)で型安全になりました。
型安全はDart 1.x時代からオプションモード(Strong Mode)としてリリースされてきましたが、Dart 2.0で標準となると同時にSDK 2.0.0では型不安全(いわばWeak Mode)は削除されました。つまり、型安全モードが標準でかつ型不安全モードが併存するSDKバージョンは存在しません。Flutter以前のDart 人口は今と比べ物にならないほど少なかったので、明確な並行運用期間のないままこのような破壊的変更が可能だったと言えます。
DartのNull安全 (広義の型安全の一部)
Dartは2.12(SDK 2.12.0は2021/3/3リリース)でNull安全が導入されました。
続いてSDK 2.13.0ではNull安全が標準になりましたが、SDKのみならず言語仕様としてもNull不安全モードが現在(2.17)まで存続し、同一アプリ内での混在時の動作までも定義されています(unsound null safety機能)。これはFlutterの普及によりDart人口が急拡大し、もはや乱暴な破壊的変更が難しくなったことに対する解です(Language evolution | Dart)。ただし、Null不安全モードやその混在モード(unsound null safety)は明確に移行措置と位置づけられており、Dart 3.0では無くなるはずです。
予備知識
型安全
本記事ではクラスベースのオブジェクト指向言語を対象に、少なくとも値(=クラスインスタンス=オブジェクト)には型情報が有る前提とします(そうでない言語もあるが)。
その上で、ここでの型安全とは、変数にも型情報が有り、その初期化・代入時に右辺値(値、変数等)の型との互換性を検査(型検査)すること、とします。
動的型付けと静的型付け
型安全のための型検査を、動的型付け言語は実行時に行うのに対し、静的型付け言語はコンパイル時(デプロイ前または起動時等の実行前)に行います。
ちなみに、JavaSccriptはここで言うところの静的型付け言語でも動的型付け言語でもなく、型無し言語です。
健全な型安全
健全(Sound)な型安全とは、変数にその型(と互換)の型の値が入っていることを保証することです。
明示的キャスト等がある場合、健全な型安全には静的型付けと動的型付けの両方が必要となります。
私の知る限り、SwiftとDart 2.xが健全な型安全です。
TypeScriptやKotlinを含む他の多くの静的型付け言語は不健全(unsound)な型安全です。
メンバ解決 (蛇足)
静的型付け言語でもメンバの解決の多くは実行時に行われます(継承とポリモーフィズムがあるので)。
本題
誤解1: 型修飾を省略(var
で宣言等)した変数は動的型付け
Dart 2.xは常に静的型付けです。
var
を使った場合、その変数は型推論によって静的に型付けされます。
型推論のヒントが何も与えられなかった場合もObject?
に静的型付けされます。
また、dynamic
と型修飾するとその変数はdynamic
という特殊な型に静的に型付けされます。
これは、Object?
と型修飾するとその変数がObject?
に静的に型付けされ、Object
とそのサブクラス型のインスタンス(とnull
)(つまりなんでも)が代入できるのとよく似ています。
Object?
との違いは、dynamic
では静的にも動的にも型検査しないことです。
Object?
で型修飾した場合は型検査の結果、その変数経由で値のhashCode
、runtimeType
、noSuchMethod
、toString
、Operator ==
以外のメンバを評価しようとするとコンパイル時エラーですが、dynamic
の場合は実行時にその変数経由で値のメンバ解決を試み、そこで失敗するとはじめて実行時エラーとなります。
Dart 1.xの型 (補足)
Dart 1.xは一部の例外を除いてJavaScriptと同じく型無し言語でした。
変数に任意で型修飾ができますが、これはあくまでIDEによるリコメンドやコンパラによる警告のためのものであって、コンパイル結果や実行時動作には全く反映されませんでした。
Dart 2.xが常に静的型付けであり、かつ必要に応じて動的型付けも行う健全な型安全であるのと極めて対照的です。
なお、一部の例外とはデバッグモードで動かした時で、動的型付け言語として動作しました。
健全性の意図的な穴 (補足)
dynamic
は静的にも動的にも型検査しないので健全性の穴です。
これはJSON等の型無し言語とのインタフェースのための避難口として用意されています。
従って、何でも入る型として通常はObject?
を利用すること、使うときにはキャストすることが推奨されています。
SwiftにおけるAny
も同様です(たぶん)。
プログラマに明示的にdynamic
と修飾することを強制して意識させることで、dynamic
があることのみをもって言語全体が不健全とは位置づけていません。
誤解2: 全ての値はオブジェクトなので未初期化の変数はnull
Dartでは1
等を含み全ての値がオブジェクト(クラスインスタンス)です。
また、変数が保持するのはオブジェクトへの参照です。(ここまでは良い)
しかし、v == null
はv
が何も参照しない状態ではなく、Null
クラスのインスタンスnull
への参照を保持する状態です。
また、const
やfinal
な変数では暗黙の初期値が未定義であり、final
な変数を初期化前に評価(null
比較を含む)するとコンパイル時エラーです。
Dart 2.10まではconst
でもfinal
でもない変数は暗黙的にnull
への参照で初期化されていました。
しかし、Null安全を導入したDart 2.12以降はint
等はNull不可型であり、その暗黙の初期値は未定義であり、const
やfinal
と同様に初期化前に評価(null
比較を含む)すると多くの場合コンパイル時エラーです。
なお、late final
とすると初期化前の評価エラーは実行時に検出されます。
つまり、Null可型(例: int?
)だがconst
でもfinal
でも無い変数(例: int? v;
)だけが暗黙の初期値をもち、それがnull
です。
最後に
Dart、Flutterは公式ドキュメントが充実していることで評価されています。
私の記事を含めて間違っている可能性のある二次情報にあたる前に、公式ドキュメントを読むことを強く推奨します。
英語しかありませんが、ソフトウェアエンジニアは現実的に英語を避けて通れないものと諦めて、または英語を学ぶ絶好のチャンスと前向きに捉えて、公式ドキュメントを読みましょう。