web-dev-qa-db-ja.com

出力ファイルで使用するためにオブジェクトをカスタム文字列形式にシリアル化するためのベストプラクティス

特定のビジネスクラスにToString()のオーバーライドを実装して、後で取得して処理する出力ファイルに書き込むためのExcel対応の形式を作成しようとしていました。データは次のようになります。

_5555555 "LASTN SR, FIRSTN"  5555555555  13956 STREET RD     TOWNSVILLE  MI  48890   25.88   01-003-06-0934
_

書式文字列を作成してToString()をオーバーライドするだけで大​​した問題ではありませんが、これにより、この方法でシリアル化することにしたオブジェクトのToString()の動作が変更され、 ToString()はすべてライブラリ全体で不揃いです。

今、私は IFormatProvider を読んでいて、それを実装するクラスは良い考えのように聞こえますが、このすべてのロジックが存在する場所とフォーマッタを構築する方法についてまだ少し混乱していますクラス。

オブジェクトからCSV、タブ区切り、またはその他の非XMLの任意の文字列を作成する必要がある場合、何をしますか?

29
Chris McCall

次に、リフレクションを使用してオブジェクトのリストからCSVを作成する一般的な方法を示します。

    public static string ToCsv<T>(string separator, IEnumerable<T> objectlist)
    {
        Type t = typeof(T);
        FieldInfo[] fields = t.GetFields();

        string header = String.Join(separator, fields.Select(f => f.Name).ToArray());

        StringBuilder csvdata = new StringBuilder();
        csvdata.AppendLine(header);

        foreach (var o in objectlist) 
            csvdata.AppendLine(ToCsvFields(separator, fields, o));

        return csvdata.ToString();
    }

    public static string ToCsvFields(string separator, FieldInfo[] fields, object o)
    {
        StringBuilder linie = new StringBuilder();

        foreach (var f in fields)
        {
            if (linie.Length > 0)
                linie.Append(separator);

            var x = f.GetValue(o);

            if (x != null)
                linie.Append(x.ToString());
        }

        return linie.ToString();
    }

ToCsv()でファイルに直接書き込む、StringBuilderをIEnumerableおよびyieldステートメントで置き換えるなど、多くのバリエーションを作成できます。

66
Per Hejndorf

これは、Per HejndorfのCSVアイデアの簡略化されたバージョンです(各行が順番に生成されるため、メモリのオーバーヘッドはありません)。一般的な需要があるため、Concatを使用してフィールドと単純なプロパティの両方をサポートしています。

2017年5月18日更新

この例は、完全なソリューションになることを意図したものではなく、Per Hejndorfによって投稿された元のアイデアを進めただけです。有効なCSVを生成するには、テキスト内のテキスト区切り文字を2つの区切り文字のシーケンスで置き換える必要があります。例えば単純な.Replace("\"", "\"\"")

2016年2月12日更新

今日のプロジェクトで自分のコードを再び使用した後、@Per Hejndorfの例から始めたとき、何も当然のことではないことに気づきました。 "、"(コンマ)のデフォルトの区切り文字を想定し、区切り文字を2番目のパラメーターoptionalにすることがより意味があります。私自身のライブラリバージョンでは、ヘッダー行を返すかどうかを制御する3番目のheaderパラメーターも提供しています。

例えば.

public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
{
    FieldInfo[] fields = typeof(T).GetFields();
    PropertyInfo[] properties = typeof(T).GetProperties();
    if (header)
    {
        yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
    }
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
            .Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
    }
}

したがって、コンマ区切りでこのように使用します:

foreach (var line in ToCsv(objects))
{
    Console.WriteLine(line);
}

または、別の区切り文字(TABなど)の場合はこのようにします):

foreach (var line in ToCsv(objects, "\t"))
{
    Console.WriteLine(line);
}

実用例

コンマ区切りのCSVファイルにリストを書き込む

using (TextWriter tw = File.CreateText("C:\testoutput.csv"))
{
    foreach (var line in ToCsv(objects))
    {
        tw.WriteLine(line);
    }
}

またはタブ区切りで記述

using (TextWriter tw = File.CreateText("C:\testoutput.txt"))
{
    foreach (var line in ToCsv(objects, "\t"))
    {
        tw.WriteLine(line);
    }
}

複雑なフィールド/プロパティがある場合は、select句からそれらをフィルタリングする必要があります。


以前のバージョンと以下の詳細:

以下はPer HejndorfのCSVアイデアの簡略化されたバージョンです(各行が順番に生成されるため、メモリのオーバーヘッドはありません)そしてコードは4行しかない:)

public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
    FieldInfo[] fields = typeof(T).GetFields();
    yield return String.Join(separator, fields.Select(f => f.Name).ToArray());
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString()).ToArray());
    }
}

このように反復できます:

foreach (var line in ToCsv(",", objects))
{
    Console.WriteLine(line);
}

ここで、objectsは強く型付けされたオブジェクトのリストです。

このバリエーションには、パブリックフィールドと単純なパブリックプロパティの両方が含まれます。

public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
{
    FieldInfo[] fields = typeof(T).GetFields();
    PropertyInfo[] properties = typeof(T).GetProperties();
    yield return String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p=>p.Name)).ToArray());
    foreach (var o in objectlist)
    {
        yield return string.Join(separator, fields.Select(f=>(f.GetValue(o) ?? "").ToString())
            .Concat(properties.Select(p=>(p.GetValue(o,null) ?? "").ToString())).ToArray());
    }
}
33
Gone Coding

経験則として、デバッグ用のツールとしてtoStringをオーバーライドすることのみを推奨します。ビジネスロジックの場合は、クラス/インターフェイスの明示的なメソッドである必要があります。

このような単純なシリアル化では、CSV出力ライブラリと、シリアル化をビジネスオブジェクト自体にプッシュするのではなく、シリアル化を行うビジネスオブジェクトについて知っている別のクラスを用意することをお勧めします。

このようにして、モデルのビューを生成する出力形式ごとのクラスが作成されます。

永続化のためにオブジェクトグラフを書き出そうとしているより複雑なシリアライゼーションについては、ビジネスクラスに配置することを検討しますが、コードがよりクリーンになる場合のみです。

8
Tom

これまでに見つけたソリューションの問題は、プロパティのサブセットではなく、オブジェクト全体のみをエクスポートできることです。ほとんどの場合、CSVでデータをエクスポートする必要がある場合は、その形式を正確に「調整」する必要があるため、Func<T, string>マッピングを指定します。

public static string ToCsv<T>(this IEnumerable<T> list, params Func<T, string>[] properties)
{
    var columns = properties.Select(func => list.Select(func).ToList()).ToList();

    var stringBuilder = new StringBuilder();

    var rowsCount = columns.First().Count;

    for (var i = 0; i < rowsCount; i++)
    {
        var rowCells = columns.Select(column => column[i]);

        stringBuilder.AppendLine(string.Join(",", rowCells));
    }

    return stringBuilder.ToString();
}

使用法:

philosophers.ToCsv(x => x.LastName, x => x.FirstName)

生成:

Hayek,Friedrich
Rothbard,Murray
Brent,David
1
tocqueville

HiTech Magicのバリエーションが同じ値の2つのプロパティであり、1つだけが入力されるという問題がありました。これはそれを修正したようです:

        public static IEnumerable<string> ToCsv<T>(string separator, IEnumerable<T> objectlist)
    {
        FieldInfo[] fields = typeof(T).GetFields();
        PropertyInfo[] properties = typeof(T).GetProperties();
        yield return String.Join(separator, fields.Select(f => f.Name).Union(properties.Select(p => p.Name)).ToArray());
        foreach (var o in objectlist)
        {
            yield return string.Join(separator, (properties.Select(p => (p.GetValue(o, null) ?? "").ToString())).ToArray());
        }
    }
0
Mark Jones

Gone Codingの回答は非常に役に立ちました。出力をホースするテキストgremlinsを処理するために、いくつか変更を加えました。

 /******************************************************/
    public static IEnumerable<string> ToCsv<T>(IEnumerable<T> objectlist, string separator = ",", bool header = true)
    {
       FieldInfo[] fields = typeof(T).GetFields();
       PropertyInfo[] properties = typeof(T).GetProperties();
       string str1;
       string str2;

       if(header)
       {
          str1 = String.Join(separator, fields.Select(f => f.Name).Concat(properties.Select(p => p.Name)).ToArray());
          str1 = str1 + Environment.NewLine;
          yield return str1;
       }
       foreach(var o in objectlist)
       {
          //regex is to remove any misplaced returns or tabs that would
          //really mess up a csv conversion.
          str2 = string.Join(separator, fields.Select(f => (Regex.Replace(Convert.ToString(f.GetValue(o)), @"\t|\n|\r", "") ?? "").Trim())
             .Concat(properties.Select(p => (Regex.Replace(Convert.ToString(p.GetValue(o, null)), @"\t|\n|\r", "") ?? "").Trim())).ToArray());

          str2 = str2 + Environment.NewLine;
          yield return str2;
       }
    }
0
Chris Barnes