Así que aquí está la situación. Quiero definir una clase de caso así:
case class A(val s: String)
y quiero definir un objeto para asegurarme de que cuando creo instancias de la clase, el valor de 's' siempre esté en mayúsculas, así:
object A {
def apply(s: String) = new A(s.toUpperCase)
}
Sin embargo, esto no funciona ya que Scala se queja de que el método apply (s: String) se define dos veces. Entiendo que la sintaxis de la clase de caso la definirá automáticamente para mí, pero ¿no hay otra forma de lograr esto? Me gustaría seguir con la clase de caso ya que quiero usarla para la coincidencia de patrones.
scala
pattern-matching
case-class
John S
fuente
fuente
Respuestas:
El motivo del conflicto es que la clase de caso proporciona exactamente el mismo método apply () (misma firma).
En primer lugar, me gustaría sugerirle que use require:
case class A(s: String) { require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s) }
Esto generará una excepción si el usuario intenta crear una instancia donde s incluye caracteres en minúscula. Este es un buen uso de las clases de casos, ya que lo que pones en el constructor también es lo que obtienes cuando usas la coincidencia de patrones (
match
).Si esto no es lo que desea, entonces crearía el constructor
private
y obligaría a los usuarios a usar solo el método de aplicación:class A private (val s: String) { } object A { def apply(s: String): A = new A(s.toUpperCase) }
Como puede ver, A ya no es un
case class
. No estoy seguro si las clases de casos con campos inmutables están destinadas a la modificación de los valores entrantes, ya que el nombre "clase de casos" implica que debería ser posible extraer los argumentos del constructor (sin modificar) usandomatch
.fuente
toCharArray
llamada no es necesaria, también podrías escribirs.exists(_.isLower)
.s.forall(_.isUpper)
es más fácil de entender que!s.exists(_.isLower)
.s.forall(_isupper)
es más fácil de leer. Lo usaré junto con la sugerencia de @ olle.match
."ACTUALIZACIÓN 2016/02/25:
Si bien la respuesta que escribí a continuación sigue siendo suficiente, también vale la pena hacer referencia a otra respuesta relacionada con esto con respecto al objeto complementario de la clase de caso. Es decir, cómo se reproduce exactamente el objeto acompañante implícito generado por el compilador que ocurre cuando solo se define la clase de caso en sí. Para mí, resultó ser contrario a la intuición.
Resumen:
Puede alterar el valor de un parámetro de clase de caso antes de que se almacene en la clase de caso de manera bastante simple mientras sigue siendo un ADT (tipo de datos abstracto) válido (ated). Si bien la solución fue relativamente simple, descubrir los detalles fue un poco más desafiante.
Detalles:
si desea asegurarse de que solo se puedan instanciar instancias válidas de su clase de caso, lo cual es una suposición esencial detrás de un ADT (tipo de datos abstracto), hay una serie de cosas que debe hacer.
Por ejemplo, un
copy
método generado por el compilador se proporciona de forma predeterminada en una clase de caso. Por lo tanto, incluso si tuvo mucho cuidado de asegurarse de que solo se crearan instancias a través delapply
método del objeto complementario explícito que garantizaba que solo podrían contener valores en mayúsculas, el siguiente código produciría una instancia de clase de caso con un valor en minúscula:val a1 = A("Hi There") //contains "HI THERE" val a2 = a1.copy(s = "gotcha") //contains "gotcha"
Además, las clases de casos se implementan
java.io.Serializable
. Esto significa que su cuidadosa estrategia de tener solo instancias en mayúsculas se puede alterar con un simple editor de texto y deserialización.Por lo tanto, para todas las diversas formas en que se puede usar su clase de caso (con benevolencia y / o maldad), estas son las acciones que debe tomar:
apply
método con exactamente la misma firma que el constructor principal para su clase de casonew
operador y proporcionando una implementación vacía{}
{}
debe proporcionar la implementación vacía porque se declara la clase de casoabstract
(consulte el paso 2.1)abstract
apply
método en el objeto complementario que es lo que estaba causando el error de compilación "el método se define dos veces ..." (paso 1.2 anterior)private[A]
readResolve
métodocopy
métodos: String = s
)Aquí está su código modificado con las acciones anteriores:
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) }
Y aquí está su código después de implementar el requisito (sugerido en la respuesta de @ollekullberg) y también identificar el lugar ideal para colocar cualquier tipo de almacenamiento en caché:
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) }
Y esta versión es más segura / robusta si este código se usará a través de la interoperabilidad de Java (oculta la clase de caso como una implementación y crea una clase final que evita derivaciones):
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) }
Si bien esto responde directamente a su pregunta, hay incluso más formas de expandir esta vía en torno a las clases de casos más allá del almacenamiento en caché de instancias. Para las necesidades de mi propio proyecto, he creado una solución aún más amplia que he documentado en CodeReview (un sitio hermano de StackOverflow). Si termina revisándolo, usando o aprovechando mi solución, considere dejarme comentarios, sugerencias o preguntas y, dentro de lo razonable, haré todo lo posible para responder dentro de un día.
fuente
No sé cómo anular el
apply
método en el objeto complementario (si eso es posible), pero también puede usar un tipo especial para cadenas en mayúsculas: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"))
Los resultados del código anterior:
A(HELLO)
También debería echar un vistazo a esta pregunta y sus respuestas: Scala: ¿es posible anular el constructor de clases de caso predeterminado?
fuente
Proxy
. Aunque podría ser mejor hacerlos.toUpperCase
una vez .toUpperCase
se llama más de una vez.val self
, nodef self
. Acabo de tener C ++ en el cerebro.Para las personas que lean esto después de abril de 2017: A partir de Scala 2.12.2+, Scala permite anular aplicar y cancelar de forma predeterminada . Puede obtener este comportamiento dando la
-Xsource:2.12
opción al compilador en Scala 2.11.11+ también.fuente
-Xprint
unamatch
declaración, verá que no se usa).Funciona con variables var:
case class A(var s: String) { // Conversion s = s.toUpperCase }
Aparentemente, esta práctica se recomienda en clases de casos en lugar de definir otro constructor. Mira aquí. . Al copiar un objeto, también conserva las mismas modificaciones.
fuente
Otra idea, manteniendo la clase de caso y sin defs implícitas u otro constructor, es hacer que la firma sea
apply
ligeramente diferente pero desde la perspectiva del usuario sea la misma. En algún lugar he visto el truco implícito, pero no puedo recordar / encontrar qué argumento implícito era, así que elegíBoolean
aquí. Si alguien me puede ayudar y terminar el truco ...object A { def apply(s: String)(implicit ev: Boolean) = new A(s.toLowerCase) } case class A(s: String)
fuente
Enfrenté el mismo problema y esta solución está bien para mí:
sealed trait A { def s:String } object A { private case class AImpl(s:String) def apply(s:String):A = AImpl(s.toUpperCase) }
Y, si se necesita algún método, simplemente defínalo en el rasgo y anótelo en la clase case.
fuente
Si está atascado con una scala anterior donde no puede anular de forma predeterminada o no desea agregar la marca del compilador como se mostró @ mehmet-emre, y necesita una clase de caso, puede hacer lo siguiente:
case class A(private val _s: String) { val s = _s.toUpperCase }
fuente
A partir de 2020 en Scala 2.13, el escenario anterior de anular un método de aplicación de clase de caso con la misma firma funciona totalmente bien.
case class A(val s: String) object A { def apply(s: String) = new A(s.toUpperCase) }
el fragmento de código anterior se compila y se ejecuta bien en Scala 2.13 tanto en modo REPL como en modo no REPL.
fuente
Creo que esto funciona exactamente como ya lo quieres. Aquí está mi sesión de REPL:
scala> case class A(val s: String) defined class A scala> object A { | def apply(s: String) = new A(s.toUpperCase) | } defined module A scala> A("hello") res0: A = A(HELLO)
Esto está usando Scala 2.8.1.final
fuente