Visual Studio 2015およびXamarin Studio 5.9で使えるようになったC#6は、自動実装プロパティの初期化、ラムダ式によるメンバー関数の記述、using staticなどによって、ソースを書いている間のストレスが少なかったC#が、より快適に書けるようになった。
C++からC#に移ったとき、その書きやすさに開いた口が塞がらなかったけれど、それがより進化していっているから驚きだ。
書きやすいから、エンジニアは純粋にロジックに集中できる。C#は、そうして書かれたソースの読みやすさも担保しているから、エンジニアフレンドリーだなと日々感じている。
さて、よりフレンドリーになったC#6にあって、特別に取り上げたいのが null条件演算子 だ。
経緯はわからないけれど、Appleのプログラミング言語 Swift にあったものをそのまま持ってきたような機能で、C#がSwiftっぽくなったというのが第一印象。しかしながら、C#がSwiftに成れない理由もあり、今日はそこのところを考えようと思う。
null条件演算子
今まで、変数がnullだったときの処理をどう書いていただろう?
例えば、文字列を作る処理でnullチェックが必要なパターンを考える。この例では、タクシー運転手の名前を取得するが、運転手がいる時(not null)はその人の名前を使い、いない時(null)は代わりの名前を用意する。
1 2 3 4 5 6 7 8 9 10 11 12 |
class Taxi { public Driver Driver { get; } = new Driver(); } class Driver { public string Name { get; } = "Travis"; } var driver = new Taxi().Driver; var driverName = driver != null ? driver.Name : "Betsy"; |
一度、driverを取り出さないといけないところにちょっとした面倒くささを感じる。
これがC#6ではたった一行になる。
1 |
var driverNameIn6 = new Taxi().Driver?.Name ?? "Betsy"; |
この例では二行が一行になっただけでうまみは薄いけれど、Driverから更にオブジェクトを辿る場合はどうだろうか? その度にオブジェクトを取り出すのかと考えると、それだけでクラクラきちゃう。
null条件演算子を使えば、どれだけオブジェクトを辿ろうと、null合体演算子(??演算子) と併せて使うことで処理がワンラインで済む。
一方、Swiftは?
Swiftにもこの機能はある。
1 2 3 4 5 6 7 8 9 |
class Taxi { internal var driver: Driver? = Driver() } class Driver { internal var name: String = "Travis" } let driverName = Taxi().driver?.name ?? "Betsy" |
使い方はまったく同じで、nilである可能性のあるオブジェクトに “?” をつけてメソッドチェーンを続ける。メソッドチェーンの間でnilが出てきた場合、nilへのアクセスを回避しつつ、nilの場合の値が欲しいとき、“??” と併せると非常に簡潔なソースになる。
C#が越えられない壁
とてもよく似た機能がC#6とSwiftにあるわけだけれど、C#が越えられない壁というものがある。
Swiftは、nil (null) チェックが抜けていることをコンパイルエラーで検出できるけれど、C#ではそれができないということだ。
先のC#とSwiftの作例で、Taxiが持つDriver (driver) プロパティの型に注目すると、Swiftバージョンは “?” が付けられている。こうすることで、driverがnilになる可能性がある = オプショナル型の変数だということをコンパイラーに伝えられるから、driverに対してnilチェックをしていないとコンパイルエラーとなる。
1 |
let errorName = Taxi().driver.name : "Betsy" |
コードを書いていて、うっかりdriverのnilチェック(この場合は “?” をつける)を忘れた場合、ちゃんとコンパイルエラーで教えてくれるから安心安全なことこの上ない。
Swiftのクラスがnilにならないという仕様のおかげでこういうチェックができるわけだ。
ところがC#では、クラス型オブジェクトはデフォルトでnullになれる。だから、C#の作例はこう書いてもエラーにならない。
1 |
var errorName = new Taxi().Driver.Name ?? "Betsy"; |
Swiftではエラーにしてくれるパターンだけれど、C#の場合はこれでコンパイルが通り、実行できてしまう。もしDriverプロパティがnullを返す場合、NullReferenceExceptionなる例外が飛んでnullボカンだ。
〜Out of C#6. そしてC#7へ〜
さすがに今からこの仕様をひっくり返す(クラス型にnullを認めない)のは影響が大きすぎるからか、あるいは少し分離した問題だからか、C#6ではnull条件演算子を取り入れるに留まった。
Swiftなど他の言語のように、参照型がデフォルトでnullをとれないようにする変更は、現状の言語仕様で書かれたソースとの互換性を考えると、気軽に非null許容な参照型(Optional型)を入れられないのだろう。
Optional型(nullを取れる参照型)をC#に取り入れるなら、既存の参照型はnullにできなくなる。こうすることで、null参照の危険性をコンパイル時にチェックできるから、その有り難みは語り尽くせない。是非C#にも、このようなSwiftっぽい仕様を導入してほしいと思っていたら、次期バージョンのC#7で、呆気にとられる方法で導入していた。
この新しい提案では、既存の参照型
T
は以下の要件になる。
T
は非null許容型を表す。T?
はnull許容の参照型を表す- 以下の条件の時、コンパイラは警告を出力する。
- null許容の
T?
型の参照が外れている、または非null許容型に変換される場合- 非null許容型の変数に
null
/default(T)
が割り当てられる場合- null許容参照がnullにならないことをフロー分析で検出した場合
- コンストラクタが処理を戻す前に非null許容参照に割り当てをしない場合
- 非null許容参照がコンストラクタ内で割り当ての前に使われる場合
「つまり、どういうことだってばよ?」
「説明しよう! nullな参照型をコンパイルエラーにすればいいと思っていた我々の想像を越えたのだ!」
C#7で参照型にnullを入れられなくなった場合、間違いなく既存のソースはコンパイルエラーとなって大事になる。しかし、コンパイル時にnullボカンの危険性をチェックできるのは垂涎レベルでありがたい。
このアンビバレントな要望を解決するため、既存のソース(参照型にnullを入れているソース)はコンパイル警告にして今までどおり動かせるけれど、本当に安全なコード書きたいならSwiftっぽくしてね☆というのは、なんというか、「そういうやり方があったのか!」と驚くことしきりな解決方法だった。
もちろんこれはエレガントではないし、完璧でもない。エンジニアのヒューマンエラーで、警告を放置すれば、それがnullボカンに繋がるのだから。ただ、実際的問題として、これまでのC#ソースを生かさなければいけないことを考えると、Good Enoughな解決策だと思う。
警告を見れば既存のソースをリファクタリングできる + C#7ベースで書くならSwiftライクを意識すればいい + 互換性をころさない etc… この解決策のスマートじゃないけれどスマートな感じ、好きだな。
仕様変更に強いC#ソースを書きたいエンジニアへ