DOBON.NETプログラミング道掲示板

No35252 の記事


■35252 / )  Re[5]: Excel Com オブジェクトの増殖
□投稿者/ 魔界の仮面弁士 大御所(1491回)-(2022/11/27(Sun) 22:32:57)
  • アイコンNo35250に返信(たこさんの記事)
    >   Private _xlsApplication As Excel.Application = Nothing
    >   Friend ReadOnly Property xlsApplication As Excel.Application
    >     Get
    >       Return _xlsApplication
    >     End Get
    >   End Property
    これではあまり意味が無いと思いますよ。結局のところ、
     Friend ReadOnly xlsApplication As Excel.Application
    な読み取り専用フィールドと、さほど変わらないように見えます。


    COM オブジェクトを直接公開してしまうと、ExcelEx の外部で
     ExEx1.xlsApplication.Workbooks.Add()
    などと書かれてしまえば、COM オブジェクトの解放漏れに繋がります。

    IDisposable としてカプセル化するのであれば、Excel の COM オブジェクトは
    外部からは直接操作できないようにして、すべてクラス内に隠蔽します。
    戻り値と返すのも COM オブジェクトではなく、.NET のマネージオブジェクトにします。


    > Option Strict Onをすると『Option Strict On では、遅延バインディングを使用できません。』といっぱい怒られます(^^ゞ
    書き換えながら、御自身で正解に向かったところもあるようですが、一応ひとつひとつ見ていきましょうか。


    > If Not check Then
    >   _xlsWorkSheet = xlsApplication.Sheets.Add()
    この処理には、問題点が 2 つあります。

    1 つは「.」による COM オブジェクトの連続呼び出しであり、
    Sheets プロパティから返されるコレクションの解放が漏れています。

    もう一つは、処理の曖昧さ。上記だと Excel 内で複数のワークブックが同時に開かれていた場合に、
    どのワークブックに対してシートを追加しようとしているのか曖昧になりますよね?
    たとえ VBA であったとしても、あまり望ましくない記述方法と言えます。


    > Else
    >   _xlsWorkSheet = xlsApplication.Sheets(strSheetName)
    これも同様に、Sheets の解放が漏れています。

    後程詳しく紹介しますが、この場合は xlsApplication.Sheets(strSheetName) ではなく、
     Dim obj1 = 対象ワークブック.Sheets 'これは Sheets 型
     Dim obj2 = obj1(strSheetName) 'これは Object 型となることに注意
     _xlsWorkSheet = DirectCast(obj2, Excel.Worksheet)
    のように、途中の COM オブジェクトを変数に受け取る必要があります。
    この obj1 は COM オブジェクトなので、後ほど MRComObject せねばなりません。

    また、_xlsWorkSheet と obj2 は同一インスタンス(Is で比較すると Trueになるはず) なので、
    実際には obj2 を省略して
     Dim obj1 = 対象ワークブック.Sheets
     _xlsWorkSheet = DirectCast(obj1(strSheetName), Excel.Worksheet)
     MRComObject(obj1)
    と書きますし、実際のところは
      _xlsWorkSheet = DirectCast(xlsSheets(strSheetName), Excel.Worksheet)
    とすることになるでしょう。



    > For Each sh In xlsSheets
    >   If sh.Name = strSheetName Then  '←ここで怒られましたが、これは『Ctype(sh,Excel.Worksheet).Name = strSheetName』とする事で解決。
    それで OK です。

    ただし厳密にいえば、xlsSheets によって列挙されるシートが Excel.Worksheet である保証はありません。
    Excel.Chart や Excel.DialogSheet が列挙される可能性もあることは頭の片隅に置いといてください。

    まぁ、実際に使うのはせいぜい Chart (グラフシート) ぐらいで、
    DialogSheet (Excel 5 ダイアログ シート) を使うことはまず無いでしょうけれども。


    それはそれとして、実は For Each はできる限り使わない方が無難です。

    COM に対する For Each は、COM の IEnumVARIANT インターフェイスを通じて行われるため、
    稀に、これが解放の妨げになることがあるためです。
    https://learn.microsoft.com/ja-jp/windows/win32/api/oaidl/nn-oaidl-ienumvariant
    https://divakk.co.jp/aoyagi/csharp_tips_vssenum.html

    なので For Each ループではなく、For ループを Count プロパティと共に使う方が安全です。
    https://qiita.com/mima_ita/items/aa811423d8c4410eca71


    > If Not check Then
    >   _xlsWorkSheet = xlsApplication.Sheets.Add()  '←ここでも怒られますが、『CType(xlsApplication.Excel.Sheets, Excel.Worksheet).Add()』としてみましたが、怒られたままです><
    xlsApplication は Excel.Application 型ですよね。

    書き直した結果の方について見てみると、xlsApplication.Excel.Sheets と書かれていますが、
    Excel.Application 型に Excel というプロパティやメソッドは存在しませんので、
    xlsApplication.Excel.Sheets 以前の問題として
    xlsApplication.Excel とすら書けません。

    ですから当然、
    >   '『 CType(CType(xlsApplication.Excel, Excel.Application).Sheets, Excel.Worksheet).Add()』とかしてみましたが、駄目でした><
    も当然 エラーになったわけです。

    もしかしたら
     CType(xlsApplication.Sheets, Excel.Worksheet).Add()
    と書くつもりだったのかもしれませんが、Sheets プロパティが返す型は
    Excel.Worksheet 型では無く Excel.Sheets 型なので、これもおかしいでsね。



    そもそも何故、元のコードが Option Strict On 時に怒られているのかと言えば、
    代入処理において、右辺にある Add メソッドの戻り値が As Object であるのに、
    左辺にある代入先の変数が Object ではなく Excel.Worksheet だからです。

    これはつまり、Add メソッドの戻り値の Object 型をキャストして、
    本来の Worksheet 型にする必要があったと…という事を意味します。



    > '>> 基本的に、『.』を連続して使うのは赤信号だと思ってください。ここにヒントがある気はしまして…
    連続利用と言っても、たとえば「名前空間.列挙型名.列挙値」などは OK です。
    最初に投稿いただいたコードにある Excel.XlWindowState.xlMaximized などはコレなので安全です。

    一方、「何某.プロパティ名.メソッド名」の場合、
    プロパティが返す値が COM オブジェクトの場合は NG となります。
    プロパティが返す値が マネージ オブジェクトなら OK です。

    そして xlsApplication.Sheets.Add() の場合、Sheets オブジェクトが返すものが
    COM オブジェクトなので NG である、ということです。

    すなわち
     _xlsWorkSheet = xlsApplication.Sheets.Add()
    という処理は、
     Dim a = xlsApplication.Sheets
     _xlsWorkSheet = DirectCast(a.Add(), Excel.Worksheet)
    のようにした方が望ましい、ということです。
    これなら、後で a を MRComObject できますので、解放漏れを防げます。
    (MRComObject しなければ同じことですけれどね…)


    ただし上記の修正もまだ十分ではありません。『xlsApplication.Sheets』を使うと、
    先の例と同様、どのワークブックの Sheets を操作するのかが曖昧になるので、
    Application オブジェクトの Sheets プロパティではなく
    Workbook オブジェクトの Sheets プロパティを用いることが望ましいです。
    これは VB.NET だけでなく、VBA から呼び出す場合にも言えることです。


    >   'いろいろやってみました。『CType(xlsSheets.Add(), Excel.Worksheet)』で良さそうな感じが…(まだエラーだらけで実行出来ていません(^^ゞ
    それで OK です。


    > Else
    >   _xlsWorkSheet = xlsApplication.Sheets(strSheetName)  '←怒られた
    >   '『 CType(xlsApplication.Sheets(strSheetName), Excel.Worksheet)』で良さそう…

    怒られたのは、代入式の左辺が Excel.Worksheet 型、右辺が Object 型だからなので、
    たしかにその書き換えによってコンパイルエラーは防げます。

    しかし、この修正ではいけません。COM オブジェクトの解放漏れにつながります。


    そもそも「xlsApplication.Sheets(strSheetName)」の Sheets メンバーというモノは、
     Property Sheets(n As String) As Object
    なプロパティでもなければ
     Function Sheets(n As String) As Object
    なメソッドでもありません。

    その実態は
     ReadOnly Property Sheets() As Excel.Sheets
    という『引数の無いメンバー』です。

    そして Sheets オブジェクトは、既定のプロパティ(いわゆるインデクサ)を持っているので、
    実際にはこれは、
     _xlsWorkSheet = xlsApplication.Sheets().Item(strSheetName)
    に相当する操作を意味します。「『.』を連続して使うのは赤信号」でしたよね?

    そして、Sheets プロパティが返すのは COM オブジェクトなので、
    これは解放漏れ案件に当たります。

    VB の場合、引数の無いプロパティの呼び出しでは括弧を省略できるので、上記は
     _xlsWorkSheet = xlsApplication.Sheets.Item(strSheetName)
    と書けますし、この .Item は Sheets 型の既定のプロパティであるため、省略して
     _xlsWorkSheet = xlsApplication.Sheets(strSheetName)
    になっていた…というだけです。

    元の「xlsApplication.Sheets(strSheetName)」表記だと、一見すると
    『.』の連続には見えないので注意が必要ですよね。


    こうした「既定のプロパティ」によって、「『.』の連続」が見えにくくなる事象は
    他の COM コレクション型に対しても発生します。たとえば
     Dim r As Excel.Range = Sheet1.Cells(1, 1) 'Cells プロパティの引数に 1,1 を渡しているように見えるが…?
     r.Value = 123
    なども解放漏れ要因になるので NG ですね。

    上記で解放漏れを防ぐには、
     Dim r1 As Excel.Range = Sheet1.Cells '実は Cells プロパティは、「引数を持たない」仕様です
     Dim r2 As Excel.Range = r1(1, 1) 'そしてこれは、Range オブジェクトの .Item(1, 1) に相当します
     r2.Value = 123
     MRComObject(r2)
     MRComObject(r1)
    などのような操作が求められます。



    >   '最終『_xlsWorkSheet = CType(xlsSheets(strSheetName), Excel.Worksheet)』で良さそう…
    そうですね。それでよいと思います。
    ついでに CType を DirectCast にしておくと良さそう。


    > For Each sh In xlsSheets
    >   If CType(sh, Excel.Worksheet).Name = strSheetName Then
    >     check = True
    >     Exit For
    >   End If
    > Next

    For Each を For に置き換えるとこんな感じ。

    Dim found As Boolean = False
    For n As Integer = 1 To xlsSheets.Count
      Dim ws = DirectCast(xlsSheets(n), Excel.Worksheet)
      Dim nm = ws.Name
      MRComObject(ws)
      If nm = strSheetName Then
        found = True
        Exit For
      End If
    Next
違反を報告
返信 削除キー/


Mode/  Pass/


- Child Tree -