为什么 scala 没有围绕整数溢出进行设计?



我曾是一名Java开发人员,最近我观看了Venkat Subramaniam教授为Java开发人员对Scala的深刻而有趣的介绍(https://www.youtube.com/watch?v=LH75sJAR0hc)。

引入的一个要点是消除已声明的类型,而不是"类型推理"。据推测,这意味着高阶编译器通过上下文识别出我想要使用的类型。

作为一名应用程序安全专家,我尝试做的第一件事就是打破这种类型推断。。。示例:

// declare a function that returns the square of an input Int. The return type is to be inferred.
scala> val square = (x:Int) => x*x
square: Int => Int = <function1>
// I can see the compiler inferred an Int for the output value, which I do not agree with.
scala> square(2147483647)
res1: Int = 1
// integer overflow

我的问题是,为什么编译器没有发现"*"是一个有溢出威胁的运算符,并将输入封装在一个更具保护性的东西中,比如BigInteger?

根据教授的说法,我应该忘记内部实现,只继续我的业务逻辑。但在我的快速演示之后,对于一个不了解编译器对我的方法做了什么的程序员来说,我不太确定Scala是否安全。

我认为@rightføld在某种程度上夸大了溢出发生或不发生的频率(尤其是当考虑到一个攻击者正在主动试图溢出您时)。但我同意他的基本观点。将所有数学转换为BigInteger几乎肯定会对Java产生巨大的性能影响。对于开发人员来说,要选择这样一种语言,他们必须以这样的成本获得一些可见的东西。

对于许多操作,字符串对象的性能开销比cstrings小得多。它们还为开发人员提供了非常明显的好处,这就是人们使用它们的原因,而不是安全性本身。字符串对象通过cstrings可以轻松完成许多常见的事情。BigInteger没有提供这些。它需要完全相同的代码,速度只有它的一小部分,但不会溢出(很少有开发人员每天都会看到这个错误,即使安全人员更频繁地看到它)。

等效的是一个cstring(带有strcmp、strcpy、strcat等),它以很小的速度运行,但不需要null终止符。我认为也没有多少人会欣然使用它,无论这对null终止字符串的安全性有多大帮助。如果语言需要的话,我没有看到很多人急于使用这种语言。

正如@rightføld在评论中所建议的那样,与Java的互操作性将被破坏,因为大多数(如果不是全部的话)数字最终都将是BigInteger。您会不断地进行转换,这在增加大量代码复杂性(以及更多性能影响)的同时,也会引发同样的溢出危险。

如果一种从头开始的语言有很多其他引人注目的功能,那么它可能会摆脱无处不在的BigInteger(比如python),但要改造成一种想要从Java(以及与Java)自然过渡的语言是非常困难的。

除了上面的答案之外,我认为这个问题误解了静态类型语言中类型推理的目的。类型推理不会做出你所指的选择——将Int提升为BigInt。它仅限于在编译时根据已知的子表达式类型"推断"表达式的类型。

Int中的*函数在提供Int输入参数时返回Int

def *(x: Int): Int

在这种情况下,由于x被声明为Int,因此基于*的签名,x*x必须是Int

如果我们真的想要这种行为,我们可以定义一个函数,在相乘时将Int提升为BigInt

implicit class SafeInt(x: Int) {
  def safeMult(a: Int): scala.math.BigInt = scala.math.BigInt(x)*a
}

然后,当我们可以定义一个具有所需属性的正方形时:

scala> val square = (x: Int) => x safeMult x
square: Int => scala.math.BigInt = <function1>

编译器根据可用的方法进行推断。Int有一个方法*(Int): Int,据编译器所知,它定义得非常好;2147483647*2147483647是一个非常好的方法调用,结果为1,它不会抛出ClassCastException或类似的东西。

为什么Int类型是这样写的?主要用于Java/JVM兼容性;Scala的许多部分为了Java兼容性而在设计上有所妥协。如果您不需要该功能,您可能更喜欢使用Haskell或类似的语言。(我怀疑,即使没有JVM兼容性的要求,Scala也会希望公开机器的本地整数类型,这样用户就可以在需要的地方进行性能/正确性的权衡。不过,它们可能不是默认的)

如果你在Scala中进行数字计算,你可能想使用Spire库,它可以很容易地对数字类型进行抽象,并提供几种具有特定属性的高性能数字类型。特别是,它有一个SafeLong类型,可以处理任意精度的整数,但对于Long范围内的值,它的性能比BigInt好得多,类似于Python的整数类型。

因为在实践中几乎从未发生过溢出,而且BigIntegerInt相比慢得像狗。让Int上的所有*操作返回BigInteger也是最不方便的。

"识别我想要使用的类型"并不是scala尝试做什么的准确描述。它在上下文施加的约束下推断出最通用的类型。因此,如果您编写List(Nil, "1"),您将获得List[Serializable],因为SerializableListString共享的接口,而不考虑Serializable可能根本不在您的脑海中。

你所问的问题可以更准确地说是"为什么Int是数字文字的类型,而不是BigInteger?"——推理与此无关。

在这个话题上,我们可以随心所欲地发表意见,但有一个最准确的答案可以描述为什么Scala是这样的:"因为Java"。

如果你想要你似乎想要的安全类型,那么一种方法是通过一个分部函数来定义,该函数可以防止数字溢出,然后返回Option[Int],甚至可能返回Erie[Int,BigInteger]。

平方函数的类型推断是正确的,因为它是从您指定的输入类型和*函数的类型中推断出来的。。。在我看来,它并没有真正坏掉。

相关内容

  • 没有找到相关文章

最新更新