LoginSignup
1
2

More than 5 years have passed since last update.

ちゃんとDisposeできてる? - use、using、そしてIDisposable

Posted at

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によるリソースの確保と解放の例
(* テキストファイルを一行ずつ読み込んで処理を実行(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によるリソースの確保と解放の例
(* テキストファイルを一行ずつ読み込んで処理を実行(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できてる?

では、useusingによって本当にDisposeメソッドが実行されているのか確認してみます。メッセージを表示するDisposeメソッドを持つSystem.IDisposableオブジェクトを定義し、それをuseusingに設定し処理を実行するとどうなるでしょうか。Disposeメソッドが実行されるタイミングに注目です。

本当に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を実装していなければエラーとなります。

System.IDisposableが実装されているかをletでチェック
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式:?で表される型テスト演算子でチェックしてみます。

System.IDisposeを実装しているかを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にキャストできるかどうかでもチェックできます。

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を実装していなければエラーとなります。

ジェネリックの型変数に対して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が含まれているかをチェックすれば、'TSystem.IDisposableを実装していることを確認できます。

typeofで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>インターフェースの実装を戻り値とします。このIEnumeratorSystem.IDisposableを継承しているのです。そのためIEnumerableを実装するものは内部でIEnumeratorIDisposableを実装している可能性があります。

IEnumerable<'T>はF#のseq<'T>forに対応していますので、これが実装されたクラスなどを知っておくと、さまざまなところで役に立ちます。

たとえばSystem.String(文字列:string)にはSystem.Collections.Generic.IEnumerable<char>が実装されていて、forで1文字ずつ抽出しながらの繰り返し処理を実行できます。

文字列から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を使っていますが、それをIEnumeratorDisposeメソッドで解放するため、useは使っていません。

Stringを拡張したAllLinesプロパティの使用例
// 改行込みの文字列
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
文字列から1行ずつ抽出して繰り返し処理をできるようにするAllLinesプロパティの拡張(型はseq)
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のメソッドはとても充実していますので、実際にはuseusingもいらなかった...ということもあります。

冒頭に掲げた1行ずつの処理はFile.ReadAllLinesメソッドを使えばuseusingも改めて書かなくて済みます(ただしファイルとメモリの容量には注意)。余分な労力をかけないためにも、どんなメソッドがあるかをきちんと調べておいたほうがよさそうです。

実は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

ここをご覧いただいた方々の参考になればと思います。

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2