C# 将对象序列化为自定义字符串格式以在输出文件中使用的最佳实践
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/1179816/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Best practices for serializing objects to a custom string format for use in an output file
提问by Chris McCall
I was just about to implement an override of ToString() on a particular business class in order to produce an Excel-friendly format to write to an output file, which will be picked up later and processed. Here's what the data is supposed to look like:
我正准备在特定业务类上实现 ToString() 的覆盖,以生成 Excel 友好的格式以写入输出文件,稍后将提取并处理该文件。数据应该是这样的:
5555555 "LASTN SR, FIRSTN" 5555555555 13956 STREET RD TOWNSVILLE MI 48890 25.88 01-003-06-0934
It's no big deal for me to just make a format string and override ToString()
, but that will change the behavior of ToString()
for any objects I decide to serialize this way, making the implementation of ToString()
all ragged across the library.
对我来说,只制作一个格式字符串和 override 没什么大不了的ToString()
,但这将改变ToString()
我决定以这种方式序列化的任何对象的行为,使ToString()
整个库中的所有实现都参差不齐。
Now, I've been reading up on IFormatProvider, and a class implementing it sounds like a good idea, but I'm still a little confused about where all this logic should reside and how to build the formatter class.
现在,我一直在阅读IFormatProvider,一个实现它的类听起来是个好主意,但我仍然对所有这些逻辑应该驻留在何处以及如何构建格式化程序类感到有些困惑。
What do you guys do when you need to make a CSV, tab-delimited or some other non-XML arbitrary string out of an object?
当您需要从对象中生成 CSV、制表符分隔或其他一些非 XML 任意字符串时,你们会怎么做?
采纳答案by Per Hejndorf
Here is a generic fashion for creating CSV from a list of objects, using reflection:
这是使用反射从对象列表创建 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();
}
Many variations can be made, such as writing out directly to a file in ToCsv(), or replacing the StringBuilder with an IEnumerable and yield statements.
可以进行许多变体,例如直接在 ToCsv() 中写入文件,或用 IEnumerable 和 yield 语句替换 StringBuilder。
回答by Tom
As rule of thumb I advocate only overriding toString as a tool for debugging, if it's for business logic it should be an explicit method on the class/interface.
根据经验,我主张只覆盖 toString 作为调试工具,如果用于业务逻辑,它应该是类/接口上的显式方法。
For simple serialization like this I'd suggest having a separate class that knows about your CSV output library and your business objects that does the serialization rather than pushing the serialization into the business objects themselves.
对于像这样的简单序列化,我建议有一个单独的类,它知道您的 CSV 输出库和您的业务对象进行序列化,而不是将序列化推入业务对象本身。
This way you end up with a class per output format that produces a view of your model.
通过这种方式,您最终会为每个输出格式生成一个类,以生成模型的视图。
For more complex serialization where you're trying to write out an object graph for persistence I'd consider putting it in the business classes - but only if it makes for cleaner code.
对于更复杂的序列化,您试图为持久性写出一个对象图,我会考虑将它放在业务类中 - 但前提是它可以使代码更清晰。
回答by Gone Coding
Here is a simplified version of Per Hejndorf's CSV idea (without the memory overhead as it yields each line in turn). Due to popular demand it also supports both fields and simple properties by use of Concat
.
这是 Per Hejndorf 的 CSV 想法的简化版本(没有内存开销,因为它依次产生每一行)。由于流行的需求,它还通过使用Concat
.
Update 18 May 2017
2017 年 5 月 18 日更新
This example was never intended to be a complete solution, just advancing the original idea posted by Per Hejndorf. To generate valid CSV you need to replace any text delimiter characters, within the text, with a sequence of 2 delimiter characters. e.g. a simple .Replace("\"", "\"\"")
.
这个例子从来没有打算成为一个完整的解决方案,只是推进了 Per Hejndorf 发表的原始想法。要生成有效的 CSV,您需要将文本中的任何文本分隔符替换为 2 个分隔符的序列。例如一个简单的.Replace("\"", "\"\"")
.
Update 12 Feb 2016
2016 年 2 月 12 日更新
After using my own code again in a project today, I realised I should not have taken anything for granted when I started from the example of @Per Hejndorf
. It makes more sense to assume a default delimiter of "," (comma) and make the delimiter the second, optional, parameter. My own library version also provides a 3rd header
parameter that controls whether a header row should be returned as sometimes you only want the data.
今天在一个项目中再次使用我自己的代码后,我意识到当我从@Per Hejndorf
. 假设默认定界符为“,”(逗号),并使定界符成为第二个可选参数更有意义。我自己的库版本还提供了第三个header
参数,用于控制是否应返回标题行,因为有时您只需要数据。
e.g.
例如
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());
}
}
so you then use it like this for comma delimited:
所以你然后像这样使用它来逗号分隔:
foreach (var line in ToCsv(objects))
{
Console.WriteLine(line);
}
or like this for another delimiter (e.g. TAB):
或者像这样用于另一个分隔符(例如 TAB):
foreach (var line in ToCsv(objects, "\t"))
{
Console.WriteLine(line);
}
Practical examples
实际例子
write list to a comma-delimited CSV file
将列表写入以逗号分隔的 CSV 文件
using (TextWriter tw = File.CreateText("C:\testoutput.csv"))
{
foreach (var line in ToCsv(objects))
{
tw.WriteLine(line);
}
}
or write it tab-delimited
或以制表符分隔
using (TextWriter tw = File.CreateText("C:\testoutput.txt"))
{
foreach (var line in ToCsv(objects, "\t"))
{
tw.WriteLine(line);
}
}
If you have complex fields/properties you will need to filter them out of the select clauses.
如果您有复杂的字段/属性,则需要将它们从 select 子句中过滤掉。
Previous versions and details below:
以前的版本和详细信息如下:
Here is a simplified version of Per Hejndorf's CSV idea (without the memory overhead as it yields each line in turn) and has only 4 lines of code :)
这是 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());
}
}
You can iterate it like this:
你可以像这样迭代它:
foreach (var line in ToCsv(",", objects))
{
Console.WriteLine(line);
}
where objects
is a strongly typed list of objects.
whereobjects
是对象的强类型列表。
This variation includes both public fields and simple public properties:
此变体包括公共字段和简单的公共属性:
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());
}
}
回答by Mark Jones
I had an issue the HiTech Magic's variation were two properties with the same value, only one would get populated. This seems to have fixed it:
我有一个问题,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());
}
}
回答by Chris Barnes
Gone Coding's answer was very helpful. I made some changes to it in order to handle text gremlins that would hose the output.
Gone Coding 的回答非常有帮助。我对其进行了一些更改,以处理会影响输出的文本小精灵。
/******************************************************/
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;
}
}
回答by tocqueville
The problem with the solutions I found so far is that they don't let you export a subset of properties, but only the entire object. Most of the time, when we need to export data in CSV, we need to "tailor" its format in a precise way, so I created this simple extension method that allows me to do that by passing an array of parameters of type Func<T, string>
to specify the mapping.
到目前为止我发现的解决方案的问题是它们不允许您导出属性的子集,而只能导出整个对象。大多数时候,当我们需要以 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();
}
Usage:
用法:
philosophers.ToCsv(x => x.LastName, x => x.FirstName)
Generates:
产生:
Hayek,Friedrich
Rothbard,Murray
Brent,David
回答by Neo
ServiceStack.Textis a popular NuGet package that supports CSV serialization. Then this will be all the code you need:
ServiceStack.Text是一个流行的 NuGet 包,支持CSV 序列化。那么这将是您需要的所有代码:
CsvSerializer.SerializeToCsv(foo)
If you don't want the headers, use this code first:
如果您不想要标题,请先使用此代码:
CsvConfig<Foo>.OmitHeaders = true;