Scalaを勉強していて出てきた言葉、共変と反変。
共変は直感的に理解しやすかったですが、反変は最初よく分かりませんでした。
それが、以下の様な例えを考えたら何となく分かってきました。
はじめに、型パラメータとなるクラス
こんな2つのクラスがあるとします。
class Food
class Sushi extends Food
これらのクラスを型パラメータとするクラスを、これから考えていきます。
わりと理解しやすい共変
共変の例として、こんなクラスがあるとします。
class Restaurant[+T] {
def serve(): T = // 何かしらT型を返す実装...
}
そうすると、こんな変数を定義できます。
val familyRestaurant: Restaurant[Food] = ...
// familyRestaurant.serve()はFoodを返す。
val sushiya: Restaurant[Sushi] = ...
// sushiya.serve()はSushiを返す。
型パラメータTが共変ということは、Restraurant[Food]型の変数にRestaurant[Sushi]型の変数を代入できる、つまり以下の様に考えられます。
val restaurant1: Restaurant[Food] = familyRestaurant
// とりあえずこれはOK。restaurant1.serve()はFoodを返す。
val restaurant2: Restaurant[Food] = sushiya
// 型パラメータが共変だからこれもOK! restaurant2.serve()はFoodを返す。
// restaurant2の実体は寿司屋だけど、寿司屋が返すSushiはFoodの一種だから問題なし。
val restaurant3: Restaurant[Sushi] = familyRestaurant
// これはコンパイルエラー。restaurant3.serve()はSushiを返さないといけない。
// だけどrestaurant3の実体はファミレス。
// ファミレスが返すのは大まかな括りであるFoodであり、Sushiとは限らないのでコンパイルエラー。
こんな風に考えて、共変が分かった気になりました。
共変は直感的にも理解しやすいですよね。
そして反変
いよいよ小難しそうな反変を考えてみます。
例として、こんなクラスがあるとします。
class Eater[-T] {
def eat(menu: T) = // T型の引数を受け取ってモグモグする実装...
}
そうすると、さっきと同じようにこんな変数を定義できます。
val kuishinbo: Eater[Food]
// 食いしん坊は何でも食べられる。
// kuishinbo.eat(new Food)とすることもできるし、
// kuishinbo.eat(new Sushi)でもよい。
val edokko: Eater[Sushi]
// 江戸っ子は寿司しか食べない。
// edokko.eat(new Sushi)はいいけど、
// edokko.eat(new Food)はコンパイルエラーになってしまう。
型パラメータTが反変ということはえーと、[...]の中身の関係が共変と逆だから、Eater[Sushi]型の変数にEater[Food]型の変数を代入できる、つまり以下の様に考えられます。
val customer1: Eater[Food] = kuishinbo
// とりあえずこれはOK。customer1.eat()はFoodを受け取れる。
val customer2: Eater[Sushi] = kuishinbo
// 型パラメータが反変だからこれもOK! customer2.eat()はSushiを受け取れないといけない。
// customer2の実体は食いしん坊だから、喜んでSushiを食べられる。
val customer3: Eater[Food] = edokko
// これはコンパイルエラー。 customer3.eat()はFoodを受け取れないといけない。
// だけどcustomer3の実体は江戸っ子。Sushiならいいけど、Food全般となると無理。
いやー、これで反変も分かった気になりました。
さらっと流しましたが、共変の型パラメータの使い所はメソッドの返り値(例でいうとserve())、反変の型パラメータの使い所はメソッドの引数(例でいうとeat())、というのが基本になります。