コンテンツへスキップ
GitHub リポジトリ フォーラム RSS-ニュースフィード

Null Pointer Exception

Ary Borenzweig

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 を返します。そのため、capitalizenil で呼び出される可能性があります。

そして、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 値をチェックする必要がないためです。

この記事を締めくくるにあたり、最後に言っておきたいことは、Ruby から Crystal への Crystal パーサーの移植中に、Null Pointer Exception の可能性があるため、Crystal がコンパイルを拒否したことです。そして、それは正しかったのです。ある意味、Crystal は自分自身のバグを発見したのです :-)