Null Pointer Exception
Null Pointer Exception(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
つい2日前にも、バスのチケット購入時に支払いページで「Object reference not set to an instance of an object」というエラーが表示され、チケットを購入できませんでした。
良いニュースがあります!**Crystal では Null Pointer Exception が発生しません。**
最も簡単な例から始めましょう。
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 ^
Null Pointer Exceptionの可能性(この場合は、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 は if
の「then」部分では line
が nil ではないことを認識します。
これは表現力豊かで、実行速度も速くなります。実行時に各メソッド呼び出しで nil
値をチェックする必要がないためです。
この記事を締めくくるにあたり、最後に言っておきたいことは、Ruby から Crystal への Crystal パーサーの移植中に、Null Pointer Exception の可能性があるため、Crystal がコンパイルを拒否したことです。そして、それは正しかったのです。ある意味、Crystal は自分自身のバグを発見したのです :-)