2011-11-17 1 views
14

Questo è legato a una questione preliminare mia C# Generic List conversion to Class implementing List<T>utilizzando LINQ per creare un elenco <T> dove T: someClass <U>

ho il seguente codice:

public abstract class DataField 
{ 
    public string Name { get; set; } 
} 

public class DataField<T> : DataField 
{ 
    public T Value { get; set; } 
} 

public static List<DataField> ConvertXML(XMLDocument data) { 
    result = (from d in XDocument.Parse(data.OuterXML).Root.Decendendants() 
         select new DataField<string> 
         { 
          Name = d.Name.ToString(), 
          Value = d.Value 
         }).Cast<DataField>().ToList(); 
    return result; 
} 

Questo funziona però mi piacerebbe essere in grado di modificare la parte di selezione della query LINQ per essere qualcosa di simile:

select new DataField<[type defined in attribute of XML Element]> 
{ 
    Name = d.Name.ToString(), 
    Value = d.Value 
} 

è questo solo un approccio povero? È possibile? Eventuali suggerimenti?

+3

Mi sento come se non sapessi mai quale sottoclasse di DataField hai comunque, quindi perché non usare solo la classe dataField {nome stringa; valore dell'oggetto;} '? –

+0

Non voglio veramente fare riferimento a tutti i valori come oggetto. Se dovessi adottare un approccio simile, sembrerebbe più semplice usare solo la classe DataField {nome stringa; tipo di stringa; valore stringa}. –

+1

Certo, ma poi dovrai analizzare i tuoi dati ogni volta prima di usarlo. Se si pre-analizzano i dati negli oggetti e quindi li si memorizzano nel campo dati, è possibile riprodurli solo quando si è pronti per utilizzarli. Questo è ciò che ADO.NET fa con DataReader (sebbene DataReader ritardi l'esecuzione dell'analisi fino a quando non sono necessari i valori della colonna). Ad esempio, 'if (myField.Value è DateTime {/ * fai quella data * /}' o 'if (myField.Name ==" importantDateThing ") {var date = (DateTime) myField.Value;/* fai importanty data thing * /} ' –

risposta

8

Ecco una soluzione di lavoro: (e 'necessario specificare i nomi di tipo completo per il vostro attributo Type altrimenti è necessario configurare una mappatura in qualche modo. ..)

Ho usato la parola chiave dinamica, è possibile utilizzare la riflessione per impostare il valore, invece, se non si dispone di C# 4 ...

public static void Test() 
{ 
    string xmlData = "<root><Name1 Type=\"System.String\">Value1</Name1><Name2 Type=\"System.Int32\">324</Name2></root>"; 

    List<DataField> dataFieldList = DataField.ConvertXML(xmlData); 

    Debug.Assert(dataFieldList.Count == 2); 
    Debug.Assert(dataFieldList[0].GetType() == typeof(DataField<string>)); 
    Debug.Assert(dataFieldList[1].GetType() == typeof(DataField<int>)); 
} 

public abstract class DataField 
{ 
    public string Name { get; set; } 

    /// <summary> 
    /// Instanciate a generic DataField<T> given an XElement 
    /// </summary> 
    public static DataField CreateDataField(XElement element) 
    { 
     //Determine the type of element we deal with 
     string elementTypeName = element.Attribute("Type").Value; 
     Type elementType = Type.GetType(elementTypeName); 

     //Instanciate a new Generic element of type: DataField<T> 
     dynamic dataField = Activator.CreateInstance(typeof(DataField<>).MakeGenericType(elementType)); 
     dataField.Name = element.Name.ToString(); 

     //Convert the inner value to the target element type 
     dynamic value = Convert.ChangeType(element.Value, elementType); 

     //Set the value into DataField 
     dataField.Value = value; 

     return dataField; 
    } 

    /// <summary> 
    /// Take all the descendant of the root node and creates a DataField for each 
    /// </summary> 
    public static List<DataField> ConvertXML(string xmlData) 
    { 
     var result = (from d in XDocument.Parse(xmlData).Root.DescendantNodes().OfType<XElement>() 
         select CreateDataField(d)).ToList(); 

     return result; 
    } 
} 

public class DataField<T> : DataField 
{ 
    public T Value { get; set; } 
} 
4

Diverse istanze di una classe generica sono in realtà classi diverse.
I.e. DataField<string> e DataField<int> non sono della stessa classe (!)

Ciò significa che non è possibile definire il parametro generico durante l'esecuzione, poiché deve essere determinato durante la compilazione.

+1

Non è completamente giusto. È possibile creare un tipo generico in fase di runtime mediante reflection. Quello che dici è più simile a come si comportano i modelli C++. – svick

+0

Inoltre, DataField e DataField possono ancora ereditare dalla stessa classe genitore, e quindi possono implementare metodi astratti da chiamare tramite un riferimento di classe genitore. Questo è stato detto molto male, ma hai capito l'idea. –

5

Non è possibile farlo facilmente in C#. L'argomento di tipo generico deve essere specificato al momento della compilazione. È possibile utilizzare la reflection per fare altrimenti

   int X = 1; 
      Type listype = typeof(List<>); 
      Type constructed = listype.MakeGenericType( X.GetType() ); 
      object runtimeList = Activator.CreateInstance(constructed); 

Qui abbiamo appena creato un int List <>. Si può fare con il vostro tipo

3

è possibile creare tipo generico per riflessione

var instance = Activator.CreateInstance(typeof(DataField) 
         .MakeGenericType(Type.GetType(typeNameFromAttribute)); 
    // and here set properties also by reflection 
1

Non è impossibile, come si può fare questo con la riflessione. Ma questo non è il motivo per cui i generici sono stati progettati e non è il modo in cui deve essere eseguito il . Se avete intenzione di utilizzare la reflection per rendere il tipo generico, si può anche non utilizzare un tipo generico a tutti e basta usare la seguente classe:

public class DataField 
{ 
    public string Name { get; set; } 
    public object Value { get; set; } 
} 
4

direi che questo è un approccio povero. In realtà, anche dopo aver analizzato il tuo file XML, non saprai quali tipi di "DataField" hai. Potresti anche solo analizzarli come oggetti.

Tuttavia, se si sa che si sta sempre e solo andando ad avere x numero di tipi, si può fare in questo modo:

var Dictionary<string, Func<string, string, DataField>> myFactoryMaps = 
{ 
    {"Type1", (name, value) => { return new DataField<Type1>(name, Type1.Parse(value); } }, 
    {"Type2", (name, value) => { return new DataField<Type2>(name, Type2.Parse(value); } }, 
}; 
+0

Intelligente ... e terrò a mente questo. –

+0

Mai sottovalutare il potere dell'astrazione. :) –

1

sarà necessario inserire la logica per determinare il tipo di dati da il tuo XML e aggiungere tutti i tipi è necessario utilizzare, ma questo dovrebbe funzionare:

  result = (from d in XDocument.Parse(data.OuterXML).Root.Descendants() 
         let isString = true //Replace true with your logic to determine if it is a string. 
         let isInt = false //Replace false with your logic to determine if it is an integer. 
         let stringValue = isString ? (DataField)new DataField<string> 
         { 
          Name = d.Name.ToString(), 
          Value = d.Value 
         } : null 
         let intValue = isInt ? (DataField)new DataField<int> 
         { 
          Name = d.Name.ToString(), 
          Value = Int32.Parse(d.Value) 
         } : null 
         select stringValue ?? intValue).ToList(); 
4

risposta di Termit è certamente eccellente. Ecco una piccola variante.

 public abstract class DataField 
     { 
       public string Name { get; set; } 
     } 

     public class DataField<T> : DataField 
     { 
       public T Value { get; set; } 
       public Type GenericType { get { return this.Value.GetType(); } } 
     } 

     static Func<XElement , DataField> dfSelector = new Func<XElement , DataField>(e => 
     { 
       string strType = e.Attribute("type").Value; 
       //if you dont have an attribute type, you could call an extension method to figure out the type (with regex patterns) 
       //that would only work for struct 
       Type type = Type.GetType(strType); 
       dynamic df = Activator.CreateInstance(typeof(DataField<>).MakeGenericType(type)); 

       df.Name = e.Attribute("name").Value; 
       dynamic value = Convert.ChangeType(e.Value , type); 
       df.Value = value; 
       return df; 
     }); 

     public static List<DataField> ConvertXML(string xmlstring) 
     { 
       var result = XDocument.Parse(xmlstring) 
             .Root.Descendants("object") 
             .Select(dfSelector) 
             .ToList(); 
       return result; 
     } 


     static void Main(string[] args) 
     { 
       string xml = "<root><object name=\"im1\" type=\"System.String\">HelloWorld!</object><object name=\"im2\" type=\"System.Int32\">324</object></root>"; 

       List<DataField> dfs = ConvertXML(xml); 
     } 
2

@Termit e @Burnzy formulate buone soluzioni che coinvolgono factory methods.

Il problema è che si sta caricando la routine di analisi con un po 'di logica in più (più test, più errori) per rendimenti dubbi.

Un altro modo per farlo sarebbe utilizzare un DataField basato su stringhe semplificato con metodi di lettura tipizzati - la risposta migliore per la domanda this.

Un'implementazione di un metodo del valore tipizzato che sarebbe bello, ma funziona solo per i tipi di valore (che non include le stringhe, ma include DateTimes):

public T? TypedValue<T>() 
    where T : struct 
{ 
    try { return (T?) Convert.ChangeType(this.Value, typeof(T)); } 
    catch { return null; } 
} 


che sto supponendo che si' re vuole usare le informazioni sul tipo per fare cose come assegnare dinamicamente controlli utente al campo, regole di validazione, tipi SQL corretti per persistenza, ecc.

Ho fatto un sacco di questo genere di cose con approcci che sembrano un un po 'come il tuo

Alla fine della giornata dovresti separare i tuoi metadati dal codice - @ La risposta di Burnzy sceglie il codice basato sui metadati (un attributo "tipo" dell'elemento DataField) ed è un esempio molto semplice di questo.

Se si ha a che fare con XML, gli XSD sono una forma di metadati molto utile ed estensibile.

Per quanto riguarda ciò che si memorizzano i dati di ogni campo - uso stringhe perché:

  • sono annullabili
  • possono memorizzare i valori parziali
  • in grado di memorizzare i valori non validi (fa dicendo che l'utente Ordina il loro atto più trasparente)
  • possono memorizzare liste
  • casi speciali non invaderà il codice estraneo, perché non ce ne sono
  • imparare le espressioni regolari, convalidare, essere felice
  • è possibile convertirli in tipi più forti molto facilmente

ho trovato molto gratificante per sviluppare piccoli quadri come questo - si tratta di un'esperienza di apprendimento e si verrà fuori capire molto di più su UX e la realtà della modellazione da esso.

ci sono quattro gruppi di casi di test che vi consiglio di affrontare per primo:

  • date, orari, timestamp (quello che io chiamo DateTime), periodi (periodo)
    • in particolare, assicurati di testare una località server diversa da quella del cliente.
  • liste - selezione multipla chiavi esterne ecc
  • valori nulli
  • valido di ingresso - si tratta in generale mantenendo il valore originale

Utilizzando stringhe semplifica tutto questo molto, perché ti permette di chiarezza delimitare le responsabilità all'interno del tuo quadro. Pensa di creare campi contenenti elenchi nel tuo modello generico: diventa piuttosto peloso piuttosto rapidamente ed è facile finire con un caso speciale per gli elenchi in quasi tutti i metodi. Con gli archi, il dollaro si ferma lì.

Infine, se si desidera una solida implementazione di questo tipo di cose senza dover fare molto, si consideri lo DataSets - vecchia scuola che conosco - fanno ogni sorta di cose meravigliose che non ti aspetteresti ma devi RTFM .

Lo svantaggio principale di questa idea sarebbe che non è compatibile con l'associazione dei dati WPF, anche se la mia esperienza è stata che la realtà non è compatibile con l'associazione dei dati WPF.

spero ho interpretato le vostre intenzioni in modo corretto - buona fortuna in entrambi i casi :)

2

Purtroppo, non esiste alcuna relazione ereditarietà tra C<T> e C<string> per esempio. Tuttavia, è possibile ereditare da una classe non generica comune e oltre a implementare un'interfaccia generica. Qui utilizzo l'implementazione dell'interfaccia esplicita per poter dichiarare una proprietà Value digitata come oggetto, oltre a una proprietà Value specificatamente tipizzata. I valori sono di sola lettura e possono essere assegnati solo tramite un parametro del costruttore digitato. La mia costruzione non è perfetta, ma digita sicuro e non usa il riflesso.

public interface IValue<T> 
{ 
    T Value { get; } 
} 

public abstract class DataField 
{ 
    public DataField(string name, object value) 
    { 
     Name = name; 
     Value = value; 
    } 
    public string Name { get; private set; } 
    public object Value { get; private set; } 
} 

public class StringDataField : DataField, IValue<string> 
{ 
    public StringDataField(string name, string value) 
     : base(name, value) 
    { 
    } 

    string IValue<string>.Value 
    { 
     get { return (string)Value; } 
    } 
} 

public class IntDataField : DataField, IValue<int> 
{ 
    public IntDataField(string name, int value) 
     : base(name, value) 
    { 
    } 

    int IValue<int>.Value 
    { 
     get { return (int)Value; } 
    } 
} 

La lista può quindi essere dichiarata con la classe di base astratta DataField come parametro generico:

var list = new List<DataField>(); 
switch (fieldType) { 
    case "string": 
     list.Add(new StringDataField("Item", "Apple")); 
     break; 
    case "int": 
     list.Add(new IntDataField("Count", 12)); 
     break; 
} 

accedere al campo fortemente tipizzato tramite l'interfaccia:

public void ProcessDataField(DataField field) 
{ 
    var stringField = field as IValue<string>; 
    if (stringField != null) { 
     string s = stringField.Value; 
    } 
} 
2

Mentre le altre domande per lo più proposto una soluzione elegante per convertire i tuoi elementi XML in un'istanza di classe generica, ho intenzione di affrontare le conseguenze del prendere l'approccio per modellare il Classe DataField come generica come DataField < [tipo definito nell'attributo dell'elemento XML]>.

Dopo aver selezionato l'istanza DataField nell'elenco si desidera utilizzare questi campi. Il suo polimorfismo entra in gioco! Vuoi iterare i tuoi DataField e trattarli in modo uniforme. Le soluzioni che usano i generici spesso finiscono in uno strano switch/se orgia poiché non esiste un modo semplice per associare il comportamento basato sul tipo generico in C#.

Potreste aver visto il codice come questo (sto cercando di calcolare la somma di tutte le istanze DataField numerici)

var list = new List<DataField>() 
{ 
    new DataField<int>() {Name = "int", Value = 2}, 
    new DataField<string>() {Name = "string", Value = "stringValue"}, 
    new DataField<float>() {Name = "string", Value = 2f}, 
}; 

var sum = 0.0; 

foreach (var dataField in list) 
{ 
    if (dataField.GetType().IsGenericType) 
    { 
     if (dataField.GetType().GetGenericArguments()[0] == typeof(int)) 
     { 
      sum += ((DataField<int>) dataField).Value; 
     } 
     else if (dataField.GetType().GetGenericArguments()[0] == typeof(float)) 
     { 
      sum += ((DataField<float>)dataField).Value; 
     } 
     // .. 
    } 
} 

Questo codice è un disastro completo!

Andiamo provare l'attuazione polimorfica con il tipo DataField generico e aggiungere qualche metodo Somma ad esso che accetta il vecchio un po 'e restituisce il (eventualmente modificato) nuova somma:

public class DataField<T> : DataField 
{ 
    public T Value { get; set; } 
    public override double Sum(double sum) 
    { 
     if (typeof(T) == typeof(int)) 
     { 
      return sum + (int)Value; // Cannot really cast here! 
     } 
     else if (typeof(T) == typeof(float)) 
     { 
      return sum + (float)Value; // Cannot really cast here! 
     } 
     // ... 

     return sum; 
    } 
} 

Si può immaginare che il tuo codice di iterazione diventa molto più chiaro ora ma hai ancora questo strano switch/if statement nel tuo codice. E qui arriva il punto: i generici non ti aiutano qui è lo strumento sbagliato nel posto sbagliato. I generici sono progettati in C# per darti sicurezza del tipo a tempo di compilazione per evitare potenziali operazioni di cast non sicure. Essi inoltre aggiungono alla leggibilità del codice, ma che non è il caso qui :)

Diamo uno sguardo alla soluzione polimorfico:

public abstract class DataField 
{ 
    public string Name { get; set; } 
    public object Value { get; set; } 
    public abstract double Sum(double sum); 
} 

public class IntDataField : DataField 
{ 
    public override double Sum(double sum) 
    { 
     return (int)Value + sum; 
    } 
} 

public class FloatDataField : DataField 
{ 
    public override double Sum(double sum) 
    { 
     return (float)Value + sum; 
    } 
} 

Credo che non avrete bisogno di troppa fantasia per immaginare quanto aggiunge alla vostra leggibilità/qualità del codice.

L'ultimo punto è come creare istanze di queste classi. Semplicemente utilizzando alcuni convenzione TypeName + "DataField" e Activator:

Activator.CreateInstance("assemblyName", typeName); 

Versione breve:

Generics non è l'approccio appropriato per il problema perché non aggiunge valore al trattamento dei casi DataField . Con l'approccio polimorfico è possibile eseguire facilmente con le istanze di DataField!