Creare un'istanza di tipo generico il cui costruttore richiede un parametro?


230

Se BaseFruitha un costruttore che accetta un int weight, posso creare un'istanza di un frutto in un metodo generico come questo?

public void AddFruit<T>()where T: BaseFruit{
    BaseFruit fruit = new T(weight); /*new Apple(150);*/
    fruit.Enlist(fruitManager);
}

Un esempio viene aggiunto dietro i commenti. Sembra che posso farlo solo se do BaseFruitun costruttore senza parametri e poi riempio tutto attraverso le variabili membro. Nel mio vero codice (non sulla frutta) questo è piuttosto poco pratico.

-Aggiornamento-
Quindi sembra che non possa essere risolto in alcun modo dai vincoli. Dalle risposte ci sono tre soluzioni candidate:

  • Modello di fabbrica
  • Riflessione
  • Activator

Tendo a pensare che il riflesso sia il meno pulito, ma non riesco a decidere tra gli altri due.


1
A proposito: oggi probabilmente lo risolverei con la libreria IoC preferita.
Boris Callens,

Reflection e Activator sono in realtà strettamente correlati.
Rob Vermeulen,

Risposte:


335

Inoltre un esempio più semplice:

return (T)Activator.CreateInstance(typeof(T), new object[] { weight });

Si noti che l'utilizzo del vincolo new () su T è solo per fare in modo che il compilatore controlli un costruttore pubblico senza parametri al momento della compilazione, il codice effettivo utilizzato per creare il tipo è la classe Activator.

Dovrai assicurarti del costruttore specifico esistente, e questo tipo di requisito potrebbe essere un odore di codice (o piuttosto qualcosa che dovresti semplicemente cercare di evitare nella versione corrente su c #).


Dal momento che questo costruttore è sulla baseclass (BaseFruit), so che avrà un costruttore. Ma in effetti, se un giorno decido che il frutto di base necessita di più parametri, potrei essere fregato. Esaminerà tuttavia la classe ACtivator. Non ne avevo mai sentito parlare prima.
Boris Callens,

3
Questo ha funzionato bene. C'è anche una procedura CreateInstance <T> (), ma che non ha un sovraccarico di parametri per alcuni rason ..
Boris Callens,

20
Non v'è alcuna necessità di utilizzare new object[] { weight }. CreateInstanceè dichiarato con params, public static object CreateInstance(Type type, params object[] args)quindi puoi semplicemente farlo return (T) Activator.CreateInstance(typeof(T), weight);. Se ci sono più parametri, passali come argomenti separati. Solo se hai già costruito un enumerabile elenco di parametri dovresti preoccuparti di convertirlo object[]e passarlo a CreateInstance.
ErikE

2
Ciò avrà problemi di prestazioni che ho letto. Utilizzare invece una lambda compilata. vagifabilov.wordpress.com/2010/04/02/…
David

1
@RobVermeulen - Penso a qualcosa di simile a una proprietà statica su ogni classe Fruit, che contiene un oggetto Funcche crea la nuova istanza. Supponiamo che l' Appleuso del costruttore sia new Apple(wgt). Quindi aggiungi alla Appleclasse questa definizione: static Func<float, Fruit> CreateOne { get; } = (wgt) => new Apple(wgt);In Factory definisci public static Fruit CreateFruitGiven(float weight, Func<float, Fruit> createOne) { return createOne(weight); } Utilizzo: Factory.CreateFruit(57.3f, Apple.CreateOne);- che crea e restituisce un Apple, con weight=57.3f.
ToolmakerSteve

92

Non è possibile utilizzare alcun costruttore con parametri. È possibile utilizzare un costruttore senza parametri se si dispone di un " where T : new()" vincolo.

È un dolore, ma tale è la vita :(

Questa è una delle cose che vorrei affrontare con "interfacce statiche" . Saresti quindi in grado di vincolare T per includere metodi statici, operatori e costruttori, e quindi chiamarli.


2
Almeno PUOI fare tali vincoli - Java mi delude sempre.
Marcel Jackwerth,

@JonSkeet: se ho esposto l'API con .NET generico per essere chiamato in VB6.0 .. Funziona ancora?
Roy Lee,

@Roylee: Non ne ho idea, ma sospetto di no.
Jon Skeet,

Penserei che le interfacce statiche possano essere aggiunte da un compilatore di lingue senza modifiche al runtime, anche se sarebbe bene che i team linguistici si coordinassero sui dettagli. Specificare che ogni classe che afferma di implementare un'interfaccia statica deve contenere una classe nidificata con un particolare nome relativo all'interfaccia, che definisce un'istanza singleton statica del proprio tipo. Associato all'interfaccia sarebbe un tipo generico statico con un campo di istanza che dovrebbe essere caricato con il singleton una volta tramite Reflection, ma potrebbe essere utilizzato direttamente dopo.
supercat

Un vincolo del costruttore con parametri potrebbe essere gestito più o meno allo stesso modo (usando un metodo factory e un parametro generico per il suo tipo restituito); in nessun caso nulla impedirebbe al codice scritto in un linguaggio che non supporta tale funzionalità di pretendere di implementare l'interfaccia senza definire il tipo statico appropriato, quindi il codice scritto usando tali linguaggi potrebbe fallire in fase di esecuzione, ma Reflection potrebbe essere evitato nell'utente codice.
supercat

61

Sì; cambia il tuo posto dove essere:

where T:BaseFruit, new()

Tuttavia, questo funziona solo con costruttori senza parametri . Dovrai avere altri modi per impostare la tua proprietà (impostare la proprietà stessa o qualcosa di simile).


Se il costruttore non ha parametri, questo mi sembra sicuro.
PerpetualStudent,

Mi hai salvato la vita. Non sono riuscito a limitare la classe T alla parola chiave new ().
Genotypek,

28

La soluzione più semplice Activator.CreateInstance<T>()


1
Grazie per il suggerimento, mi ha portato dove dovevo essere. Sebbene ciò non ti permetta di usare un costruttore con parametri. Ma potresti usare la variante non generica: Activator.CreateInstance (typeof (T), nuovo oggetto [] {...}) dove l'array di oggetti contiene gli argomenti per il costruttore.
Rob Vermeulen,

19

Come ha sottolineato Jon, questa è la vita per vincolare un costruttore senza parametri. Tuttavia, una soluzione diversa consiste nell'utilizzare un modello di fabbrica. Questo è facilmente vincolante

interface IFruitFactory<T> where T : BaseFruit {
  T Create(int weight);
}

public void AddFruit<T>( IFruitFactory<T> factory ) where T: BaseFruit {    
  BaseFruit fruit = factory.Create(weight); /*new Apple(150);*/    
  fruit.Enlist(fruitManager);
}

Un'altra opzione è quella di utilizzare un approccio funzionale. Passare un metodo di fabbrica.

public void AddFruit<T>(Func<int,T> factoryDel) where T : BaseFruit { 
  BaseFruit fruit = factoryDel(weight); /* new Apple(150); */
  fruit.Enlist(fruitManager);
}

2
Un buon suggerimento - anche se se non stai attento puoi finire nell'inferno dell'API DOM Java, con molte fabbriche :(
Jon Skeet

Sì, questa è una soluzione che mi stavo concedendo. Ma speravo in qualcosa nella linea dei vincoli. Suppongo di no ..
Boris Callens,

@boris, sfortunatamente il linguaggio dei vincoli che stai cercando non esiste in questo momento
JaredPar

11

Puoi farlo usando la riflessione:

public void AddFruit<T>()where T: BaseFruit
{
  ConstructorInfo constructor = typeof(T).GetConstructor(new Type[] { typeof(int) });
  if (constructor == null)
  {
    throw new InvalidOperationException("Type " + typeof(T).Name + " does not contain an appropriate constructor");
  }
  BaseFruit fruit = constructor.Invoke(new object[] { (int)150 }) as BaseFruit;
  fruit.Enlist(fruitManager);
}

EDIT: aggiunto costruttore == controllo null.

EDIT: una variante più veloce usando una cache:

public void AddFruit<T>()where T: BaseFruit
{
  var constructor = FruitCompany<T>.constructor;
  if (constructor == null)
  {
    throw new InvalidOperationException("Type " + typeof(T).Name + " does not contain an appropriate constructor");
  }
  var fruit = constructor.Invoke(new object[] { (int)150 }) as BaseFruit;
  fruit.Enlist(fruitManager);
}
private static class FruitCompany<T>
{
  public static readonly ConstructorInfo constructor = typeof(T).GetConstructor(new Type[] { typeof(int) });
}

Anche se non mi piace il sovraccarico della riflessione, come altri hanno spiegato, questo è solo il modo in cui è attualmente. Vedendo come questo costruttore non sarà chiamato troppo, potrei andare con questo. O la fabbrica. Non lo so ancora.
Boris Callens,

Questo è attualmente il mio approccio preferito perché non aggiunge più complessità sul lato dell'invocazione.
Rob Vermeulen,

Ma ora ho letto del suggerimento di Activator, che ha una cattiveria simile alla soluzione di riflessione sopra, ma con meno righe di codice :) Prenderò l'opzione Activator.
Rob Vermeulen,

1

In aggiunta al suggerimento dell'utente1471935:

Per creare un'istanza di una classe generica utilizzando un costruttore con uno o più parametri, è ora possibile utilizzare la classe Activator.

T instance = Activator.CreateInstance(typeof(T), new object[] {...}) 

L'elenco degli oggetti sono i parametri che si desidera fornire. Secondo Microsoft :

CreateInstance [...] crea un'istanza del tipo specificato utilizzando il costruttore che corrisponde meglio ai parametri specificati.

Esiste anche una versione generica di CreateInstance ( CreateInstance<T>()) ma quella non consente di fornire parametri del costruttore.


1

Ho creato questo metodo:

public static V ConvertParentObjToChildObj<T,V> (T obj) where V : new()
{
    Type typeT = typeof(T);
    PropertyInfo[] propertiesT = typeT.GetProperties();
    V newV = new V();
    foreach (var propT in propertiesT)
    {
        var nomePropT = propT.Name;
        var valuePropT = propT.GetValue(obj, null);

        Type typeV = typeof(V);
        PropertyInfo[] propertiesV = typeV.GetProperties();
        foreach (var propV in propertiesV)
        {
            var nomePropV = propV.Name;
            if(nomePropT == nomePropV)
            {
                propV.SetValue(newV, valuePropT);
                break;
            }
        }
    }
    return newV;
}

Lo uso in questo modo:

public class A 
{
    public int PROP1 {get; set;}
}

public class B : A
{
    public int PROP2 {get; set;}
}

Codice:

A instanceA = new A();
instanceA.PROP1 = 1;

B instanceB = new B();
instanceB = ConvertParentObjToChildObj<A,B>(instanceA);

0

Di recente ho riscontrato un problema molto simile. Volevo solo condividere la nostra soluzione con tutti voi. Volevo creare un'istanza di a Car<CarA>da un oggetto json usando quale aveva un enum:

Dictionary<MyEnum, Type> mapper = new Dictionary<MyEnum, Type>();

mapper.Add(1, typeof(CarA));
mapper.Add(2, typeof(BarB)); 

public class Car<T> where T : class
{       
    public T Detail { get; set; }
    public Car(T data)
    {
       Detail = data;
    }
}
public class CarA
{  
    public int PropA { get; set; }
    public CarA(){}
}
public class CarB
{
    public int PropB { get; set; }
    public CarB(){}
}

var jsonObj = {"Type":"1","PropA":"10"}
MyEnum t = GetTypeOfCar(jsonObj);
Type objectT = mapper[t]
Type genericType = typeof(Car<>);
Type carTypeWithGenerics = genericType.MakeGenericType(objectT);
Activator.CreateInstance(carTypeWithGenerics , new Object[] { JsonConvert.DeserializeObject(jsonObj, objectT) });

-2

È ancora possibile, con prestazioni elevate, procedendo come segue:

    //
    public List<R> GetAllItems<R>() where R : IBaseRO, new() {
        var list = new List<R>();
        using ( var wl = new ReaderLock<T>( this ) ) {
            foreach ( var bo in this.items ) {
                T t = bo.Value.Data as T;
                R r = new R();
                r.Initialize( t );
                list.Add( r );
            }
        }
        return list;
    }

e

    //
///<summary>Base class for read-only objects</summary>
public partial interface IBaseRO  {
    void Initialize( IDTO dto );
    void Initialize( object value );
}

Le classi pertinenti devono quindi derivare da questa interfaccia e inizializzare di conseguenza. Si noti che nel mio caso, questo codice fa parte di una classe circostante, che ha già <T> come parametro generico. R, nel mio caso, è anche una classe di sola lettura. IMO, la disponibilità pubblica delle funzioni Initialize () non ha alcun effetto negativo sull'immutabilità. L'utente di questa classe potrebbe inserire un altro oggetto, ma ciò non modificherà la raccolta sottostante.

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.