空指標例外
空指標例外,也稱為 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
的條件中使用變數時,並且因為唯一為假的數值是 nil
和 false
,Crystal 知道在 if
的「then」部分內 line
不可能是 nil。
這既能表達意圖,又執行得更快,因為不需要在每次方法呼叫時都在執行階段檢查 nil
值。
總結這篇文章,最後要說的是,在將 Crystal 的剖析器從 Ruby 移植到 Crystal 時,Crystal 因為可能出現空指標例外而拒絕編譯。而且它是正確的。因此,在某種程度上,Crystal 在自身中發現了一個錯誤 :-)