将对象序列化为自定义字符串格式以在输出文件中使用的最佳实践

我刚要在特定的业务类上实现ToString()的覆盖,以便生成一个Excel友好的格式来写入输出文件,稍后将对其进行拾取和处理。 这是数据应该是什么样子:

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

创建一个格式字符串并覆盖ToString()对我来说没什么大不了的,但这会改变我决定以这种方式序列化的任何对象的ToString()行为,使得ToString()的实现在整个库中都变得粗糙。

现在,我一直在阅读IFormatProvider ,并且实现它的类听起来是个好主意,但我仍然对所有这些逻辑应该驻留的位置以及如何构建formatter类感到困惑。

当你需要从对象中制作CSV,制表符分隔或其他非XML任意字符串时,你们会怎么做?

以下是使用reflection从对象列表创建CSV的通用方式:

  public static string ToCsv(string separator, IEnumerable 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()中的文件,或者用IEnumerable和yield语句替换StringBuilder。

这是Per Hejndorf的CSV概念的简化版本(没有内存开销,因为它依次产生每一行)。 由于受欢迎的需求,它还通过使用Concat支持字段和简单属性。

2017年5月18日更新

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

2016年2月12日更新

在今天的项目中再次使用我自己的代码后,我意识到当我从@Per Hejndorf的例子开始时,我不应该把任何事情@Per Hejndorf 。 假设默认分隔符“,”(逗号)并使分隔符成为第二个可选参数更有意义。 我自己的库版本还提供了第3个参数,它控制是否应该返回标题行,因为有时您只需要数据。

例如

 public static IEnumerable ToCsv(IEnumerable 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 ToCsv(string separator, IEnumerable 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 ToCsv(string separator, IEnumerable 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()); } } 

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

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

这样,您最终会得到每个输出格式的类,从而生成模型视图。

对于更复杂的序列化,你试图写出一个持久性的对象图,我会考虑把它放在业务类中 – 但只有它能使代码更清晰。

到目前为止我发现的解决方案的问题是它们不允许您导出属性的子集,而只导出整个对象。 大多数情况下,当我们需要以CSV格式导出数据时,我们需要以精确的方式“定制”其格式,因此我创建了这个简单的扩展方法,允许我通过传递类型为Func的参数数组来实现这一点。 Func指定映射。

 public static string ToCsv(this IEnumerable list, params Func[] 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 

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

  public static IEnumerable ToCsv(string separator, IEnumerable 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()); } } 

Gone Coding的答案非常有用。 我对它进行了一些更改,以便处理会影响输出的文本格式。

  /******************************************************/ public static IEnumerable ToCsv(IEnumerable 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; } }