如何提高基于String或Double的值的类型安全性?

时间:2015-04-17 16:28:13

标签: scala

我想知道在我的代码实现类型安全的最佳方法时,各种值可能都是String s或Double s,但仍然不兼容。例如,我可能有单位为磅和千克,但我应该被禁止分配给另一个。同样地,我可能将人员ID作为String,将动物ID的查找表作为Map[String,Int],但我应该禁止在动物表中查找某人。

从概念上讲,我正在寻找类似的东西:

class PersonId extends String
class AnimalId extends String

var p : PersonId = "1234"
var tab : Map[AnimalId,Int] = Map("foo" -> 5, "bar" -> 6)
tab.get(p)  // Want this to cause a compile error

但是有几个问题使得它不起作用。建议适合这种精神?

4 个答案:

答案 0 :(得分:5)

我为此使用了value classes。它的行为与普通的case类几乎相同,但编译器对它有一些限制,通常它实际上不必浪费时间/内存来创建包装器对象 - 它通常可以直接使用底层值。

case class Person(value: String) extends AnyVal
case class Animal(value: String) extends AnyVal

答案 1 :(得分:3)

由于显而易见的原因,您无法扩展String。我建议使用案例类:

case class PersonId(id:String)
case class AnimalId(id:String)

语法变得有点复杂,但不是那么多。在模式匹配时,您可以轻松使用案例类!

var p: PersonId = PersonId("1234")
var tab: Map[AnimalId,Int] = Map(AnimalId("foo") -> 5, AnimalId("bar") -> 6)

答案 2 :(得分:2)

一个简单的解决方案就是使用

case class PersonId(id:String)
case class AnimalId(id:String)

此解决方案通常足够好。

如果您想使用Scala的类型系统,您可以做类似的事情 -

  trait Person

  trait Animal

  case class IdOf[T](s: String) extends AnyVal

  implicit def string2idOf[T](s: String): IdOf[T] = IdOf(s)

  var p: IdOf[Person] = "1234"
  var tab: Map[IdOf[Animal], Int] = Map(("foo": IdOf[Animal]) -> 5, ("bar": IdOf[Animal]) -> 6)
  tab.get(p) 
// Error:(25, 11) type mismatch;
// found   : com.novak.Program.IdOf[com.novak.Program.Person]
// required: com.novak.Program.IdOf[com.novak.Program.Animal]
// tab.get(p)
      ^

答案 3 :(得分:1)

另一个选择是Scalaz的tagged type。可能在some cases中很有用,因为它可以将您的类型与其他类型组合在一起而无需创建另一种类型的新实例(值类对于基本类型是相似的);然而,新的Scalaz需要明确地将其取消装箱(使用Tag.unwrap),因此没有太多有用的东西。

示例:

trait Person
val Person = Tag.of[Person]
val person = Prsn("Me")
Person.unwrap(person)
trait Animal
val Animal = Tag.of[Animal]
val animal = Anml("Me")
Animal.unwrap(person) //error
Animal.unwrap(animal)

只是引用:

  

假设我们想要一种用千克表示质量的方法,因为kg是   单位的国际标准。通常我们会传递Double   并称它为一天,但我们无法区别于其他Double   值。我们可以为此使用案例类吗?

case class KiloGram(value: Double) 
     

虽然确实增加了类型安全性,   使用起来并不好玩,因为我们每次需要时都要调用x.value   从中提取出来的价值。标记类型以拯救。

scala> sealed trait KiloGram defined trait KiloGram

scala> def KiloGram[A](a: A): A @@ KiloGram = Tag[A, KiloGram](a)
KiloGram: [A](a: A)scalaz.@@[A,KiloGram]

scala> val mass = KiloGram(20.0) mass: scalaz.@@[Double,KiloGram] =
20.0

scala> sealed trait JoulePerKiloGram
defined trait JoulePerKiloGram

scala> def JoulePerKiloGram[A](a: A): A @@ JoulePerKiloGram = Tag[A, JoulePerKiloGram](a)
JoulePerKiloGram: [A](a: A)scalaz.@@[A,JoulePerKiloGram]

scala> def energyR(m: Double @@ KiloGram): Double @@ JoulePerKiloGram =
         JoulePerKiloGram(299792458.0 * 299792458.0 * Tag.unsubst[Double, Id, KiloGram](m))
energyR: (m: scalaz.@@[Double,KiloGram])scalaz.@@[Double,JoulePerKiloGram]

scala> energyR(mass)
res4: scalaz.@@[Double,JoulePerKiloGram] = 1.79751035747363533E18

scala> energyR(10.0)
<console>:18: error: type mismatch;
 found   : Double(10.0)
 required: scalaz.@@[Double,KiloGram]
    (which expands to)  AnyRef{type Tag = KiloGram; type Self = Double}
              energyR(10.0)
                      ^