如何防止 AnyVal 导数中的无效值

How do I prevent invalid values in an AnyVal derivative

为了保持强类型、防止无效状态并保持 JVM 基本类型的效率,我尝试执行以下操作,它返回编译错误“此语句在值中不允许 class - 断言(!((double < -180.0d) || ...".

case class Longitude(double: Double) extends AnyVal {
  assert(!((double < -180.0d) || (double > 180.0d)), s"double [$double] must not be less than -180.d or greater than 180.0d")

  def from(double: Double): Option[Longitude] =
    if ((double < -180.0d) || (double > 180.0d))
      None
    else
      Some(Longitude(double))
}

我想要的效果是防止存在无效实例,例如经度 (-200.0d)。我有哪些选择可以达到预期的效果?

有一个很棒的库 Refined 旨在解决这类问题:在类型级别证明某些验证。这种方法在社区中也被称为“使非法状态无法表示”。不仅如此 - 它提供编译级别检查以及运行时验证。

在您的情况下,可能的解决方案可能如下所示:

import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
import eu.timepit.refined.boolean._

type LongtitudeValidation = Greater[W.`180.0`.T] Or Less[W.`-180.0`.T]

/**
* Type alise for double which should match condition `((double < -180.0d) || (double > 180.0d))` at type level
*/
type Longtitude = Double Refined LongtitudeValidation

val validLongTitude: Longtitude = refineMV(190.0d))

val invalidLongTitude: Longtitude = refineMV(160.0d)) //this won't compile because of validation failures
//error you will see: Both predicates of ((160.0 > 180.0) || (160.0 < -180.0)) failed. Left: Predicate failed: (160.0 > 180.0). Right: Predicate failed: (160.0 < -180.0).

您也可以通过refineV方法使用运行时验证:

type LongtitudeValidation = Greater[W.`180.0`.T] Or Less[W.`-180.0`.T]
type Longtitude = Double Refined LongtitudeValidation

val validatedLongitude1: Either[String, Longtitude] = refineV(190.0d)
println(validatedLongitude1)

val validatedLongitude2: Either[String, Longtitude] = refineV(160.0d)
println(validatedLongitude2)

这将打印出:

Right(190.0)
Left(Both predicates of ((160.0 > 180.0) || (160.0 < -180.0)) failed. Left: Predicate failed: (160.0 > 180.0). Right: Predicate failed: (160.0 < -180.0).)

你可以在Scatie里自己玩看:https://scastie.scala-lang.org/CQktleObQlKWKYby0vaszA

更新:

感谢@LuisMiguelMejíaSuárez 建议使用 scala-newtype 精炼以避免额外的内存分配。

您可以考虑使用以下方法:

  case class Longitude private (double: Double) extends AnyVal
  object Longitude {
     def apply(double: Double): Longitude = {
      if((double < -180.0d) || (double > 180.0d)) throw new RuntimeException(s"double [$double] must not be less than -180.d or greater than 180.0d")
      else new Longitude(double)
    }
  }
  Longitude(179)
  Longitude(190) // java.lang.RuntimeException: double [190.0] must not be less than -180.d or greater than 180.0d

此外,您可以使用 EitherOption 作为 @Luis Miguel Mejía Suárez 和@Ivan Kurchenko 在下面提到的

Either 示例:

  def DoSomething = println("Exception happened")
  def DoSomethingElse = println("Instance created")
  case class Longitude private(double: Double) extends AnyVal

  object Longitude {
    def apply(double: Double): Either[String, Longitude] = {
      if ((double < -180.0d) || (double > 180.0d)) Left(s"double [$double] must not be less than -180.d or greater than 180.0d")
      else Right(new Longitude(double))
    }
  }

  Longitude(181).fold(
    ex => DoSomething,
    longitude => DoSomethingElse
  )

或者将两者合并:

  case class Longitude private (double: Double) extends AnyVal
  object Longitude {
    def apply(double: Double): Longitude =
      safeCreate(double).getOrElse(throw new IllegalArgumentException((s"double [$double] must not be less than -180.d or greater than 180.0d")))

    private def safeCreate(double: Double) =
      if (!((double < -180.0d) || (double > 180.0d))) Some(new Longitude(double))
      else None
  }
  Longitude(170)
  Longitude(181) // java.lang.IllegalArgumentException: double [181.0] must not be less than -180.d or greater than 180.0d

但可能更好的解决方案是使用@Ivan answer

虽然我真的喜欢,但出于我的需要,它既引入了一个新库,又为这个特殊的小用例生成了太多样板。一个未指定的要求是我将把它展示给 Scala 新手,这个解决方案对他们来说太先进了。

给了我试图发现的基本模式。然而,它也不完全存在。

以下是我最终选择做的事情。您可以在 this Scastie link:

查看和使用代码
object Longitude extends (Double => Longitude) {
  def apply(double: Double): Longitude =
    applyFp(double) match {
      case Right(longitude) => longitude
      case Left(errorDetails) => throw new IllegalArgumentException(errorDetails)
    }

  def applyFp(double: Double): Either[String, Longitude] =
    if (!((double < -180.0d) || (double > 180.0d)))
      Right(new Longitude(double))
    else
      Left(s"double [$double] must not be less than -180.d or greater than 180.0d")
}
final case class Longitude private (double: Double) extends AnyVal


//Tests
Longitude.applyFp(179.0d)
Longitude.applyFp(180.0d)
Longitude.applyFp(190.0d) //returns Either.Left

Longitude(179.0d)
Longitude(180.0d)
//Remove comment from next line which will then throw the exception "java.lang.IllegalArgumentException: double [190.0] must not be less than -180.d or greater than 180.0d"
//Longitude(190.0d)

感谢那些提供有见地答案的人。这是一次非常宝贵的学习经历。

对于那些了解案例 类 工作原理的更多内部细节的人,特别是围绕编译器自动提供 copy 方法,这里更新了代码以适应“安全漏洞”它可以在 this Scastie link:

object Longitude extends (Double => Longitude) {
  def apply(double: Double): Longitude =
    applyFp(double) match {
      case Right(longitude) => longitude
      case Left(errorDetails) => throw new IllegalArgumentException(errorDetails)
    }

  def applyFp(double: Double): Either[String, Longitude] =
    if (!((double < -180.0d) || (double > 180.0d)))
      Right(new Longitude(double))
    else
      Left(s"double [$double] must not be less than -180.d or greater than 180.0d")
}
final case class Longitude private (double: Double) extends AnyVal {
  def copy(double: Double = double): Longitude =
    Longitude.apply(double)
}


//Tests
Longitude.applyFp(179.0d)
Longitude.applyFp(180.0d)
Longitude.applyFp(190.0d) //returns Either.Left

Longitude(179.0d)
Longitude(180.0d)
//Remove comment from next line which will then throw the exception "java.lang.IllegalArgumentException: double [190.0] must not be less than -180.d or greater than 180.0d"
//Longitude(190.0d)

val longitude = Longitude(-170.0d)
//Remove comment from next line which will then throw the exception "java.lang.IllegalArgumentException: double [-200.0] must not be less than -180.d or greater than 180.0d"
//longitude.copy(-200.0d)