F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。
実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。
Nullableって何ですか?
ここで取り上げるNullable
はC#などでも提供されるSystem.Nullable<'T>のことです。これはNull許容型を表す型です。これにより、通常はnull
に対応しない型でも、それに対応できるようになります。このような型はSystem.ValueTypeを継承した数値型のほか、EnumやDateTimeなども該当します。
F#でのNullable
F#ではNullableにどう対応しているかを、以下のポイントから探ってみることにします。
- Nullableオブジェクト
- Null許容演算子
- Nullable対応のキャスト
Nullableオブジェクト
Nullable<'T>は'T
型の値を持つNullable
オブジェクトとして宣言できます。例えばNullable 1
は1
を持つNullable
オブジェクトとなります。
このオブジェクトのHasValue
プロパティの値はtrue
となり、何らかの値を持つことがわかります。そしてValue
プロパティでその値を得られます。
open System
let mutable n: Nullable<int> = Nullable 1 // Nullablleのインスタンス
n.HasValue // true
n.Value // 1
また、Nullable<int> ()
などのように、<'T>
で型を示したうえで引数なしで実行して得られたオブジェクトではHasValue
の値がfalse
となり、Value
で値を得ようとしてもSystem.InvalidOperationException
という例外が発生します。
open System
n <- Nullable<int>()
n.HasValue // false
n.Value // 例外発生「System.InvalidOperationException: Null 許容のオブジェクトには値を指定しなければ なりません。」
デフォルト値の設定
Value
による例外を発生させないように、GetValueOrDefault()
メソッドでデフォルト値を設定できます。引数なしなら0もしくは0.0(数値型による)、引数があれば、それがデフォルト値となります。
open System
let mutable n = Nullable<int>(1) // 引数: 1
n.GetValueOrDefault() // 1
n <- Nullable<int>() // 引数なし
n.GetValueOrDefault() // 0
n.GetValueOrDefault(3) // 3
Nullable型の判定
Nullable<'T>
オブジェクトで注意すべき点として、型を調べると'T
型と判定されるというのがあります。
open System
let n1 = Nullable 1
n1.GetType() = typeof<int> // true
printfn "%A" <| n1.GetType() // "System.Int32"
そこでコードクォート(<@ ... @>
)を使うと、Nullable
かどうかと'T
の型それぞれを確認できます。
open System
let n1 = Nullable 1
// Nullable<'T>型かどうかを調べる
<@ n1 @>.Type = typeof<Nullable<int>> // true
printfn "%A" <| <@ n1 @>.Type // "System.Nullable`1[System.Int32]"
// 'T型が何かを調べる
<@ n1 @>.Type.GenericTypeArguments.[0] = typeof<int> // true
printfn "%A" <| <@ n1 @>.Type.GenericTypeArguments.[0] // "System.Int32"
=, <>はデフォルトで対応
Nullable
オブジェクトは、デフォルトで=
, <>
の演算に対応していますが、算術演算や大小の比較演算は対応していません。ですが、Null許容演算子を使えば算術演算や比較演算にも対応します。
open System
(Nullable 2) = (Nullable 2) // true
(Nullable 2) <> (Nullable 3) // true
(Nullable 2) < (Nullable 3) // サポートされない(Null許容演算子で対応)
(Nullable 2) + (Nullable 3) // サポートされない(Null許容演算子で対応)
Null許容演算子
Null許容演算子とは、左辺と右辺のどちらか、あるいはいずれもがNullable
オブジェクトの場合に利用できる演算子の総称です。大別すると算術演算と比較演算で提供されており、どれも通常の演算子とNullable
オブジェクトに対応する側に?
が並ぶ記号で表されます。これらはMicrosoft.FSharp.Linq.NullableOperatorsモジュールで実装されています。
算術演算のNull許容演算子
算術演算のNull許容演算子は、両辺のどちらかがNullable()
のときは結果もNullable()
となりますが、両辺ともに値を持つNullable
オブジェクトであれば、計算の結果を持つNullable
オブジェクトを得られます。
open System
open Microsoft.FSharp.Linq.NullableOperators
(* 以下いずれも結果は Nullable 3 *)
(Nullable 1) ?+ 2 // 左辺がNullable
1 +? (Nullable 2) // 右辺がNullable
(Nullable 1) ?+? (Nullable 2) // 両辺がNullable
(* 以下いずれも結果はNullable<int>() *)
Nullable<int>() ?+ 2
1 +? Nullable<int>()
Nullable<int>() ?+? Nullable<int>()
比較演算のNull許容演算子
比較演算のNull許容演算子は、Nullable
オブジェクトが持つ値どうしを比較します。結果はbool
型です。Nullable<'T>()
については、<>
や同じ'T型どうしのときtrue
となります。
open System
open Microsoft.FSharp.Linq.NullableOperators
(* 以下いずれもtrue *)
(Nullable 3) ?> 1 // 左辺がNullable
3 >? (Nullable 1) // 右辺がNullable
(Nullable 3) ?>? (Nullable 1) // 両辺がNullable
1 <>? (Nullable<int>()) // 右辺がNullable<int>()
(Nullable<int>()) ?=? (Nullable<int>()) // 両辺がNullable<int>()
Nullable対応のキャスト
Nullable<'T>型から'T型へのキャスト
F#ではキャスト(型変換)を行う関数が提供されていますが、これらはNullable
オブジェクトにも対応しています。ただし、処理の内容はValue
と等価のNullable<'T> -> 'T
であって、'T
を別の型には変換できません。また、引数がNullable<'T>()
のときはSystem.InvalidOperationException
という例外が発生します。
open System
Nullable 1 |> int // 1
Nullable<int>() |> int // 例外発生「System.InvalidOperationException: Null 許容のオブジェクトには値を指定しなければ なりません。」
同様の処理を行う関数にはunbox<'T>
もあります。こちらは引数がNullable<'T>()
のときSystem.NullReferenceException
という例外が発生します。
open System
Nullable 1 |> unbox<int> // 1
Nullable<int> () |> unbox<int> // 例外発生「System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。」
Nullable<'T>型が持つ値を別の型へのキャスト
Nullable<'T>
が持つ値を別の型に変換するキャスト関数はMicrosoft.FSharp.Linq.Nullableモジュールに含まれており、Nullable.int
などで提供されます。これらはopen Microsoft.FSharp.Linq
を実行すると利用できます。
open System
open Microsoft.FSharp.Linq
Nullable 1 |> Nullable.float // Nullable<float> 1.0
Nullable 1 |> Nullable.float |> float // 1.0 (float)
nullなんですか?
実は、Nullable<'T> ()
のように引数なしで生成されたNullable
オブジェクトはnull
とみなされることがあります。実際にdefaultof
の戻り値はnull
となります。
open System
open Microsoft.FSharp.Core.Operators.Unchecked
defaultof<Nullable> // null
defaultof<Nullable<int>> // null
defaultof<int> // 0
defaultof<System.DateTime> // 0001/01/01 0:00:00
そのため、Nullable
オブジェクトの操作で例外が発生することがあります。
さきほどのunbox<'T>
でも、引数がNullable<'T>()
のときSystem.NullReferenceException
という例外が発生することを紹介しました。
open System
Nullable<int>() |> unbox<int> // 例外発生「System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。」
また、この型を調べようとして、直接GetType()
メソッドを実行しても同様の例外が発生してしまいますので注意が必要です。
open System
(Nullable<int>()).GetType() // 例外発生「System.NullReferenceException: オブジェクト参照がオブジェクト インスタンスに設定されていません。」
ちなみに、このオブジェクトに対してToString()
メソッドを実行すると""
(長さ0の文字列)となります。
open System
(Nullable<int>()).ToString() // ""(長さ0の文字列)
[おまけ] 'T option型に変換する型拡張
F#のプログラムでは、結果を得られないかもしれないとき、例外の発生を回避するために'T option
(もしくはOption<'T>
)という型が使われることがあります。
さきほどのunbox<'T>
では引数がNullable<'T>()
のとき例外が発生してしまいますが、tryUnbox<'T>
という関数では、その際にNone
を返すことで例外の発生を回避します。
tryUnbox<int> (System.Nullable<int>()) // None (int option)
tryUnbox<int> (System.Nullable<int>(1)) // Some 1
そこで、F#の型拡張というしくみを利用して、Nullable a
のように値を持つ場合はSome a
とし、Nullable<'T> ()
のように値を持たないときはNone
とするToOption
メソッドを追加する方法を考えてみました。型拡張による追加メソッドであれば、新たなopen
をせずに済み、メソッドで改めて<'T>
を指定しなくてもオブジェクトに対して直接実行できるのが便利かと思います。
type System.Nullable<'T when 'T : (new : unit -> 'T)
and 'T :> System.ValueType
and 'T : struct> with
member this.ToOption() =
if this.HasValue then Some this.Value else None
(* 使用例 *)
(System.Nullable 1).ToOption() // Some 1
(System.Nullable<int>()).ToOption() // None
最後に改めてF#におけるNullable
についての注意点をまとめると
-
Nullable<'T>
型が'T
型と判定されることがある -
Nullable
オブジェクトがnull
とみなされることがある
となります。こちらをご覧いただいた方々の参考になればと思います。