最近、ちょっとしたシミュレーションなどでCSVファイルを読み込むことが多いので、CSVReaderクラスを作ってみました。
初投稿ですが、色々と厳しい意見をお願いします。
まず、LineDataという抽象クラスを定義します。
これは、入力したいCSVの1行にどのようなデータが入っているかを定義しておくクラスの基本クラスです。
namespace CSV
{
public abstract class LineData
{
public LineData() { }
public abstract void SetDataFrom(string[] s);
}
}
次にCSVReaderクラスですが、filepathからCSVファイルを読み込んでLineDataを放出し続けます。このため、LineDataの派生型を型引数とするジェネリックにしてあり、IEnumerableも実装しています。
using System;
using System.Collections.Generic;
using System.IO;
namespace CSV
{
public class CSVReader<T> : IEnumerable<T>, IDisposable
where T : LineData, new()
{
private StreamReader reader;
private string filepath;
private bool skip;
public CSVReader(string filepath, bool skip = true)
{
if (!filepath.EndsWith(".csv", StringComparison.CurrentCultureIgnoreCase))
{
throw new FormatException("拡張子が.csvではないファイル名が指定されました。");
}
this.filepath = filepath;
this.skip = skip;
reader = new StreamReader(filepath);
if (skip) reader.ReadLine();
}
public void Dispose()
{
reader.Dispose();
}
public IEnumerator<T> GetEnumerator()
{
string line;
while ((line = reader.ReadLine()) != null)
{
var data = new T();
data.SetDataFrom(line.Split(','));
yield return data;
}
reader = new StreamReader(filepath);
if (skip) reader.ReadLine();
yield break;
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
}
利用側では、まずLineDataを派生させSetDataFromを実装してもらいます。
class IntIntLine : LineData
{
public int data1,data2;
public override void SetDataFrom(string[] s)
{
data1 = int.Parse(s[0]);
data2 = int.Parse(s[1]);
}
}
そして、この派生型を型引数に用いてCSVReaderをusingするだけです。
class Program
{
static void Main()
{
using (var reader = new CSVReader<IntIntLine>(@"example.csv"))
{
var s = reader.Where(i => i.data1 > 0).Select(i => i.data2).Sum();
Console.WriteLine(s);
}
return;
}
}
このCSVReaderの利点としては、IEnumerableを実装しているため、foreachやLinqが使い放題になることでしょう。またLinqの遅延評価を生かし、メモリを節約しながらCSV全体を計算に使用できます。
ちなみにLineDataをインターフェースにしていないのは、常に制約条件new()を満たすためだけにコンストラクタを定義させるのは面倒だろうということで、仕方なくこのような形になりました。何かいいアイデアはありませんか?