case class(ケースクラス)
case classとは以前に説明した通り、class宣言時にclassの前にcaseを付けると便利なメソッドやコンパニオンオブジェクトを自動生成してくれるものである。
前回は詳しく解説しなかったが、ここで少し詳しく解説する。
case classによって自動生成されるものをは次のとおりである。
自動生成内容 | 説明 |
---|---|
valフィールド | 基本コンストラクターの引数全てに「val」を付けた状態になる。 つまりフィールドが自動的に定義される。 (対象はあくまで基本コンストラクターであり、補助コンストラクターは無関係) 引数定義時に「var」を明示的につけていれば「var」になる。 |
equals() | 各フィールドの内容を比較するメソッド。 |
hashCode() | 各フィールドの内容を元にハッシュ値を算出するメソッド。 |
toString() | 自分のクラスメイト各フィールドの内容を出力するメソッド。 |
canEaual() | 自分と同じクラスだったらtrueを返すメソッド。 |
copy() | 値をコピーして新しいインスタンスを生成するメソッド。 基本コンストラクターと同様の引数を受け取り、自分と同じクラスのインスタンスを返す。 引数は可変で、足りない部分は自分のインスタンスの値が使われる。 基本コンストラクターの変数名を使用した指定(名前指定引数)も可能 |
productAiry productElement() productIterator productPrefix |
Productトレイトがミックスインされる。 つまり、Productの各メソッドが呼び出せる。 例えば、productAiryでコンストラクターの引数(フィールド)の個数、productElement(n).productIteratorで引数の値、productPrefixでクラス名の文字列が取れる。 |
apply() | コンパニオンオブジェクトが作られ、apply()メソッドが実装される。 |
unapply() unapplySeq() |
コンパニオンオブジェクトが作られ、unapply()メソッドが実装される。 (基本コンストラクターの引数が可変長だった場合はunapplySeq()) つまりmatch式のcaseに指定できる。 |
caseを付けた際に生成される、unapply()もしくはunapplySeq()メソッドによってmatch式のcaseに指定できるようになる。
そのため、caseを付けなければmatch式のcaseに指定できないということである。
パターンマッチングの真価が発揮されるのは、標準ライブラリまたはユーザが定義したクラス(case class)によるデータ型の定義が必要です。
簡単なcase classを定義してみます。 sealedを使うと、同一ソースファイル内のクラスは継承できるが、別ファイルのクラスからは継承できなくなります。 抽象クラスの場合はabstractを付けます。
sealed abstract class DayOfWeek
case object Sunday extends DayOfWeek
case object Monday extends DayOfWeek
case object Tuesday extends DayOfWeek
case object Wednesday extends DayOfWeek
case object Thursday extends DayOfWeek
case object Friday extends DayOfWeek
case object Saturday extends DayOfWeek
val x: DayOfWeek = Sunday
x match {
case Sunday => 1
case Monday => 2
case Tuesday => 3
case Wednesday => 4
case Thursday => 5
case Friday => 6
// case Saturday => 7
}
パターンマッチに漏れがあった場合、コンパイラが警告してくれます。
この警告は、sealed修飾子をスーパークラス/トレイトに付けることによって、その(直接)サブクラス/トレイトは同じファイル内にしか定義できないという性質を利用して実現されています。
この用途以外でsealdeはめったに使われないので、ケースクラスのスーパークラス/トレイトにはsealedを付けるものだと覚えておけば良いでしょう。
ScalaのパターンマッチがCやJavaの列挙型と異なるのは、各々のデータは独立してパラメータを持つことができることです。また、パターンマッチの際はそのデータ型お種類によって 分岐するだけでなく、データを分解することができることが特徴です。
例として四則演算を表す構文木を考えてます。各ノードExpを継承し(つまり、全てのノードは式である)、二項演算を表すノードはそれぞれの子としてlhs(左辺)、rhs(右辺)を持つこととします。
葉ノードとして整数リテラル(Lit)も入れます。これはIntの値を取るものとします。また、二項演算の結果として小数点が現れた場合は小数部を切り捨てることとします。
これらを表すデータ型をScalaで定義すると次のようになります。
sealed abstract class Exp
case class Add(lhs: Exp, rhs: Exp) extends Exp
case class Sub(lhs: Exp, rhs: Exp) extends Exp
case class Mul(lhs: Exp, rhs: Exp) extends Exp
case class Div(lhs: Exp, rhs: Exp) extends Exp
case class Lit(value: Int) extends Exp
def eval(exp: Exp): Int = exp match {
case Add(l, r) => eval(l) + eval(r)
case Sub(l, r) => eval(l) - eval(r)
case Mul(l, r) => eval(l) * eval(r)
case Div(l, r) => eval(l) / eval(r)
case Lit(v) => v
}
//1 + ((2 * 3) / 2)
val sample = Add(Lit(1), Div(Mul(Lit(2), Lit(3)), Lit(2)))
println(eval(sample))
ここで注目すべきは、
- ノードの種類と構造によって分岐する。
- ネストしたノードを分解する。
- ネストしたノードを分解した結果を変数に束縛する。
という3つの動作が同時に行えていることです。これがケースクラスを使ったデータ型とパターンマッチングの組み合わせの強力さです。
match式中のパターンマッチングのみを扱ってきましたが、実は変数宣言でもパターンマッチングを行うことができます。
例えば、次のようなケースクラスPointがあったとします。
case class Point(m: Int, n: Int)
val Point(m, n) = Point(10, 20)
println(m + "," + n)
とすると、mに10がnに20が束縛されます。もしパターンにマッチしなかった場合は例外が発生してしまうので、変数宣言におけるパターンマッチングは、それが必ず成功すると型情報から確信できる場合にのみ使いましょう。