将 CSV 数据转换为对象






4.96/5 (17投票s)
2006年5月26日
8分钟阅读

109412

1291
使用自定义属性和 .NET 反射从 CSV 文件加载对象。
引言
CSV 文件仍然随处可见,开发人员经常面临解析和处理这些数据的场景。通常,我们希望使用 CSV 数据来初始化对象。在本文中,我们将探讨一种将传入的 CSV 数据映射到我们自己对象的方法。为简洁起见,我将假设您已经开发了一种方法来解析给定的 CSV 输入行并将其转换为字符串数组。
背景
我最初想到这个问题是因为一位客户问我,是否有简单的方法可以将传入的 CSV 数据映射到对象。他已经想出了如何使用正则表达式解析他读入应用程序的文本行,以创建一个包含数据文件中所有字段的数组。然后,问题就变成了如何从该数组创建对象。
显而易见且最直接的方法是这样的:
Customer customerObj = new Customer();
customerObj.Name = datafields[0];
customerObj.DateOfBirth = DateTime.Parse(datafields[1]);
customerObj.Age = int.Parse(datafields[2]);
这会相当直接,但如果对象或属性很多,就会变得非常繁琐。而且,它没有考虑在将输入数据分配给字段之前进行任何自定义处理。您还可以为每个类创建一个特殊的构造函数,该构造函数接受一个数据数组并正确设置对象,这可能是稍微好一些的方法。
方法
面对这个问题,我最初的想法是:
- 拥有某种加载器来处理传入的数组数据并实例化任意类并填充数据是有意义的,并且
- 有一种方法可以轻松地将数组数据映射到类数据。
带着这两个想法(因此将我其他的剩余想法限制在一个,因为我一次只能处理三件事),我开始尽可能快地清空我的想法队列。
基于这两个想法,我挑选了三个关键点来驱动我的思考:
- 我需要某种 `Loader` 类。我最初的想法是,我希望有几个静态方法,它们可以接受一个现有对象来用给定的字符串数组填充,或者它可以根据数组数据被告知要创建的对象类型,然后返回一个完全填充好的正确类型的新对象。
- 因为 `Loader` 需要与任何类一起工作,所以我需要使用 .NET 反射来检查类,以了解需要更新哪些信息。
- 由于我需要一个映射功能并且在一定程度上需要反射,所以使用自定义 .NET 属性来“标记”类上的属性,以便 `Loader` 知道如何将数组数据映射到属性。
创建自定义属性
我开始着手处理这个想法,从列表的最后开始。首先,我需要一个可以使用的 .NET 属性。如果您从未用过自定义属性,它们非常棒,尽管它们几乎总是会导致使用反射。我认为许多开发人员出于某种原因(因为您在许多可以让生活更轻松的场景中看不到它)被反射吓倒了,这很可惜。反射实际上非常直接,所以请确保它是您工具箱的一部分。要创建自定义属性,您只需要定义一个继承自 `System.Attribute` 的类,添加一些公共字段,至少一个构造函数,然后就可以开始工作了。这是我在项目中声明的属性:
[AttributeUsage(AttributeTargets.Property)]
public class CSVPositionAttribute : System.Attribute
{
public int Position;
public CSVPositionAttribute(int position)
{
Position = position;
}
public CSVPositionAttribute()
{
}
}
在这种情况下,用户需要在属性作为一部分提供 `Position` 值。关于此属性要注意的另一件事是类声明上方使用的 `[AttributeUsage(AttributeTargets.Property)]` 属性。此属性声明我的自定义属性只能分配给类的属性,而不能用于类本身、方法、字段等。
要使用这个自定义属性,我只需要做以下事情:
public class SomeClass
{
private int _age;
[CSVPosition(2)]
public int Age
{
get { return _age;}
set {_age = value;}
}
}
`[CSVPosittion]` 属性将 `Position` 字段设置为二。请注意,即使我们的自定义属性类名是 `CSVPositionAttribute`,在实际使用该属性来标记属性时,我也可以将其缩短为 `CSVPosition`(删除 `Attribute` 后缀)。这为我提供了一种简单的方法来标记要用从 CSV 文件行派生的数组中的信息加载的对象。
使用反射创建加载器
下一步是找到一种方法来弄清楚如何获取任意类,找出哪些属性要用 CSV 数据填充,并用该数据更新对象。为此,我将使用 .NET 反射。我开始创建一个名为 `Loader` 的新类,该类将(暂时)具有一个方法,如下所示:
public class ClassLoader
{
public static void Load(object target,
string[] fields, bool supressErrors)
{
}
}
`Load` 方法是一个静态方法,它接受要从 CSV 数据加载的任何目标对象、一个字符串数组(从 CSV 文件单行解析的数据)以及一个标志,指示在处理数据过程中遇到的错误是应被抑制还是不。一个快速说明是,我使用一个非常简单的方法来处理此演示中的错误。当然,有更丰富、更健壮的处理错误的方法,但我将其留给您,亲爱的读者,根据需要来实现。
我需要做的第一件事是评估传入对象的所有可用属性,并检查这些属性是否具有 `CSVPosition` 属性。使用反射可以非常轻松地获取对象属性列表:
Type targetType = target.GetType();
PropertyInfo[] properties = targetType.GetProperties();
然后,我可以遍历 `properties` 数组,并使用 `PropertyInfo` 对象来确定是否需要使用 CSV 字段数组中的数据来加载给定属性。
foreach (PropertyInfo property in properties)
{
// Make sure the property is writeable (has a Set operation)
if (property.CanWrite)
{
// find CSVPosition attributes assigned to the current property
object[] attributes =
property.GetCustomAttributes(typeof(CSVPositionAttribute),
false);
// if Length is greater than 0 we have
// at least one CSVPositionAttribute
if (attributes.Length > 0)
{
// We will only process the first CSVPositionAttribute
CSVPositionAttribute positionAttr =
(CSVPositionAttribute)attributes[0];
//Retrieve the postion value from the CSVPositionAttribute
int position = positionAttr.Position;
try
{
// get the CSV data to be manipulate
// and written to object
object data = fields[position];
// set the value on our target object with the data
property.SetValue(target,
Convert.ChangeType(data, property.PropertyType), null);
}
catch
{
// simple error handling
if (!supressErrors)
throw;
}
}
}
}
您应该能够通过阅读上面的注释来理解发生了什么。基本上,我们检查每个属性以查看是否可以写入它,如果可以,我们就检查它是否具有 `CSVPosition` 属性。如果确实如此,我们就会检索位置值,从字段数组中提取相应的字符串,然后设置该属性的值。这一切都相当直接。需要注意的一件事是,理论上某人可以为给定的属性分配多个 `CSVPosition` 属性。但是,代码的编写方式只使用第一个 `CSVPosition` 属性。
实现数据转换
您可能还会想知道为什么在我们的 `Load` 例程中使用了以下代码行:
// get the CSV data to be manipulate and written to object
object data = fields[position];
我们不能同样容易地将 `fields[position]` 数据元素直接传递给 `SetValue` 方法吗?当然可以。但是,那一行引导我们研究了我想要解决的下一个问题。那就是,如果传入的字符串值需要被处理或格式化,以便其默认状态可以按原样使用,会发生什么?例如,可能有一个值“One”,我们想将其分配给一个整数值,或者我们可能想在将某个字符串分配给目标属性之前以某种方式格式化它。我们希望能够指向 `Load` 例程,指向一个特殊的数据转换例程,这个例程很可能对每个属性都不同。我们该怎么做?
再一次,.NET 反射将提供帮助。使用 .NET 反射,我们可以动态地调用给定对象上的方法,即使我们在设计时不知道这些方法的名称。所以,问题很快就变成了——我们如何让我们的处理例程知道
- 某个属性在分配 CSV 数据之前需要使用特殊的数据转换,并且
- 该转换方法的名称?
我们将通过扩展我们的 `CSVPosition` 属性并修改我们的 `Load` 方法来解决这两个问题。
我们新的 `CSVPositionAttribute` 类现在看起来像这样:
[AttributeUsage(AttributeTargets.Property)]
public class CSVPositionAttribute : System.Attribute
{
public int Position;
public string DataTransform = string.Empty;
public CSVPositionAttribute(int position,
string dataTransform)
{
Position = position;
DataTransform = dataTransform;
}
public CSVPositionAttribute(int position)
{
Position = position;
}
public CSVPositionAttribute()
{
}
}
如您所见,我们所做的只是添加了一个名为 `DataTransform` 的新公共字段。该字段将保存同一类上另一个方法的名称,该方法将用作数据转换例程。也许也有使用委托来实现这一点的方法,但我还没有找到。所以,通过我的直接方法,我们现在可以修改我们的 `Load` 例程,使其看起来像:
try
{
// get the CSV data to be manipulate and written to object
object data = fields[position];
// check for a Tranform operation that needs to be executed
if (positionAttr.DataTransform != string.Empty)
{
// Get a MethodInfo object pointing to the method declared by the
// DataTransform property on our CSVPosition attribute
MethodInfo method = targetType.GetMethod(positionAttr.DataTransform);
// Invoke the DataTransform method and get the newly formated data
data = method.Invoke(target, new object[] { data });
}
// set the ue on our target object with the data
property.SetValue(target, Convert.ChangeType(data,
property.PropertyType), null);
}
代码现在检查 `DataTrasform` 值,如果存在,则通过反射调用该方法,并将返回的数据传递给目标属性。我假设任何可能使用的转换例程都是正在更新其属性的同一对象上的方法。这似乎是合理的,因为对象应该负责控制其数据的格式。
我做的最后一件事是向我的 `Loader` 类添加一个额外的方法:
public static X LoadNew<X>(string[] fields, bool supressErrors)
{
// Create a new object of type X
X tempObj = (X) Activator.CreateInstance(typeof(X));
// Load that object with CSV data
Load(tempObj, fields, supressErrors );
// return the new instanace of the object
return tempObj;
}
Using the Code
这是如何使用此代码的简要示例。
我有一个 `Customer` 类,我想用一些 CSV 数据来填充它。`Customer` 类已按如下方式标记:
class Customer
{
private string _name;
private string _title;
private int _age;
private DateTime _birthDay;
[CSVPosition(2)]
public string Name
{
get { return _name; }
set { _name = value; }
}
[CSVPosition(0,"TitleFormat")]
public string Title
{
get { return _title; }
set { _title = value; }
}
[CSVPosition(1)]
public int Age
{
get { return _age; }
set { _age = value; }
}
[CSVPosition(3)]
public DateTime BirthDay
{
get { return _birthDay; }
set { _birthDay = value; }
}
public Customer()
{
}
public string TitleFormat(string data)
{
return data.Trim().ToUpper();
}
public override string ToString()
{
return "Customer object [" + _name + " - " +
_title + " - " + _age + " - " + _birthDay + "]";
}
}
使用 `Loader` 类填充此类的实例可以通过两种方式之一进行。首先,我们可以实例化我们的类的一个实例并将其传递给 `Loader` 进行填充。或者,我们可以使用我们 `Loader` 类上的 `LoadNew` 方法,并让它自己返回一个已填充的对象。下面展示了这两种示例:
static void Main(string[] args)
{
string[] fields = { " Manager", "38", "John Doe", "4/1/68" };
Customer customer1 = new Customer();
ClassLoader.Load(customer1, fields, true);
Console.WriteLine(customer1.ToString());
Customer customer2 = ClassLoader.LoadNew<Customer>(fields,false);
Console.WriteLine(customer2.ToString());
Console.ReadLine();
}
就这样。希望有所帮助,祝您编码愉快。