2 回答
TA贡献1829条经验 获得超4个赞
发生冲突的原因是案例类提供了完全相同的apply()方法(相同的签名)。
首先,我建议您使用require:
case class A(s: String) {
require(! s.toCharArray.exists( _.isLower ), "Bad string: "+ s)
}
如果用户尝试创建s包含小写字符的实例,则将引发Exception。这是用例类的一种很好的用法,因为使用模式匹配(match)时,您在构造函数中输入的内容也同样如此。
如果这不是您想要的,那么我将创建构造函数private并强制用户仅使用apply方法:
class A private (val s: String) {
}
object A {
def apply(s: String): A = new A(s.toUpperCase)
}
如您所见,A不再是case class。我不确定具有不可变字段的case类是否用于修改传入的值,因为名称“ case class”表示应该可以使用提取(未修改的)构造函数参数match。
TA贡献1818条经验 获得超11个赞
尽管我在下面编写的答案仍然足够,但值得一提的是与案例类的伴随对象相关的另一个答案。即,如何精确地重现编译器生成的隐式伴随对象,该对象仅在定义案例类本身时发生。对我来说,事实证明这与直觉相反。
摘要:
您可以更改案例类参数的值,然后再将其存储在案例类中,非常简单,同时仍保留有效的(已指定)ADT(抽象数据类型)。尽管解决方案相对简单,但是发现细节却更具挑战性。
详细信息:
如果要确保只能实例化case类的有效实例,这是ADT(抽象数据类型)背后的基本假设,则必须执行许多操作。
例如,copy默认情况下,案例类提供了编译器生成的方法。因此,即使您非常小心以确保仅通过显式伴随对象的apply方法创建了仅能包含大写值的实例,以下代码仍将生成具有小写值的case类实例:
val a1 = A("Hi There") //contains "HI THERE"
val a2 = a1.copy(s = "gotcha") //contains "gotcha"
此外,案例类还实现了java.io.Serializable。这意味着可以通过简单的文本编辑器和反序列化来颠覆只包含大写实例的谨慎策略。
因此,对于使用案例类的各种方式(善意和/或恶意),您必须采取以下措施:
对于您的显式伴侣对象:
使用与案例类完全相同的名称创建它
可以访问案例类的私有部分
创建一个apply与您的case类的主构造函数具有完全相同的签名的方法
一旦完成步骤2.1,它将成功编译
提供一个使用new运算符获取案例类实例的实现,并提供一个空的实现{}
现在,这将严格按照您的条件实例化案例类
{}由于声明了案例类,因此必须提供空实现abstract(请参阅步骤2.1)。
对于您的案例类:
声明它 abstract
防止Scala编译器apply在伴随对象中生成方法,而该方法正是导致“方法被定义两次...”的编译错误(上面的步骤1.2)
将主要构造函数标记为 private[A]
现在,主要构造函数仅可用于case类本身及其伴随对象(我们在上面的步骤1.1中定义的对象)
创建一个readResolve方法
提供一个使用apply方法的实现(上面的步骤1.2)
创建一个copy方法
定义它与案例类的主要构造函数具有完全相同的签名
对于每一个参数,使用相同的参数名称添加的默认值(例如:s: String = s)
提供一个使用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使用此代码,则此版本将更安全/更可靠(将case类隐藏为实现,并创建一个防止派生的最终类):
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姐妹网站)上进行了记录。如果您最终查看,使用或利用我的解决方案,请考虑给我留下反馈,建议或问题,并在合理的范围内,我将尽力在一天之内做出答复。
TA贡献2036条经验 获得超8个赞
我不知道如何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)
您还应该看看这个问题及其答案:Scala:是否可以覆盖默认的case类构造函数?
- 2 回答
- 0 关注
- 471 浏览
添加回答
举报