今回は Dart の基礎の一部を記事にしてみました。オブジェクト指向にも関わるところです。
応用的なところまで広く含んでいて、時々見返して思い出すのにも使えると思います。
本記事に含めていた null safety に関する部分は別記事に分離しましたので、あわせてお読みください。
型の比較
あまり気にしたことがない方が多いかもしれませんが、とても重要です。
しっかりと理解しておきましょう。
オブジェクトと型の比較
is
を使います。
class Person {}
class Mario extends Person{}
class Luigi extends Person{}
final mario = Mario();
print(mario is Mario); // true
print(mario is Luigi); // false
もし ==
を使ってしまうと、間違った比較方法なので常に false
になります。
analysis_options.yaml で unrelated_type_equality_checks のルールを有効にしておくと 「Equality operator ==
invocation with references of unrelated types.」と警告してくれて安全です。
print(mario == Mario); // false
print(mario == Luigi); // false
is
の否定形は is!
です。
!is
ではありません。
print(mario is! Mario); // false
print(mario is! Luigi); // true
Null 型
Dart では null
にも型があり、Null
というクラスとして定義されています。
そのコードは下記のとおりです。
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
型の(唯一の)インスタンスだからです。
また、型があるため、先ほどと同様の型比較が一応可能です。
print(null is Null); // true
ただし、このように比較すると「Tests for null should be done with '== null'.」という警告が出ます。
この警告メッセージに書かれているとおり ==
で比較するようにしましょう。
int v;
print(v == null); // true
型同士の比較
「オブジェクト vs 型」でなく「型 vs 型」の比較なら普通の比較なので等価演算子(==
)を使います。
print(Mario == Mario); // true
print(Mario == Luigi); // false
「Type 型という値 vs Type 型という値」とも言えます。
下記の t1 と t2 はどちらも Type 型です。
final t1 = Mario;
final t2 = Mario;
print(t1 == t2); // true
親との比較
Mario
というクラスは Person
を継承しています。
Mario
のオブジェクトは Mario
型であると同時に Person
型でもあるはずです。
検証してみましょう。
final mario = Mario();
print(mario is Mario); // true
print(mario is Person); // true
思ったとおりですね。
これを使えば、あるクラスを継承しているかどうかを判定できることになります。
では型同士の比較はどうでしょうか?
print(Mario == Mario); // true
print(Mario == Person); // false
Mario
型は Person
型ではないと判定されました。
継承関係は考慮されず、単純に同一の型かどうかという判定になります。
Object 型
Object
はあらゆるクラスの基底となるものです。
つまり、次のように is Object
という比較の結果は常に true
となります(Dart 2.12 未満)。1
※Null 安全においては Null 型はこれに該当しません(詳細は null safety 編 をご覧ください)。
print(mario is Object); // true
print(10 is Object); // true
print(null is Object); // true(Dart 2.12 以降では false)
Object
が基底となっていることを知っていると仕組みを理解しやすくなる場合があるかもしれません。
現在の型を得る
ランタイムの型を取得するには runtimeType
を使います。
Dart のあらゆるオブジェクトの基底クラスとなっている Object
クラスが持つプロパティで、Type
型です。
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
型のままになります。
つまり
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
を用いてしまうミスもやってしまいがちです。
気を付けましょう。
if (mario.runtimeType is Person) { ... } // ダメ
if (mario.runtimeType == Person) { ... } // ダメ
正しい方法は下記です(親との比較 のところで見たとおりです)。
if (mario is Person) { ... }
型チェック
Mario
クラスだけにメソッドを持たせてみます。
class Mario extends Person {
void greet() {
print("Hi! I'm Mario.");
}
}
先ほど person1
と person2
の runtimeType
は Mario
のままでした。
それなら Mario
クラスだけが持つメソッドを呼べそうな気がしなくもないです。どうでしょうか。
mario.greet(); // Hi! I'm Mario.
person1.greet(); // ダメ
person2.greet(); // ダメ
ダメですね。
runtimeType
では Mario
型となっても中身は Person
型であり、Mario
のメソッドにはアクセスできません。
この場合、方法は二つあります。
// 方法1
(person1 as Mario).greet();
// 方法2
if (person1 is Mario) {
person1.greet();
}
方法 1
Mario
型にダウンキャストする方法です。
これはキャストできないものに対して使ってしまうと例外が発生しますのでご注意ください。
null
の場合も例外が発生します。2
方法 2
型をチェックする方法で、こちらのほうが安全です。
「チェック」という表現にしていますが、先述の比較と同じです。
is
を使って Mario
として扱える型(Mario
自体かその派生)であることをチェックしただけです。
ポイントは、チェックが済んでいる位置では Dart が「person1
は Mario
型だ」として扱ってくれるようになって、Mario
にしかない greet()
が利用可能になるという点です。
ジェネリクス
ジェネリクスはまさに型に関するものです。
ここでジェネリックなピーチ姫の登場です。
活用例としてはいまいちですが、親しみやすそうな例としてこれにしました。
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 クラス内で
Person _husband;
void marry(Person husband) {
_husband = husband;
}
と書かれているのと同じ意味になります。
ピーチ姫にとって結婚相手なんて「人なら誰でもいいわ」という状態です。
マリオさんは Person
を継承しているので marry()
に渡すことができます。
Peach<Person>()
..marry(mario)
..speak(); // I'm of type Peach<Person>, and my husband is of type Person.
結婚できる型を絞って Mario
型にしてみましょう。
「マリオさんしか嫌!」というピーチ姫です。
Peach<Mario>()
..marry(mario)
..speak(); // I'm of type Peach<Mario>, and my husband is of type Mario.
「ルイージさんしか嫌!」とするとマリオさんは結婚相手の条件から外れます。
Peach<Luigi>().marry(mario); // marioはLuigi型でもその派生型でもないのでダメ
雰囲気が掴めましたか?
<T>
のところは <T extends RichPerson>
のように継承関係を指定することや <T, U>
など複数を指定することもできます。
慣れたらご自分で試してみてください。
型を変数に入れる
型自体を変数に代入したいときがあります。
先ほども Mario
という型を変数に入れました。
final t = Mario;
これは
final Type t = Mario;
ということです。
Type
という型の変数に型自体(を表す値)を入れておくことができるわけです。
runtimeType
も Type
型で、変数に普通に代入できます。
final mario = Mario();
final t = mario.runtimeType;
ジェネリクスの場合
しかしジェネリクスを使ったクラスの型はなんと単純な代入ができません。
Dart の言語仕様による制限です。
Dart 2.15 でできるようになりました。
⇒ 詳細情報
サポートされる前の情報(クリックで開閉)
final Type peachType = Peach<Mario>; // ダメ
これを可能にする方法が Stack Overflow にありました。
Type typeOf<T>() => T;
main() {
Type type = typeOf<MyClass<int>>();
print(type);
}
flutter - How to get generic Type? - Stack Overflow
少しややこしいのですが、
- ジェネリック型
T
を受け取ってType
型としてT
を返すメソッドtypeOf()
を用意 - そのメソッドの戻り値は
Type
型だからType
型の変数に代入できる
という Hack です。
これを先ほどの Peach<T>
でもやってみましょう。
Type typeOf<T>() => T;
final peachType = typeOf<Peach<Mario>>();
print(peachType); // Peach<Mario>
できました。
ついでですが、引数で型を渡したいときにわざわざ typeOf()
を用意したくなかったらどうしますか?
void printType(Type type) {
print(type);
}
渡す関数はこうなっているとします。下の答えを見ずに、どうやって渡すかちょっと考えてみてください。
・・・
自力でできましたか?
printType((<T>() => T)<Peach<Mario>>()); // Peach<Mario>
このようにできますが、可読性が低くなりますからやはり typeOf()
を用意するほうが良いでしょう。
型エイリアス
型には typedef
でエイリアス(別名)を付けることができます。
この機能はもともと関数型にしか使えませんでしたが、Dart 2.13 にて他の型にも使えるように拡張されました。3
ポイントは、あくまで別名だという点です。
int
に MyInt
という別名を付けるとすると、次のようにその二つの型を使っても実際には同一です。
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
という短い名前にして使うと記述が少しすっきりします。
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}
ジェネリクスを組み合わせることもできます。
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
にも使えてしまいます。
typedef Price = int;
typedef Tax = int;
...
final Tax tax = 10;
final Price price = tax;
ちゃんと区別したいなら独自の型(クラス)を作るほうが良いでしょう。
オーバーライド
メソッド等の戻り値や引数をオーバーライドしたい特殊なケースが稀ながらあり得ます。
必要となったときに読み返せるようにまとめておきます。
メソッドやゲッターの戻り値
親クラスのメソッドやゲッターをオーバーライドするとき、戻り値を派生型に狭めることができます。
下記の例ではオーバーライドした側で num
の派生型である int
にしています。
class Mario {
final _height = 175.5;
num get height => _height;
}
class Mario2 extends Mario {
@override
int get height => _height.toInt();
}
この逆(親側が int
、子側が num
)はできないようです。
メソッドやセッターの引数
メソッドやセッターの引数をオーバーライドするときは基底型に変えることができます。
戻り値とは逆なので注意が必要です。
class Mario {
num _height;
set height(int height) => _height = height;
...
}
class Mario2 extends Mario {
@override
set height(num height) => _height = height;
}
covariant
covariant
というキーワードを付ければ引数を派生型に変えることもできます。
付けるのは親側でも子側でも OK です。
class Mario {
num _height;
set height(covariant num height) => _height = height;
...
}
class Mario2 extends Mario {
@override
set height(int height) => _height = height;
}
オブジェクトの同一性判定
オブジェクトが同一かどうかを判定できるようにしたい場合、これもまた型が関わってきます。
オブジェクトを比較可能にするには、演算子をオーバーライドして比較の条件を自分で設定します。
例えば等価の判定には等価演算子をオーバーライドします(同時に hashCode
のオーバーライドも必要になります)。4 5
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
に持たせました。
こうしておけば次のように等価演算子で比較できます。
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
クラスに持たせました。
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
型かそれを継承した型」かつ「口ひげがある」ことを同一性の判定条件にしました。
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 に特化した続編もご覧ください。
参考
- Language tour の Type test operators の部分
- Effective Dart: Design の Equality の部分
-
必ず
true
となる比較なのでわざわざするのは意味はなくて無駄ですが、説明用にあえて比較しています。「Unnecessary type check, the result is always true.」という警告が出ます。 ↩ -
null
は(person1 as Mario)?.greet()
のように?.
を使えば対策できます。 ↩ -
https://medium.com/dartlang/announcing-dart-2-13-c6d547b57067 ↩
-
オーバーライドの対象は、基底クラスたる Object クラス が持つ比較演算子と
hashCode
です。 ↩ -
Object.hashAll([...])
は Dart 2.14 で導入されました。他に Object.hash([object1, object2, ...]) と Object.hashAllUnordered([...]) があります。 ↩