65.9K
CodeProject 正在变化。 阅读更多。
Home

类型安全(但通用)导入“char”分隔的基于行的文件到对象中

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.73/5 (8投票s)

2008年3月3日

CPOL

7分钟阅读

viewsIcon

26388

downloadIcon

117

一篇关于使用反射将基于文本的数据导入对象的通用方法的文章。支持海量数据处理、过滤、高级处理和转换以及一些其他技巧。

介绍  

我又一次要完成一项烦人的工作——将基于文本的数据导入对象。数据集不适用,所以我决定使用 .net 反射并构建一个对象列表。导入器应处理用户定义的过滤器、类型转换和拆分机制,当然还有不同的编码文件。另一方面,我希望有一个“现成可用”的解决方案,而无需设置大量属性。我的目标是有一个这样的机制: 

public class ImportDemo
{
    public string EmployeeName;
    public DateTime HiredSince;
    public Double Salary;
}

...
...

var importer = new GenericTextImporter<ImportDemo>();
importer.ParseFile("myFile", out List<ImportDemo> dataList, bool hasBeenCanceled);

背景 

这里有两点需要说明

  • 代码并不关注速度。更重要的是该机制是否可以尽可能轻松地被其他人用于构建导入任务。但是,您可以在生产环境中使用它,它并不算太慢。
  • 我的母语是德语……不是英语 :-)

幕后花絮...

好的,现在,我们来看一些细节。解析器非常直接:读取一行,使用 string.Split() 进行拆分,然后逐列转换为类中成员的匹配类型。

首先,我们必须看一下类声明中的一个条件

public class GenericTextImporter<T>
    where T: class 
{
  ...
}

这确保了我们可以使用对象创建 GenericTextImporter 的实例。我们不允许值类型在这里。此条件是必须的;否则,我们将无法执行像 T item = null 这样的赋值,因为编译器将无法确定它是值类型还是对象类型(并将给我们一个错误消息)。

using (StreamReader streamReader = new StreamReader(fileName, _fileEncoding));

上面打开了要读取的文件。读取器使我们能够通过 streamReader.ReadLine() 逐行获取内容。属性 EnCsvImportColumnAssignMethod.ColumnAssignMethod 允许更改文本列与对象成员之间的映射。目前,只实现了 EnCsvImportColumnAssignMethod.FieldSequenceDefinesOrder,但添加映射并不难。

正如通常那样,文件将在循环中读取。对于解析器获得的每一行,它必须执行几个步骤

  • 将行拆分成几部分(列)。
  • 创建要填充的对象实例。
  • 将字符串转换为成员的类型并赋值。

使用 string.Split() 函数进行拆分——默认分隔符是逗号,但您可以使用任何您想要的。还支持拆分字符数组。在某些情况下,这已足够,但有时您需要更多控制。本文稍后将对此进行解释。

创建类实例也很容易

T item = Activator.CreateInstance<T>;

Activator 创建了一个 T 的实例……记住,这是我们在创建类时使用的类型。

那么,最后一件事是——如何分配成员值。为了获取目标类的相关信息,解析器包含一个重要的成员

FieldInfo[] _fieldInfo = typeof(T).GetFields(BindingFlags.NonPublic | 
            BindingFlags.Instance | BindingFlags.Public);

这将生成我们稍后需要的所有信息。根据 ColumnAssignMethod,解析器会调用一个赋值方法;在我们的例子中是 AssignColumnValuesAnonymous。 

private bool AssignColumnValuesAnonymous(string[] values, T item)
{
	var result = true;

	for (var i = 0; i < values.Length; i++)
	{
	    result = i < _fieldInfo.Length && AssignFieldValue(_fieldInfo[i], values[i], item);

	    if (!result) break;
	}

	return result;    
}    

代码循环遍历所有值并将每个值分配给实例的成员。

private bool AssignFieldValue(FieldInfo fieldInfo, string value, T item)
{
    try
    {
	switch (fieldInfo.FieldType.FullName)
	{
	    case "System.Net.IPAddress":
	        fieldInfo.SetValue(item, ConvertIPAddress == null ? 
                    System.Net.IPAddress.Parse(value) : 
                    ConvertIPAddress(fieldInfo, value));
	    break;
			.....
        
    }
}

FieldInfo 包含一些非常有趣的方法。fieldInfo.SetValue(destinationObject, value) 允许我们设置值(您可以使用 GetValue() 读取它们)。记住:我们在类开始时用以下代码构建字段信息

FieldInfo[] _fieldInfo = typeof(T).GetFields(..);

我们现在要做的就是将字符串值(代表行中的列值)转换为字段类型并赋值。大多数 .NET 类型都提供一个可以使用的 .Parse() 方法。

解析器已添加了最常见的类型转换。但是,在某些情况下,内置转换会失败。本文稍后将讨论此主题。

用户定义的拆分器

如前所述,将使用 string.Split() 方法来获取行的片段。但现在,让我们想象一下以下场景:分隔符字符是逗号(默认),我们想导入以下行

"Mustermann, Max", 2008-01-01, 1000

哎呀……发生了什么……解析器将检测到四列(按逗号分隔),但我们的类只有三个成员。要解决这个问题,有一个回调函数被定义

public delegate string[] LineSplitterDelegate(string aLine);
public event LineSplitterDelegate LineSplitter;

这使您能够挂钩解析器并定义自己的拆分算法。在这种情况下,我们需要编写一小段代码来引用字符串中的逗号。挂钩会返回一个拆分值的字符串数组。

用户定义的类型转换

现在,我们将更深入地研究类型转换。通常,解析器会调用类型的内置转换,而此方法通常命名为 type.Parse(string)。在大多数情况下,这已足够,但让我们看看以下场景

public class ImportDemo2
{
    public string EmployeeName;
    public DateTime HiredSince;
    public Double Salary;
    public bool HasCompanyStocks;
} 

我们想导入以下行

"Mustermann, Max", 2008-01-01, 1000, JA

德语的“JA”(是的)无法解析并转换为布尔值(在本例中是“true”)。

public delegate System.Boolean 
       ConvertBooleanDelegate(FieldInfo fieldInfo, string value);
public event ConvertBooleanDelegate ConvertBoolean;

使用此挂钩,我们可以轻松添加一个执行我们想要的操作的小转换器。

importer.ConvertBoolean += myBooleanConverter;

...

private bool myBooleanConverter(FieldInfo fieldInfo, string value)
{
    // In this simple example we do not check which string field is meant
    switch (value.ToLower())
    {
        case 'ja':
        case 'yes':
        case 'wahr':
        case 'true':
            return true;
        default:
            return false;
    }
}

每次解析器尝试转换布尔值时,都会调用此函数。

目前支持以下类型

  • System.Net.IPAddress
  • System.String
  • System.Char
  • System.Int16
  • System.Int32
  • System.Int64
  • System.UInt16
  • System.UInt32
  • System.UInt64
  • System.Decimal
  • System.Double
  • System.DateTime
  • System.TimeSpan
  • System.Guid
  • System.Boolean

如果您查看此列表,您可能会想为什么会实现字符串转换挂钩。答案很简单:这允许您实现高级值处理。让我们以上面的例子为例

"Mustermann, Max", 2008-01-01, 1000, JA

目前,第一列包含一个姓名字段,顺序为“姓,名”,并且包含“”,但您导入的值应该是“名-姓”的顺序,没有逗号和引号。只需分配挂钩事件即可添加您自己的转换器,就是这样。

importer.ConvertString += myStringConverter;

...

private bool myStringConverter(FieldInfo fieldInfo, string value)
{
    // Check whether we inspect the correct field
    if (fieldInfo.Name != "EmployeeName") return value;
    
    // no error handling, no performance optimization - just an example :-)
    string[] values = value.Trim(new char[]{ '"' }).Split(new char[]{ '"' });
    
    return values[1] + " " + values[0];
}

用户定义的过滤器 

这与上面提到的挂钩类似。首先,这是声明

public delegate bool ItemFilterDelegate(T item);
public event ItemFilterDelegate ItemFilter;

要将项添加到列表中,只需返回 true。在过滤器挂钩内部,您可以做任何您想做的事情。请注意,仅当未定义 ItemProcessor(请参阅下一章)时,才会调用过滤器。 

单个项处理

有时,我不得不导入一个长列表。问题不在于耗时,而在于内存。所以,我实现了一个可变开关

public delegate void ItemProcessorDelegate(T item, out bool cancel);
public event ItemProcessorDelegate ItemProcessor; 

这允许您使用单个项。处理完它(在您的例程中)后,它将进入 .NET 的垃圾回收 (GC) 状态。为了避免永久创建/删除项,您可以定义

public delegate void ItemProcessorResetDelegate(T item);
public event ItemProcessorResetDelegate ItemReset; 

在这种情况下,解析器使用相同的对象实例,但由您负责清理值。

错误处理

解析器返回 true/false 以通知调用者是否成功。如果发生错误,您可以查看 LastError 以了解出了什么问题。

在类型转换错误的情况下,您可以为此属性影响行为

public enum EnErrorBehaviour
{
    /// <summary />
    /// Will add the element also if not all values have been assigned
    /// </summary />
    Ignore,
    /// <summary />
    /// Stops parsing input file
    /// </summary />
    StopParsing,
    /// <summary />
    /// Skips element, will increase RejectedLines counter
    /// </summary />
    SkipElement
}

解析完成后,您应该检查(根据您的设置)属性 RejectedLines 是否具有非零值。

结论

当然,反射比我在这段小型软件中所用的要多得多。但是,也许您已经了解了如何轻松利用有关类型的信息来使标准任务更简单、工作量更少。希望您喜欢这篇文章。请随时在论坛上发言,或给我发电子邮件。

关注点

反射很简单。 

在未来的版本中,列/字段映射将得到增强(通过属性)。目前,成员和列的顺序是相同的。

历史

  • 2012-06-23 - 重写部分代码,将导入器放入自己的程序集中,支持 c# 4.0,更新了文本
  • 2008-09-16 - 更新,希望更易读。修复了一些小的文本错误。
  • 2008-03-04 - 更新,解决了代码片段中显示 < 和 > 的问题。
  • 2008-03-03 - 更新,添加了示例,正式发布。
  • 2008-02-12 - 初始版本。
© . All rights reserved.