空指针异常
空指针异常,也称为 NPE,是一种非常常见的错误。
- 在 Java 中:java.lang.NullPointerException
- 在 Ruby 中:undefined method '...' for nil:NilClass
- 在 Python 中:AttributeError: 'NoneType' object has no attribute '...'
- 在 C# 中:Object reference not set to an instance of an object
- 在 C/C++ 中:segmentation fault
前两天,我因为在付款页面收到了一个友好的“Object reference not set to an instance of an object”错误,而无法购买公交车票。
好消息是:Crystal 不允许出现空指针异常。
让我们从最简单的例子开始
nil.foo
编译上面的程序会给出以下错误
Error in foo.cr:1: undefined method 'foo' for Nil nil.foo ^~~
nil
是 Nil 类唯一的实例,它与 Crystal 中的任何其他类一样。由于它没有名为“foo”的方法,因此会在编译时发出错误。
让我们尝试一个稍微复杂一点的,但也是虚构的例子
class Box
getter :value
def initialize(value)
@value = value
end
end
def make_box(n)
case n
when 1, 2, 3
Box.new(n * 2)
when 4, 5, 6
Box.new(n * 3)
end
end
n = ARGV.size
box = make_box(n)
puts box.value
你能发现错误吗?
编译上面的程序,Crystal 会显示
Error in foo.cr:20: undefined method 'value' for Nil puts box.value ^~~~~ ================================================================================ Nil trace: foo.cr:19 box = make_box n ^ foo.cr:19 box = make_box n ^~~~~~~~ foo.cr:9 def make_box(n) ^~~~~~~~ foo.cr:10 case n ^
它不仅告诉你可能会出现空指针异常(在本例中,当 n 不是 1、2、3、4、5、6 时),而且还告诉你 nil
的来源。它位于 case
表达式中,该表达式有一个默认的空 else
子句,该子句的值为 nil
。
最后一个例子,很可能就是真实的代码
require "socket"
# Create a new TCPServer at port 8080
server = TCPServer.new(8080)
# Accept a connection
socket = server.accept
# Read a line and output it capitalized
puts socket.gets.capitalize
现在你能发现错误吗?事实证明 TCPSocket#gets (IO#gets,实际上) 在文件末尾或在本例中连接关闭时返回 nil
。所以 capitalize
可能会在 nil
上调用。
Crystal 阻止你编写这样的程序
Error in foo.cr:10: undefined method 'capitalize' for Nil puts socket.gets.capitalize ^~~~~~~~~~ ================================================================================ Nil trace: std/file.cr:35 def gets ^~~~ std/file.cr:40 size > 0 ? String.from_cstr(buffer) : nil ^ std/file.cr:40 size > 0 ? String.from_cstr(buffer) : nil ^
为了防止出现此错误,你可以执行以下操作
require "socket"
server = TCPServer.new(8080)
socket = server.accept
line = socket.gets
if line
puts line.capitalize
else
puts "Nothing in the socket"
end
最后一个程序编译正常。当你在一个 if
条件中使用一个变量时,并且由于唯一虚假的值是 nil
和 false
,Crystal 知道 line
在 if
的“then”部分中不可能为 nil。
这既具有表达性,又执行速度更快,因为在运行时不需要在每次方法调用时检查 nil
值。
总结这篇文章,最后要说的是,在将 Crystal 解析器从 Ruby 移植到 Crystal 时,Crystal 拒绝编译,因为可能存在空指针异常。而且它是正确的。所以从某种意义上说,Crystal 自己发现了一个 bug :-)