跳至內容
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
    ^~~

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。因此,可能會在 nil 上呼叫 capitalize

而 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 知道在 if 的「then」部分內 line 不可能是 nil。

這既能表達意圖,又執行得更快,因為不需要在每次方法呼叫時都在執行階段檢查 nil 值。

總結這篇文章,最後要說的是,在將 Crystal 的剖析器從 Ruby 移植到 Crystal 時,Crystal 因為可能出現空指標例外而拒絕編譯。而且它是正確的。因此,在某種程度上,Crystal 在自身中發現了一個錯誤 :-)