- PR -

COM参照を確実に解放するコードの可読性をあげたい

投稿者投稿内容
ぼのぼの
ぬし
会議室デビュー日: 2004/09/16
投稿数: 544
投稿日時: 2006-03-15 18:02
私なりに考えてみました。

コード:


Private Shared Sub BonoBonoAA()
Dim xlApplication As Excel.Application
Dim xlWorkBooks As Excel.Workbooks
Dim xlWorkBook As Excel.Workbook
Dim xlWorkSheets As Excel.Sheets
Dim xlSheet1 As Excel.Worksheet
Dim xlCells As Excel.Range
Dim xlRange As Excel.Range

Try
xlApplication = New Excel.Application
xlApplication.Visible = True
xlWorkBooks = xlApplication.Workbooks
xlWorkBook = xlWorkBooks.Add()
xlWorkSheets = xlWorkBook.Worksheets
xlSheet1 = DirectCast(xlWorkSheets("Sheet1"), Excel.Worksheet)
xlCells = xlSheet1.Cells
xlRange = DirectCast(xlCells(1, 1), Excel.Range)
xlRange.Value2 = "COM Interop is hated!"
xlApplication.DisplayAlerts = False
xlWorkBook.SaveAs("C:\Book1.xls")
Finally
Dim lastEx As Exception
ReleaseComObj(xlRange, lastEx)
ReleaseComObj(xlCells, lastEx)
ReleaseComObj(xlSheet1, lastEx)
ReleaseComObj(xlWorkSheets, lastEx)
CloseBook(xlWorkBook, lastEx)
ReleaseComObj(xlWorkBook, lastEx)
CloseBooks(xlWorkBooks, lastEx)
ReleaseComObj(xlWorkBooks, lastEx)
QuitApp(xlApplication, lastEx)
ReleaseComObj(xlApplication, lastEx)

If Not lastEx Is Nothing Then
Throw New ApplicationException("Excel操作の処理で例外が発生しました。", lastEx)
End If
End Try
End Sub

Private Shared Sub CloseBook(ByVal book As Excel.Workbook, ByRef outEx As Exception)
If book Is Nothing Then Return
Try
book.Close()
Catch ex As Exception
outEx = ex
End Try
End Sub

Private Shared Sub CloseBooks(ByVal books As Excel.Workbooks, ByRef outEx As Exception)
If books Is Nothing Then Return
Try
books.Close()
Catch ex As Exception
outEx = ex
End Try
End Sub

Private Shared Sub QuitApp(ByVal app As Excel.Application, ByRef outEx As Exception)
If app Is Nothing Then Return
Try
app.Quit()
Catch ex As Exception
outEx = ex
End Try
End Sub

Private Shared Sub ReleaseComObj(ByVal comObj As Object, ByRef outEx As Exception)
If comObj Is Nothing Then Return
Try
System.Runtime.InteropServices.Marshal.ReleaseComObject(comObj)
Catch ex As Exception
outEx = ex
End Try
End Sub


とりあえず、最後に発生した例外をthrowする仕様です。
ParamArrayは使用せず、解放対象のオブジェクト毎にサブルーチンを呼び出す。
ただし、nullチェックとTry〜Catch〜Finallyはサブルーチン側で面倒見ます、
というつくりにしてみました。

#サンプルコードにいらない引数が残ってたのを削除

[ メッセージ編集済み 編集者: ぼのぼの 編集日時 2006-03-16 12:56 ]
Jitta
ぬし
会議室デビュー日: 2002/07/05
投稿数: 6267
お住まい・勤務地: 兵庫県・海手
投稿日時: 2006-03-15 21:08
 じゃんぬさんの1つ目のレスポンスだけが付いているときに書いているので、ちょっとちぐはぐします。

 単純に、外(他のメソッド)に放り出せば、可読性は上がりますよね。
私のブログエントリの、投稿時点でのコード:
コード:
Dim worksheets As Excel.Sheets = Nothing
Dim worksheet As Excel.WorkSheet = Nothing
Dim range As Excel.Range = Nothing

Try
    worksheets = CType(workbook.WorkSheets, Excel.Sheets)
    worksheet = CType(worksheets("シート"), Excel.WorkSheet)
    range = worksheet.Range("レンジ名")
    range.Value = rangeValue

Finally
    ' 面倒なので例外は発生しないと過信
    If Not range Is Nothing Then Marshal.ReleaseComObject(range)
    If Not worksheet Is Nothing Then Marshal.ReleaseComObject(worksheet)
    If Not worksheets Is Nothing Then Marshal.ReleaseComObject(worksheets)
End Try


を例にすると、最終的に欲しいのは「レンジ名」の Excel.Range インターフェイスを実装したインスタンスです。「面度なので」としているように、Marshal.ReleaseComObject が例外を生成しないと過信すれば、このようにまとめることで、本体の可読性を高めることは出来ます。
 また、他のスレッドであったと思いますが、Finally ブロックで囲み込む方法もあります。
コード:
Finally
    Try
        If Not range Is Nothing Then
            Marshal.ReleaseComObject(range)
        End Try
    Finally
        Try
            If Not worksheet Is Nothing Then
                Marshal.ReleaseComObject(worksheet)
            End If
        Finally
            If Not worksheets Is Nothing Then
                Marshal.ReleaseComObject(worksheets)
            End If
        End Try
    End Try
End Try


あるいは、ひとつのメソッドの中ですべてをしようとするから可読性が落ちるので、メソッドを分解すれば、そうでもないでしょう。
コード:
        ' 可読性バッチリ!?
        range = GetRange(excelInstance, "WorkBookName", "WorkSheetName", "RangeName")
        range.Value = rangeValue
    Finally
        If Not range Is Nothing Then
            Marshal.ReleaseComObject(range)
        End If
    End Try
End Sub

Function GetRange(xls As Excel.Application _
 , bookName As String _
 , sheetName As String _
 , rangeName As String) As Excel.Range
    Dim sheet As Excel.WorkSheet = Nothing
    Try
        sheet = GetWorkSheet(xls, bookName, sheetName)
        return sheet.Range(rangeName)
    Finally
        If Not sheet Is Nothing Then
            Marshal.ReleaseComObject(sheet)
        End If
    End Try
End Function

Function GetSheet(xls As Excel.Application _
 , bookName As String _
 , sheetName As String) As Excel.WorkSheet
    Dim sheets As Excel.Sheets = Nothing
    Try
        sheets = GetWorkSheets(xls, bookName)
        return sheets(sheetName)
    Finally
        If Not sheets Is Nothing Then
            Marshal.ReleaseComObject(sheets)
        End If
    End Try
End Function
後は略してもいいよね?


 細分化することで、メソッド内で解放しなければならないものが限定されますから、確実に解放可能です。

引用:

この例のように複数のFinallyブロックで例外が発生し得る処理を、
Catchを使って書き換える場合、皆さんならどのように考えますか?

  • 元のコードと同じく、最後に発生した例外のみ通知できれば良い
  • 逆に、最初に発生した例外のみ通知した方が良い
  • いやいや、ArrayListやCollectionを使って発生した全ての例外を通知すべきだ



 ん〜?タイトルからずれています?
 まず、.NET においては、例外は「継続不可能であるべき」です。継続可能であることは、予測可能であることを意味します。
 ここに2〜3例外があります。「入力された文字列が、日付として意味がある事を確認する」場合、Date.Parse で例外をキャッチするのが、手っ取り早い方法です。こういう、「検査」の意味を持たせる場合を除いて、例外は「キャッチしない」のが基本です。ついでに、.NET Framework 2.0 では、TryParse というメソッドが追加されており、例外を発生させずに(メソッド内で発生しているにしても)検査することが可能になりました。
むやみにキャッチしないでね。ゴールキーパー以外はハンドで反則ですよ。

 この原則から行くと、「最初に発生した例外のみ通知」ですが、「InnerException プロパティによりすべて通知」を追加しておきます(意味合い的には「例外が発生するもとになった例外」なので、ちょっと違うけど)。


ここから、2006-03-15 15:22 以降に追記:::さらに進んでいますが

 で、
> (1)メソッドの入力(引数)が同じで、
ですか。Private でどう実装しようが、外部の使用者には関係ないですね フッ(~ー~)

〆 written by Jitta@わんくま同盟 on 2006/03/15
□ Microsoft MVP for Visual Developer ASP/ASP.NET October, 2005 - September, 2006
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2006-03-15 23:29
引用:

余計なことをしようとすればするほど、参照カウントが増える機会が多くなります。



COM ラッパの参照をやり取りしても、ラップされている COM オブジェクトの参照カウントは増えたりしないですよ?

_________________
// 渋木宏明 (Hiroaki SHIBUKI)
// http://hidori.jp/
// Microsoft MVP for Visual C#
//
// @IT会議室 RSS 配信中: http://hidori.jp/rss/atmarkIT/
じゃんぬねっと
ぬし
会議室デビュー日: 2004/12/22
投稿数: 7811
お住まい・勤務地: 愛知県名古屋市
投稿日時: 2006-03-16 10:07
引用:

渋木宏明(ひどり)さんの書き込み (2006-03-15 23:29) より:

COM ラッパの参照をやり取りしても、ラップされている COM オブジェクトの
参照カウントは増えたりしないですよ?


そっか、別のルートでなければインクリメントされないんでしたね。
もうちょっと良いアスペクトを考えてみようかな。

_________________
C# と VB.NET の入門サイト
じゃんぬねっと日誌
囚人
ぬし
会議室デビュー日: 2005/08/13
投稿数: 1019
投稿日時: 2006-03-16 10:17
またまたよく分かってませんが一言。
引用:

COM ラッパの参照をやり取りしても、ラップされている COM オブジェクトの参照カウントは増えたりしないですよ?


という事みたいなのに、何故 COM ラッパはファイナライザ等で参照カウントを減らすという事をしないのでしょうか。
何か理由があるのでしょうか。

_________________
囚人のジレンマな日々
じゃんぬねっと
ぬし
会議室デビュー日: 2004/12/22
投稿数: 7811
お住まい・勤務地: 愛知県名古屋市
投稿日時: 2006-03-16 10:51
引用:

囚人さんの書き込み (2006-03-16 10:17) より:

何故 COM ラッパはファイナライザ等で参照カウントを減らすという事をしないのでしょうか。


どちらも "参照" という言葉になるので、わかりにくいかもしれません...
まず、ReleaseComObject メソッドを省いた、かなり危険な香りがするメソッドを書きます。

コード:

    Private Shared Sub MosaMosaAA()
        Dim xlApplication As Excel.Application
        Dim xlWorkBooks   As Excel.Workbooks
        Dim xlWorkBook    As Excel.Workbook
        Dim xlWorkSheets  As Excel.Sheets
        Dim xlSheet1      As Excel.Worksheet
        Dim xlCells       As Excel.Range
        Dim xlRange       As Excel.Range

        Try
            xlApplication = New Excel.Application()
            xlApplication.Visible = True
            xlWorkBooks = xlApplication.Workbooks
            xlWorkBook = xlWorkBooks.Add()
            xlWorkSheets = xlWorkBook.Worksheets
            xlSheet1 = DirectCast(xlWorkSheets("Sheet1"), Excel.Worksheet)
            xlCells = xlSheet1.Cells
            xlRange = DirectCast(xlCells(1, 1), Excel.Range)
            xlRange.Value2 = "COM Interop is hated!"
            xlApplication.DisplayAlerts = False
            xlWorkBook.SaveAs("C:\Book1.xls")
        Finally
            '/ アプリケーションの終了だけ (;^-^)
            xlApplication.Quit()
        End Try
    End Sub
    '/ (1) ローカル変数なので COM ラッパへの 参照自体はここで解放される
    '/     ※COM オブジェクトそのものではない


最後に書いたように、ローカル変数は未到達になります。
このメソッドを呼び出したその後に、GC に回収を依頼してみましょう。(;^-^)

コード:

    Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles Button1.Click
        MosaMosaAA()

        '/ (1) で未到達になっているので GC に回収を頼んでみる
        GC.Collect()
    End Sub


どうなると思いますか? (^^)
参照カウントのガンとも言える、複雑な「循環参照」があった場合はどうかなぁ?
とか。

_________________
C# と VB.NET の入門サイト
じゃんぬねっと日誌
渋木宏明(ひどり)
ぬし
会議室デビュー日: 2004/01/14
投稿数: 1155
お住まい・勤務地: 東京
投稿日時: 2006-03-16 11:00
引用:

という事みたいなのに、何故 COM ラッパはファイナライザ等で参照カウントを減らすという事をしないのでしょうか。



しますよ。

ただ、それが「いつ」なのかが問題なのです。

また、マネージコード上のやりとりでは参照カウントの増加はありません(ないはず)ですが、COM 側に参照を引き渡せばその時点で参照カウントが増加します。

しかも、その参照カウントの増加について、.NET ランタイムは関知しないというのがまた問題の種に。。。

_________________
// 渋木宏明 (Hiroaki SHIBUKI)
// http://hidori.jp/
// Microsoft MVP for Visual C#
//
// @IT会議室 RSS 配信中: http://hidori.jp/rss/atmarkIT/
囚人
ぬし
会議室デビュー日: 2005/08/13
投稿数: 1019
投稿日時: 2006-03-16 11:16
多分、というか絶対私の言う事は的外れだと思いますが、納得したいので続けます。

引用:

(1) ローカル変数なので COM ラッパへの 参照自体はここで解放される
・・・
(1) で未到達になっているので GC に回収を頼んでみる


問題ないのでは、と思います。
今ここで、実際いくつのインスタンスが作成されたかは分かりませんが(例えば xlWorkBooks は実際インスタンスを作成したのか、既にあるインスタンスの参照を得ただけなのかが分からないという意味)、COM オブジェクトを参照しているインスタンスは、責任をもって参照カウントを減らせば良いことになります。

引用:

参照カウントのガンとも言える、複雑な「循環参照」があった場合はどうかなぁ?


いくら循環参照しても、COM ラッパへのインスタンスを参照しているだけであって、COM オブジェクトそのものを参照しているインスタンスは一つであると考えます。
例えば、一つの COM オブジェクトに対して、インスタンスが二つであれば、参照カウントは二つ上がっていて。そのインスタンスを参照している者がいくつあろうと、参照カウント二つ。
インスタンスのルートが存在しなれば、ファイナライザによって、参照カウントを一つ減らせばよい事いになります。

_________________
囚人のジレンマな日々

スキルアップ/キャリアアップ(JOB@IT)