【问题标题】:Algebraic types that can be instantiated via a sentinel function only只能通过哨兵函数实例化的代数类型
【发布时间】:2014-11-13 09:52:06
【问题描述】:

我想使用案例类来更有表现力地描述我的数据类型,以便从更高的静态正确性中受益。目标是 100% 静态确定存在的任何 Age 值始终包含有效的人类年龄(撇开封装规则可以使用反射绕过这一事实不谈)。

例如,我没有使用Int 来存储人的年龄,而是:

case class Age(x: Int) extends AnyVal
def mkAge(x: Int) = if (0 <= x && x <= 150) Some(Age(x)) else None
def unwrapAge(age: Age) = age.x

但是,此实现存在以下事实:Age 仍然可以在不经过 mkAgeunwrapAge 的情况下被实例化。

接下来,我尝试将构造函数设为私有:

case class Age private(x: Int) extends AnyVal
object Age {
  def make(x: Int) = if (0 <= x && x <= 150) Some(Age(x)) else None
  def unwrap(age: Age) = age.x
}

然而,虽然这确实阻止了 Age 使用 new 实例化(例如 new Age(3)),但 object Age 中自动生成的 apply(x: Int) 仍然很容易访问。

那么,问题来了:除了Age.makemkAge 之外,如何隐藏伴随对象中的构造函数和默认apply 方法?

我想避免必须使用常规(非case)类并手动正确复制class Ageobject Age 中的自动生成方法。

【问题讨论】:

    标签: scala static-typing


    【解决方案1】:

    你快到了:

    case class Age private(private val x:Int) extends AnyVal
    object Age {
      def mkAge(x:Int) = if(0<=x && x<=150) Some(Age(x)) else None
      def unwrapAge(age:Age) = age.x
    }
    

    注意案例类构造函数中额外的private val

    【讨论】:

    • 这至少在 2.10.3 中无法编译:“错误:值类需要有一个可公开访问的 val 参数”。即使有,也无济于事。
    • @AlexeyRomanov 抱歉,2.10 无法实现。我用2.11编译它。它确实达到了您的目标:构造函数和伴随对象的 apply 方法都不能从外部调用,并且 x 也不能公开访问。哪方面没有帮助?
    • "也不能从外部调用伴生对象的 apply 方法" 那么这是一个 Scala 错误,因为 Age.apply 的可见性不应该依赖于 x 的。根据scala-lang.org/files/archive/spec/2.11/…,它始终是公开的。
    • x 不可访问,但问题中没有提出。
    【解决方案2】:

    那么,问题来了:如何在除了 Age.make 或 mkAge 之外的任何对象中隐藏构造函数和默认的 apply 方法?

    我想避免必须使用常规(非大小写)类,并手动正确复制类 Age 和对象 Age 中的自动生成方法。

    我认为这是不可能的,但https://stackoverflow.com/a/25538287/9204 详细介绍了一个(相当重要的)解决方案。

    【讨论】:

    • 该解决方案存在一些问题: A) 正如您所提到的,它非常复杂。 B)它会产生警告,因为不鼓励扩展案例类,并且可能在未来的 Scala 版本中被禁止。 C)如果不自己重写它们,您将无法获得案例类的好处,这是一个要求。 D)我不确定是否有可能拥有一个抽象值类,即使在什么时候,实例也很可能在大多数情况下被装箱。
    • @Madoc C) 你失去了什么好处?所有方法仍然由匿名类生成和继承。您唯一需要重写的是copy,我认为您无法解决这个问题。 D) 是的,您失去了价值等级;如果这是一个要求,这将不起作用。
    【解决方案3】:

    我认为 Age 不需要成为一个案例类。因为它是一个值类,你不需要重写equals和hashcode,它也只有一个字段,所以复制构造函数没有任何好处。你不能对伴随对象中的 apply() 做任何事情。如果您仍然想使用案例类,您可以添加 require 但这并不能解决您的实例化问题。

    object A extends App {
    
      import Age._
    
      println(mkAge(150))
      println(mkAge(151))
      //println(new Age(51))   //Error!
    
    
      val a = mkAge(15) match {
        case Some(Age(x)) => x
        case None => 0
      }
    
      print(a)
    }
    
    class Age private(val x: Int) extends AnyVal {
      override def toString = s"A($x)"
    }
    
    object Age {
      def mkAge(x: Int) = if (0 <= x && x <= 150) Some(new Age(x)) else None
      def unwrapAge(age: Age) = age.x
      def unapply(age: Age) = if (age == null) None else Some(age.x)
    }
    

    【讨论】:

    • 如果是值类型,确实需要equals和hashcode;此外,它只有一个字段的事实是任意的——它很可能有 10 个字段。
    • 它将不再是一个超过 1 个字段的值类。因此,我认为,对于值类来说,也是一个案例类并不是一个优势。成为价值阶层重要吗?
    • 我没有说 AnyVal — 值类型是一个通用术语。 AnyVal 只是一个优化。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-08-28
    • 2021-01-20
    • 1970-01-01
    相关资源
    最近更新 更多