Option Strict Onにするとエラーになってしまう

Category: visual studio vb_ja

Question

こまってます on Tue, 15 Dec 2020 00:59:37


Excelファイルへ出力するプログラムを作っています。VS2015です。
以下のコードは、Option Strict Offでは問題ないのですが、Onにするとエラーになってしまいます。
CTypeで型変換すればよいのでしょうが、Onject型なのでどう記述すればよいかわかりません。
Onにしてもエラーにならないようにするにはどうすればよいのでしょう?

Dim myExcel As Object = CreateObject("Excel.Application")
myExcel.Visible = True
myExcel.Workbooks.Add
myExcel = Nothing

Replies

魔界の仮面弁士 on Tue, 15 Dec 2020 02:50:13


Object 型で受けるのではなく、Excel を参照設定して、固有の型 (Workbook 型など)で受ける必要があります。

参照設定なしで Option Strict On にしたい場合には、CallByName 経由にすれば呼び出せます。

こまってます on Tue, 15 Dec 2020 05:15:07


ありがとうございます。
具体的な記述を教えていただければ幸いです。
できれば参照設定ありとなし両方いただければと。

魔界の仮面弁士 on Tue, 15 Dec 2020 06:11:25


Option Strict On  '事前に参照設定が必要です
Imports Excel = Microsoft.Office.Interop.Excel
Imports System.Runtime.InteropServices
Module Module1

    Sub Main()
        Dim saveFileName As String = "C:\TEMP\SAMPLE.XLSX"

        Dim myExcel As New Excel.Application() With {.Visible = True}
        Dim myBooks As Excel.Workbooks = myExcel.Workbooks
        Dim myBook As Excel.Workbook = myBooks.Add()
        ' myBook.SaveAs(saveFileName, Excel.XlFileFormat.xlWorkbookDefault)
        ReleaseComObject(myBook)
        ReleaseComObject(myBooks)
        ' myExcel.Quit()
        ReleaseComObject(myExcel)
    End Sub

    Public Sub ReleaseComObject(Of T As Class)(ByRef o As T)
        If o IsNot Nothing AndAlso Marshal.IsComObject(o) Then
            Marshal.ReleaseComObject(o)
            o = Nothing
        End If
    End Sub

End Module

Option Strict Off   '追加の参照設定は不要
Imports System.Runtime.InteropServices
Module Module1

    Sub Main()
        Dim saveFileName As String = "C:\TEMP\SAMPLE.XLSX"

        Dim myExcel As Object = CreateObject("Excel.Application")
        CallByName(myExcel, "Visible", CallType.Let, True)
        Dim myBooks As Object = CallByName(myExcel, "Workbooks", CallType.Get)
        Dim myBook As Object = CallByName(myBooks, "Add", CallType.Method)
        ' Const xlWorkbookDefault As Integer = 51
        ' CallByName(myBook, "SaveAs", CallType.Method, saveFileName, xlWorkbookDefault)
        ReleaseComObject(myBook)
        ReleaseComObject(myBooks)
        ' CallByName(myExcel, "Quit", CallType.Method)
        ReleaseComObject(myExcel)
    End Sub

    Public Sub ReleaseComObject(Of T As Class)(ByRef o As T)
        If o IsNot Nothing AndAlso Marshal.IsComObject(o) Then
            Marshal.ReleaseComObject(o)
            o = Nothing
        End If
    End Sub

End Module

可能な限り、参照設定したコード(前者)にすることをお奨めしておきます。

後者のように Object 型で処理した場合、COM オブジェクトを引数に受け取るプロパティやメンバーを呼び出した際に、暗黙の型変換によって COM オブジェクトの参照カウントが増加してしまい、オブジェクトの解放が行われにくくなることがあるためです。また、列挙型などの定数を自前で再定義する必要がありますし、イベントも直接扱えなくなってしまうといった弊害があります。

こまってます on Tue, 15 Dec 2020 07:54:48


ありがとうございます。
ですがすみません、せっかくコードを乗せていただいたのに、当方のレベル的にまだ難しかったようです…orz
こちらからコードを乗せたほうが話が早いかもしれませんね。
つまるところ、このコードをどう記述すればOption Strict Onでもエラーにならないのか?が知りたいです。
現状、oAppの部分でエラーになります。

Public Sub fTuneExcel_Test(ByVal psFilePath As String, ByVal pintSprRowCnt As Integer, ByVal pintSprColCnt As Integer, ByVal pi印刷方向 As Integer)
    Dim oApp As Object = CreateObject("Excel.Application")
    oApp.Application.Workbooks.Open(psFilePath)
    oApp.Application.Cells.Select()
    oApp.Application.Cells.EntireColumn.AutoFit()
    oApp.Application.Range("A1").Select()
    oApp.ActiveSheet.Unprotect()
    oApp.Range(oApp.Cells(1, 1), oApp.Cells(pintSprRowCnt + 2, pintSprColCnt + 1)).select()
    oApp.Selection.Borders(5).LineStyle = -4142
    oApp.selection.Borders(6).LineStyle = -4142
    With oApp.Application.ActiveSheet.PageSetup
        .Zoom = False
        .PrintHeadings = False
        .FitToPagesWide = 1
        .FitToPagesTall = False
        .Orientation = pi印刷方向
    End With
    oApp.Application.ActiveWorkbook.Save()
    oApp.Application.Visible = True
End Sub

それとすみません、「参照の設定」というのも、実はよくわかってませんでした。
プロジェクトのプロパティの参照で、何かをチェックすればよいのかと思ってたのですが、「Microsoft.Office.Interop.Excel」のようなものがあるのかと思ってましたが、どうやらなさそうで…。
それとも「参照」の右クリックメニューの「参照の追加」で表示される「参照マネージャー」で何かを設定するのでしょうか?
質問ばかりでほんと、すみません…。

魔界の仮面弁士 on Tue, 15 Dec 2020 08:17:18


このままだと、質問ではなくプログラムの書き換え依頼になってしまうのと、今は Visual Studio を触れる場所に居ないので、一般論だけ。

まずはとにかく、参照の追加の [COM] タブにて、Microsoft Excel x.x Object Library を追加しましょう。

その上で、CreateObject や As Object の部分を、先のコードのように置き換えていった方が、入力ヒント(IntelliSense)の支援も受けられるため、コーディングが楽になるはずです。

oApp.Application.Workbooks.Open(psFilePath)

まず、oApp と oApp.Application は同一のインスタンスを意味します。
VBA であれ .NET であれ、このような呼び出しは冗長なので避けましょう。

次に…上記は「オブジェクト.プロパティ.プロパティ.メソッド(引数)」のような構文になっていますが、このように、プロパティやメソッドの「.」が連続するようなコードを書いてはいけません。これは、.NET のメモリ管理と COM/ActiveX オブジェクトの管理方法の違いに起因するものです。

VB6 や VBA から呼ぶ場合にはこの記法でも問題ないのですが、.NET から利用する場合、このような書き方が多用されると COM オブジェクト参照の解放漏れに繋がり、終了させたはずの Excel プロセスが非表示状態のまま生き残ってしまったり、2 回目以降の操作が誤動作を起こす要因となりえます。(プロパティやメソッドの「.」ではなく、列挙型や名前空間を繋げるための「.」であれば連続しても構わないのですが)

上記 URL でも述べられていますが、面倒でも、各オブジェクトごとに変数に受け取って利用するようにし、可能であれば、使用後に Marshal.ReleaseComObject メソッドを使って、COM オブジェクトを明示的に解放することが望ましいです。


こまってます on Wed, 16 Dec 2020 00:08:28


ご丁寧にありがとうございます。
参照の設定はでき、エラーは減りました。
が、まだ以下の部分でエラーになってしまいます…。
ActiveSheetやSelectionが、結局Object型なんですよね。

oApp.Application.ActiveSheet.Unprotect()
oApp.Application.ActiveSheet.PageSetup
oApp.Selection.Borders(5).LineStyle = -4142
oApp.Sheets(1).Select()
oApp.Sheets(1).name = "TEST"
oApp.Sheets(3).Delete()
oApp.Cells(1, 1).font.size = 16

「Option Strict Off」にすると、遅延パインディングというのができるけど、Onの場合は事前に必要、なんですね。
この「遅延パインディング」というのがよくわからないけど、
要するに「参照の設定」を事前に行うか(Onの場合)実行時に行うか(Offの場合)の違い、って解釈であってますか?
今までOffで正常に動いてたので、参照の設定を行うことで思わぬ不具合が起こらないかが心配…。

一歩前進したかと思えばまた問題…。
正直、Onにするのがこんなに大変とは思いませんでした。なかば諦めモードです…。
やっぱりファイルの一部だけOffにできたらなぁ…。

魔界の仮面弁士 on Wed, 16 Dec 2020 02:15:30


この「遅延パインディング」というのがよくわからないけど、

半濁点ではなく濁点です。 (^^;)
pine-ding ではなく binding ですね。

bind は「結びつき」を意味する英単語です。VB においては、プロパティやメソッドを操作する際に、その対象のオブジェクトが Object 型変数の場合に、実行時結合とか実行時バインディングとか遅延(late)バインディングとか動的(dynamic)バインディングなどと呼ばれます。

Object 型ではなく、固有型によるアクセスが行われていた場合には、コンパイル時にスペルミス等もチェックされますし、処理速度も向上します。既に御存知のように、それを強制するのが Option Strict On というオプションです。こちらは事前結合とかコンパイル時バンディングとかアーリー(early)バインディングとか静的(static)バインディングなどと呼ばれます。

ActiveSheetやSelectionが、結局Object型なんですよね。

シートオブジェクトには複数の種類があり、それぞれで振る舞いが異なります。

  • Worksheet 型 … ワークシート、Excel 4.0 マクロシート
  • DialogSheet 型 … Excel 5.0 ダイアログシート
  • Chart 型 … グラフシート

別々の型を一つのプロパティで表現するため、固有型ではなく、汎用 Object 型で定義されています。

たとえば Worksheet 型には Type プロパティや Range プロパティがありますが、Chart 型にはありません。その逆に、Chart 型で使える Axes メソッドや CharTitle プロパティが、Worksheet 型にはありません。

そのため ActiveSheet は固有型とならず、汎用の Object 型として定義されています。

Selection プロパティも同様に、「何を選択しているか」によって振る舞いが変わるため、やはり汎用 Object 型で定義されているというわけです。

こうしたオブジェクトを使う場合には、
「Dim 変数 As 固有型 = DirectCast(Selection, 固有型)」
のようにして、本来のデータ型にキャストしてから利用します。

oApp.Application.ActiveSheet.Unprotect()

先にも述べましたが、Application プロパティの呼び出しは無意味なので避けましょう。

「oApp」も「oApp.Application」も「oApp.Application.Application」も「oApp.Application.Application.Application」も、いずれも同じオブジェクトへの参照を指し示すものなので、冗長的です。

また、「Selection プロパティ」「ActiveSheet プロパティ」「Select メソッド」「Activate メソッド」などは、できる限り排除してください。
Active何某に頼りすぎたコードは、それがどのオブジェクトを指し示しているのかが曖昧になりますし、処理効率も悪くなってしまいます。

たとえば、VB でフォーム上のコントロールを操作する際には、
 TextBox1.Select()
 ActiveControl.Text = "新しい文字列"
 Button1.Select()
 ActiveControl.Enabled = False
と書くのではなく、単に、
 TextBox1.Text = "新しい文字列"
 Button1.Enabled = False
とするのが普通ですよね。それと同じことです。

oApp.Cells(1, 1).font.size = 16

Cells プロパティを Application オブジェクトから直接呼び出すのも、あまり良くありません。どのワークブックの、どのワークシートにある .Cells(1, 1) を示しているのかが曖昧になるためです。これも上記の、Active何某に頼りすぎたコードの一種と言えます。

複数のワークブックを同時に開いているのでなければ、アクティブなブックは明らかなので、oApp.Cells というコードでも誤りではありませんが、そもそもセル範囲はワークシート上にあるものですから、Application オブジェクトのメンバーではなく、Worksheet オブジェクトのメンバーの方を用いた方が適切かと。

oApp.Sheets(1).Select()

oApp.Sheets もまた、曖昧さを残したコードです。複数のワークブックが同時に開かれていた場合に備え、Application オブジェクトの .Sheets ではなく、Workbook オブジェクトの .Sheets にアクセスした方が望ましいです。

oApp.Selection.Borders(5).LineStyle = -4142

「.LineStyle = -4142」のようなマジックナンバーの利用も避けましょう。
代わりに、「.LineStyle = Excel.XlLineStyle.xlLineStyleNone」のような、列挙型や定数名を指定したコードに置き換えます。

同様に、.Borders(5) の「5」も「Excel.XlBordersIndex.xlDiagonalDown」に置き換えて、どの罫線部を操作しているのかを明確にした方が良いでしょう。

Dim myExcel As New Excel.Application() With {.Visible = True}
Dim myBooks As Excel.Workbooks = myExcel.Workbooks
myExcel.SheetsInNewWorkbook = 1
Dim myBook As Excel.Workbook = myBooks.Add()
' 毎回 myBook.Sheets(1) でアクセスするのではなく、変数にキャッシュしてそれを使う Dim mySheets As Excel.Sheets = myBook.Sheets Dim ws1 As Excel.Worksheet = DirectCast(mySheets(1), Excel.Worksheet) ws1.Name = "ABC" ' Cells プロパティは、引数を持たないプロパティです。
' ws1.Cells(1, 1) というのは、ws1.Cells._Default(1, 1) の意味であり、
' それぞれが Excel.Range 型のオブジェクトを返すため、
' 個別の型に受けておき、後で ReleaseComObject できるようにします。
Dim cells As Excel.Range = ws1.Cells Dim cellRange As Excel.Range cellRange = DirectCast(cells(1, 1), Excel.Range) cellRange.Value = "あいうえお"
' Font オブジェクトも COM オブジェクトなので、変数に受けておきます。 Dim f As Excel.Font = cellRange.Font f.Size = 21 ReleaseComObject(f) ' Font オブジェクトを解放 Dim borders As Excel.Borders = cellRange.Borders Dim border As Excel.Border = borders(Excel.XlBordersIndex.xlDiagonalDown) border.LineStyle = Excel.XlLineStyle.xlContinuous ReleaseComObject(border) ReleaseComObject(borders)
ReleaseComObject(cellRange) ' 同じ cellRange 変数を使いまわす場合は、参照先を変更する前に、 ' 以前の参照を Marshal.ReleaseComObject で解放しておきます。
cellRange = ws1.Range("B3:C5") cellRange.Value = New Object(,) {{"ABC", 123}, {"DEF", 456}, {"GHI", 789}} ReleaseComObject(cellRange) ReleaseComObject(cells) ReleaseComObject(ws1) ReleaseComObject(mySheets) ReleaseComObject(myBook) ReleaseComObject(myBooks) ReleaseComObject(myExcel)

jzkey on Wed, 16 Dec 2020 02:59:10


classをpartialにして、好きなメソッド単位でstrict offなファイルに移せばよいのでは、、、?

こまってます on Wed, 16 Dec 2020 06:57:45


魔界の仮面弁士様

無知な私の質問にここまで根気強くお付き合いいただき、本当にありがとうございます。
おかげさまで、少しずつ…本当に少しずつですが、エラーを取り除くことができるようになりました。

それにしても…ちゃんとしたコードを記述するのって、けっこう大変なんですねぇ。
添削だらけですよ、きっと。
ActiveSheetsとか使いまくってました。よくないんですね、コレって。

今回の件で、「Option Strict Off」を部分指定できればいいのに、ってつくづく思いました。
たくさんある関数の中で、1個だけなんですよ、Offにしないといけないのは。
そのためにソースファイル全体をOffにしなきゃいけないのかよ!って思います。
かといってjzkeyさんが仰るように、1個の関数のためだけに別ファイルにしたくもないですし…。

愚痴っぽくなってしまいましたが、あとは自力でなんとかしてみます。
分かりやすく丁寧な説明で、ためになりました。
本当にありがとうございました。

kumo-msft on Thu, 17 Dec 2020 05:35:38


こまってますさん、こんにちは。フォーラムオペレーターのKumoです。
MSDNフォーラムにご投稿くださいましてありがとうございます。

本件、魔界の仮面弁士さんより参考になる投稿が寄せられたようでなによりです。

[回答としてマーク]機能は設定された投稿が後から参照しやすくなりますので、
同じ問題でお困りの方のためにも参考になった投稿に設定いただけますと幸いです。

お手数ですが、ご協力の程どうかよろしくお願いいたします。

引き続きMSDNフォーラムをご利用いただけますようお願い申し上げます。