public static void Import(string path, ...) { using (var sr = new System.IO.StreamReader(path)) using (var csv = new CsvHelper.CsvReader(sr)) { csv.Configuration.RegisterClassMap(); var records = csv.GetRecords(); foreach (var record in records) { // 読み込んだあとの処理 } } }
のようなイメージで、ある程度定型的なものになるかと思います。
そんな中、一つのプログラムで複数フォーマットのCSVファイルを連続して読み込む必要が出てきました。何も考えずにいると定型的な部分をコピペしそうな感じだったので、いろいろと試したメモを残します。
環境
- Windows10
- Visual Studio 2015 Update1
- .NET Framework 4.6.1
- CsvHelper 2.13.5
準備
方針
今回は、
- コンソールアプリとして作成
- CSVファイルとC#オブジェクトは、手動でマッピングする
- 共通処理をメソッドに切り出す
- 独自処理はラムダ式で作り、共通処理のメソッドへ渡す
- ラムダ式はActionデリゲートで受け取る
- 参考: C# - 【LINQの前に】ラムダ式?デリゲート?Func<T, TResult>?な人へのまとめ【知ってほしい】 - Qiita
とします。
CSVファイル
date.csv
name,date hoge,20160101
content.csv
name,content hoge,Hello world
マッピング先のオブジェクト用クラス
public class DateCsv { public string Name { get; set; } public DateTime Date{ get; set; } } public class ContentCsv { public string Name { get; set; } public string Content { get; set; } }
マッピングクラス
public class DateMapper : CsvHelper.Configuration.CsvClassMap<DateCsv> { public DateMapper() { Map(m => m.Name).Index(0); Map(m => m.Date).Index(1).TypeConverterOption("yyyyMMdd"); } } public class ContentMapper : CsvHelper.Configuration.CsvClassMap<ContentCsv> { public ContentMapper() { Map(m => m.Name).Index(0); Map(m => m.Content).Index(1); } }
RegisterClassMap()やGetRecords()にTypeを渡して共通化する
ジェネリックを使おうかと考えつつVisualStudioを使っていたところ、
のようなインテリセンスが出ました。
RegisterClassMap()にてTypeを引数に取れるのが気になったので、CsvHelperのソースコードを読むと
Configuration.RegisterClassMap()
に、Typeを渡すオーバーロードGetRecords()
に、Typeを渡すオーバーロード
などを見つけました。
2013年のプルリクエストで取り込まれたようです。
Added new method to CsvConfiguration, ClassMapping(Type classMapType) by vcaraulean · Pull Request #159 · JoshClose/CsvHelper
そのため、まずはRegisterClassMap()やGetRecords()にType
を渡す共通化を試してみます。
Typeを渡すには
Type.GetType("対象の名前空間付クラスの文字列")
を使うtypeof
演算子を使う
などが考えられたため、それぞれ実装してみました。
Type.GetType()を使う
共通処理部分
Type.GetType()で使うため、共通処理のメソッドは
public static void ImportUsingGetType(Action<object> action, string path, string csvClassName, string mapperClassName)
と、名前空間付のクラス名を引数で受け取るようにします。
文字列で指定したクラスのインスタンスを作成するには? - @IT
その引数を
csv.Configuration.RegisterClassMap(Type.GetType(mapperClassName)); var records = csv.GetRecords(Type.GetType(csvClassName));
と、RegisterClassMap()やGetRecords()で使います。
独自処理部分
上記のcsv.GetRecords(Type.GetType(csvClassName));
の戻り値はIEnumerable<object>
となります。
そのため、共通処理に渡すActionデリゲートでは
Action<object> dateActionByObject = obj => { var record = (DateCsv)obj; Console.WriteLine($"date.csvの中身:{record.Name} - {record.Date}"); };
と、object型から本来の型へと変換してから、実際の独自処理を記述します。
呼び出し元
ImportUsingGetType(dateActionByObject, datePath, "FunctionSharingSample.DateCsv", "FunctionSharingSample.DateMapper");
のようにして、共通処理を呼び出します。
ここまでが、Type.GetType()を使って共通化する方法です。
型引数(型パラメータ) + typeof演算子を使う
Type.GetType()を使う方法だと、クラス名を文字列で渡す必要があり、VisualStudioのインテリセンスに頼れません。
そのため、文字列を排除できるよう、Type.GetType()の代わりにtypeof
演算子を使います。
共通処理部分
typeof演算子で使うため、共通処理のメソッドに型引数を追加します。今回は、
の2つを型引数として受け取ります。
public static void ImportUsingTypeof<TCsv, TMapper>(Action<TCsv> action, string path)
csv.Configuration.RegisterClassMap(typeof(TMapper)); var records = csv.GetRecords(typeof(TCsv));
という形でTypeを取得するように変更します。
また、foreachの変数も
foreach (TCsv record in records) { action(record); }
と、型引数のTCsv
を使います。
独自処理部分
foreachで型引数を使ったことから、Actionデリゲートも型を指定できるようになったため、
Action<DateCsv> dateActionByGeneric = obj =>
{
Console.WriteLine($"date.csvの中身:{obj.Name} - {obj.Date}");
};
という形へと変更します。
呼び出し元
ImportUsingTypeof<DateCsv, DateMapper>(dateActionByGeneric, datePath);
となります。
ここまでが、型引数(型パラメータ) + typeof演算子を使って共通化する方法です。
RegisterClassMap()やGetRecords()に型引数を渡して共通化する
CSVHelperのドキュメントを読むと、RegisterClassMap()・GetRecords()メソッドにも型引数を指定できます。
そのため、共通処理内を
csv.Configuration.RegisterClassMap<TMapper>(); var records = csv.GetRecords<TCsv>();
へと置き換えて、typeof演算子を排除できます。
ただ、単純に置き換えるだけだと、
エラー CS0314 型 'TMapper' はジェネリック型またはメソッド 'CsvConfiguration.RegisterClassMap
()' 内で型パラメーター 'TMap' として使用できません。'TMapper' から 'CsvHelper.Configuration.CsvClassMap' へのボックス変換または型パラメーター変換がありません。
というコンパイルエラーが発生します。
コンパイラ エラー CS0314 - MSDN
そのため、where
コンテキストキーワードを使って、型引数TMapper
に対する制限を追加します。
public static void ImportUsingTypeParameter<TCsv, TMapper>(Action<TCsv> action, string path) where TMapper : CsvHelper.Configuration.CsvClassMap
あとは、foreachのところの型名をvar
にしておきます。
foreach (var record in records) { action(record); }
独自処理部分や呼び出し元の実装は、typeof演算子の時の実装と変わりませんので、省略します。
ここまでが、RegisterClassMapやGetRecordsに型引数を渡して共通化する方法です。
動作確認
上記の3パターンを含むコンソールアプリを実行すると、
いずれのパターンでも複数フォーマットのCSVファイル読込処理ができているようでした。
ソースコード
GitHubに上げました。FunctionSharingSample
プロジェクトが今回のソースコードです。
thinkAmi-sandbox/CsvHelperSample