2012-08-17 18 views
6

Ich analysiere eine CSV-Datei und platziere die Daten in einer Struktur. Ich benutze die TextFieldParser von this question und es funktioniert wie ein Charme, außer dass es eine String[] zurückgibt. Zur Zeit habe ich den hässlichen Prozess:Struktur mit String [] füllen?

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]; 

Die Struktur all diese Felder sind String s besteht mit Ausnahme AccountID ein int zu sein. Es ärgert mich, dass sie nicht stark typisiert sind, aber lassen Sie uns das jetzt überblicken. Vorausgesetzt, dass parser.ReadFields() gibt String[] gibt es eine effizientere Möglichkeit, eine Struktur (möglicherweise einige Werte wie row[0] muss ein int werden) mit den Werten im Array zu füllen?

** EDIT: ** Eine Einschränkung ich vergaß zu erwähnen, dass kann Auswirkungen, welche Art von Lösungen funktionieren wird, ist, dass diese Struktur [Serializable] und wird Tcp woanders geschickt werden.

+1

Verwenden Sie Reflexion. – Grozz

+0

Reflexion wäre auf jeden Fall weniger effizient, würde ich nur damit leben, wie es ist – RobJohnson

+0

CsvHelper könnte Ihnen sehr hilfreich sein https://github.com/JoshClose/CsvHelper/wiki/Basics – KeesDijk

Antwort

7

Ihr Kilometerstand kann davon abweichen, ob es eine bessere Lösung ist, aber Sie könnten reflection verwenden und eine Attribute Klasse definieren, mit der Sie Ihre Strukturmitglieder kennzeichnen. Das Attribut würde den Array-Index als Argument annehmen. Die Zuweisung des Wertes aus dem richtigen Array-Element würde dann durch Verwendung von Reflektion erfolgen.

Sie könnten Ihr Attribut wie folgt definieren:

[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; 
     } 
    } 
} 

Dies bedeutet, dass das Attribut kann einfach ein int Wert Index mit einer Eigenschaft mit dem Namen zuzuordnen verwendet werden.

Dann könnten Sie Ihre Eigenschaften in der Struktur mit diesem Attribute (nur einige beispielhaften Linien) markieren:

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

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

Die Werte dann mit dem Type Objekt für Ihren Strukturtyp festgelegt werden können (Sie können es erhalten mit dem typeof Operator):

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); 
      } 
     } 
    } 
} 

Dieser Code iteriert über alle Eigenschaften Ihres Strukturtyp. Für jede Eigenschaft wird nach dem oben definierten benutzerdefinierten Attributtyp gesucht. Wenn ein solches Attribut vorhanden ist und der Eigenschaftstyp string oder int lautet, wird der Wert aus dem entsprechenden Array-Index kopiert.

Ich suche nach string und int Eigenschaften als das ist die zwei Datentypen, die Sie in Ihrer Frage erwähnt haben. Auch wenn Sie nur einen bestimmten Index haben, der jetzt einen Wert int enthält, ist es gut für Wartbarkeit, wenn dieser Code bereit ist, jeden Index als eine Zeichenfolge oder eine Int-Eigenschaft zu behandeln.

Beachten Sie, dass für eine größere Anzahl von Typen zu verwenden, würde ich vorschlagen, keine Kette von if und else if, sondern eine Dictionary<Type, Func<string, object>>, die Eigenschaftstypen Konvertierungsfunktionen zuordnen.

0

Verwenden Sie Reflektion als @Grozz im Kommentar vorgeschlagen. Markieren Sie jede Eigenschaft der Strukturklasse mit einem Attribut (z. B. [ColumnOrdinal]) und verwenden Sie diese dann, um die Informationen mit der richtigen Spalte abzubilden. Wenn Sie double, decimal usw. als Ziel verwenden, sollten Sie auch die Verwendung von Convert.ChangeType zur korrekten Konvertierung im Zieltyp in Betracht ziehen.Wenn Sie nicht mit den Leistungen zufrieden sind, können Sie genießen, eine DynamicMethod on the fly, schwieriger, aber wirklich performant und schön zu schaffen. Die Herausforderung besteht darin, den IL-Befehl in den Speicher zu schreiben, um die von Ihnen vorgenommene "Installation" auszuführen (ich erstelle normalerweise einen Beispielcode und betrachte ihn dann mit IL-Spion als Ausgangspunkt). Natürlich werden Sie irgendwo solche dynamischen Methoden zwischenspeichern, so dass sie nur einmal erstellt werden müssen.

0

Die erste Sache, die in den Sinn kommt, ist die Verwendung von Reflektion, um über die Eigenschaften zu iterieren und sie mit den Elementen in string[] basierend auf einem Attributwert abzugleichen.

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

MyAttribute würde nur ein benutzerdefiniertes Attribut mit einem Index sein, der in der CSV in die Feldposition zusammenpassen würde.

var row = parser.ReadFields(); 

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

(Pseudocode, natürlich)

oder

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

StructLayout wird die Struktur Felder in Ordnung halten, so dass man über sie laufen kann, ohne explizit eine Spaltennummer für jedes Feld angeben, mit . Das kann einige Wartung sparen, wenn Sie viele Felder haben.

Oder könnten Sie den Prozess vollständig überspringen, und speichern Sie die Feldnamen in einem Wörterbuch:

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]); 
} 

, die einfacher zu laden, ist aber nicht wirklich nutzen starke Typisierung nehmen, die diese weniger macht als Ideal.

Sie können auch eine dynamische Methode oder eine T4-Vorlage generieren. Sie könnten Code aus einer Zuordnungsdatei im

0,WorkOrder 
1,Description 
... 

Last, die, Format erzeugen und ein Verfahren zu erzeugen, das wie folgt aussieht:

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

etc.

Dieser Ansatz in einigen verwendet Mikro-ORMs herumschweben und scheint ziemlich gut zu funktionieren.

Im Idealfall enthält Ihre CSV-Datei eine Zeile mit Feldnamen, die dies erheblich vereinfachen würden.

ODER, noch ein anderer Ansatz, verwenden Sie StructLayout zusammen mit einer dynamischen Methode, um zu vermeiden, ein Feld zu behalten: column_index Mapping neben der Struktur selbst.

OR, erstellen eine ENUM

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

Wenn Sie etwas sehr flexibel erstellen möchten, können Sie jede Eigenschaft auf DispatchCall markieren ein benutzerdefiniertes Attribut verwenden. Etwas wie dieses:

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

} 

Dies ermöglicht Ihnen, jede Eigenschaft mit einer Spalte zu verknüpfen. Für jede Zeile können Sie dann über alle Eigenschaften iterieren und mit dem Attribut können Sie der richtigen Eigenschaft den richtigen Wert zuweisen. Sie müssen eine Art Konvertierung von String zu Zahlen, Daten und vielleicht Enums vornehmen. Sie können dem Attribut zusätzliche Eigenschaften hinzufügen, um Sie bei diesem Prozess zu unterstützen.Im Beispiel erfinden ich Format, die verwendet werden soll, wenn ein DateTime analysiert wird:

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 Methoden im obigen Code verwenden kann die Fehlerbehandlung verbessern, indem es Ihnen mehr Kontext zu schaffen, wenn ein unparsable Wert angetroffen wird.

Leider ist dieser Ansatz nicht sehr effizient, da der Reflexionscode für jede Zeile in Ihrer Eingabedatei ausgeführt wird. Wenn Sie dies effizienter machen möchten, müssen Sie eine kompilierte Methode dynamisch erstellen, indem Sie einmal über reflektieren, die Sie dann für jede Zeile anwenden können. Es ist möglich, aber nicht besonders einfach.

1

Wie abhängig sind Sie von der Bibliothek, die Sie verwenden? Ich habe File Helpers gefunden, um für diese Art von Sache ziemlich nützlich zu sein. Ihr Code würde in etwa so aussehen:

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[]; 

Sie haben nun eine DispatchCall Array, und der Motor hat alle schweres Heben für Sie.

0

Wenn Sie für die Geschwindigkeit gehen, könnten Sie eine spröde Switch-Anweisung.

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); 
} 

schade Sein dass TextFieldParser Ihnen nicht erlauben zu einer Zeit, ein Feld zu lesen, vermeiden Sie könnten dann Gebäude und Indizierung der Spalten-Array.