用String []填充struct?

时间:2012-08-17 15:55:16

标签: c#

我正在解析CSV文件并将数据放在结构中。我正在使用this question中的TextFieldParser,它的工作方式就像一个魅力,但它返回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是int。令我很生气的是,他们并没有强力打字,但现在就让我们看看。鉴于parser.ReadFields()返回String[],有一种更有效的方法来填充结构(可能会转换某些值,例如row[0]需要成为int)数组?

**编辑:**一个限制我忘了提及可能影响什么样的解决方案将起作用,这个结构是[Serializable]并将在其他地方发送Tcp。 / p>

6 个答案:

答案 0 :(得分: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的{​​{1}}值与属性相关联。

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

Index

然后可以使用结构类型的Type对象设置值(您可以使用[ArrayStructField(1)] public string WorkOrder { // ... [ArrayStructField(19)] public string City { // ... 运算符获取它):

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); } } } } string,则会从相应的数组索引中复制该值。

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

请注意,要处理更多类型,我建议不要使用intif链,而应使用else if将属性类型映射到转换函数

答案 1 :(得分: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时应该使用它:

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反映一次,然后你可以在每一行上应用。这是可能的,但不是特别容易。

答案 2 :(得分:1)

您对使用的图书馆有多依赖?我发现File Helpers对于这类事情非常有用。您的代码看起来像:

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数组,引擎为你做了所有繁重的工作。

答案 3 :(得分:0)

在评论中使用@Grozz建议使用反射。使用属性(即[ColumnOrdinal])标记struct类的每个属性,然后使用此属性将信息映射到正确的列。如果你有双倍,十进制等作为目标,你还应该考虑使用Convert.ChangeType来正确转换目标类型。如果你对表演不满意,你可以尽情创造一个DynamicMethod,更具挑战性,但真的很高效。挑战在于将IL指令写入内存以执行您手动执行的“管道”(我通常会创建一些示例代码,然后以IL spy作为起点查看它内部)。当然,你会缓存某些动态方法,所以只需要创建一次。

答案 4 :(得分: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将保持struct字段的顺序,因此您可以迭代它们,而无需为每个字段显式指定列号。如果您有很多字段,这可以节省一些维护。

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

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会包含一个包含字段名称的行,这样可以更轻松。

OR,另一种方法,使用StructLayout和动态方法来避免必须保留字段:column_index映射除了struct本身。

或者,创建一个枚举

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.
}

答案 5 :(得分:0)

如果你想要速度,你可以做一个脆弱的转换声明。

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不允许您一次读取一个字段,然后您可以避免构建和索引列数组。