前:COM リソースの解放(その1)
では、「推奨される方法」です。
案件によっては、Visual Studio for Office Tools を検討することを勧めます。これであれば、旧 Office 製品のマクロのように、C# や VB.NET を使うことが出来ます。また、Office 製品の中で動作しますから、Office 製品のコントロールに対しては、参照数がどうのこうのというところを意識しなくてもかまいません。
VSTO が使えない場合、渋木さんやじゃんぬさんがコメントされているように、「スマート ポインタ」「ラッパー クラスで全部管理」が推奨されます。どちらにしても、参照をすべて明示的に把握して、解放する必要があります。
参照の解放には、System.Runtime.InteropServices 名前空間の、Marshal クラスに用意された静的メソッド、ReleaseComObject を使用します。このメソッドによって、「提供されたランタイム呼び出し可能ラッパーの参照カウントをデクリメントします。」
では、どうやって参照を明示的に把握するのか。
明示的に把握するためには、どのオブジェクトが「提供されたランタイム呼び出し可能ラッパー」なのか、知る必要があります。で、これを“完全に”知る方法が、おそらくありません。『Microsoft Visual Basic のヘルプ』から、[Microsoft Excel Visual Basic リファレンス → オブジェクト]にあるものが、すべて該当するのかなぁ?(Word の時は、当然[Microsoft Word Visual Basic リファレンス])
これら、「参照をデクリメントする必要があるオブジェクト」を、使う端から明示的に参照し、使い終わったらリリースします(原則)。
なお、ここで提示するコードは、雰囲気をつかんでいただくためのものです。動くコードではありません。
Excel.Application excel = null;
try {
excel = new Excel.Application();
Excel.WorkBooks books = null;
try {
Excel.WorkBooks = excel.WorkBooks;
Excel.Sheet sheet = null;
try {
sheet = books[1];
Excel.Range range = null;
try {
range = sheet.Range("A1");
range.Value = "ABC";
} finally {
if (range != null) {
Marshal.ReleaseComObject(range);
}
}
} finally {
if (sheet != null) {
Marshal.ReleaseComObject(sheet);
}
}
} finally {
if (books != null) {
Marshal.ReleaseComObject(books);
}
}
} finally {
if (excel != null) {
// これだとアプリケーションから切り離すだけ
Marshal.ReleaseComObject(excel);
}
}
あ・・・ WoorkBooks > WorkBook > WorkSheets > WorkSheet だった。。。セルに書き込むだけで、たくさんの参照ができ、それを全部解放しなければならないんだということは伝わるでしょうから、まぁ、いいや。
これが“正攻法”(正面突破?)でしょう。しかし、このようにネストが深くなると、とても見やすいコードとはいえません。また、WorkBooks や WorkSheets を同じレベルで宣言し、1つの finally ブロックの中でまとめて解放した場合、Marshal.ReleaseComObject メソッドが例外を送出しないという保証がないので、解放漏れが発生する可能性があります。もっとも、そんなときはアプリケーションも落ちる(落とさなければならない)でしょうから、Windows アプリケーションであるなら、かまわないんだけど。。。
同じレベルで解放する例(非推奨):
Excel.Application excel = null;
Excel.WorkBooks books = null;
Excel.WorkBook book = null;
try {
excel = new Excel.Application();
books = excel.WorkBooks;
book = books[1];
...
} finally {
if (book != null) Marshal.ReleaseComObject(book);
if (books != null) Marshal.ReleaseComObject(books);
if (excel != null) Marshal.ReleaseComObject(excel);
}
そこで、Excel を扱う箇所を別のクラスに追い出し、Excel への参照を解決するのを、そのクラスの責務とします。スマート ポインタというのは、どの様に実装すればいいのかよく分からないのですが、同じような考え方で良いのではないでしょうか???
class ExcelInterface : IDisposeable {
Excel.Application excelInstance = null;
bool disposed = false;
public void Dispose() {
if (excelInstance != null) {
Marshal.ReleaseComObject(excelInstance);
excelInstance = null;
disposed = true;
}
}
その他必要な処理一式
}
COM リリース:お勧めではないが、こんな方法も... から引っ張ってきました。この中の、「その他必要な処理一式」を考えます。それらの処理の中で、暗黙参照を作らせなければ、クラスの使用者は Dispose しさえすれば、確実に Excel 参照を無くすことが出来るからです。前のエントリでは、「既存のコードを、修正を少なくして、Excel 参照を解放する」ことを目的にしていましたので、暗黙参照があることが前提です。その為、クラスの使用者に Dispose だけでなく、GC.Collect もコールしてもらいました。
今度は GC.Collect を呼ばなくても、Excel が終了することが目標です。このためには、ExcelInterface の外に Excel への参照を持ち出させず、かつ、暗黙参照が無いようにします。
そうすると、「"book1.xls" ファイルの、"Sheet1" の、"A1" に、文字列 'string' を入れる」というのを、一例として、次のようなメソッドで実現することになります。
class ExcelInterface : IDisposeable {
public void SetRangeValue(string BookName
, string SheetName
, string RangeString
, object Value) {
WorkBooks books = null;
try {
books = excelInstance.WorkBooks;
WorkBook book = null;
try {
....
ちょっと待って。これでは場所が変わっただけで、先の深すぎるネストと同じです。もう少し考えましょう。
WorkBooks や Sheets といったコレクションは、それ自体が欲しいのではなく、コレクションの中のものが欲しいだけです。ですから、WorkBook や WrokSheet オブジェクトが取り出せれば、参照は解放しておいてかまわないでしょう。
Excel VBA には、ActiveSheet や ActiveWorkBook という、「現在アクティブになっているオブジェクト」を示すものがあります。操作対象の WorkBook や Sheet がコロコロ変わることはそうそう無いですから、「現在アクティブなオブジェクト」を設定してもかまわないでしょう。
class ExcelInterface : IDisposeable {
private Excel.WorkBook activeWorkbook = null;
private Excel.WorkSheet activeWorksheet = null;
private Excel.Application excelInstance = null;
private bool disposed = false;
public void Dispose() {
try {
if (activeWorksheet != null) {
Marshal.ReleaseComObject(activeWorksheet);
}
if (activeWorkbook != null) {
Marshal.ReleaseComObject(activeWorkbook);
}
if (excelInstance != null) {
Marshal.ReleaseComObject(excelInstance);
}
disposed = true;
} catch {
throw new
}
}
public void SetActiveWorkbook(string BookName) {
if (excelInstance == null) {
throw new ApplicationException();
}
if (BookName != null && BookName != string.Empty) {
if (activeWorkbook != null) {
Marshal.ReleaseComObject(activeWorkbook);
}
WorkBooks books = null;
try {
books = excelInstance.WorkBooks;
activeWorkbook = books[BookName];
} finally {
if (books != null) {
Marshal.ReleaseComObject(books);
}
}
}
}
public void SetActiveWorkSheet(string SheetName) {
if (activeWorkbook == null) {
throw new ApplicationException();
}
if (SheetName != null && SheetName != string.Empty) {
if (activeWorkSheet != null) {
Marshal.ReleaseComObject(activeWorkSheet);
}
...
}
}
public void SetRangeValue(string BookName
, string SheetName
, string RangeString
, object Value) {
SetActiveWorkBook(BookName);
SetActiveWorkSheet(SheetName);
if (activeWorksheet == null) {
throw new ApplicationException();
}
Range range = null;
try {
range = activeWorksheet.Range(RangeString);
range.Value = Value;
} finally {
if (range != null) {
Marshal.ReleaseComObject(range);
}
}
}
...
}
SetRangeValue メソッドの引数を、「ワークブック」「ワークシート」「レンジ」「値」としましたが、「値」「レンジ」「ワークシート」「ワークブック」とし、「ワークシート」「ワークブック」のないメソッドをオーバーロードしても良いでしょう。
ネストを深くするか、ネストする単位でメソッドを分割して平坦に見せるか。どちらにしても、読み難さは変わらないかもしれません。
結構長くなりました。こんなところで。
投稿日時 : 2006年7月21日 22:36