確認してみた理由
COMオブジェクトをどうしても使わざるを得ない状況になって、どのタイミングで何を開放すべきかいまいち判然としなかったので手を動かした。
注意点
下記コードではとりあえず準備が楽なExcel(2013)を使った。
概ねどんなものかを確認するためと、使用したいコンポーネントでの確認作業の叩き台として作成したが、別のコンポーネントでもこの通りの結果になるかどうかの保証はしかねる。都度そのコンポーネントで確認するのが望ましい。
ぶっちゃけExcel操作するだけならVBScriptかJScriptで操作するコード書いてMSScriptControlで実行する方がお気楽なので、Excelを使った意味は準備が楽だったという点以外には本当にない。
結果の概要
- インスタンスを作ったら当然開放する。
- 代入やキャストで別変数に格納した場合は全部同じCOMオブジェクト参照なので、いずれかの変数を開放すれば良い。不安なら全部開放してもそれはそれで問題はない。
- 引数で渡されたCOMオブジェクトの開放は呼び出し側でよしなにする。
- 戻り値として返すCOMオブジェクトは呼び出し側(戻り値を受け取った側)でよしなにする。
- COMオブジェクトのプロパティやメソッドで取得した物は要開放。同じプロパティやメソッド(引数)で取得した場合でも、都度別のCOMオブジェクト参照として扱われるようなので各々開放する必要がある。
確認に使ったコード
修正
- 2018/02/09 20:55
-
コンパイラにキャストが消されていたので消されないように修正。
結果としては特に変わらなかったので、コードのみ修正して結果の概要は修正箇所なし。
ついでに再帰呼び出し等でメソッドから戻ってきたモノをEqualsで正しく判断できるかのテストを追加。問題なく判定可能だった。
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