Come faccio a creare e accedere a una nuova istanza di una classe anonima passata come parametro in C #?

StackOverflow https://stackoverflow.com/questions/478013

  •  20-08-2019
  •  | 
  •  

Domanda

Ho creato una funzione che accetta un comando SQL e produce output che può quindi essere utilizzato per riempire un elenco di istanze di classe. Il codice funziona alla grande. Ho incluso una versione leggermente semplificata senza gestione delle eccezioni qui solo per riferimento - salta questo codice se vuoi saltare il problema. Se hai suggerimenti qui, però, sono tutto orecchi.

    public List<T> ReturnList<T>() where T : new()
    {
        List<T> fdList = new List<T>();
        myCommand.CommandText = QueryString;
        SqlDataReader nwReader = myCommand.ExecuteReader();
        Type objectType = typeof (T);
        FieldInfo[] typeFields = objectType.GetFields();
        while (nwReader.Read())
        {
            T obj = new T();
            foreach (FieldInfo info in typeFields)
            {
                for (int i = 0; i < nwReader.FieldCount; i++)
                {
                    if (info.Name == nwReader.GetName(i))
                    {
                        info.SetValue(obj, nwReader[i]);
                        break;
                    }
                }
            }
            fdList.Add(obj);
        }
        nwReader.Close();
        return fdList;
    }

Come ho già detto, funziona perfettamente. Tuttavia, vorrei essere in grado di chiamare una funzione simile con una classe anonima per ovvi motivi.

Domanda n. 1: sembra che devo costruire una istanza di classe anonima nella mia chiamata alla mia versione anonima di questa funzione - è giusto? Una chiamata di esempio è:

.ReturnList(new { ClientID = 1, FirstName = "", LastName = "", Birthdate = DateTime.Today });

Domanda n. 2: la versione anonima della mia funzione ReturnList è di seguito. Qualcuno può dirmi perché la chiamata a info.SetValue semplicemente non fa nulla? Non restituisce un errore o altro, ma non cambia nemmeno il valore del campo target.

    public List<T> ReturnList<T>(T sample) 
    {
        List<T> fdList = new List<T>();
        myCommand.CommandText = QueryString;
        SqlDataReader nwReader = myCommand.ExecuteReader();
        // Cannot use FieldInfo[] on the type - it finds no fields.
        var properties = TypeDescriptor.GetProperties(sample); 
        while (nwReader.Read())
        {
            // No way to create a constructor so this call creates the object without calling a ctor. Could this be a source of the problem?
            T obj = (T)FormatterServices.GetUninitializedObject(typeof(T)); 
            foreach (PropertyDescriptor info in properties)  
            {
                for (int i = 0; i < nwReader.FieldCount; i++)
                {
                    if (info.Name == nwReader.GetName(i))
                    {
                        // This loop runs fine but there is no change to obj!!
                        info.SetValue(obj, nwReader[i]);
                        break;
                    }
                }
            }
            fdList.Add(obj);
        }
        nwReader.Close();
        return fdList;
    }

Qualche idea?

Nota: quando ho provato ad usare l'array FieldInfo come ho fatto nella funzione sopra, l'array typeFields aveva zero elementi (anche se objectType mostra i nomi dei campi - strano). Pertanto, utilizzo invece TypeDescriptor.GetProperties.

Eventuali altri suggerimenti e indicazioni sull'uso di lezioni di riflessione o anonime sono appropriati qui - Sono relativamente nuovo a questo specifico angolo del linguaggio C #.

AGGIORNAMENTO: Devo ringraziare Jason per la chiave per risolverlo. Di seguito è riportato il codice rivisto che creerà un elenco di istanze di classe anonime, riempiendo i campi di ciascuna istanza da una query.

   public List<T> ReturnList<T>(T sample)
   {
       List<T> fdList = new List<T>();
       myCommand.CommandText = QueryString;
       SqlDataReader nwReader = myCommand.ExecuteReader();
       var properties = TypeDescriptor.GetProperties(sample);
       while (nwReader.Read())
       {
           int objIdx = 0;
           object[] objArray = new object[properties.Count];
           foreach (PropertyDescriptor info in properties) 
               objArray[objIdx++] = nwReader[info.Name];
           fdList.Add((T)Activator.CreateInstance(sample.GetType(), objArray));
       }
       nwReader.Close();
       return fdList;
   }

Si noti che la query è stata costruita e i parametri inizializzati nelle precedenti chiamate ai metodi di questo oggetto. Il codice originale aveva una combinazione di loop interno / esterno in modo che l'utente potesse avere campi nella sua classe anonima che non corrispondevano a un campo. Tuttavia, al fine di semplificare la progettazione, ho deciso di non consentirlo e ho invece adottato l'accesso al campo db consigliato da Jason. Inoltre, grazie anche a Dave Markle per avermi aiutato a capire meglio i compromessi nell'uso di Activator.CreateObject () rispetto a GenUninitializedObject.

È stato utile?

Soluzione

I tipi anonimi racchiudono un insieme di proprietà di sola lettura . Questo spiega

  1. Perché Type.GetFields restituisce un array vuoto quando viene chiamato sul tuo tipo anonimo: i tipi anonimi non hanno campi pubblici.

  2. Le proprietà pubbliche di un tipo anonimo sono di sola lettura e non possono avere il valore impostato da una chiamata su PropertyInfo.SetValue. Se chiami PropertyInfo.GetSetMethod su una proprietà in un tipo anonimo, riceverai indietro null.

In effetti, se cambi

var properties = TypeDescriptor.GetProperties(sample);
while (nwReader.Read()) {
    // No way to create a constructor so this call creates the object without calling a ctor. Could this be a source of the problem?
    T obj = (T)FormatterServices.GetUninitializedObject(typeof(T)); 
    foreach (PropertyDescriptor info in properties) {
        for (int i = 0; i < nwReader.FieldCount; i++) {
            if (info.Name == nwReader.GetName(i)) {
                // This loop runs fine but there is no change to obj!!
                info.SetValue(obj, nwReader[i]);
                break;
            }
        }
    }
    fdList.Add(obj);
}

a

PropertyInfo[] properties = sample.GetType().GetProperties();
while (nwReader.Read()) {
    // No way to create a constructor so this call creates the object without calling a ctor. Could this be a source of the problem?
    T obj = (T)FormatterServices.GetUninitializedObject(typeof(T));
    foreach (PropertyInfo info in properties) {
        for (int i = 0; i < nwReader.FieldCount; i++) {
            if (info.Name == nwReader.GetName(i)) {
                // This loop will throw an exception as PropertyInfo.GetSetMethod fails
                info.SetValue(obj, nwReader[i], null);
                break;
            }
        }
    }
    fdList.Add(obj);
}

riceverai un'eccezione che ti informa che non è possibile trovare il metodo del set di proprietà.

Ora, per risolvere il tuo problema, quello che puoi fare è usare Activator.CreateInstance. Mi dispiace di essere troppo pigro per digitare il codice per te, ma quanto segue dimostrerà come usarlo.

var car = new { Make = "Honda", Model = "Civic", Year = 2008 };
var anothercar = Activator.CreateInstance(car.GetType(), new object[] { "Ford", "Focus", 2005 });

Quindi, basta eseguire un ciclo, come hai fatto, per riempire l'array di oggetti che è necessario passare a <=> e quindi chiamare <=> al termine del ciclo. Qui l'ordine di proprietà è importante poiché due tipi anonimi sono uguali se e solo se hanno lo stesso numero di proprietà con lo stesso tipo e lo stesso nome nello stesso ordine.

Per ulteriori informazioni, consultare la pagina MSDN su tipi anonimi.

/ p>

Infine, e questo è davvero un aspetto a parte e non germano alla tua domanda, ma il seguente codice

foreach (PropertyDescriptor info in properties) {
    for (int i = 0; i < nwReader.FieldCount; i++) {
        if (info.Name == nwReader.GetName(i)) {
            // This loop runs fine but there is no change to obj!!
            info.SetValue(obj, nwReader[i]);
            break;
        }
    }
}

potrebbe essere semplificato da

foreach (PropertyDescriptor info in properties) {
            info.SetValue(obj, nwReader[info.Name]);
}

Altri suggerimenti

Ho avuto lo stesso problema, l'ho risolto creando un nuovo Linq.Expression che farà il vero lavoro e lo compili in un lambda: ecco il mio codice per esempio:

Voglio trasformare quella chiamata:

var customers = query.ToList(r => new
            {
                Id = r.Get<int>("Id"),
                Name = r.Get<string>("Name"),
                Age = r.Get<int>("Age"),
                BirthDate = r.Get<DateTime?>("BirthDate"),
                Bio = r.Get<string>("Bio"),
                AccountBalance = r.Get<decimal?>("AccountBalance"),
            });

a quella chiamata:

var customers = query.ToList(() => new 
        { 
            Id = default(int),
            Name = default(string),
            Age = default(int), 
            BirthDate = default(DateTime?),
            Bio = default(string), 
            AccountBalance = default(decimal?)
        });

e esegui DataReader. Ottieni le cose dal nuovo metodo, il primo metodo è:

public List<T> ToList<T>(FluentSelectQuery query, Func<IDataReader, T> mapper)
    {
        return ToList<T>(mapper, query.ToString(), query.Parameters);
    }

Ho dovuto creare un'espressione con il nuovo metodo:

public List<T> ToList<T>(Expression<Func<T>> type, string sql, params object[] parameters)
        {
            var expression = (NewExpression)type.Body;
            var constructor = expression.Constructor;
            var members = expression.Members.ToList();

            var dataReaderParam = Expression.Parameter(typeof(IDataReader));
            var arguments = members.Select(member => 
                {
                    var memberName = Expression.Constant(member.Name);
                    return Expression.Call(typeof(Utilities), 
                                           "Get", 
                                           new Type[] { ((PropertyInfo)member).PropertyType },  
                                           dataReaderParam, memberName);
                }
            ).ToArray();

            var body = Expression.New(constructor, arguments);

            var mapper = Expression.Lambda<Func<IDataReader, T>>(body, dataReaderParam);

            return ToList<T>(mapper.Compile(), sql, parameters);
        }

In questo modo, posso evitare completamente Activator.CreateInstance o FormatterServices.GetUninitializedObject roba, scommetto che è molto più veloce;)

Domanda n. 2:

Non lo so davvero, ma tenderei ad usare Activator.CreateObject () invece di FormatterServices.GetUninitializedObject (), perché l'oggetto potrebbe non essere creato correttamente. GetUninitializedObject () non eseguirà un costruttore predefinito come farà CreateObject () e non sai necessariamente cosa c'è nella scatola nera di T ...

Questo metodo memorizza una riga di una query sql in una variabile di tipo anonimo. Devi passare un prototipo al metodo. Se non è possibile trovare alcuna proprietà di tipo anonimo all'interno della query sql, viene riempito con il valore prototipo. C # crea costruttori per le sue classi anonime, i parametri hanno gli stessi nomi delle proprietà (sola lettura).

    public static T GetValuesAs<T>(this SqlDataReader Reader, T prototype)
    {
        System.Reflection.ConstructorInfo constructor = prototype.GetType().GetConstructors()[0];
        object[] paramValues = constructor.GetParameters().Select(
            p => { try               { return Reader[p.Name]; }
                   catch (Exception) { return prototype.GetType().GetProperty(p.Name).GetValue(prototype); } }
            ).ToArray();
        return (T)prototype.GetType().GetConstructors()[0].Invoke(paramValues);
    }
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top