6

我正在解析一个 CSV 文件并将数据放在一个结构中。我正在使用TextFieldParserfrom this question,它的工作原理就像一个魅力,只是它返回一个String[]. 目前我有一个丑陋的过程:

String[] row = parser.ReadFields();
DispatchCall call = new DispatchCall();
if (!int.TryParse(row[0], out call.AccountID)) {
    Console.WriteLine("Invalid Row: " + parser.LineNumber);
    continue;
}
call.WorkOrder = row[1];
call.Description = row[2];
call.Date = row[3];
call.RequestedDate = row[4];
call.EstStartDate = row[5];
call.CustomerID = row[6];
call.CustomerName = row[7];
call.Caller = row[8];
call.EquipmentID = row[9];
call.Item = row[10];
call.TerritoryDesc = row[11];
call.Technician = row[12];
call.BillCode = row[13];
call.CallType = row[14];
call.Priority = row[15];
call.Status = row[16];
call.Comment = row[17];
call.Street = row[18];
call.City = row[19];
call.State = row[20];
call.Zip = row[21];
call.EquipRemarks = row[22];
call.Contact = row[23];
call.ContactPhone = row[24];
call.Lat = row[25];
call.Lon = row[26];
call.FlagColor = row[27];
call.TextColor = row[28];
call.MarkerName = row[29];

该结构由所有这些字段组成,String除了 AccountID 是 s 之外int。它们不是强类型的,这让我很恼火,但现在让我们看一下。鉴于parser.ReadFields()返回 a是否有更有效的方法来用数组中的值String[]填充结构(可能转换一些值,例如row[0]需要成为 a )?int

**编辑:**我忘记提到的一个限制可能会影响什么样的解决方案将起作用,这个结构是[Serializable]并且将被发送到其他地方 Tcp。

4

6 回答 6

7

您的里程可能会因它是否是更好的解决方案而异,但您可以使用反射并定义一个Attribute用于标记结构成员的类。该属性将数组索引作为参数。然后通过使用反射从正确的数组元素中分配值。

你可以像这样定义你的属性:

[AttributeUsage(AttributeTargets.Property)]
public sealed class ArrayStructFieldAttribute : Attribute
{
    public ArrayStructFieldAttribute(int index)
    {
        this.index = index;
    }

    private readonly int index;

    public int Index {
        get {
            return index;
        }
    }
}

这意味着属性可以简单地用于将int命名的值Index与属性相关联。

然后,您可以使用该属性在结构中标记您的属性(只是一些示例性行):

[ArrayStructField(1)]
public string WorkOrder { // ...

[ArrayStructField(19)]
public string City { // ...

然后可以使用Type您的结构类型的对象设置值(您可以使用typeof运算符获取它):

foreach (PropertyInfo prop in structType.GetProperties()) {
    ArrayStructFieldAttribute attr = prop.GetCustomAttributes(typeof(ArrayStructFieldAttribute), false).Cast<ArrayStructFieldAttribute>().FirstOrDefault();
    if (attr != null) {
         // we have found a property that you want to load from an array element!
        if (prop.PropertyType == typeof(string)) {
            // the property is a string property, no conversion required
            prop.SetValue(boxedStruct, row[attr.Index]);
        } else if (prop.PropertyType == typeof(int)) {
            // the property is an int property, conversion required
            int value;
            if (!int.TryParse(row[attr.Index], out value)) {
                Console.WriteLine("Invalid Row: " + parser.LineNumber);
            } else {
                prop.SetValue(boxedStruct, value);
            }
        }
    }
}

此代码遍历结构类型的所有属性。对于每个属性,它都会检查我们上面定义的自定义属性类型。如果存在这样的属性,并且属性类型为stringint,则从相应的数组索引中复制该值。

我正在检查stringint属性,因为这是您在问题中提到的两种数据类型。即使您现在只有一个包含int值的特定索引,如果此代码准备将任何索引作为字符串或 int 属性处理,这对可维护性很有好处。

请注意,对于要处理的更多类型,我建议不要使用 and 链ifelse if而是使用 aDictionary<Type, Func<string, object>>将属性类型映射到转换函数。

于 2012-08-17T16:00:04.350 回答
1

如果您想创建一些非常灵活的东西,您可以DispatchCall使用自定义属性标记每个属性。像这样的东西:

class DispatchCall {

  [CsvColumn(0)]
  public Int32 AccountId { get; set; }

  [CsvColumn(1)]
  public String WorkOrder { get; set; }

  [CsvColumn(3, Format = "yyyy-MM-dd")]
  public DateTime Date { get; set; }

}

这允许您将每个属性与一列相关联。对于每一行,您可以遍历所有属性,并通过使用属性,您可以将正确的值分配给正确的属性。你必须做一些从字符串到数字、日期甚至枚举的类型转换。您可以向属性添加额外的属性以帮助您完成该过程。在我发明的示例Format中,应该在DateTime解析 a 时使用:

Object ParseValue(String value, TargetType targetType, String format) {
  if (targetType == typeof(String))
    return value;
  if (targetType == typeof(Int32))
    return Int32.Parse(value);
  if (targetType == typeof(DateTime))
   DateTime.ParseExact(value, format, CultureInfo.InvariantCulture);
  ...
}

使用TryParse上述代码中的方法可以通过在遇到不可解析的值时提供更多上下文来改进错误处理。

不幸的是,这种方法效率不高,因为反射代码将针对输入文件中的每一行执行。如果您想提高效率,您需要通过反射一次来动态创建编译方法,DispatchCall然后您可以将其应用于每一行。这是可能的,但不是特别容易。

于 2012-08-17T16:11:31.873 回答
1

您对所使用的库的依赖程度如何?我发现文件助手对这类事情非常有用。您的代码将类似于:

using FileHelpers;

// ...

[DelimitedRecord(",")]
class DispatchCall {
    // Just make sure these are in order
    public int AccountID { get; set; }
    public string WorkOrder { get; set; }
    public string Description { get; set; }
    // ...
}

// And then to call the code
var engine = new FileHelperEngine(typeof(DispatchCall));
engine.Options.IgnoreFirstLines = 1; // If you have a header row
DispatchCall[] data = engine.ReadFile(FileName) as DispatchCall[];

您现在有了一个 DispatchCall 数组,引擎为您完成了所有繁重的工作。

于 2012-08-17T16:25:49.247 回答
0

使用评论中建议的@Grozz 反射。用属性(即 )标记结构类的每个属性,[ColumnOrdinal]然后使用它来将信息映射到正确的列。如果你有double、decimal等作为目标,你还应该考虑Convert.ChangeType在目标类型中使用正确的转换。如果您对性能不满意,您可以享受动态创建DynamicMethod的乐趣,更具挑战性,但性能非常出色且美观。面临的挑战是在内存中编写 IL 指令来执行您手动执行的“管道”(我通常创建一些示例代码,然后以 IL spy 为起点查看它的内部)。当然,您将在某处缓存此类动态方法,因此只需一次请求创建它们。

于 2012-08-17T16:01:24.680 回答
0

首先想到的是使用反射来迭代属性并string[]根据属性值将它们与元素匹配。

public struct DispatchCall
{
  [MyAttribute(CsvIndex = 1)]
  public string WorkOrder { get; set; }
}

MyAttribute将只是一个自定义属性,其索引与 CSV 中的字段位置匹配。

var row = parser.ReadFields(); 

    for each property that has MyAttribute...
      var indexAttrib = MyAttribute attached to property
      property.Value = row[indexAttrib.Index]
    next

(显然是伪代码)

或者

[StructLayout(LayoutKind.Sequential)] // keep fields in order
public strict DispatchCall
{
  public string WorkOrder;
  public string Description;  
}

StructLayout将保持结构字段按顺序排列,因此您可以迭代它们而无需为每个字段显式指定列号。如果您有很多字段,这可以节省一些维护。

或者,您可以完全跳过该过程,并将字段名称存储在字典中:

var index = new Dictionary<int, string>();

/// populate index with row index : field name values, preferable from some sort of config file or database
index[0] = "WorkOrder";
index[1] = "Description";
...

var values = new Dictionary<string,object>();

for(var i=0;i<row.Length;i++) 
{
  values.Add(index[i],row[i]);
}

这更容易加载,但并没有真正利用强类型,这使得它不太理想。

您还可以生成动态方法或 T4 模板。您可以从格式的映射文件生成代码

0,WorkOrder
1,Description
...

加载它,并生成一个如下所示的方法:

  /// emit this
  call.WorkOrder = row[0];
  call.Description = row[1];

等等

这种方法在一些浮动的微 ORM 中使用,并且似乎工作得很好。

理想情况下,您的 CSV 将包含一行字段名称,这将使这更容易。

或者,另一种方法是StructLayout与动态方法一起使用,以避免必须将 field:column_index 映射保留在结构本身之外。

或者,创建一个枚举

public enum FieldIndex
{
WorkOrder=0
,
Description // only have to specify explicit value for the first item in the enum
, /// ....
,
MAX /// useful for getting the maximum enum integer value
}

for(var i=0;i<FieldIndex.MAX;i++)
{
  var fieldName = ((FieldIndex)i).ToString(); /// get string enum name
  var value = row[i];

  // use reflection to find the property/field FIELDNAME, and set it's value to VALUE.
}
于 2012-08-17T16:07:24.610 回答
0

如果你追求速度,你可以使用一个脆弱的 switch 语句。

var columns = parser.ReadFields();

for (var i = 0; i < columns.Length; i++)
{
    SetValue(call, i, columns[i]);
}

private static void SetValue(DispatchCall call, int column, string value)
{
    switch column
    {
        case 0:
            SetValue(ref call.AccountId, (value) => int.Parse, value);
            return;

        case 1:
            SetValue(ref call.WorkOrder, (value) => value, value);
            return;

        ...

        default:
            throw new UnexpectedColumnException();
    }      
}

private static void SetValue<T>(
    ref T property,
    Func<string, T> setter
    value string)
{
    property = setter(value);
}

遗憾的是TextFieldParser不允许您一次读取一个字段,然后您可以避免构建和索引列数组。

于 2012-08-17T16:46:20.100 回答