Instanz eines generischen Typs erstellen, deren Konstruktor einen Parameter benötigt?

229

Wenn BaseFruitein Konstruktor einen akzeptiert int weight, kann ich ein Stück Obst in einer generischen Methode wie dieser instanziieren?

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

Ein Beispiel wird hinter Kommentaren hinzugefügt. Es scheint, dass ich dies nur tun kann, wenn ich BaseFruiteinen parameterlosen Konstruktor gebe und dann alles über Mitgliedsvariablen ausfülle. In meinem wirklichen Code (nicht über Obst) ist dies ziemlich unpraktisch.

-Update-
Es scheint also, dass es dann in keiner Weise durch Einschränkungen gelöst werden kann. Aus den Antworten ergeben sich drei mögliche Lösungen:

  • Fabrikmuster
  • Reflexion
  • Aktivator

Ich neige dazu zu denken, dass Reflexion die am wenigsten saubere ist, aber ich kann mich nicht zwischen den beiden anderen entscheiden.

Boris Callens
quelle
1
Übrigens: Heute würde ich das wahrscheinlich mit der IoC-Bibliothek Ihrer Wahl lösen.
Boris Callens
Reflexion und Aktivator sind tatsächlich eng miteinander verbunden.
Rob Vermeulen

Antworten:

334

Zusätzlich ein einfacheres Beispiel:

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

Beachten Sie, dass die Verwendung der new () - Einschränkung für T nur dazu dient, dass der Compiler zur Kompilierungszeit nach einem öffentlichen parameterlosen Konstruktor sucht. Der tatsächliche Code, der zum Erstellen des Typs verwendet wird, ist die Activator-Klasse.

Sie müssen sich über den vorhandenen spezifischen Konstruktor informieren, und diese Art von Anforderung kann ein Codegeruch sein (oder eher etwas, das Sie in der aktuellen Version auf c # vermeiden sollten).

meandmycode
quelle
Da sich dieser Konstruktor in der Basisklasse (BaseFruit) befindet, weiß ich, dass er einen Konstruktor haben wird. Aber in der Tat, wenn ich eines Tages beschließe, dass Basisfrucht mehr Parameter benötigt, könnte ich geschraubt werden. Werde aber in die ACtivator-Klasse schauen. Ich habe noch nie davon gehört.
Boris Callens
3
Dieser hat gut geklappt. Es gibt auch eine CreateInstance <T> () Prozedur, aber das hat keine Überladung für Parameter für einige Rason.
Boris Callens
20
Es besteht keine Notwendigkeit zu verwenden new object[] { weight }. CreateInstancewird mit params deklariert public static object CreateInstance(Type type, params object[] args), also kannst du es einfach tun return (T) Activator.CreateInstance(typeof(T), weight);. Wenn mehrere Parameter vorhanden sind, übergeben Sie diese als separate Argumente. Nur wenn Sie bereits eine Aufzählung von Parametern erstellt haben, sollten Sie sich die Mühe machen, diese zu konvertieren object[]und an diese zu übergeben CreateInstance.
ErikE
2
Dies wird Leistungsprobleme haben, die ich gelesen habe. Verwenden Sie stattdessen ein kompiliertes Lambda. vagifabilov.wordpress.com/2010/04/02/…
David
1
@RobVermeulen - Ich denke so etwas wie eine statische Eigenschaft für jede Fruit-Klasse, die eine enthält, Funcdie die neue Instanz erstellt. Angenommen, Appledie Verwendung des Konstruktors ist new Apple(wgt). Fügen Sie dann Applediese Definition zur Klasse hinzu: static Func<float, Fruit> CreateOne { get; } = (wgt) => new Apple(wgt);In Factory Define public static Fruit CreateFruitGiven(float weight, Func<float, Fruit> createOne) { return createOne(weight); } Usage: Factory.CreateFruit(57.3f, Apple.CreateOne);- erstellt und gibt ein Apple, with zurück weight=57.3f.
ToolmakerSteve
92

Sie können keinen parametrisierten Konstruktor verwenden. Sie können einen parameterlosen Konstruktor verwenden, wenn Sie eine " where T : new()" Einschränkung haben.

Es ist ein Schmerz, aber so ist das Leben :(

Dies ist eines der Dinge, die ich mit "statischen Schnittstellen" ansprechen möchte . Sie können T dann so einschränken, dass es statische Methoden, Operatoren und Konstruktoren enthält, und diese dann aufrufen.

Jon Skeet
quelle
2
Zumindest KÖNNEN Sie solche Einschränkungen tun - Java enttäuscht mich immer.
Marcel Jackwerth
@ JonSkeet: Wenn ich die API mit .NET generic für den Aufruf in VB6.0 verfügbar gemacht habe. Funktioniert sie noch?
Roy Lee
@ Roylee: Ich habe keine Ahnung, aber ich vermute nicht.
Jon Skeet
Ich würde denken, dass statische Schnittstellen von einem Sprachcompiler ohne Änderungen an der Laufzeit hinzugefügt werden könnten, obwohl es gut wäre, wenn Sprachteams die Einzelheiten koordinieren würden. Geben Sie an, dass jede Klasse, die behauptet, eine statische Schnittstelle zu implementieren, eine verschachtelte Klasse mit einem bestimmten schnittstellenbezogenen Namen enthalten muss, der eine statische Singleton-Instanz ihres eigenen Typs definiert. Mit der Schnittstelle wäre ein statischer generischer Typ mit einem Instanzfeld verbunden, das einmal über Reflection mit dem Singleton geladen werden müsste, aber direkt danach verwendet werden könnte.
Supercat
Eine parametrisierte Konstruktorbeschränkung kann auf die gleiche Weise behandelt werden (unter Verwendung einer Factory-Methode und eines generischen Parameters für ihren Rückgabetyp). In keinem Fall würde irgendetwas verhindern, dass Code, der in einer Sprache geschrieben wurde, die eine solche Funktion nicht unterstützt, behauptet, die Schnittstelle zu implementieren, ohne den richtigen statischen Typ zu definieren, sodass Code, der mit solchen Sprachen geschrieben wurde, zur Laufzeit fehlschlagen könnte, aber Reflection im Benutzer vermieden werden könnte Code.
Supercat
61

Ja; Ändern Sie Ihren Aufenthaltsort:

where T:BaseFruit, new()

Dies funktioniert jedoch nur mit parameterlosen Konstruktoren. Sie müssen über andere Möglichkeiten zum Festlegen Ihrer Eigenschaft verfügen (Festlegen der Eigenschaft selbst oder ähnliches).

Adam Robinson
quelle
Wenn der Konstruktor keine Parameter hat, scheint mir dies sicher zu sein.
PerpetualStudent
Du hast mein Leben gerettet. Ich konnte T nicht auf class und new () Schlüsselwort beschränken.
Genotypek
28

Einfachste Lösung Activator.CreateInstance<T>()

user1471935
quelle
1
Vielen Dank für den Vorschlag, er hat mich dahin gebracht, wo ich sein musste. Dies erlaubt Ihnen jedoch nicht, einen parametrisierten Konstruktor zu verwenden. Sie können jedoch die nicht generische Variante verwenden: Activator.CreateInstance (typeof (T), neues Objekt [] {...}), wobei das Objektarray die Argumente für den Konstruktor enthält.
Rob Vermeulen
19

Wie Jon betonte, ist dies das Leben, um einen nicht parameterlosen Konstruktor einzuschränken. Eine andere Lösung besteht jedoch darin, ein Fabrikmuster zu verwenden. Dies ist leicht einschränkbar

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

Eine weitere Option ist die Verwendung eines funktionalen Ansatzes. Übergeben Sie eine Werksmethode.

public void AddFruit<T>(Func<int,T> factoryDel) where T : BaseFruit { 
  BaseFruit fruit = factoryDel(weight); /* new Apple(150); */
  fruit.Enlist(fruitManager);
}
JaredPar
quelle
2
Guter Vorschlag - obwohl, wenn Sie nicht aufpassen, Sie in der Hölle der Java DOM API landen können, mit Fabriken in Hülle und Fülle :(
Jon Skeet
Ja, das ist eine Lösung, die ich mir überlegt habe. Aber ich hatte auf etwas in der Reihe der Zwänge gehofft. Ratet mal nicht ..
Boris Callens
@boris, leider existiert die gesuchte Einschränkungssprache zu diesem Zeitpunkt nicht
JaredPar
11

Sie können dies mithilfe der Reflexion tun:

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

BEARBEITEN: Konstruktor == Nullprüfung hinzugefügt.

EDIT: Eine schnellere Variante mit einem 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) });
}
mmmmmmmm
quelle
Obwohl ich den Overhead der Reflexion nicht mag, wie andere erklärt haben, ist dies derzeit einfach so. Wenn ich sehe, dass dieser Konstruktor nicht zu oft aufgerufen wird, könnte ich damit weitermachen. Oder die Fabrik. Weiß noch nicht.
Boris Callens
Dies ist derzeit mein bevorzugter Ansatz, da er die Aufrufseite nicht komplexer macht.
Rob Vermeulen
Aber jetzt habe ich über den Activator-Vorschlag gelesen, der ähnlich unangenehm ist wie die obige Reflexionslösung, aber mit weniger Codezeilen :) Ich werde mich für die Activator-Option entscheiden.
Rob Vermeulen
1

Als Ergänzung zum Vorschlag von user1471935:

Um eine generische Klasse mithilfe eines Konstruktors mit einem oder mehreren Parametern zu instanziieren, können Sie jetzt die Activator-Klasse verwenden.

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

Die Liste der Objekte sind die Parameter, die Sie angeben möchten. Laut Microsoft :

CreateInstance [...] erstellt eine Instanz des angegebenen Typs mit dem Konstruktor, der den angegebenen Parametern am besten entspricht.

Es gibt auch eine generische Version von CreateInstance ( CreateInstance<T>()), aber diese erlaubt es Ihnen auch nicht, Konstruktorparameter anzugeben .

Rob Vermeulen
quelle
1

Ich habe diese Methode erstellt:

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

Ich benutze das so:

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

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

Code:

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

B instanceB = new B();
instanceB = ConvertParentObjToChildObj<A,B>(instanceA);
Diocleziano Carletti
quelle
0

Kürzlich bin ich auf ein sehr ähnliches Problem gestoßen. Ich wollte nur unsere Lösung mit Ihnen allen teilen. Ich wollte eine Instanz von a Car<CarA>aus einem json-Objekt erstellen, das eine Aufzählung hatte:

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) });
Farshid
quelle
-2

Mit hoher Leistung ist es immer noch möglich, Folgendes zu tun:

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

und

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

Die relevanten Klassen müssen dann von dieser Schnittstelle abgeleitet und entsprechend initialisiert werden. Bitte beachten Sie, dass dieser Code in meinem Fall Teil einer umgebenden Klasse ist, die bereits <T> als generischen Parameter hat. R ist in meinem Fall auch eine schreibgeschützte Klasse. IMO hat die öffentliche Verfügbarkeit von Initialize () -Funktionen keinen negativen Einfluss auf die Unveränderlichkeit. Der Benutzer dieser Klasse könnte ein anderes Objekt einfügen, dies würde jedoch die zugrunde liegende Sammlung nicht ändern.

cskwg
quelle