Passando argomenti a new # (generico) di tipo templated


409

Sto cercando di creare un nuovo oggetto di tipo T tramite il suo costruttore durante l'aggiunta all'elenco.

Ricevo un errore di compilazione: il messaggio di errore è:

'T': impossibile fornire argomenti durante la creazione di un'istanza di una variabile

Ma le mie lezioni hanno un argomento costruttore! Come posso farlo funzionare?

public static string GetAllItems<T>(...) where T : new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
       tabListItems.Add(new T(listItem)); // error here.
   } 
   ...
}

2
possibile duplicato di Crea istanza di tipo generico?
nawfal,

2
Proposta per ottenere questa funzionalità nella lingua: github.com/dotnet/roslyn/issues/2206
Ian Kemp

Nella documentazione di Microsoft, vedere Errore del compilatore CS0417 .
DavidRR,

1
La proposta di inserire questa funzionalità nella lingua è stata spostata su: github.com/dotnet/csharplang/issues/769
riduzione dell'attività

Risposte:


410

Per creare un'istanza di un tipo generico in una funzione è necessario vincolarla con il flag "nuovo".

public static string GetAllItems<T>(...) where T : new()

Tuttavia, funzionerà solo quando si desidera chiamare il costruttore che non ha parametri. Non è il caso qui. Dovrai invece fornire un altro parametro che consenta la creazione di oggetti in base a parametri. La più semplice è una funzione.

public static string GetAllItems<T>(..., Func<ListItem,T> del) {
  ...
  List<T> tabListItems = new List<T>();
  foreach (ListItem listItem in listCollection) 
  {
    tabListItems.Add(del(listItem));
  }
  ...
}

Puoi quindi chiamarlo così

GetAllItems<Foo>(..., l => new Foo(l));

Come funzionerebbe se chiamato internamente da una classe generica? Ho pubblicato il mio codice in una risposta di seguito. Non conosco la classe concreta internamente, in quanto è una classe generica. C'è un modo per aggirare questo. Non voglio usare l'altro suggerimento di usare la sintassi dell'inizializzatore di proprietà poiché ciò
aggirerà

aggiunto il mio codice a un'altra domanda stackoverflow.com/questions/1682310/...
ChrisCa

21
Questa è attualmente una delle limitazioni più fastidiose di C #. Vorrei rendere immutabili le mie classi: avere solo setter privati ​​renderebbe impossibile la classe in uno stato invalido con effetti collaterali. Mi piace anche usare Func e Lambda, ma so che è ancora un problema nel mondo degli affari, in quanto i programmatori non conoscono ancora lambda e questo rende la tua classe più difficile da capire.
Tuomas Hietanen,

1
Grazie. Nel mio caso conosco gli argomenti del costruttore quando chiamo il metodo, avevo solo bisogno di aggirare la limitazione del parametro Type che non poteva essere costruita con parametri, quindi ho usato un thunk . Il thunk è un parametro facoltativo per il metodo e lo uso solo se fornito: T result = thunk == null ? new T() : thunk(); il vantaggio di questo per me è consolidare la logica della Tcreazione in un posto piuttosto che a volte creare Tall'interno e talvolta fuori dal metodo.
Carl G,

Penso che questo sia uno dei luoghi in cui il linguaggio C # decide di dire di no al programmatore e di smettere di dire di sì tutto il tempo! Sebbene questo approccio sia un modo un po 'imbarazzante di creare oggetti, ma per ora devo usarlo.
AmirHossein Rezaei,

331

in .Net 3.5 e dopo potresti usare la classe activator:

(T)Activator.CreateInstance(typeof(T), args)

1
potremmo anche usare l'albero delle espressioni per costruire l'oggetto
Welly Tambunan,

4
Che cos'è args? un oggetto[]?
Rodney P. Barbati,

3
Sì, args è un oggetto [] in cui si specificano i valori da fornire al costruttore della T: "new object [] {par1, par2}"
TechNyquist


3
ATTENZIONE: se hai un costruttore dedicato solo per il bene di Activator.CreateInstancequesta cosa, sembrerà che il tuo costruttore non sia usato affatto e qualcuno potrebbe provare a "ripulire" ed eliminarlo (per causare un errore di runtime a un po 'di tempo a caso in futuro). Puoi prendere in considerazione l'aggiunta di una funzione fittizia in cui usi questo costruttore solo per ottenere un errore di compilazione se provi a eliminarlo.
jrh

51

Poiché nessuno si è preso la briga di pubblicare la risposta "Riflessione" (che personalmente ritengo sia la migliore risposta), ecco qui:

public static string GetAllItems<T>(...) where T : new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
       Type classType = typeof(T);
       ConstructorInfo classConstructor = classType.GetConstructor(new Type[] { listItem.GetType() });
       T classInstance = (T)classConstructor.Invoke(new object[] { listItem });

       tabListItems.Add(classInstance);
   } 
   ...
}

Modifica: questa risposta è obsoleta a causa di Activator.CreateInstance di .NET 3.5, tuttavia è ancora utile nelle versioni .NET precedenti.


La mia comprensione è che la maggior parte del successo delle prestazioni è in primo luogo l'acquisizione di ConstructorInfo. Non crederci sulla parola senza profilarla. In tal caso, la semplice memorizzazione di ConstructorInfo per un successivo riutilizzo potrebbe alleviare il colpo di prestazione di ripetute istanze attraverso la riflessione.
Kelsie,

19
Penso che la mancanza di controlli in fase di compilazione sia più motivo di preoccupazione.
Dave Van den Eynde,

1
@James Sono d'accordo, sono stato sorpreso di non vedere questa come "risposta". In effetti, ho cercato su questa domanda aspettandomi di trovare un semplice esempio (come il tuo) dato che è passato tanto tempo da quando ho riflettuto. Ad ogni modo, +1 da parte mia, ma +1 anche sulla risposta dell'Attivatore. Ho esaminato ciò che Activator sta facendo e si scopre che ciò che fa è una riflessione molto ben progettata. :)
Mike

La chiamata GetConstructor () è costosa, quindi vale la pena memorizzarla nella cache prima del ciclo. In questo modo, chiamando solo Invoke () all'interno del ciclo, è molto più veloce di chiamare entrambi o addirittura usare Activator.CreateInstance ().
Cosmin Rus,

30

Inizializzatore di oggetti

Se il tuo costruttore con il parametro non sta facendo nulla oltre a impostare una proprietà, puoi farlo in C # 3 o meglio usando un inizializzatore di oggetti piuttosto che chiamare un costruttore (che è impossibile, come è stato menzionato):

public static string GetAllItems<T>(...) where T : new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
       tabListItems.Add(new T() { YourPropertyName = listItem } ); // Now using object initializer
   } 
   ...
}

Usando questo, puoi sempre mettere qualsiasi logica del costruttore anche nel costruttore predefinito (vuoto).

Activator.CreateInstance ()

In alternativa, puoi chiamare Activator.CreateInstance () in questo modo:

public static string GetAllItems<T>(...) where T : new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
        object[] args = new object[] { listItem };
        tabListItems.Add((T)Activator.CreateInstance(typeof(T), args)); // Now using Activator.CreateInstance
   } 
   ...
}

Si noti che Activator.CreateInstance può avere un certo sovraccarico di prestazioni che è possibile evitare se la velocità di esecuzione è una priorità assoluta e un'altra opzione è mantenibile per l'utente.


questo impedisce Tdi proteggere i suoi invarianti (dato che Tha> 0 dipendenze o valori richiesti, ora è possibile creare istanze Tche si trovano in uno stato non valido / inutilizzabile. A meno che non Tsia qualcosa di estremamente semplice come un modello DTO o viewm, direi di evitarlo.
Sara,

20

Domanda molto vecchia, ma nuova risposta ;-)

La versione ExpressionTree : (Penso che la soluzione più veloce e più pulita)

Come ha detto Welly Tambunan , "potremmo anche usare l'albero delle espressioni per costruire l'oggetto"

Ciò genererà un "costruttore" (funzione) per il tipo / i parametri forniti. Restituisce un delegato e accetta i tipi di parametro come una matrice di oggetti.

Ecco qui:

// this delegate is just, so you don't have to pass an object array. _(params)_
public delegate object ConstructorDelegate(params object[] args);

public static ConstructorDelegate CreateConstructor(Type type, params Type[] parameters)
{
    // Get the constructor info for these parameters
    var constructorInfo = type.GetConstructor(parameters);

    // define a object[] parameter
    var paramExpr = Expression.Parameter(typeof(Object[]));

    // To feed the constructor with the right parameters, we need to generate an array 
    // of parameters that will be read from the initialize object array argument.
    var constructorParameters = parameters.Select((paramType, index) =>
        // convert the object[index] to the right constructor parameter type.
        Expression.Convert(
            // read a value from the object[index]
            Expression.ArrayAccess(
                paramExpr,
                Expression.Constant(index)),
            paramType)).ToArray();

    // just call the constructor.
    var body = Expression.New(constructorInfo, constructorParameters);

    var constructor = Expression.Lambda<ConstructorDelegate>(body, paramExpr);
    return constructor.Compile();
}

Esempio di MyClass:

public class MyClass
{
    public int TestInt { get; private set; }
    public string TestString { get; private set; }

    public MyClass(int testInt, string testString)
    {
        TestInt = testInt;
        TestString = testString;
    }
}

Uso:

// you should cache this 'constructor'
var myConstructor = CreateConstructor(typeof(MyClass), typeof(int), typeof(string));

// Call the `myConstructor` function to create a new instance.
var myObject = myConstructor(10, "test message");

inserisci qui la descrizione dell'immagine


Un altro esempio: passare i tipi come un array

var type = typeof(MyClass);
var args = new Type[] { typeof(int), typeof(string) };

// you should cache this 'constructor'
var myConstructor = CreateConstructor(type, args);

// Call the `myConstructor` fucntion to create a new instance.
var myObject = myConstructor(10, "test message");

DebugView of Expression

.Lambda #Lambda1<TestExpressionConstructor.MainWindow+ConstructorDelegate>(System.Object[] $var1) {
    .New TestExpressionConstructor.MainWindow+MyClass(
        (System.Int32)$var1[0],
        (System.String)$var1[1])
}

Ciò equivale al codice generato:

public object myConstructor(object[] var1)
{
    return new MyClass(
        (System.Int32)var1[0],
        (System.String)var1[1]);
}

Piccolo inconveniente

Tutti i parametri dei valori di valore vengono inscatolati quando vengono passati come una matrice di oggetti.


Test delle prestazioni semplice:

private void TestActivator()
{
    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 1024 * 1024 * 10; i++)
    {
        var myObject = Activator.CreateInstance(typeof(MyClass), 10, "test message");
    }
    sw.Stop();
    Trace.WriteLine("Activator: " + sw.Elapsed);
}

private void TestReflection()
{
    var constructorInfo = typeof(MyClass).GetConstructor(new[] { typeof(int), typeof(string) });

    Stopwatch sw = Stopwatch.StartNew();
    for (int i = 0; i < 1024 * 1024 * 10; i++)
    {
        var myObject = constructorInfo.Invoke(new object[] { 10, "test message" });
    }

    sw.Stop();
    Trace.WriteLine("Reflection: " + sw.Elapsed);
}

private void TestExpression()
{
    var myConstructor = CreateConstructor(typeof(MyClass), typeof(int), typeof(string));

    Stopwatch sw = Stopwatch.StartNew();

    for (int i = 0; i < 1024 * 1024 * 10; i++)
    {
        var myObject = myConstructor(10, "test message");
    }

    sw.Stop();
    Trace.WriteLine("Expression: " + sw.Elapsed);
}

TestActivator();
TestReflection();
TestExpression();

risultati:

Activator: 00:00:13.8210732
Reflection: 00:00:05.2986945
Expression: 00:00:00.6681696

L'uso Expressionsè +/- 8 volte più veloce di Invocare il ConstructorInfoe +/- 20 volte più veloce dell'uso diActivator


Hai qualche idea su cosa fare se vuoi costruire MyClass <T> con il costruttore MyClass pubblico (dati T). In questo caso, Expression.Convert genera un'eccezione e se uso la classe di base del vincolo generico per la conversione, Expression.New genera perché le informazioni del costruttore sono per un tipo generico
Mason

@ Mason (ci è voluto un po 'di tempo per rispondere ;-)) var myConstructor = CreateConstructor(typeof(MyClass<int>), typeof(int));sta funzionando bene. Non lo so.
Jeroen van Langen il

19

Questo non funzionerà nella tua situazione. Puoi specificare solo il vincolo che ha un costruttore vuoto:

public static string GetAllItems<T>(...) where T: new()

Quello che potresti fare è usare l'iniezione di proprietà definendo questa interfaccia:

public interface ITakesAListItem
{
   ListItem Item { set; }
}

Quindi potresti modificare il tuo metodo per essere questo:

public static string GetAllItems<T>(...) where T : ITakesAListItem, new()
{
   ...
   List<T> tabListItems = new List<T>();
   foreach (ListItem listItem in listCollection) 
   {
       tabListItems.Add(new T() { Item = listItem });
   } 
   ...
}

L'altra alternativa è il Funcmetodo descritto da JaredPar.


questo aggirerebbe qualsiasi logica presente nel costruttore che accetta gli argomenti, giusto? Vorrei fare qualcosa come l'approccio di Jared, ma sto chiamando il metodo internamente all'interno della classe, quindi non so quale sia il tipo concreto ... hmmm
ChrisCa

3
Bene, questo chiama la logica del costruttore predefinito T (), quindi imposta semplicemente la proprietà "Item". Se stai cercando di invocare la logica di un costruttore non predefinito, questo non ti aiuterà.
Scott Stafford,

7

Devi aggiungere dove T: new () per far sapere al compilatore che T è garantito per fornire un costruttore predefinito.

public static string GetAllItems<T>(...) where T: new()

1
AGGIORNAMENTO: Il messaggio di errore corretto è: 'T': impossibile fornire argomenti durante la creazione di un'istanza di una variabile
LB.

Questo perché non stai usando un costruttore vuoto, stai passando un argomento ad esso dell'oggetto. Non c'è modo di gestirlo senza specificare che il Tipo generico ha un nuovo parametro (oggetto).
Min

Quindi dovrai: 1. Utilizzare la riflessione 2. Passare il parametro in un metodo di inizializzazione anziché nel costruttore, dove il metodo di inizializzazione appartiene a un'interfaccia implementata dal tuo tipo e che è inclusa nel punto T: ... dichiarazione. L'opzione 1 ha il minor impatto per il resto del codice, ma l'opzione 2 fornisce il controllo del tempo di compilazione.
Richard,

Non usare la riflessione! Ci sono altri modi come indicato in altre risposte che ti danno lo stesso effetto.
Garry Shutler,

@Garry - Concordo sul fatto che la riflessione non sia necessariamente l'approccio migliore, ma ti consente di ottenere ciò che è richiesto con una modifica minima al resto della base di codice. Detto questo, preferisco di gran lunga l'approccio dei delegati di fabbrica da @JaredPar.
Richard,

7

Se vuoi semplicemente inizializzare un campo membro o una proprietà con il parametro del costruttore, in C #> = 3 puoi farlo molto più facilmente:

public static string GetAllItems<T>(...) where T : InterfaceOrBaseClass, new() 
{ 
   ... 
   List<T> tabListItems = new List<T>(); 
   foreach (ListItem listItem in listCollection)  
   { 
       tabListItems.Add(new T{ BaseMemberItem = listItem }); // No error, BaseMemberItem owns to InterfaceOrBaseClass. 
   }  
   ... 
} 

Questa è la stessa cosa che ha detto Garry Shutler, ma vorrei mettere una nota adizionale.

Ovviamente puoi usare un trucco di proprietà per fare più cose che semplicemente impostare un valore di campo. Una proprietà "set ()" può attivare qualsiasi elaborazione necessaria per impostare i campi correlati e qualsiasi altra esigenza per l'oggetto stesso, incluso un controllo per vedere se deve avvenire un'inizializzazione completa prima che l'oggetto venga utilizzato, simulando una costruzione completa ( sì, è una brutta soluzione, ma supera la nuova () limitazione di M $).

Non posso essere sicuro che si tratti di un buco pianificato o di un effetto collaterale accidentale, ma funziona.

È molto divertente come le persone con SM aggiungano nuove funzionalità al linguaggio e sembra non fare un'analisi completa degli effetti collaterali. L'intera cosa generica è una buona prova di questo ...


1
Sono necessari entrambi i vincoli. InterfaceOrBaseClass rende il compilatore consapevole del campo / proprietà BaseMemberItem. Se viene commentato il vincolo "new ()", verrà generato l'errore: Errore 6 Impossibile creare un'istanza del tipo di variabile 'T' perché non ha il vincolo new ()
fljx

Una situazione che ho riscontrato non era esattamente come la domanda posta qui, tuttavia questa risposta mi ha portato dove dovevo andare e sembra funzionare molto bene.
RubyHaus,

5
Ogni volta che qualcuno menziona Microsoft come "M $", una piccola parte della mia anima soffre.
Mathias Lykkegaard Lorenzen,

6

Ho scoperto che stavo ricevendo un errore "impossibile fornire argomenti durante la creazione di un'istanza del tipo parametro T", quindi ho dovuto fare questo:

var x = Activator.CreateInstance(typeof(T), args) as T;

5

Se hai accesso alla classe che intendi utilizzare, puoi utilizzare questo approccio che ho usato.

Crea un'interfaccia con un creatore alternativo:

public interface ICreatable1Param
{
    void PopulateInstance(object Param);
}

Crea le tue lezioni con un creatore vuoto e implementa questo metodo:

public class MyClass : ICreatable1Param
{
    public MyClass() { //do something or nothing }
    public void PopulateInstance (object Param)
    {
        //populate the class here
    }
}

Ora usa i tuoi metodi generici:

public void MyMethod<T>(...) where T : ICreatable1Param, new()
{
    //do stuff
    T newT = new T();
    T.PopulateInstance(Param);
}

Se non hai accesso, avvolgi la classe target:

public class MyClass : ICreatable1Param
{
    public WrappedClass WrappedInstance {get; private set; }
    public MyClass() { //do something or nothing }
    public void PopulateInstance (object Param)
    {
        WrappedInstance = new WrappedClass(Param);
    }
}

0

Questo è un po 'mucky, e quando dico tipo di mucky potrei voler dire ribellione, ma supponendo che tu possa fornire il tuo tipo parametrizzato con un costruttore vuoto, allora:

public static T GetTInstance<T>() where T: new()
{
    var constructorTypeSignature = new Type[] {typeof (object)};
    var constructorParameters = new object[] {"Create a T"};
    return (T) new T().GetType().GetConstructor(constructorTypeSignature).Invoke(constructorParameters);
}

Ti permetterà effettivamente di costruire un oggetto da un tipo con parametri con un argomento. In questo caso suppongo che il costruttore che voglio abbia un singolo argomento di tipo object. Creiamo un'istanza fittizia di T usando il costruttore vuoto consentito dal vincolo e quindi utilizziamo la riflessione per ottenere uno dei suoi altri costruttori.


0

A volte uso un approccio che assomiglia alle risposte usando l'iniezione di proprietà, ma mantiene il codice più pulito. Invece di avere una classe / interfaccia di base con un insieme di proprietà, contiene solo un metodo (virtuale) Initialize () - che funge da "costruttore di un uomo povero". Quindi puoi lasciare che ogni classe gestisca la propria inizializzazione proprio come farebbe un costruttore, il che aggiunge anche un modo comodo di gestire le catene di ereditarietà.

Se spesso mi trovo in situazioni in cui desidero che ogni classe della catena inizializzi le sue proprietà uniche, e quindi chiami il metodo Initialize () del genitore che a sua volta inizializza le proprietà uniche del genitore e così via. Ciò è particolarmente utile quando si hanno classi diverse, ma con una gerarchia simile, ad esempio oggetti business mappati a / da DTO: s.

Esempio che utilizza un dizionario comune per l'inizializzazione:

void Main()
{
    var values = new Dictionary<string, int> { { "BaseValue", 1 }, { "DerivedValue", 2 } };

    Console.WriteLine(CreateObject<Base>(values).ToString());

    Console.WriteLine(CreateObject<Derived>(values).ToString());
}

public T CreateObject<T>(IDictionary<string, int> values)
    where T : Base, new()
{
    var obj = new T();
    obj.Initialize(values);
    return obj;
}

public class Base
{
    public int BaseValue { get; set; }

    public virtual void Initialize(IDictionary<string, int> values)
    {
        BaseValue = values["BaseValue"];
    }

    public override string ToString()
    {
        return "BaseValue = " + BaseValue;
    }
}

public class Derived : Base
{
    public int DerivedValue { get; set; }

    public override void Initialize(IDictionary<string, int> values)
    {
        base.Initialize(values);
        DerivedValue = values["DerivedValue"];
    }

    public override string ToString()
    {       
        return base.ToString() + ", DerivedValue = " + DerivedValue;
    }
}

0

Se tutto ciò di cui hai bisogno è la conversione da ListItem al tuo tipo T, puoi implementare questa conversione in classe T come operatore di conversione.

public class T
{
    public static implicit operator T(ListItem listItem) => /* ... */;
}

public static string GetAllItems(...)
{
    ...
    List<T> tabListItems = new List<T>();
    foreach (ListItem listItem in listCollection) 
    {
        tabListItems.Add(listItem);
    } 
    ...
}

-4

Credo che devi vincolare T con un'istruzione where per consentire solo oggetti con un nuovo costruttore.

Ora ora accetta tutto senza oggetti.


1
Potresti voler cambiare questa risposta perché questa è stata modificata nella domanda dopo aver risposto, lasciando questa risposta fuori contesto.
shuttle87,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.