跳至内容
GitHub 仓库 论坛 RSS-新闻源

空指针异常

Ary Borenzweig

空指针异常,也称为 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
    ^~~

nilNil 类唯一的实例,它与 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 条件中使用一个变量时,并且由于唯一虚假的值是 nilfalse,Crystal 知道 lineif 的“then”部分中不可能为 nil。

这既具有表达性,又执行速度更快,因为在运行时不需要在每次方法调用时检查 nil 值。

总结这篇文章,最后要说的是,在将 Crystal 解析器从 Ruby 移植到 Crystal 时,Crystal 拒绝编译,因为可能存在空指针异常。而且它是正确的。所以从某种意义上说,Crystal 自己发现了一个 bug :-)