Decimal?→Decimalの変換どうするのがいい?という話です。
せっかちな人は最後の「結論」だけ読みましょう。
背景
DBのとある金額フィールドpriceがNull許容になっていたとします。
それに対応するEntityクラスのpriceフィールドは、Decimal?型になります。
しかし、最終的に計算する段階では、Decimal?のままだと困るので、Nullなら0として扱いたいわけです。
「だったら最初からDBのフィールドをNull許容にしたりせず、0を設定するようにしろよ。そもそもその値がNullというのはどういう状態を表すんだ」
という声も聞こえてきそうですが、現実の運用では色々あるんです。
とにかく今は、Decimal?→Decimalの変換をしたいわけです。
しかし、そのまま代入しようとしても、コンパイルエラーになります(もちろん、Option Strict Onです)。
Dim price As Decimal = rec.price
Option Strict Offにすればコンパイルが通るようになりますが、当然、rec.price.HasValueがFalseの時には実行時例外が発生しますので、なんの解決にもなっていないどころか害悪です。
それでは、と、下のように書くとコンパイルは通るようになりますが、rec.price.HasValueがFalseの時、rec.price.ValueがInvalidOperationExceptionが発生します。
Dim price As Decimal = rec.price.Value
priceにNothingを設定すれば0として扱われる(VBではNothingは規定値を表す為)のだから、rec.price.HasValueがFalseならNotingを返してくれればいいのにとも思いますが、Null許容型はそう単純な話でもないのでしょう。
Dim price As Decimal = Nothing
というわけで、まずはバカ正直にHasValueとValueを使って対応してみます。
というのも、VB.NETのNull許容型の公式ページには、HasValueとValueの説明しか載っていないからです。
null 許容値型の最も重要なメンバーは、HasValue プロパティと Value プロパティです。 null 許容値型の変数の場合、HasValue により、変数に定義済みの値が含まれているかどうかがわかります。 HasValue が True の場合は、Value から値を読み取ることができます。 HasValue と Value はどちらも ReadOnly プロパティであることに注意してください。
そうですか、ということで、こんなコードを書いてみます。
Dim price As Decimal
If rec.price.HasValue Then
price = rec.price.Value
Else
price = 0
End If
ちゃんと動作します。
まどろっこしすぎて泣けてきますが、上記のページでこの書き方を推奨している為、実際にこう書いている人は多いと思います。
まともなコーディングをしたいと心掛けている人は、三項演算子を使うでしょう。
Dim price As Decimal = If(rec.price.HasValue, rec.price.Value, 0)
ずいぶん見やすくなりました。でももっとシンプルに書けないのだろうかと思います。特に、rec.priceを2回も書かないといけないところなんて寒気がします。
C#ならこう書けるんですが、VB.NETにはこんなものはないのです。
decimal price = rec.price ?? 0
いや、でも待てよ? If演算子は2項でも書けたはず。Null許容型を第一引数に与えてこんな風に書けるのでは?
Dim price As Decimal = If(rec.price, 0)
これはよさそうな感じですね。
ただ、いちいち「0」と書かねばならないのは、ちょっと面倒な気もしてきます。まぁ意図が明確で良い気もするのですが。
CDec()とか使えないんでしょうか? DecimalにNothingを代入すると0になるのですから、CDec()でそのまま行ける気もします。
Dim price As Decimal = CDec(rec.price)
しかし残念ながら、rec.price.HasValueがFalseの時、上記のコードはInvalidOperationExceptionを返します。
それでは、Convert.ToDecimal(value)はどうでしょうか。
Dim price As Decimal = Convert.ToDecimal(rec.price)
これは問題なく動きます。コードの意図も明確ですし、もうこれで良い気もしてきますが、Convert.ToDecimalはそもそも、文字列等の値を変換する汎用関数です。Nullの時に0として扱いたいだけの為に呼び出すのは、恐らくコスト的によくないでしょう。
いっそ、HasValueをチェックしてデフォルト値を返すNVLメソッドを、Nullable(Of T)に対する拡張メソッドとして作ってやろうか…と思い始めます。
と、ここまで来て、「まてまて、そもそもその程度の機能、標準で用意されていないのか?」と思い、Nullable(Of T)の詳細を見に行きます。
メソッド | 説明 |
---|---|
GetValueOrDefault() | 現在のNullable<T> オブジェクトの値、または基になる型の規定値を取得します |
GetValueOrDefault(T) | 現在のNullable<T> オブジェクトの値、または指定した規定値を取得します |
…あるじゃないですか…。なんでこれ、最初の「Null許容型」の説明ページに書いてないんでしょうか。これまでの考察はなんだったのか。
というわけで…
結論
Decimal?→Decimalへの変換はGetValueOrDefault()メソッドを使います。
Dim price As Decimal = rec.price.GetValueOrDefault()
rec.priceがNullの時(実際にはNullというわけではなく、rec.price.HasValueがFalseになっている時)には、0が規定値として返ります。
もし -1 を規定値としたい場合には、次のように書きます。
Dim price As Decimal = rec.price.GetValueOrDefault(-1)
お好みで、こちらの書き方も良いと思います。
Dim price As Decimal = If(rec.price, 0)
ちなみに、Decimal型の話として書きましたが、もちろんInteger型など他のプリミティブ型でも使える話です。
「Nullの時に規定値を取得っていうけど、規定値ってどこで決まってるの?」と思った人は、こちらを参照してみてください。数値型の規定値は、全て0です。C#の記事ですが、たぶんVB.NETでも同じでしょう。