34

我正要在特定业务类上实现对 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 任意字符串时,你们会怎么做?

4

8 回答 8

73

这是使用反射从对象列表创建 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 语句。

于 2010-01-04T07:39:57.387 回答
35

这是 Per Hejndorf 的 CSV 想法的简化版本(没有内存开销,因为它依次产生每一行)。由于受欢迎的需求,它还通过使用Concat.

2017 年 5 月 18 日更新

这个例子从来没有打算成为一个完整的解决方案,只是推进了 Per Hejndorf 发布的原始想法。要生成有效的 CSV,您需要将文本中的任何文本分隔符替换为 2 个分隔符的序列。例如一个简单的.Replace("\"", "\"\"").

2016 年 2 月 12 日更新

今天在一个项目中再次使用我自己的代码后,我意识到当我从@Per Hejndorf. 假设默认分隔符为“,”(逗号)并将分隔符作为第二个可选参数更有意义。我自己的库版本还提供了第三个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);
    }
}

如果您有复杂的字段/属性,则需要将它们从选择子句中过滤掉。


以前的版本和详细信息如下:

这是 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);
}

whereobjects是一个强类型的对象列表。

此变体包括公共字段和简单的公共属性:

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());
    }
}
于 2012-06-07T09:57:23.773 回答
8

作为经验法则,我主张仅将 toString 重写为调试工具,如果它用于业务逻辑,它应该是类/接口上的显式方法。

对于像这样的简单序列化,我建议有一个单独的类来了解您的 CSV 输出库和执行序列化的业务对象,而不是将序列化推入业务对象本身。

这样,您最终会为每个输出格式生成一个类,以生成模型的视图。

对于更复杂的序列化,你试图写出一个持久化的对象图,我会考虑将它放在业务类中——但前提是它会使代码更清晰。

于 2009-07-24T20:05:39.740 回答
2

到目前为止,我发现的解决方案的问题是它们不允许您导出属性子集,而只能导出整个对象。大多数时候,当我们需要以 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
于 2018-01-10T17:01:10.757 回答
1

我遇到了一个问题,HiTech Magic 的变体是两个具有相同值的属性,只有一个会被填充。这似乎已经解决了它:

        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());
        }
    }
于 2013-06-21T14:04:54.933 回答
1

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;
       }
    }
于 2016-08-31T19:57:00.617 回答
1

ServiceStack.Text是一个流行的 NuGet 包,它支持CSV 序列化。那么这将是您需要的所有代码:

CsvSerializer.SerializeToCsv(foo)

如果您不想要标题,请先使用此代码:

CsvConfig<Foo>.OmitHeaders = true;
于 2019-10-27T00:30:31.977 回答
0

Gone Coding 的答案很棒!我对 Gone Coding 的回答进行了一些更改,以使用双引号限定字段,并让属性迭代器忽略任何没有索引参数的属性,即启用属性名称 getter 和 setter 的类中的属性:

FieldInfo[] fields = typeof(T).GetFields();
PropertyInfo[] properties = typeof(T).GetProperties().Where(x => x.GetIndexParameters().Length == 0).ToArray();
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());
}
于 2020-08-27T18:19:34.070 回答