【ゲーム開発のためのC#入門講座・基礎強化編】nullに注意しよう【#7】
ぬるぽって聞いたことありますか?
一昔前のネット用語「ぬるぽ」をご存じですか?
シュタインズ・ゲートで知った、なんて若い方もいるかもしれませんね。
この「ぬるぽ」はJavaという言語で発生するエラー「NullPointerException」が語源のネット用語です。「よく見かけるエラーで、対応が面倒な憎いやつ」であることから、せめて可愛らしく言おうぜという経緯から生まれたとかなんとか。
C#の場合は同じ原因のエラー「NullReferenceException」が、これまた 「よく見かけるエラーで、対応が面倒な憎いやつ」 として今後皆さんの前に度々登場することになります。
名前から「Null (ヌル) を参照(Reference)してエラー(Exception)になった」らしいことは何となく察せますが、この「Null(ヌル)」とはいったい何なのでしょうか?
空っぽの参照型には何が入っている?
参照型は型情報だけではメモリサイズがわからないことから、「new」という命令実行時に初めてヒープメモリにデータが作成されるのでしたね。そしてそのアドレスはスタックメモリに保存されるのでした。
それでは、下記プログラムのint型配列変数「A」には何が入っていると思いますか?
public class Hello{
public static void Main(){
int[] A;
}
}
空っぽの参照型には何が入っている? と聞くとちょっと哲学的にも思えますね。
結論からいえば、何も代入していないのだから何も入っていません。当然ですね。
では、この何も入っていない変数を参照して、何か処理を実行しようとするとどうなるでしょう?
参照型ですから、まずはスタックメモリに格納されているヒープメモリのアドレスを参照しようとしますよね。でもメモリは初期状態のままなので、実際にどこかにある実データの場所を指し示している訳ではありません。
結果、存在しないデータを参照しようとしてエラーが発生します。
このように、実データを参照することができない無効なアドレスが設定されている状態、つまり変数が空っぽの状態のことをnullといいます。
nullのデータを参照しようとすると、 先程ご紹介した「NullReferenceException」 というエラーが発生してしまうので、注意が必要です。
どういう時に注意すればいいの?
このnullで一番注意しなければいけないポイント、それは戻り値です。
例えば下記のようなプログラムがあったとします。
public class Hello{
public static void Main(){
string[] names = {
"He",
"Ro"
};
string name = FindHero(names);
System.Console.WriteLine(name.Length);
}
public static string FindHero(string[] names)
{
for (int i = 0; i < names.Length; i++)
{
if (names[i] == "Hero")
{
return names[i];
}
}
return null;
}
}
関数「FindHero」は、引数のstring型配列の中に「Hero」という文字列が存在する場合はその文字列を、存在しない場合はnullを返すというものです。
関数というのは、戻り値の型を指定した場合、必ず何らかの値を返さなければならないという決まりがあります。
そのため、上記のような「~~を検索する」系の関数の場合、該当のデータを見つけられなかった時はデータが存在しないことを意味するnullを返すしかないのです。
結果、必ず有効なデータが返されるものだと思い込んでしまうと、戻り値を使った処理を実行しようとした時に「NullReferenceException」に遭遇してしまうという訳です。
実際に実行してみると、下記のようにエラーが出てしまいます。
Unhandled Exception:
System.NullReferenceException: Object reference not set to an instance of an object
at Hello.Main () [0x0001f] in /workspace/Main.cs:8
[ERROR] FATAL UNHANDLED EXCEPTION: System.NullReferenceException: Object reference not set to an instance of an object
at Hello.Main () [0x0001f] in /workspace/Main.cs:8
nullかどうか事前にチェックしよう
対処法にはいくつか選択肢があるのですが、一番わかりやすいのは事前にnullかどうかチェックするというものです。
これは比較演算子を使って実現することができます。
試しに先程のプログラムを、エラーが発生しないよう変更してみましょう。
public class Hello{
public static void Main(){
string[] names = {
"He",
"Ro"
};
string name = FindHero(names);
// ↓ここで事前にチェックしてるよ!
if (name != null)
{
System.Console.WriteLine(name.Length);
}
else
{
System.Console.WriteLine("nullだったよ!");
}
}
public static string FindHero(string[] names)
{
for (int i = 0; i < names.Length; i++)
{
if (names[i] == "Hero")
{
return names[i];
}
}
return null;
}
}
nullだったよ!
無事、変数がnullであってもエラーを発生させずに処理を終えることができました。
戻り値としてnullが返される可能性があるならば、このように事前にチェックした上で利用するよう気を付けた方がよいでしょう。
Unityでの活躍ポイント
実はUnityで頻繁に発生しうるエラーが「NullReferenceException」です。
なぜならば、Unityはキャラクターやマップに配置したオブジェクトなど、大量の参照型データを扱う機会が非常に多いからです。そしてその検索用に用意された関数では、該当するデータが見当たらなかった場合にnullが返されるようになっています。
結果、こんな風に超膨大な量の「NullReferenceException」が発生することも珍しくありません。
nullとは何なのか、そしてそれに対処するにはどうしたらよいのかを学ぶことで、スムーズに対応ができるようになります。
忘れないようにしておきましょう!
実践演習
それでは実際にnull参照によるエラーを回避してみましょう。
演習①nullじゃない時だけ処理を実行しよう
下記のプログラムはエラーが発生してしまいます。エラーが発生しないよう、関数「CountLength」にて、引数がnullじゃなかった時だけ加算処理を実行するようにしてください。
public class Hello{
public static void Main(){
string text = null;
System.Console.WriteLine(CountLength(text, "hoge"));
}
public static int CountLength(string text, string text2)
{
int count = 0;
count += text.Length;
count += text2.Length;
return count;
}
}
演習②nullだったら別の値を返すようにしよう
下記のプログラムはエラーが発生してしまいます。エラーが発生しないよう、関数「ReplaceNull」にて、ひとつ目の引数がnullだった場合、代わりにふたつ目の引数を戻り値として返してください。
public class Hello{
public static void Main(){
string text = null;
text = ReplaceNull(text, "代替テキスト");
System.Console.WriteLine(text);
}
public static string ReplaceNull(string text, string altText)
{
// ↓ここにひとつ目の引数がnullだった場合の処理を実装しよう!
// ↑ここまで
return text;
}
}
答え合わせ
演習①の答え
public class Hello{
public static void Main(){
string text = null;
System.Console.WriteLine(CountLength(text, "hoge"));
}
public static int CountLength(string text, string text2)
{
int count = 0;
if (text != null)
{
count += text.Length;
}
if (text2 != null)
{
count += text2.Length;
}
return count;
}
}
演習②の答え
public class Hello{
public static void Main(){
string text = null;
text = ReplaceNull(text, "代替テキスト");
System.Console.WriteLine(text);
}
public static string ReplaceNull(string text, string altText)
{
// ↓ここにひとつ目の引数がnullだった場合の処理を実装しよう!
if (text == null)
{
return altText;
}
// ↑ここまで
return text;
}
}
まとめ
- nullとは、実データを参照することができない、変数が空っぽの状態のこと
- 関数(特に検索系)の戻り値で使われることが多いので、戻り値を使った処理での「NullReferenceException」に注意
- 事前にnullチェックを行うなど、対応策を忘れないようにしておきましょう
それでは、今回もお疲れ様でした!
また次の記事でお会いしましょう!
お借りした素材一覧
この記事では下記サイト様の素材をお借りしています。
ありがとうございました!
ディスカッション
コメント一覧
こんにちは、勉強させてもらっています。ここまでわかりやすくて根本的な説明までされている講座はあまり見たことがありません。
1点質問があるのですが、FindHeroメソッドについて、「return null;」という位置が気になり、この文章だと何となく「そうでなかった場合」の処理がピンとこなかったため、下記のようにreturn null部分を消してif,elseのコードに書き換えたのですが、この場合だと別のエラーが発生してしまいました。”Hero”かnullのどちらかが返ってきているはずなのですが、なぜ値がreturnされないのか、もしお時間取らせないようであればご教授いただいてもよろしいでしょうか。
for (int i = 0; i < names.Length; i++)
{
if (names[i] == "Hero") {return names[i];}
else {return null;}
}
//return null;
エラー内容は下記になります。
Main.cs(18,26): error CS0161: `Hello.FindHero(string[])': not all code paths return a value
宜しくお願い致します。
>Webster様
こんにちは、返事遅くなって申し訳ありません。
少しでもお力になれていれば何よりです!
連携いただいた内容ですが、
これから勉強していく方にとっても非常に面白いポイントですね。
コメント欄だと少し見辛いかもしれませんが、解説させてもらいます。
<解説>
まず本記事にも記載している通り、関数というものは、
「戻り値の型を指定した場合、必ず何らかの値を返さなければならない」
です。
「必ず何らかの値を返さなければならない」ということは、
例えばIf文で2通り(あるいはそれ以上)のルートを通る場合、
どのルートを通ったとしても何らかの値を返さなければならない、
ということです。
ここでエラー内容を見てみましょう。
「CS0161:`メソッド名’ not all code paths return a value」
このエラーは、
「このメソッド、何も値が返ってこないルートがあるよ!」
ということを教えてくれるエラーです。
いやいや待ってくれ、プログラムをよく見るんだ、
きちんとIfとElseにそれぞれ値を返すよう実装しているじゃないか、
と思うかもしれません。
if (names[i] == "Hero")
{
return names[i]; <=ちゃんと条件に該当する時も、
}
else
{
return null; <=Elseの時も、値を返している!
}
ですが、ここでポイントとなるのは、
実はこの処理を囲んでいるFor文の方です。
for (int i = 0; i < names.Length; i++)
{
if (names[i] == "Hero")
{
return names[i];
}
else
{
return null;
}
}
実はこの処理、ひとつだけ値が返されないパターンがあるのです。
それはずばり、
「引数「names」が0個の配列」
だった時です。
この場合、
①forループを動かすための初期化処理が動作する(int i = 0;)
②ループ処理を開始しようとするが、
この時点で既にループ終了条件である
i(0) < names.Length(0)
が成立してしまっているので、ループ処理は行われずに次の処理へ進む
③次の処理に進むとメソッドが終わってしまうが、
何も値が返されないため、CS0161のエラーが発生する
という流れになっています。
なので、If文だけを見ると2パターンのように見えるこのメソッドは、
実は3パターンの処理ルートを持っているんですね。
①namesの個数が1以上 かつ if文がtrueの場合
②namesの個数が1以上 かつ if文がfalseの場合
③namesの個数が0の場合(forループが一度も実行されない場合)
この問題を解消するために、本記事側の実装では、
「Forループを抜けた後」にreturn nullという処理を置いています。
これであれば、②のパターンでも、③のパターンでも、
必ずnullという値が返されることになりますね。
public static string FindHero(string[] names)
{
for (int i = 0; i < names.Length; i++) <=もしnamesが0個でも……
{
if (names[i] == "Hero")
{
return names[i];
}
}
return null; <=必ず値が返される!
}
解説は以上となります。
コメントだと色もつけられないので読み辛いかもしれませんが、
疑問の解消に繋がれば幸いです。
わざわざ 1質問のために丁寧にご返答いただきありがとうございました。
なるほど、C#では、たとえそのプログラム上で理論上問題なく通るはずでも、関数内で条件式を書くのであれば、その時点ですべての可能性は漏れなく記載しておかないといけないということなんですね。
ありがとうございました。