2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

.NETでCOMコンポーネントを使用する際のCOMオブジェクト開放要否を確認した

Last updated at Posted at 2018-02-09

確認してみた理由

COMオブジェクトをどうしても使わざるを得ない状況になって、どのタイミングで何を開放すべきかいまいち判然としなかったので手を動かした。

注意点

下記コードではとりあえず準備が楽なExcel(2013)を使った。
概ねどんなものかを確認するためと、使用したいコンポーネントでの確認作業の叩き台として作成したが、別のコンポーネントでもこの通りの結果になるかどうかの保証はしかねる。都度そのコンポーネントで確認するのが望ましい。
ぶっちゃけExcel操作するだけならVBScriptかJScriptで操作するコード書いてMSScriptControlで実行する方がお気楽なので、Excelを使った意味は準備が楽だったという点以外には本当にない。

結果の概要

  • インスタンスを作ったら当然開放する。
  • 代入やキャストで別変数に格納した場合は全部同じCOMオブジェクト参照なので、いずれかの変数を開放すれば良い。不安なら全部開放してもそれはそれで問題はない。
  • 引数で渡されたCOMオブジェクトの開放は呼び出し側でよしなにする。
  • 戻り値として返すCOMオブジェクトは呼び出し側(戻り値を受け取った側)でよしなにする。
  • COMオブジェクトのプロパティやメソッドで取得した物は要開放。同じプロパティやメソッド(引数)で取得した場合でも、都度別のCOMオブジェクト参照として扱われるようなので各々開放する必要がある。

確認に使ったコード

修正

2018/02/09 20:55
コンパイラにキャストが消されていたので消されないように修正。
結果としては特に変わらなかったので、コードのみ修正して結果の概要は修正箇所なし。
ついでに再帰呼び出し等でメソッドから戻ってきたモノをEqualsで正しく判断できるかのテストを追加。問題なく判定可能だった。
## コード ```VB.net:ComReleaseTester.vb Imports System.Runtime.InteropServices Imports Excel = Microsoft.Office.Interop.Excel

Class ComReleaseTester
'''


''' COMの破棄テストを実行する。
'''

''' 意図的に開放漏れを行う場合はTrue
Public Shared Sub Execute(Optional ByVal leak As Boolean = False)
Dim oExcel As Excel.Application = Nothing
Dim oBooks As Excel.Workbooks = Nothing
Dim oBook As Excel.Workbook = Nothing
Dim oSheet As Excel.Worksheet = Nothing
Dim oSheetObj As Object = Nothing
Dim oRange1 As Excel.Range = Nothing
Dim oRange2 As Excel.Range = Nothing
Dim oRange3 As Excel.Range = Nothing
Dim oRange4 As Excel.Range = Nothing
Dim oRange5 As Excel.Range = Nothing
Dim oRange6 As Excel.Range = Nothing
Dim oRange7 As Excel.Range = Nothing
Dim oRange8 As Excel.Range = Nothing
Dim oRangeObj As Object = Nothing
Dim oInterior1 As Excel.Interior = Nothing
Dim oInterior2 As Excel.Interior = Nothing

    Dim startTime As DateTime = DateTime.Now

    Try
        Try
            oExcel = New Excel.Application
            oExcel.DisplayAlerts = False

            oBooks = oExcel.Workbooks
            oBook = oBooks.Add()


            PrintSection("ダイレクトキャストすると開放できないという話を見たのでテスト")
            ' Workbook.ActiveSheetがObject型のようなのでObject型でそのまま取得し、ダイレクトキャスト
            oSheetObj = oBook.ActiveSheet
            oSheet = DirectCast(oSheetObj, Excel.Worksheet)
            Try
                Console.WriteLine("oSheet.Name=" & oSheet.Name)
            Catch ex As Exception
                PrintError("DirectCastしたCOMオブジェクトにアクセス(oSheet.Name)", ex)
            End Try
            Try
                Console.WriteLine("Marshal.ReleaseComObject(oSheet)=" & Marshal.ReleaseComObject(oSheet))
            Catch ex As Exception
                PrintError("DirectCastしたCOMオブジェクトを開放", ex)
            End Try
            Try
                Console.WriteLine("oSheet.Address=" & oSheet.Name)
            Catch ex As Exception
                PrintError("開放済みのDirectCastしたCOMオブジェクトにアクセス(oSheet.Name)", ex)
            End Try

            Try
                Console.WriteLine("oSheetObj.Name=" & oSheetObj.Name)
            Catch ex As Exception
                PrintError("キャスト元のオブジェクトも開放済みなので" &
                           "プロパティにアクセスするとエラーになる(oSheetObj.Name)", ex)
            End Try

            FinalReleaseComObjects(oSheetObj, oSheet)

            ' これ以降もシートを使うので再取得しておく
            oSheet = oBook.ActiveSheet




            PrintSection("代入やキャストで参照カウントの増加やオブジェクトの生成は行われず、" &
                         "すべて同じCOMオブジェクトとして扱われる。")
            ' COMオブジェクト取得(参照カウント+1)
            oRange1 = oSheet.Range("B5")

            ' 代入では参照カウントは増えない
            oRange2 = oRange1

            '' キャストしたインスタンスを代入しても参照カウントは増えない
            oRangeObj = oRange1
            oRange3 = DirectCast(oRangeObj, Excel.Range)
            oRange4 = CType(oRangeObj, Excel.Range)


            ' すべて同じCOMオブジェクトなので、いずれかが開放されると他も使えなくなる
            ' (COMオブジェクトではあるが使用できず、ReleaseComObjectをしても戻り値が-1)
            Console.WriteLine("oRange2 count=" & Marshal.ReleaseComObject(oRange2))

            ' oRange2で開放済みなのでoRange1は使えない
            Try
                Console.WriteLine("Marshal.IsComObject(oRange1)=" & Marshal.IsComObject(oRange1))
                Console.WriteLine("oRange1.Address=" & oRange1.Address)
            Catch ex As InvalidComObjectException
                PrintError("開放済みのCOMオブジェクトにアクセス(oRange1.Address)", ex)
            End Try

            ' oRange2,3,Objも開放済みになっている(戻り値が-1)
            Console.WriteLine("Marshal.ReleaseComObject(oRange1)=" & Marshal.ReleaseComObject(oRange1))
            Console.WriteLine("Marshal.ReleaseComObject(oRange2)=" & Marshal.ReleaseComObject(oRange2))
            Console.WriteLine("Marshal.ReleaseComObject(oRange3)=" & Marshal.ReleaseComObject(oRange3))
            Console.WriteLine("Marshal.ReleaseComObject(oRangeObj)=" & Marshal.ReleaseComObject(oRangeObj))

            ' oRange7は定義しただけなので当然COMオブジェクトの参照はなく、開放する必要も特にない。
            ' というかNullなので開放しようとすると落ちる。
            ' 開放済みのCOMオブジェクトを開放しようとしても特にエラーにはならないが、
            ' Nullの場合や非COMオブジェクトの場合は落ちるので、
            ' 解放時に最低限NullチェックとCOMオブジェクトかどうかのチェックは必要。
            Try
                Console.WriteLine("Marshal.ReleaseComObject(oRange7)=" & Marshal.ReleaseComObject(oRange7))
            Catch ex As NullReferenceException
                PrintError("Nullのオブジェクトを開放しようとした(oRange7)", ex)
            End Try
            Try
                Dim tmp As String = "TEMP"
                Console.WriteLine("Marshal.ReleaseComObject(tmp)=" & Marshal.ReleaseComObject(tmp))
            Catch ex As ArgumentException
                PrintError("非COMオブジェクトを開放しようとした(tmp)", ex)
            End Try




            PrintSection("メソッドの引数として渡しても参照カウントの増加やインスタンスの生成は行われないので、" &
                         "メソッド内で開放されると呼び出し元でも使えなくなる。")
            ' メソッドの引数として渡したCOMオブジェクトがメソッド内で開放されると呼び出し元でも使えなくなる
            oRange1 = oSheet.Range("B5")
            Console.WriteLine("oRange1.Address=" & oRange1.Address)
            ReleaseArg(oRange1)
            Try
                Console.WriteLine("oRange1.Address=" & oRange1.Address)
            Catch ex As InvalidComObjectException
                PrintError("開放済みのCOMオブジェクトにアクセス(oRange1.Address)", ex)
            End Try





            PrintSection("メソッドの戻り値として受け渡しても参照カウントの増加やインスタンスの生成は行われないので、" &
                         "戻り値がメソッド内で開放されると呼び出し側でも使えなくなる。")
            ' メソッドへの引数で渡したり戻り値として受け取ったりしても参照カウントは増えない
            Try
                ' 呼び出したメソッド中、Finally等で戻り値が開放されていると、
                ' 開放されたCOMオブジェクトが戻ってくるので使用するとエラーになる
                oRange5 = GetRange(oSheet, "B5", True)
                Console.WriteLine("oRange5.Address=" & oRange5.Address)
            Catch ex As InvalidComObjectException
                PrintError("開放済みのCOMオブジェクトにアクセス(oRange5.Address)", ex)
            End Try

            ' 戻り値が開放されていなければ当然使える
            oRange5 = GetRange(oSheet, "B5", False)
            Console.WriteLine("oRange5.Address=" & oRange5.Address)




            PrintSection("再帰呼び出し等で同じインスタンスがそのまま戻ってきた場合も同じCOMオブジェクトとして扱われるので、" &
                         "Equals等で判定して開放の要否を判断する。")
            ' 同インスタンスがそのまま戻ってくる場合
            Try
                oRange8 = Recursive(oRange5, 1, False)

                ' Equalsで比較すると同じと判定される
                Console.WriteLine("Marshal.Equals(oRange5, oRange8)=" & Marshal.Equals(oRange5, oRange8))

                ' 同じモノなのでoRange8を開放するとoRange5も開放されたことになる。
                Console.WriteLine("Marshal.ReleaseComObject(oRange8)=" & Marshal.ReleaseComObject(oRange8))
                Console.WriteLine("oRange5.Address=" & oRange5.Address)
            Catch ex As Exception
                PrintError("開放済みのCOMオブジェクトにアクセス(oRange5.Address)", ex)
            Finally
                FinalReleaseComObjects(oRange5, oRange8)
            End Try

            ' 別インスタンスが戻ってくる場合
            Try
                oRange5 = oSheet.Range("B5")

                ' 別のCOMオブジェクトが戻ってきた場合は当然別物として扱われる
                oRange8 = Recursive(oRange5, 1, True)

                ' Equalsで比較すると一致しないと判定される
                Console.WriteLine("Marshal.Equals(oRange5, oRange8)=" & Marshal.Equals(oRange5, oRange8))

                ' 別のモノなのでoRange8を開放してもoRange5には影響しない
                Console.WriteLine("Marshal.ReleaseComObject(oRange8)=" & Marshal.ReleaseComObject(oRange8))
                Console.WriteLine("oRange5.Address=" & oRange5.Address)
            Catch ex As Exception
                ' ここで例外が発生することはないはず。
                PrintError("何故かoRange8の解放後にoRange5のアクセスもエラーになった", ex)
            Finally
                FinalReleaseComObjects(oRange5, oRange8)
            End Try




            PrintSection("メソッドの引数が同じ場合や同じプロパティでCOMオブジェクトを取得しても、" &
                         "別のCOMオブジェクトとして扱われる。")
            ' 同じ引数でCOMオブジェクト(この場合はB5セルのRange)を取得しても別物として参照を扱われる
            ' この取得でoRange5の参照カウントが増えたりはしない
            oRange6 = oSheet.Range("B5")
            Console.WriteLine("Marshal.Equals(oRange5, oRange6)=" & Marshal.Equals(oRange5, oRange6))

            ' プロパティでもCOMオブジェクトを返すなら開放の必要がある
            oInterior1 = oRange6.Interior
            ' 同じプロパティでアクセスしても、都度別のCOMオブジェクトとして扱われる
            oInterior2 = oRange6.Interior

            Console.WriteLine("Marshal.Equals(oInterior1, oInterior2)=" & Marshal.Equals(oInterior1, oInterior2))

            Console.WriteLine("oInterior1 ColorIndex=" & oInterior1.ColorIndex)
            Console.WriteLine("Marshal.ReleaseComObject(oInterior1)=" & Marshal.ReleaseComObject(oInterior1))

            ' 別のCOMオブジェクトとして扱われているのでoInterior1が開放されたとしても影響を受けない
            Console.WriteLine("oInterior2.ColorIndex=" & oInterior2.ColorIndex)

            Try
                ' oInterior1は開放済みなのでアクセスするとエラーになる
                Console.WriteLine("oInterior1.ColorIndex=" & oInterior1.ColorIndex)
            Catch ex As InvalidComObjectException
                PrintError("開放済みのCOMオブジェクトにアクセス(oInterior1.ColorIndex)", ex)
            Finally
                FinalReleaseComObjects(oInterior1, oInterior2)
            End Try




            PrintSection("COMオブジェクトのプロパティを取得した際に取得元のオブジェクトのみ開放しても、" &
                         "取得したプロパティのCOMオブジェクトへのアクセスは特に問題はない。" &
                         "……が、できればやらない方が良い気はする。")
            Try
                ' RangeからInteriorプロパティを取得後にRangeの方だけ開放してみる
                oInterior1 = oRange6.Interior
                Console.WriteLine("Marshal.ReleaseComObject(oRange6)=" & Marshal.ReleaseComObject(oRange6))

                ' 影響はないようだがGCが走った時とかにどうなるか謎なので、
                ' 問題ないとは思うが変なハマりをしたくなければやらない方が無難であろう。
                Console.WriteLine("oInterior1.ColorIndex=" & oInterior1.ColorIndex)
            Catch ex As Exception
                PrintError("開放済みのCOMオブジェクトにアクセス(oInterior1.ColorIndex)", ex)
            Finally
                FinalReleaseComObjects(oInterior1)
            End Try



            ' オプションが指定されたら意図的に開放漏れする
            If (leak) Then
                Dim leakRange As Excel.Range = Nothing
                leakRange = oSheet.Range("A1")
            End If

        Catch ex As Exception
            MessageBox.Show(ex.Message)

        Finally
            If (oBook IsNot Nothing) Then oBook.Close(False)
            If (oExcel IsNot Nothing) Then oExcel.Quit()
        End Try
    Catch ex As Exception

    Finally
        FinalReleaseComObjects(oInterior1, oInterior2,
                            oRange1, oRange2, oRange3, oRange4, oRange5, oRange6, oRange7, oRange8, oRangeObj,
                            oSheetObj, oSheet, oBook, oBooks, oExcel)
    End Try

    Threading.Thread.Sleep(2000)


    ' Excelプロセスが終了したか確認する。
    PrintSection("Excelプロセスが終了したか確認する。")
    Dim ps As System.Diagnostics.Process() = System.Diagnostics.Process.GetProcessesByName("EXCEL")
    Dim pcnt As Integer = 0
    For Each p As System.Diagnostics.Process In ps
        Try
            If (Not p.HasExited AndAlso p.StartTime >= startTime) Then
                ' 処理実行後に起動したプロセスのみカウント
                pcnt += 1
            End If
        Catch ex As Exception
        End Try
    Next
    Console.WriteLine(IIf(pcnt > 0,
        "[WARN] Excelプロセスが残っているかも(count=" & pcnt & ")",
        "[INFO] 終了しました(多分Excelプロセスは残っていません)"))
End Sub

Private Shared Function GetRange(ByVal sheet As Microsoft.Office.Interop.Excel.Worksheet, ByVal addr As String, ByVal doRelease As Boolean) As Microsoft.Office.Interop.Excel.Range
    ' 引数で渡されても参照カウントは増えていないので開放の必要がない
    ' 同じ参照なので開放すると呼び出し元の変数も使えなくなってしまうので、それを意図していないなら開放してはいけない
    Dim r As Microsoft.Office.Interop.Excel.Range = sheet.Range(addr)

    Try
        ' 戻り値として使う変数も開放の必要は無い
        ' 戻り値で渡しても参照は増えないので、戻り値をFinallyとかで開放すると使えないCOMオブジェクトが返されてしまう。
        ' 呼び出し側で戻り値を受け取らないのであればここで開放しなければいけないが、
        ' 受け取る気がないのにCOMオブジェクトを返すメソッドを呼び出すのは使い方がおかしい。
        Return r
    Finally
        If (doRelease) Then
            Console.WriteLine("Marshal.ReleaseComObject(MethodResult)=" & Marshal.ReleaseComObject(r))
        End If
    End Try
End Function

Private Shared Sub ReleaseArg(ByVal r As Excel.Range)
    ' 引数を開放すると呼び出し元でも使えなくなるので基本的に開放しない
    Console.WriteLine("Marshal.ReleaseComObject(MethodArg)=" & Marshal.ReleaseComObject(r))
End Sub

Private Shared Function Recursive(ByVal r As Excel.Range, ByVal i As Integer, ByVal isRetrunNextCol As Boolean) As Excel.Range
    If (i < 10) Then
        If (i = 1) Then Console.Write("Recursive ")
        Console.Write(".")
        Return Recursive(r, i + 1, isRetrunNextCol)
    Else
        Console.WriteLine("")
        If (isRetrunNextCol) Then
            Return r.Offset(0, 1)
        Else
            Return r
        End If
    End If
End Function

Private Shared Sub PrintSection(ByVal comment As String)
    Console.WriteLine(New String("="c, 24) & vbCrLf & comment & vbCrLf & New String("-"c, 4))
End Sub

Private Shared Sub PrintError(ByVal message As String, ByRef ex As Exception)
    Console.WriteLine("[ERROR] {0} : {1}" & vbCrLf & "{2}" & vbCrLf & "{3}",
                    message, ex.GetType().FullName, vbTab & ex.Message, ex.StackTrace)
End Sub

Private Shared Sub FinalReleaseComObjects(ParamArray objects As Object())
    For Each o As Object In objects
        Try
            If (o IsNot Nothing _
            AndAlso Marshal.IsComObject(o)) Then
                Marshal.FinalReleaseComObject(o)
            End If
        Catch ex As Exception
        End Try
    Next o
End Sub

End Class

2
4
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
2
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?