【11種言語ぬるぽ】なにも無いがある、Nullとの上手な付き合い方

プログラミングで登場する、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への不用意なアクセスを防ぎます。

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は、このような動作を、mapがまとめて行います。

  • addressが有効なら、substringを呼び出し、その結果が入ったOptional<String>を返す
  • addressが無効なら、何もせずに無効のOptional<String>を返す

対策その3:「Null合体演算子」で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++

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

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に関する特別な演算子はありませんが、セイウチ演算子(:=)を使うと比較的シンプルに書けます。

セイウチ演算子(Walrus operator)の由来 :=を横向きにみるとセイウチに見えるから)

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億ドルの苦痛や損害を引き起こしたであろうということなのです。

近年のプログラミング言語には、エラーや脆弱性に対処すべくk、Null安全への仕掛け色々取り入れられています

言語ポリシーにより、実装方法は様々です。でも、より安全に、完全に。向かう方向性は同じです。

言語の仕掛けもうまく利用し、安全性の高いプログラミングを心がけましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次