F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。
実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。
なお、ここで対象とするF#のバージョンは4.1(FSharp.Core 4.4.1.0)とします。
Disposeできてる?
Disposeメソッドは、プログラムが占有(確保)していたリソース(メモリなど)を解放する処理を行います。これはSystem.IDisposableインターフェースを実装したクラスに存在しています。リソースはファイルや通信などのI/O処理、スレッドの開始などで実行中のプログラムが一時的に占有します。それらを解放する処理を行うのがDispose
メソッドです。
リソースの解放を怠ると、プログラムがリソースを占有し続けることになります。さらに占有するリソースが膨張し続ければ実行環境に余分な負荷をかけることになりかねません。そこでF#には、Dispose
メソッドを自動的に実行できる方法が提供されています。
それがuseとusingです。これらを使うとDispose
メソッドを自動的に実行し、リソースを解放してくれます。2つの違いはリソースを解放するタイミングです。use
ではブロックの処理が終わった後に同メソッドを実行するのに対して、using
は引数に設定した関数の処理が終わると同メソッドが実行されます。
例えばテキストファイルを1行ずつ読み込んで処理を行う関数では、ファイルをオープンするとき、use
はletの代わりとして使い、using
は第1引数にリソース(ファイルのオープン)、第2引数にリソースを使う処理を表す関数を設定するような使い方ができます。
(* テキストファイルを一行ずつ読み込んで処理を実行(use版) *)
let readLines file f =
(* ファイルから一行読み込んで関数を実行 *)
let rec readLine i (r: System.IO.StreamReader) =
let line = r.ReadLine() // ファイルから一行読み込む
if line <> null
then f i line // 関数を実行
readLine (i + 1) r // 次の行を読み込む
else ()
(* 処理開始 *)
use reader = System.IO.File.OpenText file // ファイルオープン(ここからブロック終了まで有効)
readLine 0 reader // 実行後、自動的にファイルクローズ
readLines @"platforms.txt"
(fun i line -> printfn "%i. %s" (i + 1) line)
(* platforms.txt *)
.NET Framework
.NET Core
Xamarin
ASP.NET
Azure
(* 結果 *)
1. .NET Framework
2. .NET Core
3. Xamarin
4. ASP.NET
5. Azure
(* テキストファイルを一行ずつ読み込んで処理を実行(using版) *)
let readLines file f =
let rec readLine i (r: System.IO.StreamReader) =
// ..... (use版に同じ)
using (System.IO.File.OpenText file) (fun r -> readLine 0 r) // rでオープンしたファイルを読み込む
readLines @"platforms.txt"
(fun i line -> printfn "%i. %s" (i + 1) line) // この関数実行後、自動的にファイルクローズ
// 結果省略
本当にDisposeできてる?
では、use
とusing
によって本当にDispose
メソッドが実行されているのか確認してみます。メッセージを表示するDispose
メソッドを持つSystem.IDisposable
オブジェクトを定義し、それをuse
とusing
に設定し処理を実行するとどうなるでしょうか。Dispose
メソッドが実行されるタイミングに注目です。
(* Disposeメソッドが実行されるとメッセージを表示するSystem.IDisposableオブジェクト *)
let disposable s = { new System.IDisposable with
member this.Dispose() = printfn "%s" s }
(* useでDisposeメソッドが実行されるか? *)
let use_disposable() =
use d = disposable "use_disposable end" // Disposeメソッドが実行されるとメッセージ表示
printfn "use_disposable start"
printfn "use_disposable function end"
use_disposable()
(* 結果 *)
(*
use_disposable start
use_disposable function end
use_disposable end (Disposeメソッド実行)
*)
(* usingでDisposeメソッドが実行されるか? *)
let using_disposable() =
using (disposable "using_disposable end") // Disposeメソッドが実行されるとメッセージ表示
(fun d -> printfn "using_disposable start")
printfn "using_disposable function end"
using_disposable()
(* 結果 *)
(*
using_disposable start
using_disposable end (Disposeメソッド実行)
using_disposable function end
*)
Dispose
メソッドが実行されるタイミングが、use
ではブロック(インデントの深さが同じ部分)の最後、using
では第2引数(関数)の実行後であることがわかります。
これはDisposeできる?
これでuseとusingによってDisposeメソッドを実行できるのは確認できました。また、それぞれでDisposeメソッドが実行されるタイミングが違うこともわかりました。では、どのクラス、インターフェース、オブジェクトにDisposeメソッドが実装されているのでしょうか。
System.IDisposableインターフェースのドキュメントには、それを実装したクラスなどの一覧があり、これらを継承したクラスなども順にドキュメントを辿って参照していけます。逆に「このクラスはSystem.IDisposableインターフェースを実装しているのか」もAPIのドキュメント冒頭にあるシグネチャを見ればわかります。
ですが、APIのドキュメントにアクセスできないときはどうしたら良いでしょうか。いくつか方法が考えられます。これら以外にも良い方法があるかもしれません。
letでチェック
あるオブジェクトでSystem.IDisposable
が実装されているかを、仮引数の型を指定したlet
でチェックしてみます。実引数の型がSystem.IDisposable
を実装していなければエラーとなります。
let checkIDisposable (x: System.IDisposable) =
try
x.GetType().FullName
|> printfn "%sはSystem.IDisposableを実装しています"
with
ex -> eprintfn "%s" ex.Message
checkIDisposable (new System.IO.MemoryStream())
// 結果: System.IO.MemoryStreamはSystem.IDisposableを実装しています
checkIDisposable (new System.Timers.Timer())
// 結果: System.Timers.TimerはSystem.IDisposableを実装しています
checkIDisposable System.Drawing.SystemFonts.DefaultFont
// 結果: System.Drawing.FontはSystem.IDisposableを実装しています
checkIDisposable <| obj()
// 結果: 型 'System.Object' は型 'System.IDisposable' と互換性がありません (エラー)
matchと(:?)でチェック
match式と:?
で表される型テスト演算子でチェックしてみます。
let checkIDisposable (x: obj) =
printfn "%sはSystem.IDisposableを実装していま%s"
<| x.GetType().FullName
<| match x with
| :? System.IDisposable -> "す" // ここでチェック!
| _ -> "せん"
checkIDisposable (new System.IO.MemoryStream())
// 結果: System.IO.MemoryStreamはSystem.IDisposableを実装しています
checkIDisposable (new System.Net.HttpListener())
// 結果: System.Net.HttpListenerはSystem.IDisposableを実装しています
checkIDisposable <| obj()
// 結果: System.ObjectはSystem.IDisposableを実装していません
キャストでチェック
System.IDisposable
にキャストできるかどうかでもチェックできます。
let checkIDisposable<'T>(x: obj) =
try
x :?> 'T |> ignore // ここでチェック!
printfn "%sは%sを実装しています"
<| x.GetType().FullName
<| typeof<'T>.FullName
with
ex -> eprintfn "%s" ex.Message
checkIDisposable<System.IDisposable> <| new System.IO.MemoryStream()
// 結果: System.IO.MemoryStreamはSystem.IDisposableを実装しています
checkIDisposable<System.IDisposable> <| obj()
// 結果: 型 'System.Object' のオブジェクトを型 'System.IDisposable' にキャストできません 。
ジェネリックでチェック
ジェネリックの型変数に対するwhen
によるチェックもできます。こちらはオブジェクトではなく型名による方法です。型変数に与えられる型がSystem.IDisposable
を実装していなければエラーとなります。
let checkIDisposable<'T when 'T :> System.IDisposable> =
typeof<'T>.FullName
|> printfn "%sはSystem.IDisposableを実装しています"
checkIDisposable<System.IO.StreamReader>
// 結果: System.IO.StreamReaderはSystem.IDisposableを実装しています
checkIDisposable<System.Data.Common.DbConnection>
// 結果: System.Data.Common.DbConnectionはSystem.IDisposableを実装しています
checkIDisposable<System.Object>
// 結果: 型 'System.Object' は型 'System.IDisposable' と互換性がありません (エラー)
typeofでチェック
typeof<'T>
でGetInterfaces
メソッドを実行すると実装されているインターフェース(System.Type
)の一覧を得られます。その中にSystem.IDisposable
が含まれているかをチェックすれば、'T
がSystem.IDisposable
を実装していることを確認できます。
let checkIDisposable<'T> =
let t = typeof<'T>
printfn "%sはSystem.IDisposableを実装していま%s"
<| t.FullName
<| if
t.GetInterfaces()
|> Array.exists (fun x -> x = typeof<System.IDisposable>) // ここでチェック
then "す"
else "せん"
checkIDisposable<System.IO.StreamReader>
// 結果: System.IO.StreamReaderはSystem.IDisposableを実装しています
checkIDisposable<System.Threading.Tasks.Task>
// 結果: System.Threading.Tasks.TaskはSystem.IDisposableを実装しています
checkIDisposable<System.Object>
// 結果: System.ObjectはSystem.IDisposableを実装していません
間接的なIDisposableの継承
System.IDisposable
の継承(および実装)はクラスに対して直接行われるものだけでなく、間接的に行われるものもあります。
System.Collections.Generic.IEnumerable<'T>インターフェースはSystem.IDisposable
を継承していないのですが、これが持つGetEnumeratorメソッドは、System.Collections.Generic.IEnumerator<'T>インターフェースの実装を戻り値とします。このIEnumerator
がSystem.IDisposable
を継承しているのです。そのためIEnumerable
を実装するものは内部でIEnumerator
とIDisposable
を実装している可能性があります。
IEnumerable<'T>
はF#のseq<'T>
やfor
に対応していますので、これが実装されたクラスなどを知っておくと、さまざまなところで役に立ちます。
たとえばSystem.String(文字列:string
)にはSystem.Collections.Generic.IEnumerable<char>
が実装されていて、for
で1文字ずつ抽出しながらの繰り返し処理を実行できます。
for c in ".NET" do printfn "%A 0x%x" c (uint32 c)
(* 結果 *)
'.' 0x2e
'N' 0x4e
'E' 0x45
'T' 0x54
これができるなら、文字列から1行ずつ抽出して繰り返し処理できるようにしたいと思い、そのためのプロパティAllLines
を拡張してみました。型はseq<string>
です。内部でSystem.IO.StringReaderを使っていますが、それをIEnumerator
のDispose
メソッドで解放するため、use
は使っていません。
// 改行込みの文字列
let platforms = ".NET Framework
.NET Core
Xamarin
ASP.NET
Azure"
// 1行ごとの繰り返し処理
platforms.AllLines
|> Seq.iteri (fun n line -> printfn "%i. %s" (n + 1) line)
(* 結果 *)
1. .NET Framework
2. .NET Core
3. Xamarin
4. ASP.NET
5. Azure
type System.String with
member this.AllLines // プロパティの拡張
with get() =
if this = null
then seq<string> []
else
let self = this
let mutable reader = new System.IO.StringReader(self) // IEnumeratorのDisposeで解放するのでuseではない
let mutable line = null
(* プロパティの値となるIEnumerableオブジェクトの実装 *)
{new System.Collections.Generic.IEnumerable<string> with
member this.GetEnumerator(): System.Collections.IEnumerator =
this.GetEnumerator() :> System.Collections.IEnumerator
member this.GetEnumerator() =
(* 繰り返し処理を行うIEnumeratorオブジェクトの実装 *)
{new System.Collections.Generic.IEnumerator<string> with
member this.Current with get(): obj = line |> box
member this.Current with get(): string = line
member this.MoveNext() =
line <- reader.ReadLine()
line <> null
member this.Dispose() =
reader.Dispose() // StringReaderのクローズ(リソースの解放)
// printfn "String.AllLines Disposed!!"
member this.Reset() =
this.Dispose()
reader <- new System.IO.StringReader(self)
line <- null
}
}
実はuseもusingもいらなかった...
.NET Frameworkのメソッドはとても充実していますので、実際にはuse
もusing
もいらなかった...ということもあります。
冒頭に掲げた1行ずつの処理はFile.ReadAllLinesメソッドを使えばuse
もusing
も改めて書かなくて済みます(ただしファイルとメモリの容量には注意)。余分な労力をかけないためにも、どんなメソッドがあるかをきちんと調べておいたほうがよさそうです。
System.IO.File.ReadAllLines(@"platforms.txt")
|> Array.iteri (fun i line -> printfn "%i %s" (i + 1) line)
(* platforms.txt *)
.NET Framework
.NET Core
Xamarin
ASP.NET
Azure
(* 結果 *)
1. .NET Framework
2. .NET Core
3. Xamarin
4. ASP.NET
5. Azure
ここをご覧いただいた方々の参考になればと思います。