【问题标题】:What's the correct way to enforce constraints on case class values对案例类值实施约束的正确方法是什么
【发布时间】:2014-06-02 20:09:15
【问题描述】:

在构造时抛出异常通常被认为不是一件很有礼貌的事情,但我不确定在尝试对案例类值强制执行约束时如何避免这种情况。

例如,我需要表示一个范围,并且两个边界都必须是正数。立即实施是:

case class Range(from: Int, to: Int)

然而,这并不能确保fromto 都是正数,也不能确保to 大于from

我的第一反应是按如下方式实现:

case class Range(from: Int, to: Int) {
  require(from >= 0)
  require(to >= 0)
  require(to >= from)
}

然而,这使得Range 的构造函数不安全。

是否有一种通用模式来保持案例类的易用性、强制值约束并避免抛出异常?

【问题讨论】:

  • "强制值约束,避免抛出异常?" - 强制执行是什么意思?当有人尝试使用无效的 from/to 构造 Range 时,您希望发生什么?
  • 谢谢保罗,这是我问题的关键。我应该做什么?我试图防止创建无效实例,并且似乎有一个共识,即构造函数或构造方法不应该抛出。我不知道如何做一个没有另一个,有没有我没有看到的解决方案?
  • 理想主义的答案是类型级编程(但学习曲线陡峭)。创建这样的东西应该是编译时错误。但是,如果不这样做,您似乎有四种选择:忽略问题(接受无效参数),抛出异常,强制参数有效范围,使用标志将实例标记为无效(并在调用任何方法时抛出异常)它?)。我,我会在构造函数中抛出一个异常......
  • "用标志将实例标记为无效(并在调用任何方法时抛出异常?)。" - 一个变体:返回 Range 的 InvalidRange 子类(或对象),该子类在所有内容上都会出错。
  • 我非常喜欢您的最后一个建议,它干净且类型安全,而且一切都很好。它只是与我根深蒂固的“快速失败”反射相冲突,如果我知道发生了不应该发生的事情,我应该立即失败,而不是尝试返回(可能不正确的)结果。

标签: scala


【解决方案1】:

这是相当主观的,但我会尝试。

这取决于你的类是如何构建的。在内部代码中随意使用的简单实用范围类的情况下,任何使用它的程序员都应该知道,提供错误的范围值可能会导致奇怪的结果。您可以假设对类的合理使用确实不会遇到这个问题,并且在这些罕见的情况下抛出异常可能不是一个坏习惯,表明有人真的搞砸了。如果从上下文中可以明显看出应该为班级提供哪些价值观,我认为如果人们未能满足这些对理智的期望,抛出异常并不是特别糟糕的品味。

将此与 NullPointerExceptions 或 ArithmethicExceptions 的行为进行比较,这些行为有时仍会在 Scala 中发生。有时,当面对足够多的“疯狂”时,进行防御性编程是不合理的。

另一方面,如果你的类被你控制较少的值填充,即它们是用户输入的直接结果,你必须简单地对你的构造进行更大的控制。使用返回 OptionEither 的函数创建伴随对象:

 object Range {
     def createSafe(from: Int, to: Int): Option[Range] = {
         if(from >= 0 && to >= 0 && to >= from)
            Some(Range(from, to))
         else
            None
     }
 }

然后,您甚至可以通过覆盖 apply 作为其他答案的建议,在案例类的实例化中重用验证逻辑。

 object Range {
    def createSafe ...
    def apply(from: Int, to: Int): Range = {
        createSafe(from, to).getOrElse(throw new IllegalArgumentException(...))
    }
 }

【讨论】:

  • 请注意,我的回答中可能存在无限递归(apply 调用 createSafe 调用 apply 其中...),但你明白我的意思。
【解决方案2】:

您可以在伴生对象中重载apply 运算符来实现您想要做的事情:

object Range{
  def apply(from: Int, to: Int) ={
    val _from = Math.max(0, from)
    val _to = Math.max(0, to)
    if(_from < _to) new Range(_from, _to)
    else new Range(_to, _from)
  }
}

请注意,使用to 作为变量名会导致一些“有趣”的结果。

【讨论】:

  • 虽然我对使用类型编程的建议是一个更一般的技能值得学习,但对于这种特殊情况,这个建议将更容易实现。 :)
  • 只是为了确保我正确理解这一点:将无效输入强制转换为有效输入比失败更可取?例如,Range(-1, -20) 应该返回 Range(1, 20),即使我们绝对确定这不是调用者想要的(不管他想要的是否可能)?
  • @wheaties 我觉得奇怪的是假设需要这个解决方案。我也会选择require。为什么要更改参数?
  • @Kigyo 他提到他宁愿不抛出异常,所以我将其纳入我的答案。通常情况下,我自己会使用require 抛出异常,但有时“空”结果比无效结果更好。
  • @wheaties 我明白了。我几乎害怕你会给我一个改变我过去编写的代码的理由,我不时使用require。 :)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-01-19
  • 1970-01-01
  • 1970-01-01
  • 2012-06-13
  • 1970-01-01
相关资源
最近更新 更多