目次

Dartの型システム

Dart言語は型安全です。静的型チェックとランタイムチェックを組み合わせて、変数の値が常に変数の静的型と一致するようにします。これは、健全な型付けと呼ばれることもあります。は必須ですが、型推論により、型の注釈はオプションです。

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

ほとんどの静的解析エラーは、ジェネリッククラスに型注釈を追加することで修正できます。最も一般的なジェネリッククラスは、コレクション型`List`と`Map`です。

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

リストの作成時に型注釈(`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で試す.

健全性とは?

#

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

Dartの型システムは、JavaやC#の型システムと同様に、健全です。静的チェック(コンパイル時エラー)とランタイムチェックを組み合わせて健全性を強制します。たとえば、`String`を`int`に割り当てることは、コンパイル時エラーです。オブジェクトを`as String`を使用して`String`にキャストすると、オブジェクトが`String`でない場合はランタイムエラーで失敗します。

健全性のメリット

#

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

  • コンパイル時に型関連のバグを明らかにする。
    健全な型システムは、コードが型について明確になることを強制するため、実行時に見つけにくい可能性のある型関連のバグがコンパイル時に明らかになります。

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

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

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

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

#

静的型のほとんどのルールは理解しやすいです。あまり明らかではないルールを以下に示します。

  • メソッドをオーバーライドするときは、健全な戻り値の型を使用する。
  • メソッドをオーバーライドするときは、健全なパラメーターの型を使用する。
  • dynamicリストを型付きリストとして使用しない。

次の型階層を使用する例とともに、これらのルールを詳しく見ていきましょう。

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`)を受け取ることができるのはOKです。

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

  @override
  Animal get parent => ...
}

次のコードでは、`chase()`メソッドのパラメーターが`Animal`から`Animal`のサブクラスである`Mouse`に厳密化されています。

✗ 静的解析: 失敗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`リストは、さまざまな種類のものをリストに含めたい場合に適しています。ただし、`dynamic`リストを型付きリストとして使用することはできません。

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

次のコードでは、`Dog`の`dynamic`リストを作成し、それを`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型を使用します。

以下は、ジェネリックスを使用した型推論の例です。この例では、argumentsという名前の変数は、文字列キーとさまざまな型の値のペアを保持するマップを保持します。

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

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

あるいは、varまたはfinalを使用して、Dartに型を推論させることもできます。

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>を推論するときに上方情報として使用します。

型の置換

#

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

型を置換するときは、コンシューマープロデューサーという観点で考えると役立ちます。コンシューマーは型を吸収し、プロデューサーは型を生成します。

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

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

単純な型代入

#

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

次の型階層を考えてみましょう。

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など、異なる動作を持つCat型を提供する可能性があるからです。

✗ 静的解析: 失敗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ゲッターメソッドなど)の場合、戻り値の型をサブタイプで置き換えることができます。

詳細については、メソッドをオーバーライドするときは適切な戻り値の型を使用するメソッドをオーバーライドするときは適切なパラメーター型を使用するを参照してください。

その他のリソース

#

次のリソースには、健全なDartに関する詳細情報が記載されています。