web-dev-qa-db-ja.com

ケースクラスコンパニオンで適用をオーバーライドする方法

状況は次のとおりです。ケースクラスを次のように定義します。

case class A(val s: String)

そして、クラスのインスタンスを作成するときに、「s」の値が常に大文字になるようにオブジェクトを定義します。

object A {
  def apply(s: String) = new A(s.toUpperCase)
}

ただし、Scalaはapply(s:String)メソッドが2回定義されていると不平を言っているため、これは動作しません。これを実現する別の方法はありますか?パターンマッチングに使用したいので、ケースクラスに固執したいと思います。

81
John S

競合の理由は、ケースクラスがまったく同じapply()メソッドを提供するためです(同じシグネチャ)。

まず、requireを使用することをお勧めします。

case class A(s: String) {
  require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}

Sに小文字の文字が含まれるインスタンスをユーザーが作成しようとすると、例外がスローされます。コンストラクターに入れるものは、パターンマッチング(match)を使用するときに得られるものでもあるため、これはケースクラスの適切な使用法です。

これが望んでいない場合は、コンストラクタをprivateにして、ユーザーにonlyを適用メソッドを使用するように強制します。

class A private (val s: String) {
}

object A {
  def apply(s: String): A = new A(s.toUpperCase)
}

ご覧のとおり、Aはcase classではなくなりました。 「ケースクラス」という名前は、matchを使用して(変更されていない)コンストラクター引数を抽出できることを意味するため、不変フィールドを持つケースクラスが着信値の変更を意味するかどうかはわかりません。

88
olle kullberg

更新2016/02/25:
以下に書いた回答で十分ですが、ケースクラスのコンパニオンオブジェクトに関して、これに関連する別の回答も参照する価値があります。つまり、 コンパイラが生成した暗黙的なコンパニオンオブジェクトを正確に再現する方法 これは、ケースクラス自体を定義するだけの場合に発生します。私にとっては、直感に反することがわかりました。


要約:
ケースクラスパラメータの値は、まだ有効な(指定された)ADT(抽象データ型)のままで、ケースクラスに格納する前に変更できます。解決策は比較的簡単でしたが、詳細を見つけるのはかなり困難でした。

詳細:
ADT(Abstract Data Type)の背後にある必須の前提である、ケースクラスの有効なインスタンスのみをインスタンス化できるようにしたい場合、やらなければならないことがいくつかあります。

たとえば、コンパイラによって生成されたcopyメソッドは、デフォルトでケースクラスで提供されます。したがって、大文字の値のみを含めることができることを保証する明示的なコンパニオンオブジェクトのapplyメソッドを介してインスタンスのみが作成されるように非常に慎重にしたとしても、次のコードは小文字のケースクラスインスタンスを生成します値:

val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"

さらに、ケースクラスはJava.io.Serializableを実装します。つまり、大文字のインスタンスのみを使用するという慎重な戦略は、単純なテキストエディターと逆シリアル化によって覆すことができます。

そのため、ケースクラスを使用するさまざまな方法(好意的および/または悪意的に)について、以下のアクションを実行する必要があります。

  1. 明示的なコンパニオンオブジェクトの場合:
    1. ケースクラスとまったく同じ名前を使用して作成します
      • これは、ケースクラスのプライベートパーツにアクセスできます。
    2. ケースクラスのプライマリコンストラクターとまったく同じシグネチャを持つapplyメソッドを作成します
      • これは、ステップ2.1が完了すると正常にコンパイルされます
    3. new演算子を使用してケースクラスのインスタンスを取得し、空の実装{} を提供する実装を提供します
      • これにより、厳密に条件に基づいてケースクラスがインスタンス化されます。
      • ケースクラスがabstractとして宣言されているため、空の実装{}を提供する必要があります(ステップ2.1を参照)
  2. ケースクラスの場合:
    1. abstract を宣言します
      • Scalaコンパイラがコンパニオンオブジェクトでapplyメソッドを生成しないようにします。これにより、 "method is defined ..."コンパイルエラーが発生します(上記のステップ1.2)
    2. プライマリコンストラクターをprivate[A] としてマークします。
      • プライマリコンストラクターは、ケースクラス自体とそのコンパニオンオブジェクト(上記でステップ1.1で定義したもの)でのみ使用できるようになりました
    3. readResolveメソッドを作成します
      1. Applyメソッドを使用して実装を提供します(上記のステップ1.2)
    4. copyメソッドを作成します
      1. ケースクラスのプライマリコンストラクターとまったく同じシグネチャを持つように定義します
      2. パラメーターごとに、同じパラメーター名を使用してデフォルト値を追加します(例:s: String = s
      3. Applyメソッドを使用して実装を提供する(以下のステップ1.2)

上記のアクションで変更されたコードは次のとおりです。

object A {
  def apply(s: String, i: Int): A =
    new A(s.toUpperCase, i) {} //abstract class implementation intentionally empty
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

そして、require(@ollekullbergの回答で推奨)を実装し、あらゆる種類のキャッシングを配置する理想的な場所を特定した後のコードを次に示します。

object A {
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i) {} //abstract class implementation intentionally empty
  }
}
abstract case class A private[A] (s: String, i: Int) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

そして、このコードがJava interopを介して使用される場合、このバージョンはより安全/堅牢です(実装としてケースクラスを非表示にし、派生を防ぐ最終クラスを作成します):

object A {
  private[A] abstract case class AImpl private[A] (s: String, i: Int)
  def apply(s: String, i: Int): A = {
    require(s.forall(_.isUpper), s"Bad String: $s")
    //TODO: Insert normal instance caching mechanism here
    new A(s, i)
  }
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
  private def readResolve(): Object = //to ensure validation and possible singleton-ness, must override readResolve to use explicit companion object apply method
    A.apply(s, i)
  def copy(s: String = s, i: Int = i): A =
    A.apply(s, i)
}

これはあなたの質問に直接答えますが、インスタンスキャッシングを超えてケースクラスを中心にこの経路を拡張する方法はさらにあります。私自身のプロジェクトのニーズに合わせて、私は さらに広範なソリューションを作成しました を持っています CodeReviewに文書化されています (StackOverflowの姉妹サイト)。私のソリューションを使用したり活用したりして見直した場合は、フィードバック、提案、質問を残してください。理由の範囲内で、私は一日以内に対応するために最善を尽くします。

25

コンパニオンオブジェクトのapplyメソッドをオーバーライドする方法がわかりません(可能な場合)が、大文字の文字列に特別な型を使用することもできます。

class UpperCaseString(s: String) extends Proxy {
  val self: String = s.toUpperCase
}

implicit def stringToUpperCaseString(s: String) = new UpperCaseString(s)
implicit def upperCaseStringToString(s: UpperCaseString) = s.self

case class A(val s: UpperCaseString)

println(A("hello"))

上記のコード出力:

A(HELLO)

また、この質問とその答えを見てください: スカラ:デフォルトのケースクラスコンストラクタをオーバーライドすることは可能ですか?

12
Frank S. Thomas

2017年4月以降にこれを読んでいる人の場合:Scala 2.12.2+以降、Scala は適用と適用解除のオーバーライドを許可しますデフォルトで 。この動作は、Scala 2.11.11+)で-Xsource:2.12オプションをコンパイラに指定することで取得できます。

6
Mehmet Emre

ケースクラスを保持し、暗黙のdefや別のコンストラクタを持たない別のアイデアは、applyの署名をユーザーの観点からは少し異なるようにすることです。どこかで暗黙のトリックを見たことがありますが、どの暗黙の引数であったかを思い出せない/見つけることができないので、ここでBooleanを選択しました。誰かが私を助けてトリックを完了することができるなら...

object A {
  def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase)
}
case class A(s: String)
4
Peter Schmitz

Var変数で動作します:

case class A(var s: String) {
   // Conversion
   s = s.toUpperCase
}

このプラクティスは、別のコンストラクタを定義する代わりに、ケースクラスで明らかに推奨されます。 こちらをご覧ください 。オブジェクトをコピーするときも、同じ変更を保持します。

4
Mikaël Mayer

私は同じ問題に直面し、この解決策は私にとっては問題ありません:

sealed trait A {
  def s:String
}

object A {
  private case class AImpl(s:String)
  def apply(s:String):A = AImpl(s.toUpperCase)
}

また、メソッドが必要な場合は、トレイトで定義し、ケースクラスでオーバーライドします。

2
Pere Ramirez

古いscalaでデフォルトでオーバーライドできない場合、または@ mehmet-emreが示したようにコンパイラフラグを追加したくない場合、ケースクラスが必要な場合は、以下:

case class A(private val _s: String) {
  val s = _s.toUpperCase
}
0
critium