为什么 scala 允许私有 case 类字段?

Pop*_*nel 8 scala

在scala中,这样写是合法的case class Foo(private val bar: Any, private val baz: Any)

这就像人们希望的那样工作,Foo(1, 2) == Foo(1, 2)并且还生成了一种复制方法。在我的测试中,将整个构造函数标记为私有将复制方法标记为私有,这很棒。

但是,无论哪种方式,您仍然可以这样做:

Foo(1, 2) match { case Foo(bar, baz) => bar } // 1

因此,通过模式匹配,您可以提取值。这似乎更像privateprivate val bar: Any一个建议。我看到有人说“如果你想要封装,案例类不是正确的抽象”,我认为我同意这是一个有效的观点。但这提出了一个问题,如果这种语法可能具有误导性,为什么还允许它呢?

Ali*_*iel 4

这是因为案例类最初并不是为了与私有构造函数或字段一起使用。它们的主要目的是对不可变数据进行建模。正如您所看到的,有一些解决方法可以获取字段,因此在案例类上使用私有构造函数或私有字段通常是代码异味的标志。

尽管如此,语法是允许的,因为就编译器而言,代码在语法和语义上都是正确的。但从程序员的角度来看,它的极限是“这样使用它有意义吗?” 可能不会。

将 a 的构造函数标记case class为 private 不会达到您想要的效果,并且它不会使该copy方法成为私有的,至少在 Scala 2.13 中不会。

稍后编辑:在 Scala 3 中,将构造函数标记为私有,确实使applycopy方法成为私有的。此更改也是针对 Scala 2 开发的,可以在此处找到- 但它被推迟到 2.14 未来版本。它无法进入 Scala 2.13 的原因是因为它是一个重大更改。

case class Foo private (bar: Int, baz: Int)
Run Code Online (Sandbox Code Playgroud)

我唯一不能做的就是:

val foo1 = new Foo(1, 2)   // no constructor accessible from here
Run Code Online (Sandbox Code Playgroud)

但我可以这样做:

  val foo2 = Foo(1, 2)       // ok
  val foo3 = Foo.apply(1, 2) // ok
  val foo4 = foo2.copy(4)    // ok - Foo(4,2)
Run Code Online (Sandbox Code Playgroud)

case class顾名思义,A意味着该对象恰好是模式匹配或“可区分大小写”的 - 这意味着您可以像这样使用它:

case Foo(x, y) =>       // do something with x and y
Run Code Online (Sandbox Code Playgroud)

或这个:

val foo2 = Foo(1, 2)
val Foo(x, y) = foo2    // equivalent to the previous
Run Code Online (Sandbox Code Playgroud)

否则你为什么要把它标记为 acase class而不是 a class?当然,有人可能会说 acase class更方便,因为它附带了很多方法(请放心,所有这些都会在运行时产生开销,作为所有创建的方法的权衡),但如果您追求的是隐私,那么它们不要把任何事情带到谈判桌上。

Acase class使用一个方法创建一个伴生对象unapply,当给定一个 your 实例时case class,它会将其解构/解构到其初始化字段中。这也称为提取器方法。

伴生对象及其类可以访问彼此的成员 - 包括私有构造函数和字段。它就是这样设计的。现在applyunapply是在伴随对象上定义的公共方法,这意味着 - 您仍然可以使用 创建新对象apply,并且如果您的字段是私有的 - 您仍然可以从 访问它们unapply

case class不过,如果您确实希望您的对象是私有的,您可以在伴生对象中覆盖它们。但大多数时候,这样做是没有意义的,除非您有一些非常具体的要求:

  case class Foo2 private (private val bar: Int, private val baz: Int)

  object Foo2 {
    private def apply(bar: Int, baz: Int) = new Foo2(bar, baz)
    private def unapply(f: Foo2): Option[(Int, Int)] = Some(f.bar, f.baz)
  }

  val foo11          = new Foo2(1, 2)   // won't compile
  val foo22          = Foo2(1, 2)       // won't compile
  val foo33          = Foo2.apply(1, 2) // won't compile
  val Foo2(bar, baz) = foo22            // won't compile

  println(Foo2(1, 2) == Foo2(1, 2))     // won't compile

  val sum = foo22 match {
    case Foo2(x, y) => x + y            // won't compile
  }
Run Code Online (Sandbox Code Playgroud)

Foo2尽管如此,您仍然可以通过打印它来查看它的内容,因为案例类也会覆盖toString,并且您不能将其设为私有,因此您必须覆盖它才能打印其他内容。我将把它留给你去尝试。

 print(foo11)  // Foo2(1,2)
Run Code Online (Sandbox Code Playgroud)

正如您所看到的, acase class为其构造函数和字段带来了多个访问点。这个例子只是为了理解这个概念。这不是一个好的设计的例子。通常在 OOP 中,您需要某个类的实例来对其执行操作。因此,根本无法实例化的类并不比 Scala 更有用object。如果您发现自己阻止了创建某个类或案例类的实例的所有方法,则表明您可能需要一个object替代方案,因为object在 Scala 中已经是单例了。