プログラミングで登場する、Null(ヌル), Nil(ニル), None(ナン)といったキーワード。
なにも無い、設定されてない、無効である…そういった意味を持つ値です。
そんなNull、プログラミングではなかなか厄介な存在。ちょっと扱い間違えれば、プログラムがすぐに止まります。
近年のプログラミング言語には、問題をうまく回避する「Null安全(Null Safety)」の考えが広まってきました。言語側で、問題を起こさないような機構をいろいろサポートしてします。
ただ言語によって、その考え方や表現方法はバラエティ豊か。いろいろなプログラミング言語での対応方法を見てみましょう。
「Null安全」とはなにか
まずは、Nullの問題とはなにか、どういう考えで回避するのか、確認します。
Null問題のサンプルプログラム
簡単なサンプルプログラムです。言語はJava。
会員制クラブの管理システム。会員クラス”Member”に、住所を取得するgetAddress
メソッドがあります。
class Member {
public String getAddress() {
...
}
}
会員リスト表示のため、住所を取得します。でも長すぎるので先頭5文字だけ取り出します。
...
String address = member.getAddress();
String displayAddress = address.substring(0, 5);
...
実行してみると…NullPointerExceptionエラーが発生しました。(ぬるぽ)
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.substring(int, int)" ..
どうも、会員は住所を未登録にできるらしく、そのときはaddress=nullになるらしい。nullオブジェクトに対してsubstring
を呼んだためにエラーになったのですね。
Nullオブジェクトに何かをしてエラーになる。よくある問題ですね。
古典的な対処方法
基本の対処は、nullかどうかをチェックして分岐する。
未登録の場合は”N/A” (Not Applicable) と表示することにして、見直します。
String address = member.getAddress();
String displayAddress;
if (address != null) {
// アドレスがnullでなければ先頭5文字を設定
displayAddress = address.substring(0, 5);
} else {
// nullならN/Aを設定
displayAddress = "N/A";
}
これで問題は解消します。特に困ることはありません。
けれど、この対処には、以下のようなデメリットがあります。
- ユーザー間の取り決めでしかない。無効値ならnullとするというのは、所詮は人同士の決め事でしかない
- 言語的に意図的なのか区別がつかない。そのnullは、意図的なのか、はたまた設定が漏れただけか
- 言語的にガードできない。nullにアクセスされたら、言語はエラーにするしかない
プログラミング言語側からすると、Nullに対して防御する術がないのです。地雷地帯でも慎重に進めば大丈夫…といっているのと同じ。
これに対して「できる限りNullで失敗しないようにガードをかけよう」。これがNull安全の考え方です。
対処その1:「オプショナル型」で無効値を表す
まずは、Nullという、意味があるのかないのか分からない存在を、なんとかします。
オプショナル型を導入します。「無効である」ことを明確に表現できる型です。
- 有効ならば、その値
- 無効ならば、無効であること
この2つの状態を同時に取り扱える型
オプショナル型で、有効か無効かを明確な手続きで判断したり、無効値に対するセーフティ機構を導入することができます。
class Member {
public Optional<String> getAddress() {
//...有効値の場合
return Optional.of("tokyo,japan");
//...無効値の場合
return Optional.empty();
}
}
isPresent
(有効か)などのメソッドを使うと、判断の意図が明確になりますね。
// オプショナル型
Optional<String> address = member.getAddress();
String displayAddress;
if (address.isPresent()) {
// アドレスが有効なら先頭5文字を設定
displayAddress = address.get().substring(0, 5);
} else {
// 無効ならN/Aを設定
displayAddress = "N/A";
}
System.out.println(displayAddress);
対策その2:「Null安全演算子」でNullアクセスを防御する
Nullでよく起こる問題が、Nullオブジェクトに誤ってアクセスしてしまうこと。
Null安全演算子で、Nullへの不用意なアクセスを防ぎます。
オブジェクトに対し、
- 有効なオブジェクトなら:指定処理を呼び出して、結果を反映する
- 無効なオブジェクトなら:何もせずに無効オブジェクトとして返す
という判断を自動的に行う演算子
Javaなら、map演算子が使えます。
Optional<String> address = member.getAddress();
// アドレスの先頭5文字を取り出す
Optional<String> abbreviatedAddress = address.map(n -> n.substring(0, 5));
String displayAddress;
if (address.isPresent()) {
displayAddress = abbreviatedAddress.get();
} else {
displayAddress = "N/A";
}
このような動作を、mapが自動的に判断して行います。
- addressが有効なら、substringを呼び出し、その結果が入ったOptional<String>を返す
- addressが無効なら、何もせずに無効のOptional<String>を返す
いずれの場合も、結果はオプショナル型で返ります。
対策その3:「Null合体演算子」でNull判定を簡単に
Nullを扱うときの頻出パターンが、「Nullでなければそのままの値、Nullなら特別な値」にする。
これを簡潔に記述するのが、Null合体演算子です。
オブジェクトに対し、
- それがNullでなければ、その値をそのまま返す
- それがNullなら、特別な値を返す
を行う演算子
書き換えてみましょう。JavaのorElse演算子を使います。
Optional<String> address = member.getAddress();
Optional<String> abbreviatedAddress = address.map(n -> n.substring(0, 5));
String displayAddress = abbreviatedAddress.orElse("N/A");
orElseで、
- abbreviatedAddressが有効ならそのままの値
- 無効なら”N/A”
が返ってくるというわけです。
対策全部入りでシンプルに
この対策を全部入れると、シンプルかつ、Nullに対してガードが効いたコードにすることができます。
変更前はこう。
String address = member.getAddress();
String displayAddress;
if (address != null) {
displayAddress = address.substring(0, 5);
} else {
displayAddress = "N/A";
}
変更後はこうなります。
Optional<String> address = member.getAddress();
String displayAddress = member.getAddress().map(n -> n.substring(0, 5)).orElse("N/A");
とてもすっきりした記述になりますね。
言語ごとのNull安全対応
現在ではさまざまなプログラミング言語が、Null安全の考え方を取り入れています。
基本的な考えは似通っていますが、その表現方法はさまざまです。言語ごとに見ていきましょう。
C++
C++17から、オプショナル型としてstd::optionalが使えます。
optional型のvalue_orで、Null合体演算子の操作ができます。さらにC++23では、optional型にand_thenといった操作が実装されています。
std::optional<std::string> address = member.get_address();
std::string display_address = address.and_then(
[&](std::string s) -> std::optional<std::string> {
return s.substr(0, 5);
}
).value_or("N/A");
呪文のようになっていますが、Nullに係わる処理が1stepで完了しているのが分かります。
Java
Javaでは、先ほどの紹介の通り、Optional型が使えます。
Optional<String> address = member.getAddress();
String displayAddress = member.getAddress().map(n -> n.substring(0, 5)).orElse("N/A");
Javaの弱点として、Optionla<T>型の変数にnullが代入できてしまう点があります。これはJavaという言語仕様的に防げないところ。注意しましょう。
C#
C#には、オプショナル型のような型定義がありません。
代わりに、オブジェクト型変数へのNull代入を許容しない、ということを、コンパイルオプションで指定することができます。
Nullable(Null許容)オプションをenableとして、コンパイルします。こうすると、通常オブジェクトにnullを代入するようなコードがあるとコンパイルエラーになります。これで不用意なNull代入を抑止できます。
String address = null;
// warning CS8600: Converting null literal or possible null value to non-nullable type.
型に?をつけると、Null許容参照型となり、Null代入可能となります。Null条件演算子(?.)およびNull合体演算子(??)も使えます。
String? address = member.GetAddress();
String displayAddress = address?.Substring(0, 5) ?? "N/A";
Python
Pythonは動的型付け言語なので、変数にどんな型でも格納できます。
ただ、指定できる型や返却型を記述できる、型ヒントの仕掛けが導入されています。型ヒントで、戻り値などの型を指定することが可能です。
ただしこれはあくまで可読性向上や静的チェックのため。実行時に異なる型で設定したとしても、エラーにはならないので注意です。
class Member:
def get_address(self) -> str | None:
return None
Noneに関する特別な演算子はありませんが、セイウチ演算子(:=)を使うと比較的シンプルに書けます。変数の代入と使用を同時に行う演算子です。
member = Member()
if (address := member.get_address()) is not None:
display_naddress = address[:5]
else:
display_address = "N/A"
JavaScript/TypeScript
JavaScriptでは、Null安全演算子のような役割のオプショナルチェーン(?.)や、Null合体演算子(??)が使えます。
let address = member.address;
let displayAddress = address?.substring(0, 5) ?? "N/A";
さらに、型定義ができるTypeScriptでは、Nullable型が明示的に宣言できます。型チェックの厳密性や可読性が向上します。
let address : string | null = member.address;
let displayAddress: string = address?.substring(0, 5) ?? "N/A";
PHP
PHPでは、関数の引数や戻り値に、型を指定することができます。
このとき通常型ではnullが指定不可になり、null指定して実行するとエラーとなります。動的型付けが多いスクリプト型言語では珍しい仕様です。
class Member
{
function getAddress(): string
{
return null;
// Uncaught TypeError: Return value must be of type string, null returned in ..
}
}
型の後ろに、?をつけると、Nullable型として定義できます。
<?php
class Member
{
function getAddress(): ?string
{
return null;
}
}
$member = new Member();
$address = $member->getAddress() ;
$displayAddress = $address ? substr($address, 0, 5) : 'N/A';
?>
Ruby
Rubyでは、セーフナビゲーション演算子(&.)が使えます。ぼっち演算子とも呼ばれます。
またOR演算子(||)は、nilをfalseと判定するので、Null合体の演算子として使えます。
display_address = member.get_address&.slice(0, 5) || "N/A"
nilガード演算子(||=)というものもあります。nilかどうか判定し、nilなら変数自身に代入しなおします(自己代入演算子とも言います)。他の言語では見かけない、Ruby特有の演算子です。
address = nil
address ||= "N/A" # nilでなければそのまま、nilなら右辺代入
Kotlin
Kotlinでは、nullを代入可能とするか否か、を明確に定義することができます。
- null代入不可とするnull値非許容型
- nullを代入できるnull値許容型
null値非許容型にnullを代入しようとすると、コンパイルエラーになります。これでnullの不用意な使用を抑止できます。
var address: String = null
// null can not be a value of a non-null type String
null値許容型にするときは、型名の後に?を付与します。
var address: String? = null
// OK
null値許容型には、セーフコール演算子(?.)、エルビス演算子(?:)、が使えます。
class Member {
var address: String? = null
set(value) {
field = value
}
get() {
return field;
}
}
var displayAddress = member.address?.substring(0, 5) ?: "N/A";
null値非許容型にこれらの演算子を使おうとすると、コンパイル時に警告で知らせてくれます。
class Member {
var address: String = null
...
}
var displayAddress = member.address?.substring(0, 5) ?: "N/A";
// warning: unnecessary safe call on a non-null receiver of type String
// elvis operator (?:) always returns the left operand of non-nullable type String
Swift
Swiftは、通常の変数にはnilを代入できません。不用意なnilの使用を抑止しています。
let address : String = nil;
// 'nil' cannot initialize specified type 'String'
// add '?' to form the optional type 'String?'
nilを代入するには、オプショナル型で宣言する必要があります。型名の後に?を付与します。
let address : String? = nil;
// OK
??演算子や、オプショナルチェーン(?.)を使用することができます。
class Member {
func getAddress() -> String? {
return nil;
}
}
let displayAddress = member.getAddress()?.prefix(5) ?? "N/A";
オプショナル型でない変数にこれらの演算子を使おうとすると、コンパイル時にエラーとなります。
class Member {
func getAddress() -> String {
return nil;
// error: 'nil' is incompatible with return type 'String'
}
}
let displayAddress = member.getAddress()?.prefix(5) ?? "N/A";
// error: cannot use optional chaining on non-optional value of type 'String'
Go
Goは、ほかの言語とは明らかに立ち位置が異なります。
Goでは、nilを特に否定せず、セーフティを保つための特別な仕掛けもありません。古典的な手法で対応することとなります。
シンプルな構文を保つ続けようとする、Goのポリシーと言えるでしょう。
Rust
Rustは、これまでの言語と比べても異色の対応を取っています。
Rustの重要な仕様として、nullやnilといった無効値定義を撤廃しています。無効値がある値は、必ずOption型で定義します。
enum Option<T> {
Some(T),
None,
}
そして、制御フロー演算子matchで、有効か無効かに従い、対処が分岐します。
let address: Option<String> = get_address();
let display_address = match address {
Some(x) => String::from(&x[..5]),
None => String::from("N/A"),
};
Rustでは、つぎの思想に基づいて意図的に設計されています。
- nullになる可能性のある値を保持するには、その値の型をOptionにしないといけない
- その値を処理するには、nullである場合を明示的に処理する必要がある
これにより、Option以外のすべての箇所で、Nullが登場しないことが約束されるということになります。
まとめ
Nullへの対応は、プログラマーにとって重要であり、かつ悩ましい課題です。
プレゼンテーション “Null References: The Billion Dollar Mistake”(Null参照: 10億ドルの間違い)には、こう記載されています。
私はそれを10億ドルの失敗と呼んでいます。
その頃、私は、オブジェクト指向言語の参照に対する、 最初のわかりやすい型システムを設計していました。私の目標は、 どんな参照の使用も全て完全に安全であるべきことを、コンパイラにそのチェックを自動で行ってもらって保証することだったのです。
しかし、null参照を入れるという誘惑に打ち勝つことができませんでした。それは、単純に実装が非常に容易だったからです。
これが無数のエラーや脆弱性、システムクラッシュにつながり、過去40年で10億ドルの苦痛や損害を引き起こしたであろうということなのです。
近年のプログラミング言語に取り入れられる、Null安全への仕掛け。
言語により実装方法は様々ですが、向かう方向性は同じです。より安全に、完全に。
言語の仕掛けもうまく利用し、安全性の高いプログラミングを心がけましょう。
コメント