106

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

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

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

クラスまたはそのクライアントにどのような制限を課しますか?

非ケース クラスを使用する必要がある状況はありますか?

4

5 に答える 5

100

まず良い点:

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

varはい。必要に応じて(を使用して)オーバーライドすることもできます。

ゲッターは自動的に定義されます

パラメータの前にプレフィックスを付けることで、どのクラスでも可能val

まともなtoString()実装

はい、非常に便利ですが、必要に応じてどのクラスでも手作業で実行できます

準拠equals()およびhashCode()

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

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

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

このリストには、Scala2.8に来るのに最適なものの1つである超強力なコピー方法も含める必要があります


次に悪いことに、ケースクラスにはほんの一握りの実際の制限があります。

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

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

サブクラス化することはできません

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

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

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

これだけ多くのパラメータを持つクラスの悪用をやめることを除いて、実際の回避策はありません:)

また...

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

これは、高機能のレイジーデータ構造を実装する場合に関連し、Scalaの将来のリリースにレイジーパラメーターを追加することで解決されることを願っています。

于 2011-01-11T10:37:42.113 に答える
51

1 つの大きな欠点: ケース クラスはケース クラスを拡張できません。それが制限です。

完全を期すために記載されている、見逃したその他の利点:準拠したシリアル化/逆シリアル化、作成に「new」キーワードを使用する必要はありません。

ミュータブルな状態、プライベートな状態、または状態のないオブジェクト (ほとんどのシングルトン コンポーネントなど) には、非ケース クラスを好みます。他のほとんどすべてのケース クラス。

于 2011-01-11T02:02:56.823 に答える
11

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

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

于 2011-01-11T21:55:29.277 に答える
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 次元での拡張性の問題は、式の問題と呼ばれています。

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

ここに画像の説明を入力

于 2016-06-09T12:24:59.767 に答える