2020/8/27 Dart : The Dart type systemの訳

導入

Dart言語は、型安全です。静的な型チェックと、「変数の値が常にその変数のstatic typeにマッチすることを確実にする実行時のチェックのコンビネーションです。時にサウンドタイピングと呼ばれます。

しかしながら、型は強制的にもかかわらず、型注釈はオプショナルです

型システムを含むDart言語全体のイントロダクションに関しては、language tourをご覧ください。

静的な型チェックの一つの恩恵は、バグ発見をコンパイル時にDartの静的アナライザを使って行えることです。

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

例えば、下のコードで、printInts()関数は引数としてList<int>型を受け取り、それを表示します。main()関数でリストを生成し、それをprintInts()関数に渡しています。

✖️static analysis:error/warning
void printInts(List<int> a) => print(a);

void main() {
  var list = [];
  list.add(1);
  list.add('2');
  printInts(list); //←ここでエラー発生
}

上記コードはprintInts()関数呼び出し時にエラーが発生します。

error - The argument type 'List<dynamic>' can't be assigned to the parameter type 'List<int>' at lib/strong_analysis.dart:27:17 - (argument_type_not_assignable)

上記のコードでは

printInts(list);

の部分でList<dynamic>型からList<int>型への不健全なキャスト(型変換)を暗黙的に行おうとします。(null-safety無し時の話)

list変数は静的型List<dynamic>型と推論されます。

変数listの宣言時に型引数に関してdynamic型よりも詳しい型に関する情報を宣言していなかったからです。

printInts()関数は引数としてList<int>型を期待しますので、型のミスマッチが発生します。暗黙的なキャストを行おうとするができない。

✔︎static analysis:success
void printInts(List<int> a) => print(a);

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

上記のように変数listへ代入するリストリテラルを生成する際に<int>型であることを明示すれば、listに対してString型の要素

“2”

を追加しようとした時に、アナライザが警告してくるので、

list.add(“2”);

list.add(2);

に修正すればエラー無しで実行することができるようになります。


What is soundness?

Soundness(健全性)とはプログラムが特定の無効な(不正な)状態にならないようにすることです。信頼できる型システムとは式が静的な型と一致しない値に評価される状態になることは決してないということです。

例えば、式の静的な型がString型の場合、その式を評価した時、実行時にstring(文字列)のみを取得できることが保証されます。

Dartの型システムは、JavaやC#の型システムのように、信頼できます。静的チェック(コンパイル時エラー)とランタイムチェックの組み合わせを使用して、その健全性を強化します。

例えば、int型の変数にString型の値を代入するとコンパイル時エラーとなります。そのオブジェクトがstringでない場合、as stringを使用してObjectをstringにキャストすると実行時エラーとなり失敗します↓。

//null-safety有り
void main(){

  Object obj1=4;
  
  String str2=obj1 as String;
  print(str2);
  
}

/*
Uncaught Error: TypeError: 4: type 'JSInt' is not a subtype of type 'String'
*/

 


The benefits of soundness

sound(健全な)型システムはいくつかの利益があります。

  • コンパイル時に型に関連したバグを見つけられる。

健全な型システムは型に関して誤解のないコードを強制します。そのため、実行時に見つけるのが難しい型関連のバグがコンパイル時に明らかになります。

  • より読みやすいコード。
    指定したタイプの値を実際に使用できるため、コードが読みやすくなります。健全なDartでは、型は嘘をつきません。

 

  • より保守しやすいコード。

健全な型システムでは、1つのコードを変更すると、型システムは、壊れたばかりの他のコードについて警告することができます。


Tips for passing static analysis

静的な型に関するほとんどのルールは理解しやすいものです。それほど明白ではない規則の一部を示します。

  • メソッドをオーバーライドするときは、適切な戻り値の型を使用してください。
  • メソッドをオーバーライドするときに適切な引数の型を使用します。
  • 動的リストを型付きリストとして使用しないでください。

次のタイプ階層を使用する例を使用して、これらのルールを詳しく見てみましょう。

スーパータイプがAnimalで、サブタイプがAlligator、Cat、HoneyBadgerである動物の階層。 猫にはライオンとメインクーンのサブタイプがあります

HoneyBadger : 蜜穴熊(アフリカ南アジア樹木の茂った地域にすむ夜行性のアナグマのような肉食動物)

メソッドをオーバーライドするときは、適切な戻り値の型を使用する

サブクラスのメソッドの戻り値の型は、スーパークラスのメソッドの戻り値の型のサブタイプ(戻り値の型、あるいは戻り値の型を継承した型?)でないといけません。

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

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

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

 




Use sound parameter types when overriding methods

オーバーライドされたメソッドの引数は、スーパークラスの対応する引数の型と同じ型、あるいはそのスーパータイプでなければなりません。

タイプを元のパラメーターのサブタイプで置き換えることにより、パラメータータイプを「タイト化」しないでください。

class Robot{
  Robot(this.name);
    String name;
}

class Animal {
  Animal({this.parent});
  Animal? parent;
  
  void chase(Animal a) {
    print("animal is chasing $a");
  }
  Animal? get par => parent;
}


class HoneyBadger extends Animal {
  HoneyBadger({HoneyBadger? parent}):super(parent:parent);
  
  @override
  void chase(Animal a) {
    print("honeybadger is chasing $a");
  
  }

  @override
  HoneyBadger get par => (parent as HoneyBadger);
}



void main(){
  Robot robo1=Robot('robo1');
  HoneyBadger hb1=HoneyBadger();
  HoneyBadger hb2=HoneyBadger(parent:hb1);
  Animal ani1=Animal();
  
  hb1.chase(ani1);
  print(hb2.par);
}

 

注:サブタイプを使用する正当な理由がある場合は、covariantキーワードを使用できます 。

Animalクラスのchase(Animal)メソッドについて考えてみましょう。

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

chase()メソッドは引数にAnimal型の値をとります。HoneyBadgerは何を追いかけてもOKです。chase()メソッドをオーバーライドして引数に何らかのObjectをとることはOKです。

class HoneyBadger extends Animal {
  @override
  void chase(Object a) { ... }
  Animal get parent => ...
}
//↑問題無し

以下のコードはAnimalクラスから(Animalクラスのサブクラスである)Mouseクラスへと引数を絞っています。

class Mouse extends Animal {...}

class Cat extends Animal {
  void chase(Mouse x) { ... }
}
//↑エラー

このコードは型安全ではありません。なぜなら以下のようにCat型のインスタンスを宣言して、chaseメソッドにalligatorを渡すことができてしまうからです。

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

変数aはAnimal型と宣言されているので、

上記サンプルで、a.chase()呼び出し時に引数にはAnimal型、あるいはそのサブクラスが渡せることになる。と言うことでAlligator型がAnimal型のサブクラスなら、Alligator型インスタンスも渡せる。

しかしCat型のchase()メソッドの引数はMouse型を期待している。と言うことでこのままではMouse型の引数にAlligator型インスタンスが渡されてしまう。

Cat型のchaseメソッドのボディ内でMouse型特有のプロパティ・メソッドなどが使われている場合(とにかくAlligator型には無いものが使われている場合)、それを呼び出そうとしても、Alligator型インスタンスには無いので、実行時エラーになる。

こういうことができてしまうと困るので、実際このセクション「Use sound parameter types when overriding methods」に書いてあるルールに反する型の使い方をすると、

'HoneyBadger.chase' ('void Function(HoneyBadger)') isn't a valid override of 'Animal.chase' ('void Function(Animal)') - line 22

↑のようなコンパイル時エラー(オーバーライドの方法が不正)が出るので、結局ここに書いてあるルールに反する型の使い方はできないようになっている

ただまあ、このルールを知らないと、なぜエラーが出ているのかもよくわからず、修正もできない、ということになるか。

 


Don’t use a dynamic list as a typed list

List<dynamic>は、さまざまな種類のものが含まれるリストが必要な場合に適しています。しかし、型付けされたlistとしてList<dynamic>を使うことはできません。

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

以下のコードはDog型を要素に持つList<dynamic>を生成しています。そしてそれをList<Cat>型の変数に代入しようとしています。これは静的解析中にエラーが出ます。

class Cat extends Animal { ... }

class Dog extends Animal { ... }

void main() {
  List<Cat> foo = <dynamic>[Dog()]; // Error
  List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}

実行時チェック

Dart VMdartdevcなどのツールのランタイムチェックは 、アナライザーがキャッチできないタイプセーフティの問題を処理します。

例えば、次のコードは実行時に例外をthrowします。List<Cat>型の変数に、Dog型の要素を持つlistを代入しているからです。

void main() {
  List<Animal> animals = [Dog()];
  List<Cat> cats = animals;
}

Type inference

型推論

アナライザーは、フィールド、メソッド、ローカル変数、および最も一般的な型引数の型を推測できます。アナライザーが特定のタイプを推測するのに十分な情報を持っていない場合、アナライザーはそのdynamicタイプを使用します。

次に、型推論がジェネリック型でどのように機能するかの例を示します。この例では、argumentsという名前の変数が、文字列キーをさまざまなタイプの値とペアにするマップを保持しています。

もし明示的に変数の型を型付けするなら次のように書くでしょう。

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

その代わりに、varキーワードを使用してDartに型推論をさせることもできます。

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

マップリテラルはエントリから型を推測し、変数はマップリテラルの型から型を推測します。このマップでは、キーはどちらも文字列ですが、値のタイプは異なります(Stringとint、両者共通のスーパークラスとしてObjectがあります)。したがって、マップリテラルは型Map<String, Object>と推論され、arguments変数もMap<String, Object>と推論されます。


Field and method inference

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

型が宣言されておらず継承もしていないフィールドで初期値があるものは、その初期値の型に基づいて型推論されます。


Static field inference

staticフィールドの推論

スタティックフィールドと変数の型は初期値から推論されます。

工事中🏗


Local variable inference

ローカル変数の型推論は初期値(初期化時)に基づいて推論されます(初期値があれば)。初期化の後の代入は関係ありません。場合によっては型推論が正確すぎる場合もあるかもしれません。その場合は、自分で型注釈(type annotation)を加えることもできます。

↓型注釈無しなのでint型として型推論された。その後double型を代入しようとしてエラー。

var x = 3; // x is inferred as an int.
x = 4.0;

num y = 3; // A num can be double or int.
y = 4.0;

num型と型注釈すれば、int型もdouble型も受け取ることができます。

int型もdoube型もnum型を継承している。


Type argument inference

型引数の型推論

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

✔︎static analysis:success
// 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型と型推論されます。

map()メソッドの型引数を推論するときに、Dartはこの返り値の型(つまりint型)を上方情報として利用します。


Substituting types

工事中🏗

タイプを置き換えるときは、消費者 (consumers)と生産者(producers)の観点から考えると役立ちます。消費者は型を吸収し、生産者は型を生成します。

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

単純な型の割り当てとジェネリック型による割り当ての例を見てみましょう。


Simple type assignment

単純な型割り当て

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

次のタイプ階層について考えてみます。

スーパータイプがAnimalで、サブタイプがAlligator、Cat、HoneyBadgerである動物の階層。 猫にはライオンとメインクーンのサブタイプがあります

下記のシンプルな代入を考えてみます。

Cat c

がコンシューマー(consumer)であり、

Cat()

がプロデューサー(producer)です。

Cat c = Cat();

コンシューマー(受け取る)側では、

「特定の型を受け取るタイプ(Cat型)」を「任意の方を受け取るタイプ(Animal型)」に取り換えるのは安全です。

ですから、

Cat c

Animal c

に置き換えることは可能です。Animal型はCat型のスーパークラスだからです。

✔︎static analysis:success
Animal c = Cat();

しかし、コンシューマー(受け取る)側を、

Cat c

MaineCoon c

に置き換えるのは型安全でなくなります。なぜなら、

 

MaineCoon c = Cat();

プロデューサー側(代入する側)

 

 

参考

https://dart.dev/guides/language/type-system

カテゴリーDart

コメントを残す

メールアドレスが公開されることはありません。