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 は自分自身のバグを発見したのです :-)