メインコンテンツにスキップ

Dart の型システム

Dart 言語は型安全です。静的型チェックと 実行時チェック を組み合わせて、変数の値が常にその変数の静的型と一致することを保証します。これは、健全な型付け (sound typing) とも呼ばれます。*型* は必須ですが、型推論 のおかげで、型 *アノテーション* はオプションです。

静的型チェックの利点の 1 つは、Dart の 静的アナライザー を使用してコンパイル時にバグを発見できることです。

ほとんどの静的解析エラーは、ジェネリッククラスに型アノテーションを追加することで修正できます。最も一般的なジェネリッククラスは、コレクション型である List<T>Map<K,V> です。

たとえば、次のコードでは、printInts() 関数は整数リストを出力し、main() はリストを作成して printInts() に渡します。

✗ 静的解析: 失敗dart
void printInts(List<int> a) => print(a);

void main() {
  final list = [];
  list.add(1);
  list.add('2');
  printInts(list);
}

上記のコードは、printInts(list) の呼び出しで list に型エラーが発生します (上記でハイライトされています)。

error - The argument type 'List<dynamic>' can't be assigned to the parameter type 'List<int>'. - argument_type_not_assignable

このエラーは、List<dynamic> から List<int> への健全でない暗黙的なキャストを強調しています。list 変数の静的型は List<dynamic> です。これは、初期化宣言 var list = [] では、アナライザーが dynamic より具体的な型引数を推論するのに十分な情報を提供しないためです。printInts() 関数は List<int> 型のパラメータを期待しているため、型の不一致が発生します。

リストの作成時に型アノテーション (<int>) を追加すると (以下でハイライト)、アナライザーは文字列引数が int パラメータに代入できないと警告します。list.add('2') の引用符を削除すると、静的解析をパスし、エラーや警告なしで実行できるコードになります。

✔ 静的解析: 成功dart
void printInts(List<int> a) => print(a);

void main() {
  final list = <int>[];
  list.add(1);
  list.add(2);
  printInts(list);
}

DartPad で試す.

健全性 (Soundness) とは?

#

健全性 (Soundness) とは、プログラムが特定の無効な状態に陥らないようにすることです。健全な*型システム*とは、式がその式の静的型に一致しない値に評価される状態に決して陥らないことを意味します。たとえば、式の静的型が String である場合、実行時に評価しても文字列しか得られないことが保証されます。

Java や C# の型システムと同様に、Dart の型システムは健全です。静的チェック (コンパイル時エラー) と実行時チェックを組み合わせて健全性を強制します。たとえば、Stringint に代入することはコンパイル時エラーです。as String を使用してオブジェクトを String にキャストする場合、オブジェクトが String でなければ実行時エラーが発生します。

健全性のメリット

#

健全な型システムにはいくつかの利点があります。

  • 型関連のバグをコンパイル時に検出する。
    健全な型システムは、コードの型について曖昧さをなくすため、実行時に見つけるのが難しい型関連のバグがコンパイル時に検出されます。

  • より読みやすいコード。
    値が指定された型を実際に持つと信頼できるため、コードは読みやすくなります。健全な Dart では、型は嘘をつくことができません。

  • より保守しやすいコード。
    健全な型システムを使用すると、コードの一部を変更したときに、壊れた他のコードについて型システムが警告を発することができます。

  • より優れた事前 (AOT) コンパイル。
    AOT コンパイルは型なしでも可能ですが、生成されるコードははるかに効率が悪くなります。

静的解析をパスするためのヒント

#

静的型のほとんどのルールは理解しやすいです。ここでは、あまり明白でないルールのいくつかを説明します。

  • メソッドのオーバーライド時に健全な戻り値の型を使用する。
  • メソッドのオーバーライド時に健全な引数の型を使用する。
  • 動的リストを型付きリストとして使用しない。

これらのルールを、以下の型階層を使用した例で詳しく見てみましょう。

a hierarchy of animals where the supertype is Animal and the subtypes are Alligator, Cat, and HoneyBadger. Cat has the subtypes of Lion and MaineCoon

メソッドのオーバーライド時に健全な戻り値の型を使用する

#

サブクラスのメソッドの戻り値の型は、スーパークラスのメソッドの戻り値の型と同じ型またはサブタイプである必要があります。Animal クラスの getter メソッドを検討してください。

dart
class Animal {
  void chase(Animal a) {
     ...
  }
  Animal get parent => ...
}

parent getter メソッドは Animal を返します。HoneyBadger サブクラスでは、getter の戻り値の型を HoneyBadger (または Animal の任意のサブタイプ) に置き換えることができますが、無関係な型は許可されません。

✔ 静的解析: 成功dart
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) {
     ...
  }

  @override
  HoneyBadger get parent => ...
}
✗ 静的解析: 失敗dart
class HoneyBadger extends Animal {
  @override
  void chase(Animal a) {
     ...
  }

  @override
  Root get parent => ...
}

メソッドのオーバーライド時に健全な引数の型を使用する

#

オーバーライドされたメソッドのパラメータは、スーパークラスの対応するパラメータと同じ型またはスーパタイプである必要があります。サブタイプで型を置き換えることによって、パラメータの型を「タイトにする」ことは避けてください。

Animal クラスの chase(Animal) メソッドを検討してください。

dart
class Animal {
  void chase(Animal a) {
     ...
  }
  Animal get parent => ...
}

chase() メソッドは Animal を受け取ります。HoneyBadger は何でも追いかけます。chase() メソッドをオーバーライドして、何でも (Object) 受け取るようにすることは問題ありません。

✔ 静的解析: 成功dart
class HoneyBadger extends Animal {
  @override
  void chase(Object a) {
     ...
  }

  @override
  Animal get parent => ...
}

次のコードは、chase() メソッドのパラメータを Animal から Mouse (Animal のサブクラス) にタイトにします。

✗ 静的解析: 失敗dart
class Mouse extends Animal {
   ...
}

class Cat extends Animal {
  @override
  void chase(Mouse a) {
     ...
  }
}

このコードは型安全ではありません。なぜなら、猫を定義してアリゲーターを追いかけることが可能になるからです。

dart
Animal a = Cat();
a.chase(Alligator()); // Not type safe or feline safe.

動的リストを型付きリストとして使用しない

#

dynamic リストは、さまざまな種類のものをリストに入れたい場合に便利です。ただし、dynamic リストを型付きリストとして使用することはできません。

このルールは、ジェネリック型のインスタンスにも適用されます。

次のコードは Dogdynamic リストを作成し、それを Cat 型のリストに代入しますが、これは静的解析中にエラーを生成します。

✗ 静的解析: 失敗dart
void main() {
  List<Cat> foo = <dynamic>[Dog()]; // Error
  List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

実行時チェック

#

実行時チェックは、コンパイル時に検出できない型安全性に関する問題に対処します。

たとえば、次のコードは、犬のリストを猫のリストにキャストすることはエラーであるため、実行時に例外をスローします。

✗ 実行時: 失敗dart
void main() {
  List<Animal> animals = <Dog>[Dog()];
  List<Cat> cats = animals as List<Cat>;
}

dynamic からの暗黙的なダウンキャスト

#

静的型が dynamic である式は、より具体的な型に暗黙的にキャストできます。実際の型が一致しない場合、キャストは実行時にエラーをスローします。次の assumeString メソッドを検討してください。

✔ 静的解析: 成功dart
int assumeString(dynamic object) {
  String string = object; // Check at run time that `object` is a `String`.
  return string.length;
}

この例では、objectString であればキャストは成功します。int のように String のサブタイプでない場合、TypeError がスローされます。

✗ 実行時: 失敗dart
final length = assumeString(1);

型推論

#

アナライザーは、フィールド、メソッド、ローカル変数、およびほとんどのジェネリック型引数の型を推論できます。アナライザーが特定の型を推論するのに十分な情報を持っていない場合、dynamic 型を使用します。

ここでは、ジェネリックにおける型推論の例を示します。この例では、arguments という名前の変数に、文字列キーとさまざまな型の値のペアを持つマップが格納されています。

明示的に変数の型を指定する場合、次のように記述できます。

dart
Map<String, Object?> arguments = {'argA': 'hello', 'argB': 42};

Alternatively, you can use var or final and let Dart infer the type

dart
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>

マップリテラルは、そのエントリから型を推論し、その後、変数はマップリテラルの型から型を推論します。このマップでは、キーは両方とも文字列ですが、値は異なる型 (Stringint、上限は Object) です。したがって、マップリテラルの型は Map<String, Object> であり、arguments 変数も同様です。

フィールドとメソッドの推論

#

指定された型を持たず、スーパークラスのフィールドまたはメソッドをオーバーライドするフィールドまたはメソッドは、スーパークラスのメソッドまたはフィールドの型を継承します。

宣言または継承された型を持たず、初期値で宣言されたフィールドは、初期値に基づいた推論型を取得します。

静的フィールドの推論

#

静的フィールドと変数は、初期化子から型が推論されます。推論がサイクルに遭遇した場合 (つまり、変数の型を推論することがその変数の型を知ることに依存している場合)、推論は失敗することに注意してください。

ローカル変数の推論

#

ローカル変数の型は、初期化子があればそこから推論されます。後続の代入は考慮されません。これは、あまりにも正確な型が推論される可能性があることを意味します。その場合、型アノテーションを追加できます。

✗ 静的解析: 失敗dart
var x = 3; // x is inferred as an int.
x = 4.0;
✔ 静的解析: 成功dart
num y = 3; // A num can be double or int.
y = 4.0;

型引数の推論

#

コンストラクタ呼び出しやジェネリックメソッド呼び出しの型引数は、発生箇所のコンテキストからの下向き情報と、コンストラクタまたはジェネリックメソッドの引数からの上向き情報の組み合わせに基づいて推論されます。推論が意図したとおりに機能しない場合は、いつでも型引数を明示的に指定できます。

✔ 静的解析: 成功dart
// Inferred as if you wrote <int>[].
List<int> listOfInt = [];

// Inferred as if you wrote <double>[3.0].
var listOfDouble = [3.0];

// Inferred as Iterable<int>.
var ints = listOfDouble.map((x) => x.toInt());

最後の例では、x は下向き情報を使用して double と推論されます。クロージャの戻り値の型は、上向き情報を使用して int と推論されます。Dart は、この戻り値の型を上向き情報として使用して、map() メソッドの型引数: <int> を推論します。

境界を使用した推論

#

境界を使用した推論機能により、Dart の型推論アルゴリズムは、既存の制約と宣言された型境界を組み合わせて制約を生成し、単なる最善の近似ではありません。

これは、F-bounded 型にとって特に重要であり、境界を使用した推論では、次の例で XB にバインドできることを正しく推論します。この機能がない場合、型引数は明示的に指定する必要があります: f<B>(C())

dart
class A<X extends A<X>> {}

class B extends A<B> {}

class C extends B {}

void f<X extends A<X>>(X x) {}

void main() {
  f(B()); // OK.

  // OK. Without using bounds, inference relying on best-effort approximations
  // would fail after detecting that `C` is not a subtype of `A<C>`.
  f(C());

  f<B>(C()); // OK.
}

これは、intnum のような Dart の日常的な型を使用した、より現実的な例です。

dart
X max<X extends Comparable<X>>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2;

void main() {
  // Inferred as `max<num>(3, 7)` with the feature, fails without it.
  max(3, 7);
}

境界を使用した推論により、Dart は型引数を*分解*し、ジェネリック型パラメータの境界から型情報を抽出できます。これにより、次の例の f のような関数が、具体的なイテラブル型 (List または Set) と要素型の両方を保持できるようになります。境界を使用した推論の前は、型安全性や具体的な型情報を失うことなく、これは不可能でした。

dart
(X, Y) f<X extends Iterable<Y>, Y>(X x) => (x, x.first);

void main() {
  var (myList, myInt) = f([1]);
  myInt.whatever; // Compile-time error, `myInt` has type `int`.

  var (mySet, myString) = f({'Hello!'});
  mySet.union({}); // Works, `mySet` has type `Set<String>`.
}

境界を使用した推論がない場合、myInt の型は dynamic になります。以前の推論アルゴリズムは、コンパイル時に不正な式 myInt.whatever を検出できず、代わりに実行時に例外をスローしていました。逆に、境界を使用した推論がない場合、mySet.union({}) はコンパイル時エラーになります。なぜなら、以前のアルゴリズムは mySetSet であるという情報を保持できなかったからです。

境界を使用した推論の詳細については、設計ドキュメント を参照してください。

型の代入

#

メソッドをオーバーライドする場合、1 つの型 (古いメソッド) のものを、新しい型を持つ可能性のあるもの (新しいメソッド) に置き換えています。同様に、関数に引数を渡す場合、1 つの型 (宣言された型のパラメータ) を持つものを、別の型 (実際の引数) を持つものに置き換えています。いつ、ある型を持つものを、サブタイプまたはスーパタイプを持つものに置き換えることができるのでしょうか?

型を代入する場合、*コンシューマー*と*プロデューサー*の観点から考えると役立ちます。コンシューマーは型を吸収し、プロデューサーは型を生成します。

コンシューマーの型はスーパタイプに、プロデューサーの型はサブタイプに置き換えることができます。

単純な型代入とジェネリック型を使用した代入の例を見てみましょう。

単純な型の代入

#

オブジェクトをオブジェクトに代入する場合、いつ型を別の型に置き換えることができますか? 答えは、オブジェクトがコンシューマーかプロデューサーかによります。

次の型階層を検討してください。

a hierarchy of animals where the supertype is Animal and the subtypes are Alligator, Cat, and HoneyBadger. Cat has the subtypes of Lion and MaineCoon

Cat c が*コンシューマー*で、Cat() が*プロデューサー*である次の単純な代入を検討してください。

dart
Cat c = Cat();

コンシューミング位置では、特定の型 (Cat) を消費するものを何でも消費するもの (Animal) に置き換えることは安全です。したがって、Cat cAnimal c に置き換えることは許可されます。なぜなら、AnimalCat のスーパタイプだからです。

✔ 静的解析: 成功dart
Animal c = Cat();

しかし、Cat cMaineCoon c に置き換えると型安全性が損なわれます。なぜなら、スーパークラスは Lion のような異なる動作をする猫の型を提供する可能性があるからです。

✗ 静的解析: 失敗dart
MaineCoon c = Cat();

プロデュース位置では、型 (Cat) を生成するものをより具体的な型 (MaineCoon) に置き換えることは安全です。したがって、以下は許可されます。

✔ 静的解析: 成功dart
Cat c = MaineCoon();

ジェネリック型の代入

#

ジェネリック型でもルールは同じですか? はい。動物のリストの階層を検討してください。CatListAnimalList のサブタイプであり、MaineCoonList のスーパタイプです。

List<Animal> -> List<Cat> -> List<MaineCoon>

次の例では、List<MaineCoon>List<Cat> のサブタイプであるため、MaineCoon リストを myCats に代入できます。

✔ 静的解析: 成功dart
List<MaineCoon> myMaineCoons = ...
List<Cat> myCats = myMaineCoons;

では、逆方向はどうでしょうか? Animal リストを List<Cat> に代入できますか?

✗ 静的解析: 失敗dart
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals;

この代入は静的解析をパスしません。なぜなら、Animal のような非dynamic 型からの暗黙的なダウンキャストが許可されていないためです。

この種のコードを静的解析でパスさせるには、明示的なキャストを使用できます。

dart
List<Animal> myAnimals = ...
List<Cat> myCats = myAnimals as List<Cat>;

ただし、明示的なキャストは、キャストされるリスト (myAnimals) の実際の型によっては、実行時に失敗する可能性があります。

メソッド

#

メソッドをオーバーライドする場合、プロデューサーとコンシューマーのルールは引き続き適用されます。たとえば、

Animal class showing the chase method as the consumer and the parent getter as the producer

コンシューマー (chase(Animal) メソッドなど) の場合、パラメータの型をスーパタイプに置き換えることができます。プロデューサー (parent getter メソッドなど) の場合、戻り値の型をサブタイプに置き換えることができます。

詳細については、メソッドのオーバーライド時に健全な戻り値の型を使用する および メソッドのオーバーライド時に健全な引数の型を使用する を参照してください。

共変パラメータ

#

まれに使用されるコーディングパターンの中には、パラメータの型をサブタイプでオーバーライドして型をタイトにすることに依存するものがありますが、これは無効です。この場合、covariant キーワードを使用して、意図的にこれを行っていることをアナライザーに伝えることができます。これにより、静的エラーが削除され、代わりに実行時に無効な引数型がチェックされます。

以下は、covariant の使用方法を示しています。

✔ 静的解析: 成功dart
class Animal {
  void chase(Animal x) {
     ...
  }
}

class Mouse extends Animal {
   ...
}

class Cat extends Animal {
  @override
  void chase(covariant Mouse x) {
     ...
  }
}

この例ではサブタイプでの covariant の使用を示していますが、covariant キーワードはスーパークラスまたはサブクラスのメソッドのいずれかに配置できます。通常、スーパークラスのメソッドに配置するのが最適です。covariant キーワードは単一のパラメータに適用され、セッターやフィールドでもサポートされています。

その他のリソース

#

健全な Dart に関する追加情報については、以下のリソースを参照してください。