如何防止 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
此外,您可以使用 Either
、Option
作为 @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)
为了保持强类型、防止无效状态并保持 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
此外,您可以使用 Either
、Option
作为 @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
虽然我真的喜欢
以下是我最终选择做的事情。您可以在 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)