Excelファイルを C# と VB.NET で読み込む "正しい" 方法

#はじめに
"Excel C#" や "Excel VB.NET" でググった新人プログラマが、古い情報や間違った情報で茨の道を選ばずに済むようにと思って書きました。

+ この記事は、Windows で Visual Studio を使用したデスクトップアプリケーション開発を想定しています。
+ VB.NET でも作成可能ですが、サンプルコードでは C# 6.0 を使用しています。どちらでもいいなら C# を使いましょう。

#C# または VB.NET でExcelファイルを読み込むには
##Google検索の罠
2016/4/11現在、日本版Googleで [「Excel C#」で検索](https://www.google.co.jp/#q=Excel+c%23) または[「Excel VB.NET」で検索](https://www.google.co.jp/#q=Excel+VB.NET) すると、1ページ目に出てくるのはすべて`Microsoft.Office.Interop.Excel`を使ったCOM参照による方法です。

これはどういう方法かと言うと、Microsoft Office Excelアプリケーションを`Microsoft.Office.Interop.Excel`というインターフェース経由で外部から操作することでExcelファイルを読み込もうという手法です。

しかし、Excelファイルを読み書きするために`Microsoft.Office.Interop.Excel`を使うというのは今では古い手法です。
その中でも特に、**COMオブジェクト解放(ReleaseComObject)が書かれていない情報は絶対に参考にしてはいけません。**理由は後述します。
（そして残念なことに検索上位のページでも多くがCOMオブジェクト解放について考慮されていません）

##どうして検索上位がMicrosoft.Office.Interop.Excelを使う方法ばかりなのか
+ Excel操作といえば VBA であり、豊富な VBA のサンプルが数多く存在すること
+ VB.NET と VBA は構文がほぼ一緒であるため、VBA で書かれたコードの多くが`Microsoft.Office.Interop.Excel`を使えば VB.NET でもそのまま動作すること
+ C# と VB.NET は共通のライブラリが使用できること

という歴史的、言語的な理由からだと思われます。

しかし、`Microsoft.Office.Interop.Excel`を使う方法は以下に挙げる理由により**オススメしません**。
今から書くのであれば、まず ClosedXML や NPOI といった**オープンソースライブラリの使用を検討するべき**です。

（以下、「COM参照」を「`Microsoft.Office.Interop.Excel`を使ってExcelを外部操作する手法」という意味で使用させていただきます。COMの意味については https://ja.wikipedia.org/wiki/Component_Object_Model などを参照してください）

#Microsoft.Office.Interop.Excelを使う方法を推奨しない理由
##処理が遅い
正しくは、「注意して書かなければ」とても処理が遅くなる、です。
COM参照では、実際に裏でExcelアプリケーションを立ち上げて操作することでExcelファイルを読み書きします。
そのためまず最初に`EXCEL.EXE`が起動するための時間がかかります。
起動してからもシートやセルへのアクセスはExcelを経由するため、できるだけアクセス回数を減らすような実装方法を意識する必要があります。

##Excelのインストールが必須
当然ですが、Excelアプリケーションを操作するためにはExcelがインストールされている必要があります。
exe形式にして配布する場合、ExcelがインストールされていないPCでは動作しません。
また、サーバーサイドで実装した場合はOfficeのライセンス違反となる可能性があります。
（参考: https://support.microsoft.com/ja-jp/kb/257757 ）

##プロセス解放漏れのリスク
**推奨できない最大の理由**。
冒頭で述べた、COMオブジェクト解放が書かれていない情報を参考にしてはいけないのはこのためです。
C# および VB.NET では、不要になったメモリをガベージコレクタが自動的に解放してくれます。
しかしCOM参照の場合は、「このExcelはもう使用しません」と宣言しない限り、自動的に解放されることはありません。
この宣言が非常に厄介で、参照したブック、参照したシート、参照したセルの全てに対して`System.Runtime.InteropServices.Marshal.ReleaseComObject()`をしなければいけないため、正しくコードを書かなければ簡単にCOMオブジェクト解放漏れが発生します。
もし解放漏れが残ったままプログラムが途中で終了してしまった場合、プロセスに`EXCEL.EXE`がいつまでも残り続けることになります。
![20160407.PNG](https://qiita-image-store.s3.amazonaws.com/0/57132/2920317a-d496-806c-2ad5-d391eb73583d.png)
↑ 極端な例ですが、わりと冗談ではないです

#オープンソースライブラリを使用する
上記の問題を避けるためにも`EXCEL.EXE`を使用しないライブラリを使って開発することを強くオススメします。
ここでは [ClosedXML](https://github.com/ClosedXML/ClosedXML) と [NPOI](https://github.com/nissl-lab/npoi) の2つを紹介します。
自分は使ったこと無いのですが [EPPlus](https://github.com/JanKallman/EPPlus) も評判が良さそうです。

##ClosedXML
https://github.com/ClosedXML/ClosedXML

Microsoft は Office 2007 から、それまでの独自規格(.xls, .doc, .ppt)を廃止して、オープンな規格であるOpen XML(.xlsx, .docx, .pptx)に移行しました。
そのOpen XMLを扱うために提供されているライブラリが`DocumentFormat.OpenXml`なのですが、これがExcelファイルを扱うには使い勝手の悪いものであったため、Excel処理の部分だけを使いやすい形式にラップしたライブラリが`ClosedXML`になります。

###メリット
コア部分が Microsoft の純正ライブラリであるという安心感
後述の NPOI よりも扱いやすい

###デメリット
Open XML規格用に作られたものであるため、旧形式ファイル(.xls)を読み込むことができない

###サンプルコード
Excelファイルを読み込んで、A列のセルの値を先頭行から最終行まで取得するサンプル

```csharp:ClosedXML
using ClosedXML.Excel;

XLWorkbook workbook = new XLWorkbook("d:\\Book1.xlsx");
IXLWorksheet worksheet = workbook.Worksheet(1);
int lastRow = worksheet.LastRowUsed().RowNumber();
for (int i = 1; i <= lastRow; i++)
{
    IXLCell cell = worksheet.Cell(i, 1);
    Console.WriteLine(cell.Value);
}
```

##NPOI
https://github.com/nissl-lab/npoi

`NPOI`は、15年もの歴史を持つJavaライブラリ`Apache POI`( https://poi.apache.org/ )の.NET移植版。
2012年頃までは旧形式ファイル(.xls)にしか対応していませんでしたが、バージョン2.0になってから.xlsx形式も扱えるようになりました。

###メリット
移植元である Apache POI には長年使用されてきた実績がある
新形式ファイル(.xlsx)と旧形式ファイル(.xls)の両方に対応している
ClosedXML よりも高速

###デメリット
とくになし

###サンプルコード

```csharp:NPOI
using NPOI.SS.UserModel;

// （WorkbookFactory.Create()を使ってinterfaceで受け取れば、xls, xlsxの両方に対応できます）
IWorkbook workbook = WorkbookFactory.Create("d:\\Book1.xlsx");
ISheet worksheet = workbook.GetSheetAt(0);
int lastRow = worksheet.LastRowNum;
for (int i = 0; i <= lastRow; i++)
{
    IRow row = worksheet.GetRow(i);
    ICell cell = row?.GetCell(0);
    Console.WriteLine(cell?.StringCellValue);
}
```

個人的には、旧形式の対応が不要なら ClosedXML のほうが使いやすいと感じています。

+ ClosedXMLのほうがセルの取得方法が直感的
+ NPOIでは空行または空セルを参照するとオブジェクトが`null`になってしまう（しかし C# 6.0 でnull条件演算子`?.`が追加されたので、上記サンプルのように簡潔に書けるようになりました）
+ ClosedXMLでは行番号や列番号が0でなく1から始まる（好みの問題ですが、ExcelシートはA1から始まるので行番号とインデックスが一致する）

#どうしてもMicrosoft.Office.Interop.Excelを使いたい場合
デメリットは冒頭で挙げましたが、COM参照を使うメリットもあります。

実は ClosedXML や NPOI だと、まれにファイルによっては読み込みに失敗することがあります。
経験上、Excelシートに埋め込まれた画像やマクロなどが原因となってエラーになることが多いです。
COM参照であれば実際にファイルを開くのはExcelアプリケーションなので、マクロ付きであろうが拡張子が.csvになっていようが（Excelのバージョンが古くない限り）どんなファイルでも読み込むことができます。

##サンプルコード
先述したCOMオブジェクトの解放を確実にやるのであれば、先ほどのサンプルはここまで書く必要があります。

```csharp:Microsoft.Office.Interop.Excel
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Excel;

var excelApplication = new Microsoft.Office.Interop.Excel.Application();
try
{
    Workbooks workbooks = excelApplication.Workbooks;
    try
    {
        Workbook workbook = workbooks.Open("d:\\Book1.xlsx");
        try
        {
            Sheets worksheets = workbook.Sheets;
            try
            {
                Worksheet worksheet = worksheets[1];
                try
                {
                    // 使用範囲を一括で二次元配列にコピー
                    Object[,] rangeArray;
                    Range usedRange = worksheet.UsedRange;
                    try
                    {
                        rangeArray = usedRange.Value;
                    }
                    finally { Marshal.ReleaseComObject(usedRange); }

                    // 二次元配列に対してループを回す
                    int lastRow = rangeArray.GetLength(0);
                    for (int i = 0; i < lastRow; i++)
                    {
                        Console.WriteLine(rangeArray[i, 0]);
                    }
                }
                finally { Marshal.ReleaseComObject(worksheet); }
            }
            finally { Marshal.ReleaseComObject(worksheets); }
        }
        finally
        {
            if (workbook != null)
            {
                workbook.Close(false);
            }
            Marshal.ReleaseComObject(workbook);
        }
    }
    finally { Marshal.ReleaseComObject(workbooks); }
}
finally
{
    if (excelApplication != null)
    {
        excelApplication.Quit();
    }
    Marshal.ReleaseComObject(excelApplication);
}
```
COM参照で書く際の注意事項としては、

+ `Workbook workbook = excelApplication.Workbooks.Open("d:\\Book1.xlsx");`のように繋げて書いてしまうと、`Workbooks`オブジェクトに対して`ReleaseComObject()`できなくなるため、すべて変数に割り当てるようにします。
+ `finally{}`の中で`Marshal.ReleaseComObject()`を呼び出すことによって、どこで例外が発生しても確実にCOMオブジェクトを解放することができます（が、毎回ここまで書くのは大変なので何度も使うのであれば`IDisposable`の実装クラスを作成したほうがいいです）。
+ ループの中で直接Cellオブジェクトを読んでしまうと非常に時間がかかるため、セルの値を取得するだけであれば、あらかじめ必要なシート範囲を二次元オブジェクト配列にコピーすることで**劇的に高速化できます**。

#最後に
サンプルコードでは触りの部分だけしか紹介できていないので、書き込みや保存については各ライブラリのドキュメントや他の紹介記事を参照ください。