Dartの型の理解しておきたいあれこれ

今回は Dart の基礎の一部を記事にしてみました。オブジェクト指向にも関わるところです。
応用的なところまで広く含んでいて、時々見返して思い出すのにも使えると思います。

本記事に含めていた null safety に関する部分は別記事に分離しましたので、あわせてお読みください。

- [Dartの型の理解しておきたいあれこれ（Null safety編）](https://qiita.com/kabochapo/items/fbca2b2972546eeadcfa)

# 型の比較

あまり気にしたことがない方が多いかもしれませんが、とても重要です。
しっかりと理解しておきましょう。

## オブジェクトと型の比較

`is` を使います。

```dart
class Person {}
class Mario extends Person{}
class Luigi extends Person{}
```
```dart
final mario = Mario();
print(mario is Mario);  // true
print(mario is Luigi);  // false
```

もし `==` を使ってしまうと、間違った比較方法なので常に `false` になります。
analysis_options.yaml で [unrelated_type_equality_checks](https://dart-lang.github.io/linter/lints/unrelated_type_equality_checks.html) のルールを有効にしておくと 「Equality operator `==` invocation with references of unrelated types.」と警告してくれて安全です。

```dart
print(mario == Mario);  // false
print(mario == Luigi);  // false
```

`is` の否定形は `is!` です。
`!is` ではありません。

```dart
print(mario is! Mario);  // false
print(mario is! Luigi);  // true
```

### Null 型

Dart では `null` にも型があり、`Null` というクラスとして定義されています。
そのコードは下記のとおりです。

```dart
class Null {
  factory Null._uninstantiable() {
    throw UnsupportedError('class Null cannot be instantiated');
  }

  external int get hashCode;

  /** Returns the string `"null"`. */
  String toString() => "null";
}
```

`null` は値がないことを表すものですよね。
それなのに `null.toString()` が成り立つのは、`Null` クラスが `toString()` というインスタンスメソッドを持っていて、`null` が `Null` 型の（唯一の）インスタンスだからです。

また、型があるため、先ほどと同様の型比較が一応可能です。

```dart
print(null is Null);  // true
```

ただし、このように比較すると「Tests for null should be done with '== null'.」という警告が出ます。
この警告メッセージに書かれているとおり `==` で比較するようにしましょう。

```dart
int v;
print(v == null);  // true
```

## 型同士の比較

「オブジェクト vs 型」でなく「型 vs 型」の比較なら普通の比較なので等価演算子（`==`）を使います。

```dart
print(Mario == Mario);  // true
print(Mario == Luigi);  // false
```

「Type 型という値 vs Type 型という値」とも言えます。
下記の t1 と t2 はどちらも Type 型です。

```dart
final t1 = Mario;
final t2 = Mario;
print(t1 == t2);  // true
```

## 親との比較

`Mario` というクラスは `Person` を継承しています。
`Mario` のオブジェクトは `Mario` 型であると同時に `Person` 型でもあるはずです。

検証してみましょう。

```dart
final mario = Mario();
print(mario is Mario);   // true
print(mario is Person);  // true
```

思ったとおりですね。
これを使えば、あるクラスを継承しているかどうかを判定できることになります。

では型同士の比較はどうでしょうか？

```dart
print(Mario == Mario);   // true
print(Mario == Person);  // false
```

`Mario` 型は `Person` 型ではないと判定されました。
継承関係は考慮されず、単純に同一の型かどうかという判定になります。

### Object 型

`Object` はあらゆるクラスの基底となるものです。
つまり、次のように `is Object` という比較の結果は常に `true` となります（Dart 2.12 未満）。[^is_object]

※Null 安全においては Null 型はこれに該当しません（詳細は [null safety 編](https://qiita.com/kabochapo/items/fbca2b2972546eeadcfa#object-との比較) をご覧ください）。

```dart
print(mario is Object);  // true
print(10 is Object);     // true
print(null is Object);   // true（Dart 2.12 以降では false）
```

`Object` が基底となっていることを知っていると仕組みを理解しやすくなる場合があるかもしれません。

[^is_object]: 必ず `true` となる比較なのでわざわざするのは意味はなくて無駄ですが、説明用にあえて比較しています。「Unnecessary type check, the result is always true.」という警告が出ます。

## 現在の型を得る

ランタイムの型を取得するには `runtimeType` を使います。
Dart のあらゆるオブジェクトの基底クラスとなっている `Object` クラスが持つプロパティで、`Type` 型です。

```dart
final mario = Mario();
final Person person1 = Mario();
final person2 = Mario() as Person;  // Unnecessary cast. という警告はここでは無視で

print(mario.runtimeType);    // Mario
print(person1.runtimeType);  // Mario
print(person2.runtimeType);  // Mario
```

親クラスの型の変数に入れたり明示的にアップキャストしたりしても `Mario` 型のままになります。
つまり

```dart
print(person1.runtimeType == Mario);   // true
print(person1.runtimeType == Person);  // false

// Type型なのでisでの比較はダメ
print(person1.runtimeType is Mario);   // false
print(person1.runtimeType is Person);  // false
```

となります。

継承の確認に `runtimeType` を用いてしまうミスもやってしまいがちです。
気を付けましょう。

```dart
if (mario.runtimeType is Person) { ... }  // ダメ
if (mario.runtimeType == Person) { ... }  // ダメ
```

正しい方法は下記です（[親との比較](#親との比較) のところで見たとおりです）。

```dart
if (mario is Person) { ... }
```

## 型チェック

`Mario` クラスだけにメソッドを持たせてみます。

```dart
class Mario extends Person {
  void greet() {
    print("Hi! I'm Mario.");
  }
}
```

先ほど `person1` と `person2` の `runtimeType` は `Mario` のままでした。
それなら `Mario` クラスだけが持つメソッドを呼べそうな気がしなくもないです。どうでしょうか。

```dart
mario.greet();    // Hi! I'm Mario.
person1.greet();  // ダメ
person2.greet();  // ダメ
```

ダメですね。
`runtimeType` では `Mario` 型となっても中身は `Person` 型であり、`Mario` のメソッドにはアクセスできません。

この場合、方法は二つあります。

```dart
// 方法1
(person1 as Mario).greet();

// 方法2
if (person1 is Mario) {
  person1.greet();
}
```

**方法 1**

`Mario` 型にダウンキャストする方法です。
これはキャストできないものに対して使ってしまうと例外が発生しますのでご注意ください。
`null` の場合も例外が発生します。[^for-null]

[^for-null]: `null` は `(person1 as Mario)?.greet()` のように `?.` を使えば対策できます。

**方法 2**

型をチェックする方法で、こちらのほうが安全です。
「チェック」という表現にしていますが、先述の比較と同じです。
`is` を使って `Mario` として扱える型（`Mario` 自体かその派生）であることをチェックしただけです。

ポイントは、チェックが済んでいる位置では Dart が「`person1` は `Mario` 型だ」として扱ってくれるようになって、`Mario` にしかない `greet()` が利用可能になるという点です。

# ジェネリクス

ジェネリクスはまさに型に関するものです。

ここでジェネリックなピーチ姫の登場です。
活用例としてはいまいちですが、親しみやすそうな例としてこれにしました。

```dart
class Peach<T> {
  T _husband;

  void marry(T husband) {
    _husband = husband;
  }

  void speak() {
    print("I'm of type $runtimeType, and my husband is of type $T.");
  }
}
```

夫を入れておく `_husband` の型としてジェネリック型 `T` を使います。
ピーチ姫は `Peach<T>` の `T` で指定した型の人と結婚できるということです。

例えばジェネリック型として `Person` を指定する（`Peach<Person>` と書く）と、Peach クラス内で

```dart
Person _husband;

void marry(Person husband) {
  _husband = husband;
}
```

と書かれているのと同じ意味になります。
ピーチ姫にとって結婚相手なんて「人なら誰でもいいわ」という状態です。
マリオさんは `Person` を継承しているので `marry()` に渡すことができます。

```dart
Peach<Person>()
  ..marry(mario)
  ..speak();  // I'm of type Peach<Person>, and my husband is of type Person.
```

結婚できる型を絞って `Mario` 型にしてみましょう。
「マリオさんしか嫌！」というピーチ姫です。

```dart
Peach<Mario>()
  ..marry(mario)
  ..speak();  // I'm of type Peach<Mario>, and my husband is of type Mario.
```

「ルイージさんしか嫌！」とするとマリオさんは結婚相手の条件から外れます。

```dart
Peach<Luigi>().marry(mario);  // marioはLuigi型でもその派生型でもないのでダメ
```

雰囲気が掴めましたか？

`<T>` のところは `<T extends RichPerson>` のように継承関係を指定することや `<T, U>` など複数を指定することもできます。
慣れたらご自分で試してみてください。

# 型を変数に入れる

型自体を変数に代入したいときがあります。
先ほども `Mario` という型を変数に入れました。

```dart
final t = Mario;
```

これは

```dart
final Type t = Mario;
```

ということです。
`Type` という型の変数に型自体（を表す値）を入れておくことができるわけです。

`runtimeType` も `Type` 型で、変数に普通に代入できます。

```dart
final mario = Mario();
final t = mario.runtimeType;
```

## ジェネリクスの場合

~~しかしジェネリクスを使ったクラスの型はなんと単純な代入ができません。
Dart の言語仕様による制限です。~~

Dart 2.15 でできるようになりました。
⇒ [詳細情報](https://github.com/dart-lang/language/blob/master/accepted/2.15/constructor-tearoffs/feature-specification.md#explicitly-instantiated-classes-and-functions)

<details>
<summary>サポートされる前の情報（クリックで開閉）</summary>
<div>

```dart
final Type peachType = Peach<Mario>;  // ダメ
```

これを可能にする方法が Stack Overflow にありました。

```dart
Type typeOf<T>() => T;

main() {
  Type type = typeOf<MyClass<int>>();
  print(type);
}
```

[flutter - How to get generic Type? - Stack Overflow](https://stackoverflow.com/a/52891829/12086560)

少しややこしいのですが、

- ジェネリック型 `T` を受け取って `Type` 型として `T` を返すメソッド `typeOf()` を用意
- そのメソッドの戻り値は `Type` 型だから `Type` 型の変数に代入できる

という Hack です。
これを先ほどの `Peach<T>` でもやってみましょう。

```dart
Type typeOf<T>() => T;
final peachType = typeOf<Peach<Mario>>();
print(peachType);  // Peach<Mario>
```

できました。

ついでですが、引数で型を渡したいときにわざわざ `typeOf()` を用意したくなかったらどうしますか？

```dart
void printType(Type type) {
  print(type);
}
```

渡す関数はこうなっているとします。下の答えを見ずに、どうやって渡すかちょっと考えてみてください。

・・・

自力でできましたか？

```dart
printType((<T>() => T)<Peach<Mario>>());  // Peach<Mario>
```

このようにできますが、可読性が低くなりますからやはり `typeOf()` を用意するほうが良いでしょう。
</div>
</details>

# 型エイリアス

型には `typedef` でエイリアス（別名）を付けることができます。
この機能はもともと関数型にしか使えませんでしたが、Dart 2.13 にて他の型にも使えるように拡張されました。[^dart213]

[^dart213]: https://medium.com/dartlang/announcing-dart-2-13-c6d547b57067

ポイントは、あくまで別名だという点です。
`int` に `MyInt` という別名を付けるとすると、次のようにその二つの型を使っても実際には同一です。

```dart
typedef MyInt = int;

void main() {
  final int value1 = 10;
  final MyInt value2 = 10;

  // 値同士の比較
  print(value1 == value2);    // true

  // 値と型の比較
  print(value1 is MyInt);     // true
  print(value2 is int);       // true

  // 型同士の比較
  print(MyInt == int);        // true

  // 型をprint
  print(value2.runtimeType);  // int

  Type typeOf<T>() => T;
  print(typeOf<MyInt>());     // int

  // 元の型へのキャスト
  print(value2 as int);       // Unnecessary cast. という警告が出る
}
```

長い型に短い別名を付けて元の型と同じように使えるのは便利です。
例えば、`Map<String, List<int>>` を `IntListMap` という短い名前にして使うと記述が少しすっきりします。

```dart
typedef IntListMap = Map<String, List<int>>;

void printMapSum1(IntListMap m) {
  print(m.map((k, v) => MapEntry(k, v.reduce((a, b) => a + b))));
}

...

final map1 = {'a': [1, 2, 3], 'b': [4, 5, 6]};
printMapSum1(map1);  // {'a': 6, 'b': 15}
```

ジェネリクスを組み合わせることもできます。

```dart
typedef NumListMap<T extends num> = Map<String, List<T>>;

void printMapSum2(NumListMap<double> m) {
  print(m.map((k, v) => MapEntry(k, v.reduce((a, b) => a + b))));
}

...

final map2 = {'a': [1.1, 2.2, 3.3], 'b': [4.4, 5.5, 6.6]};
printMapSum2(map2);  // {a: 6.6, b: 16.5}
```

型エイリアスは型の意味を名前でわかりやすくする効果もあります。
上の例では型をそのまま説明する名前にしましたが、もっと用途を理解しやすい名前にすると良いと思います。

ただし、用途を説明することはできても用途を限定することまではできません。
例えば価格に使う型ということで `int` に `Price` という別名を付けても `Price` 以外の `int` にも使えてしまいます。

```dart
typedef Price = int;
typedef Tax = int;

...

final Tax tax = 10;
final Price price = tax;
```

ちゃんと区別したいなら独自の型（クラス）を作るほうが良いでしょう。

# オーバーライド

メソッド等の戻り値や引数をオーバーライドしたい特殊なケースが稀ながらあり得ます。
必要となったときに読み返せるようにまとめておきます。

## メソッドやゲッターの戻り値

親クラスのメソッドやゲッターをオーバーライドするとき、戻り値を派生型に狭めることができます。

下記の例ではオーバーライドした側で `num` の派生型である `int` にしています。

```dart
class Mario {
  final _height = 175.5;

  num get height => _height;
}

class Mario2 extends Mario {
  @override
  int get height => _height.toInt();
}
```

この逆（親側が `int`、子側が `num`）はできないようです。

## メソッドやセッターの引数

メソッドやセッターの引数をオーバーライドするときは基底型に変えることができます。
戻り値とは逆なので注意が必要です。

```dart
class Mario {
  num _height;

  set height(int height) => _height = height;

  ...
}

class Mario2 extends Mario {
  @override
  set height(num height) => _height = height;
}
```

### covariant

`covariant` というキーワードを付ければ引数を派生型に変えることもできます。
付けるのは親側でも子側でも OK です。

```dart
class Mario {
  num _height;

  set height(covariant num height) => _height = height;

  ...
}

class Mario2 extends Mario {
  @override
  set height(int height) => _height = height;
}
```

# オブジェクトの同一性判定

オブジェクトが同一かどうかを判定できるようにしたい場合、これもまた型が関わってきます。

オブジェクトを比較可能にするには、演算子をオーバーライドして比較の条件を自分で設定します。
例えば等価の判定には等価演算子をオーバーライドします（同時に `hashCode` のオーバーライドも必要になります）。[^override] [^hash]

[^override]: オーバーライドの対象は、基底クラスたる [Object クラス](https://api.dart.dev/stable/dart-core/Object-class.html) が持つ比較演算子と `hashCode` です。
[^hash]: `Object.hashAll([...])` は Dart 2.14 で導入されました。他に [Object.hash([object1, object2, ...])](https://api.dart.dev/stable/dart-core/Object/hash.html) と [Object.hashAllUnordered([...])](https://api.dart.dev/stable/dart-core/Object/hashAllUnordered.html) があります。

```dart
class Person {
  Person(this.name);

  final String name;
}

class Mario extends Person {
  Mario() : super('Mario');

  @override
  bool operator ==(Object other) =>
      identical(other, this) || other is Mario && name == other.name;

  @override
  int get hashCode => Object.hashAll([name]);
}

class Luigi extends Person {
  Luigi() : super('Luigi');
}
```

同一性を判定できる機能を `Mario` に持たせました。
こうしておけば次のように等価演算子で比較できます。

```dart
print(Mario() == Mario()); // true
print(Mario() == Luigi()); // false
```

`bool operator ==(Object other)` の `other` には、上記のように比較したときの右辺の値が渡されます。

- **identical(other, this)**
    - 両辺で同じオブジェクトを参照しているかどうか（無しでも動作します）
- **other is Mario**
    - 右辺も `Mario` 型かどうか
    - その後ろで `other.name` が使えるようにするための型チェックでもあります
        - 「どういうこと？」と思った方は [型チェック](#型チェック) のところを読み直してみてください
- **name == other.name**
    - 両辺の `name` が同一かどうか

つまり「型が `Mario`」かつ「名前が一致」していれば同じ人物だと判定されます。

## 判定のカスタマイズ

ちょっと変えて、口ひげがある人なら誰でもマリオになる世界にしてみましょう。
これもなるべく自力でやってみてください。

・・・

できましたか？

では答えの一例です。
`hasMustache` というフラグを `Person` クラスに持たせました。

```dart
class Person {
  Person(this.hasMustache);

  final bool hasMustache;
}

class Mario extends Person {
  Mario({bool hasMustache = false}) : super(hasMustache);

  @override
  bool operator ==(Object other) =>
      identical(other, this) || other is Person && hasMustache && other.hasMustache;

  @override
  int get hashCode => Object.hashAll([hasMustache]);
}

class Luigi extends Person {
  Luigi({bool hasMustache = false}) : super(hasMustache);
}
```

「`Person` 型かそれを継承した型」かつ「口ひげがある」ことを同一性の判定条件にしました。

```dart
final mario = Mario(hasMustache: true);
final luigi = Luigi(hasMustache: true);
print(mario == luigi);   // true

final mario2 = Mario();
print(mario == mario2);  // false

final mario3 = Mario();
print(mario2 == mario3); // false
```

- 口ひげがある `Mario` と口ひげがある `Luigi`
    - 同一人物
- 口ひげがある `Mario` と口ひげがない `Mario`
    - 別人
- 口ひげがない `Mario` と口ひげがない `Mario`
    - 別人

まるで同一人物のようであっても「口ひげがある」という条件に該当しなければ別人と判定されます。
こんな変わった比較はあまりしないと思いますが、そんなこともできますよという例でした。
型の面白さが見えたでしょうか？

---

以上です。
null safety に特化した続編もご覧ください。

- [Dartの型の理解しておきたいあれこれ（Null safety編）](https://qiita.com/kabochapo/items/fbca2b2972546eeadcfa)

# 参考

- Language tour の [Type test operators](https://dart.dev/guides/language/language-tour#type-test-operators) の部分
- Effective Dart: Design の [Equality](https://dart.dev/guides/language/effective-dart/design#equality) の部分