Scalaケースクラスを宣言することの欠点は何ですか?


105

多くの美しく不変のデータ構造を使用するコードを記述している場合、caseクラスは天の恵みのように見え、1つのキーワードだけで以下のすべてを無料で提供します。

  • デフォルトではすべて不変
  • 自動的に定義されたゲッター
  • まともなtoString()実装
  • 準拠するequals()およびhashCode()
  • マッチングのためのunapply()メソッドを持つコンパニオンオブジェクト

しかし、不変のデータ構造をケースクラスとして定義することの欠点は何ですか?

クラスまたはそのクライアントにはどのような制限がありますか?

ケース以外のクラスを優先する必要がある状況はありますか?


この関連質問を参照してください:stackoverflow.com/q/4635765/156410
David

18
なぜこれは建設的ではないのですか?このサイトの改造は厳しすぎる。これには有限数の可能な事実の答えがあります。
Eloff、2015年

5
Eloffに同意します。これも私が回答を求めていた質問であり、提供された回答は非常に有用であり、主観的に見えない。私は多くの「コードの抜粋を修正する方法」の質問を見て、より多くの議論と意見を引き出しました。
Herc、2015年

回答:


51

大きな欠点の1つは、ケースクラスがケースクラスを拡張できないことです。それが制限です。

完全性のためにリストされている、見逃したその他の利点:準拠したシリアライゼーション/デシリアライゼーション、作成に「新しい」キーワードを使用する必要はありません。

変更可能な状態、プライベート状態、または状態なしのオブジェクト(たとえば、ほとんどのシングルトンコンポーネント)の場合は、非ケースクラスを好みます。その他すべてのケースクラス。


48
ケースクラスをサブクラス化できます。サブクラスをケースクラスにすることもできません—これが制限です。
Seth Tisue、2011年

99

まず良いビット:

デフォルトではすべて不変

はい、var必要な場合は(を使用して)オーバーライドすることもできます

自動的に定義されたゲッター

paramsの前に val

まともなtoString()実装

はい、非常に便利ですが、必要に応じて任意のクラスで手動で実行できます

準拠equals()およびhashCode()

簡単なパターンマッチングと組み合わせると、これが人々がケースクラスを使用する主な理由です

unapply()マッチングのためのメソッドを持つコンパニオンオブジェクト

エクストラクターを使用して任意のクラスで手動で行うこともできます

このリストには、Scala 2.8で最高のコピー方法の1つである、超強力なコピー方法も含める必要があります。


次に悪いことに、caseクラスには実際の制限がほんの少ししかありません。

applyコンパイラが生成したメソッドと同じシグネチャを使用してコンパニオンオブジェクトで定義することはできません

しかし実際には、これが問題になることはめったにありません。生成された適用メソッドの動作の変更はユーザーを驚かせることが保証されており、強く推奨されません。そうすることの唯一の正当化は、入力パラメーターの検証です。メインのコンストラクター本体で実行するのが最適なタスクです(これにより、を使用するときに検証を利用できるようになりますcopy

サブクラス化できません

確かに、ケースクラス自体が子孫になることも可能です。一般的なパターンの1つは、ツリーのリーフノードとしてケースクラスを使用して、特性のクラス階層を構築することです。

sealed修飾子にも注目する価値があります。この修飾子を持つ特性のサブクラスは、同じファイルで宣言する必要あります。トレイトのインスタンスに対してパターンマッチングを行うとき、考えられるすべての具象サブクラスをチェックしていない場合、コンパイラーは警告を表示します。警告なしでコンパイルすると、ケースクラスと組み合わせると、コードに非常に高いレベルの信頼性を提供できます。

Productのサブクラスとして、ケースクラスは22を超えるパラメーターを持つことはできません

これほど多くのパラメーターを持つクラスの悪用を停止する以外は、実際の回避策はありません:)

また...

ときどき気付かれるもう1つの制限は、Scalaが(現在のところ)遅延lazy valパラメーター(sのようにパラメーターとして)をサポートしていないことです。これに対する回避策は、名前によるparamを使用し、コンストラクターで遅延valに割り当てることです。残念ながら、名前によるパラメータはパターンマッチングと混ざりません。これにより、コンパイラが生成したエクストラクタが破損するため、ケースクラスでこのテクニックを使用できなくなります。

これは、高機能の遅延データ構造を実装する場合に関連し、Scalaの将来のリリースに遅延パラメーターを追加することで解決されると期待されます。


1
包括的な答えをありがとう。「サブクラスにできない」という例外は、おそらくすぐに私を段階的にする可能性は低いと思います。
グラハムリー

15
ケースクラスをサブクラス化できます。サブクラスをケースクラスにすることもできません—これが制限です。
Seth Tisue、2011年

5
ケースクラスの22パラメータの制限は、Scala 2.11で削除されました。issues.scala-lang.org/browse/SI-7296
ジョナサンクロスマー2015年

「コンパイラが生成したメソッドと同じシグネチャを使用して、コンパニオンオブジェクトで適用を定義することはできません」と断言するのは誤りです。それは(あなたが目に見えないScalaのコンパイラによって生成されるように使用される機能を維持する場合)、そうするために、いくつかのフープを介してジャンプが必要ですが、それは最も確かに達成することができます。stackoverflow.com/a/25538287/501113
chaotic3quilibrium

私はScalaケースクラスを広範囲に使用しており、「ケースクラスパターン」(最終的にはScalaマクロになる)を考案
chaotic3quilibrium 2015

10

ここではTDDの原則が適用されると思います。過度に設計しないでください。何かをcase classとして宣言すると、多くの機能が宣言されます。これにより、将来クラスを変更する際の柔軟性が低下します。

たとえば、aにcase classequals、コンストラクターパラメーターに対するメソッドがあります。最初にクラスを作成するときは気にしないかもしれませんが、後者では、これらのパラメーターの一部を無視するか、少し異なることを行うかどうかを決定する場合があります。ただし、クライアントコードは、case class平等に依存するその間に作成される場合があります。


4
クライアントコードは、「等しい」の正確な意味に依存すべきではないと思います。「等しい」が何を意味するかを決定するのはクラス次第です。クラス作成者は、「等しい」の実装を自由に変更できるようにする必要があります。
2011

8
@pkaedingクライアントコードがプライベートメソッドに依存しないようにすることは自由です。公開されているものはすべて、あなたが同意した契約です。
ダニエルC.ソブラル

3
@ DanielC.Sobral真ですが、equals()(それが基づいているフィールド)の正確な実装は、必ずしもコントラクトに含まれているとは限りません。少なくとも、クラスを最初に作成するときに、明示的にコントラクトから除外することができます。
ハーマン2013年

2
@ DanielC.Sobralあなたは自分と矛盾しています:あなたは人々が(オブジェクトのアイデンティティを比較する)デフォルトのequals実装にさえ依存するとさえ言うでしょう。それが本当で、後で別のequals実装を作成すると、それらのコードも壊れます。とにかく、事前/事後条件と不変条件を指定し、人々がそれらを無視する場合、それは彼らの問題です。
ハーマン2013年

2
@herman私が言っていることに矛盾はありません。「彼らの問題」に関しては、それがあなたの問題にならない限り、確かに。たとえば、彼らがスタートアップの巨大なクライアントであったり、上司が変更するにはコストがかかりすぎると変更したり、変更を元に戻したりする必要がある、または変更により数百万ドルが発生することを上層部の経営者に納得させたりしたとします。バグが発生して元に戻されます。ただし、趣味のコードを記述していて、ユーザーを気にしない場合は、先に進んでください。
ダニエルC.ソブラル2013年

7

ケース以外のクラスを優先する必要がある状況はありますか?

Martin Oderskyは、クラスとケースクラスのどちらかを選択する必要があるときに使用できるScalaの関数型プログラミング原則(講義4.6-パターンマッチング)の優れた出発点を提供してくれます。Scala By Exampleの第7章にも同じが含まれています。

たとえば、算術式のインタープリターを作成したいとします。最初は物事をシンプルに保つために、数値と+演算だけに制限します。このような式は、抽象基本クラスExprをルートとし、2つのサブクラスNumberおよびSumを持つクラス階層として表すことができます。次に、式1 +(3 + 7)は次のように表されます。

new Sum(new Number(1)、new Sum(new Number(3)、new Number(7)))

abstract class Expr {
  def eval: Int
}

class Number(n: Int) extends Expr {
  def eval: Int = n
}

class Sum(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval + e2.eval
}

さらに、新しいProdクラスを追加しても、既存のコードは変更されません。

class Prod(e1: Expr, e2: Expr) extends Expr {
  def eval: Int = e1.eval * e2.eval
}

対照的に、新しいメソッドを追加するには、既存のすべてのクラスを変更する必要があります。

abstract class Expr { 
  def eval: Int 
  def print
} 

class Number(n: Int) extends Expr { 
  def eval: Int = n 
  def print { Console.print(n) }
}

class Sum(e1: Expr, e2: Expr) extends Expr { 
  def eval: Int = e1.eval + e2.eval
  def print { 
   Console.print("(")
   print(e1)
   Console.print("+")
   print(e2)
   Console.print(")")
  }
}

同じ問題がケースクラスで解決されました。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr

新しいメソッドの追加はローカルな変更です。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
  }
}

新しいProdクラスを追加するには、すべてのパターンマッチングを変更する必要がある可能性があります。

abstract class Expr {
  def eval: Int = this match {
    case Number(n) => n
    case Sum(e1, e2) => e1.eval + e2.eval
    case Prod(e1,e2) => e1.eval * e2.eval
  }
  def print = this match {
    case Number(n) => Console.print(n)
    case Sum(e1,e2) => {
      Console.print("(")
      print(e1)
      Console.print("+")
      print(e2)
      Console.print(")")
    }
    case Prod(e1,e2) => ...
  }
}

ビデオ講義4.6パターンマッチングのトランスクリプト

これらのデザインはどちらも完全に問題なく、どちらを選択するかはスタイルの問題になることがありますが、それでも重要な基準がいくつかあります。

基準の1つとして、式の新しいサブクラスをより頻繁に作成するのか、それとも新しいメソッドをより頻繁に作成するのか、などです。したがって、システムの将来の拡張性と可能な拡張パスを検討する基準となります。

主に新しいサブクラスを作成する場合は、実際にはオブジェクト指向の分解ソリューションが優位です。その理由は、evalメソッドを使用して新しいサブクラスを作成することは非常に簡単で非常にローカルな変更であるためです。機能ソリューションと同様に、evalメソッド内のコードに戻って変更し、新しいケースを追加する必要があります。それに。

一方で、多くの新しいメソッドを作成するが、クラス階層自体は比較的安定している場合は、パターンマッチングが実際に有利です。繰り返しになりますが、パターンマッチングソリューションの新しい各メソッドは、基本クラスに配置した場合でも、クラス階層の外部に配置した場合でも、ローカルでの変更すぎません。一方、オブジェクト指向分解でのshowなどの新しいメソッドでは、サブクラスごとに新しい増分が必要になります。だから、もっとパーツが必要になるでしょう。

したがって、階層に新しいクラスを追加したり、新しいメソッド、またはその両方を追加したりできる2次元でのこの拡張性の問題は、式の問題と呼ばれています

覚えておいてください。これは、唯一の基準ではなく、出発点のように使用する必要があります。

ここに画像の説明を入力してください


0

私はからこれを引用していますScala cookbookによってAlvin Alexander第6章:objects

これは、この本で私が興味深いと思った多くのことの1つです。

ケースクラスに複数のコンストラクターを提供するには、ケースクラス宣言が実際に何を行うかを知ることが重要です。

case class Person (var name: String)

Scalaコンパイラーがケースクラスの例に対して生成するコードを見ると、Person $ .classとPerson.classの2つの出力ファイルが作成されることがわかります。javapコマンドを使用してPerson $ .classを逆アセンブルすると、applyメソッドと他の多くのメソッドが含まれていることがわかります。

$ javap Person$
Compiled from "Person.scala"
public final class Person$ extends scala.runtime.AbstractFunction1 implements scala.ScalaObject,scala.Serializable{
public static final Person$ MODULE$;
public static {};
public final java.lang.String toString();
public scala.Option unapply(Person);
public Person apply(java.lang.String); // the apply method (returns a Person) public java.lang.Object readResolve();
        public java.lang.Object apply(java.lang.Object);
    }

Person.classを逆アセンブルして、その内容を確認することもできます。このような単純なクラスの場合、追加の20のメソッドが含まれます。この隠れた膨張は、一部の開発者がケースクラスを好まない理由の1つです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.