はじめに
皆さんはプログラミングをしていて、NullReferenceException(または NullPointerException)に苦しめられた経験はありませんか?
この記事では NullReferenceException を出さないようにするためには、どのようなコードを書けば良いか解説します。
サンプルコードについて
サンプルコードは C# 中心に記述していますが、他の言語経験者でも理解できるように C# 固有の記述はできるだけ避けるようにしています。
using System;
namespace Sample
{
class MainClass
{
public static void Main(string[] args)
{
string localText = "abc";
print(localText);
}
public static void print(string text)
{
Console.WriteLine(text + "の文字数は" + text.Length + "です。");
}
}
}
[コンソール出力結果]
abcの文字数は3です。
print メソッドは引数 text に渡された文字列の文字数をコンソール画面に出力します。
「string text = "abc";」の行を「string text = null;」に置き換えると NullReferenceException が発生するようになります。
このサンプルコードを例に対策法を説明していきます。
間違った対策法
最初に初心者がやってしまいがちな間違った Null 対策法を紹介します。
例外の握りつぶし
using System;
namespace Sample
{
class MainClass
{
public static void Main(string[] args)
{
string localText = null;
print(localText);
}
public static void print(string text)
{
try
{
Console.WriteLine(text + "の文字数は" + text.Length + "です。");
}
catch(Exception)
{
return;
}
}
}
}
上記のコードでは、例外をキャッチしているので例外は発生しなくなりましたが、仕様通りに動作するとは限りません。
また、例外の発生が設計者の意図通りなのか、それとも本当のエラーなのか判断できないため、バグが発生したときにプログラムの修正が困難になります。
そのため、プログラミングの世界では**「例外の握りつぶし」**と呼ばれ、絶対にやってはいけないご法度として知られています。
エラーの握りつぶし
using System;
namespace Sample
{
class MainClass
{
public static void Main(string[] args)
{
string localText = null;
print(localText);
}
public static void print(string text)
{
if (text != null)
{
Console.WriteLine(text + "の文字数は" + text.Length + "です。");
}
}
}
}
上記のコードでは、「例外の握りつぶし」はしていませんし、print メソッドの引数 text に null が渡されても正常に動作しますので、一見問題ないように見えます。
しかし、このコードには問題があります。
まず、print メソッドの引数 text に null が渡されるのは、どんな場合が想定されるのか考えてみましょう。
print メソッドは文字列の文字数をコンソール画面に出力するためのメソッドであり、引数 text に null が渡されること自体がおかしいのです。
異常事態が発生した場合は、速やかに開発者またはユーザーに通知しなくてはいけません。
このコードでも「例外の握りつぶし」と同様に、本当のエラーが検知できなくなってしまう問題が解決できていないのです。
例外の上書きスロー
using System;
namespace Sample
{
class MainClass
{
public static void Main(string[] args)
{
string localText = null;
print(localText);
}
public static void print(string text)
{
try
{
Console.WriteLine(text + "の文字数は" + text.Length + "です。");
}
catch (Exception)
{
throw new Exception("print メソッドで例外が発生しました。");
}
}
}
}
上記のコードは、例外をキャッチして新しい例外をスローしています。
元の例外を握りつぶしてしまっていますので、避けるべきコードです。
例外の再スロー
using System;
namespace Sample
{
class MainClass
{
public static void Main(string[] args)
{
string localText = null;
print(localText);
}
public static void print(string text)
{
try
{
Console.WriteLine(text + "の文字数は" + text.Length + "です。");
}
catch (Exception)
{
throw;
}
}
}
}
上記のコードは、例外をキャッチしてそのまま同じ例外をスローしています。
スタックトレースが汚れてしまい、例外の発生源が分かりづらくなりますので、避けるべきコードです。
正しい対策法
それでは、どのような Null 対策をするのが良いか見ていきましょう。
変数を初期化する
using System;
namespace Sample
{
class MainClass
{
public static void Main(string[] args)
{
string localText = "abc";
print(localText);
}
public static void print(string text)
{
Console.WriteLine(text + "の文字数は" + text.Length + "です。");
}
}
}
上記のコードでは、「string localText = "abc";」の行で localText に null ではない文字列で初期化します。
null が存在しているから NullReferenceException が発生するのです。
すごくシンプルですが、その原因を取り除いてしまえば NullReferenceException は発生しなくなります。
原則、すべての変数を null 以外の値で初期化しましょう。
配列は null の代わりに空の配列で初期化する
using System;
namespace Sample
{
class MainClass
{
public static void Main(string[] args)
{
string[] localTextArray = {};
printArray(localTextArray);
}
public static void printArray(string[] textArray)
{
foreach (string text in textArray)
{
Console.WriteLine(text + "の文字数は" + text.Length + "です。");
}
}
}
}
null はその値が存在しないことを表します。
変数を null で初期化してはいけないことは説明しましたが、値が 1 つも存在しない配列変数を初期化するにはどうしたら良いでしょうか?
答えは、上記のコードのように null ではなく、要素が 0 個の配列で初期化します。
printArray メソッドは要素が 1 つ以上存在する配列を渡すと、すべての要素をコンソール画面に出力しますが、要素が 0 個の配列を渡した場合は何も出力しません。
もちろん NullReferenceException は発生しません。
また、同様の考え方として、クラスに**空を表すオブジェクト(Nullオブジェクト)**を代入する手法は、デザインパターンの一つ「Nullオブジェクトパターン」として知られています。
戻り値はできるだけ null を返さない
using System;
using System.Collections.Generic;
namespace Sample
{
class MainClass
{
public static void Main(string[] args)
{
List<int> numberList = new List<int> { 1, 3, 5, 7 };
List<int> evenNumberList = GetEvenNumberList(numberList);
foreach(int number in evenNumberList)
{
Console.WriteLine(number);
}
}
public static List<int> GetEvenNumberList(List<int> numberList)
{
List<int> evenNumberList = new List<int> { };
foreach (int number in numberList)
{
if (number % 2 == 0)
{
evenNumberList.Add(number);
}
}
return evenNumberList;
}
}
}
これまでと違うサンプルコードです。
引数に渡された int 型リストから偶数のみの int 型リストを返す GetEvenNumberList メソッドが定義されています。
上記のコードでは、引数に渡されるリストは「1, 3, 5, 7」なので、偶数が 1 つも含まれていません。
この場合、戻り値には何を返すべきでしょうか?
何も返すべきものがないので null を返したくなるかもしれません。
しかし、null を返してしまうと、メソッドの呼び出し元で戻り値を null チェックする必要があり、それを忘れてしまうと例外が発生してしまいます。
「配列は null の代わりに空の配列で初期化する」の場合と同様の考え方で、戻り値も null ではなく空の配列を返しておきましょう。
null安全
fun main(args: Array<String>) {
var localText: String = null // ここでコンパイルエラーが発生する
println("Hello, world!")
}
上記のコードは、Kotlin で記述しています。
一見問題ないように見えますが、実際にはコンパイルエラーが発生します。
Kotlin の String 型にはデフォルトで null を代入できないのです。
fun main(args: Array<String>) {
var localText: String? = null // これでコンパイルエラーはなくなる
println("Hello, world!")
}
どうしても null を代入した場合は、上記のコードのように**「Nullable Type」**の型を指定する必要があります。
このようにモダンな言語では NullReferenceException に相当するエラーをコンパイル時にチェックする**「null安全」**と呼ばれる機能が搭載されています。
null安全のある言語に乗り換えるのも Null 対策となるかもしれません。
C# では、バージョン 8.0 で null 安全に相当する**「Null 許容参照型」**という機能が搭載される予定です。
Essential .NET - C# 8.0 と Null 許容参照型
https://msdn.microsoft.com/ja-jp/magazine/mt829270.aspx
NotNull アノテーション
package com.company;
import org.jetbrains.annotations.NotNull;
public class Sample {
public static void main(String[] args) {
String localText = null;
print(localText);
}
public static void print(@NotNull String text) {
System.out.println(text);
}
}
[コンパイル結果]
Exception in thread "main" java.lang.IllegalArgumentException: Argument for @NotNull parameter 'text' of com/company/Sample.print must not be null
at com.company.Sample.$$$reportNull$$$0(Sample.java)
at com.company.Sample.print(Sample.java)
at com.company.Sample.main(Sample.java:10)
上記のコードは、Java で記述しています。
Java では、言語の拡張機能として、変数に null でないことを表すアノテーション@NotNull
を指定して、コンパイル時に Null チェックすることができます。
さいごに
Null 対策のコツとしては、**「どうしたらうまく Null チェックできるのか?」という考え方から「どうしたらうまく Null チェック(しないといけない状況)を回避できるのか?」**という考え方に切り替えることではないかなと思います。
Google で「NullReferenceException」を検索していたときに、例外やエラーの握りつぶしを推奨するようなブログをいくつか目にしたため、敢えてタイトルに「正しい」と付けて記事にしました。
Null の対処法は言語や開発環境によってケース・バイ・ケースであり、また技術の進歩によって正しい対策法も変化していくでしょう。
本記事が、多少なりとも皆さんのお役に立てれば幸いです。
参考
・null安全でない言語は、もはやレガシー言語だ - Qiita
・もういい加減「nullチェックをしたら安全」とかわけのわからないことを言うのはやめよう - Qiita
・ReSharperでC#をnull安全っぽくするメモ - Qiita
・Null対策をしてみよう。NullPointerExceptionにおどろかない。 - Qiita
・Null非許容 - ++C++; // 未確認飛行 C ブログ
・毎回 object != null を書かないためには(Java) - ちぎっては投げるブログ