Créer une instance de type générique dont le constructeur nécessite un paramètre?


230

Si BaseFruitun constructeur accepte un int weight, puis-je instancier un fruit dans une méthode générique comme celle-ci?

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

Un exemple est ajouté derrière les commentaires. Il semble que je ne puisse le faire que si je donne BaseFruitun constructeur sans paramètre et que je remplis tout par le biais de variables membres. Dans mon vrai code (pas sur les fruits), c'est plutôt peu pratique.

-Update-
Il semble donc que cela ne puisse être résolu par aucune contrainte. D'après les réponses, il existe trois solutions possibles:

  • Modèle d'usine
  • Réflexion
  • Activateur

J'ai tendance à penser que la réflexion est la moins nette, mais je ne peux pas choisir entre les deux autres.


1
BTW: aujourd'hui, je résoudrais probablement cela avec la bibliothèque IoC de choix.
Boris Callens

La réflexion et l'activateur sont en fait étroitement liés.
Rob Vermeulen

Réponses:


335

De plus, un exemple plus simple:

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

Notez que l'utilisation de la contrainte new () sur T ne sert qu'à faire vérifier par le compilateur un constructeur public sans paramètre au moment de la compilation, le code réel utilisé pour créer le type est la classe Activator.

Vous devrez vous assurer du constructeur spécifique existant, et ce type d'exigence peut être une odeur de code (ou plutôt quelque chose que vous devriez simplement essayer d'éviter dans la version actuelle sur c #).


Puisque ce constructeur est sur la classe de base (BaseFruit), je sais qu'il aura un constructeur. Mais en effet, si un jour je décide que le basefruit a besoin de plus de paramètres, je pourrais être foutu. Se penchera cependant sur la classe ACtivator. Je n'en ai pas entendu parler auparavant.
Boris Callens

3
Celui-ci a bien fonctionné. Il y a aussi une procédure CreateInstance <T> (), mais qui n'a pas de surcharge pour les paramètres de certains rason ..
Boris Callens

20
Il n'est pas nécessaire d'utiliser new object[] { weight }. CreateInstanceest déclaré avec des paramètres, public static object CreateInstance(Type type, params object[] args)vous pouvez donc simplement le faire return (T) Activator.CreateInstance(typeof(T), weight);. S'il existe plusieurs paramètres, transmettez-les en tant qu'arguments distincts. Ce n'est que si vous avez déjà un énuméré de paramètres construit que vous devez vous soucier de le convertir object[]et de le transmettre à CreateInstance.
ErikE

2
Cela aura des problèmes de performances que j'ai lus. Utilisez plutôt un lambda compilé. vagifabilov.wordpress.com/2010/04/02/…
David

1
@RobVermeulen - Je pense que quelque chose comme une propriété statique sur chaque classe Fruit, qui contient un Funcqui crée la nouvelle instance. Supposons que l' Appleutilisation du constructeur soit new Apple(wgt). Ajoutez ensuite à la Appleclasse cette définition: static Func<float, Fruit> CreateOne { get; } = (wgt) => new Apple(wgt);En usine, définissez public static Fruit CreateFruitGiven(float weight, Func<float, Fruit> createOne) { return createOne(weight); } Usage: Factory.CreateFruit(57.3f, Apple.CreateOne);- qui crée et renvoie un Apple, avec weight=57.3f.
ToolmakerSteve

92

Vous ne pouvez utiliser aucun constructeur paramétré. Vous pouvez utiliser un constructeur sans paramètre si vous avez une where T : new()contrainte " ".

C'est une douleur, mais telle est la vie :(

C'est une des choses que j'aimerais aborder avec les "interfaces statiques" . Vous seriez alors en mesure de contraindre T à inclure des méthodes statiques, des opérateurs et des constructeurs, puis à les appeler.


2
Au moins, vous POUVEZ faire de telles contraintes - Java me déçoit toujours.
Marcel Jackwerth

@ JonSkeet: Si j'ai exposé l'API avec le générique .NET à appeler dans VB6.0..Est-ce que cela fonctionne toujours?
Roy Lee

@Roylee: Je n'en ai aucune idée, mais je ne le pense pas.
Jon Skeet

Je pense que des interfaces statiques pourraient être ajoutées par un compilateur de langage sans modifier le runtime, mais il serait bon que les équipes linguistiques se coordonnent sur les détails. Spécifiez que chaque classe prétendant implémenter une interface statique doit contenir une classe imbriquée avec un nom particulier lié à l'interface, qui définit une instance singleton statique de son propre type. Associé à l'interface serait un type générique statique avec un champ d'instance qui devrait être chargé avec le singleton une fois via Reflection, mais pourrait être utilisé directement après cela.
supercat

Une contrainte de constructeur paramétré pourrait être gérée de la même manière (en utilisant une méthode d'usine et un paramètre générique pour son type de retour); dans aucun cas, rien n'empêcherait le code écrit dans un langage qui ne prend pas en charge une telle fonctionnalité de prétendre implémenter l'interface sans définir le type statique approprié, de sorte que le code écrit à l'aide de ces langages pourrait échouer lors de l'exécution, mais la réflexion pourrait être évitée chez l'utilisateur code.
supercat

61

Oui; changez votre endroit:

where T:BaseFruit, new()

Cependant, cela ne fonctionne qu'avec des constructeurs sans paramètres . Vous devrez avoir un autre moyen de définir votre propriété (définir la propriété elle-même ou quelque chose de similaire).


Si le constructeur n'a pas de paramètres, cela me semble sûr.
PerpetualStudent

Tu m'as sauvé la vie. Je n'ai pas réussi à restreindre T à la classe et au nouveau mot-clé ().
Genotypek

28

La solution la plus simple Activator.CreateInstance<T>()


1
Merci pour la suggestion, elle m'a permis d'arriver là où je devais être. Bien que cela ne vous permette pas d'utiliser un constructeur paramétré. Mais vous pouvez utiliser la variante non générique: Activator.CreateInstance (typeof (T), nouvel objet [] {...}) où le tableau d'objets contient les arguments du constructeur.
Rob Vermeulen

19

Comme Jon l'a souligné, c'est la vie pour contraindre un constructeur sans paramètre. Cependant, une solution différente consiste à utiliser un modèle d'usine. C'est facilement contraignable

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

Encore une autre option consiste à utiliser une approche fonctionnelle. Passez dans une méthode d'usine.

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

2
Bonne suggestion - bien que si vous ne faites pas attention, vous pouvez vous retrouver dans l'enfer de l'API Java DOM, avec des usines à gogo :(
Jon Skeet

Oui, c'est une solution que je pensais moi-même. Mais j'espérais quelque chose dans la ligne des contraintes. Devinez pas alors ..
Boris Callens

@boris, malheureusement, le langage de contrainte que vous recherchez n'existe pas pour le moment
JaredPar

11

Vous pouvez le faire en utilisant la réflexion:

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: ajout du constructeur == vérification nulle.

EDIT: Une variante plus rapide utilisant un 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) });
}

Bien que je n'aime pas les frais généraux de la réflexion, comme d'autres l'ont expliqué, c'est juste la façon dont c'est actuellement. Voyant comment ce constructeur ne sera pas trop appelé, je pourrais y aller. Ou l'usine. Je ne sais pas encore.
Boris Callens

C'est actuellement mon approche préférée car elle n'ajoute pas plus de complexité du côté de l'invocation.
Rob Vermeulen

Mais maintenant, j'ai lu la suggestion de l'activateur, qui a la même méchanceté que la solution de réflexion ci-dessus, mais avec moins de lignes de code :) Je vais opter pour l'option Activator.
Rob Vermeulen

1

En complément de la suggestion de user1471935:

Pour instancier une classe générique à l'aide d'un constructeur avec un ou plusieurs paramètres, vous pouvez désormais utiliser la classe Activator.

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

La liste des objets sont les paramètres que vous souhaitez fournir. Selon Microsoft :

CreateInstance [...] crée une instance du type spécifié en utilisant le constructeur qui correspond le mieux aux paramètres spécifiés.

Il existe également une version générique de CreateInstance ( CreateInstance<T>()) mais celle-ci ne vous permet pas non plus de fournir des paramètres de constructeur.


1

J'ai créé cette méthode:

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

J'utilise cela de cette façon:

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

0

Récemment, je suis tombé sur un problème très similaire. Je voulais juste partager notre solution avec vous tous. Je voulais j'ai créé une instance d'un à Car<CarA>partir d'un objet json en utilisant qui avait une énumération:

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

Il est toujours possible, avec des performances élevées, en procédant comme suit:

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

et

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

Les classes pertinentes doivent alors dériver de cette interface et s'initialiser en conséquence. Veuillez noter que dans mon cas, ce code fait partie d'une classe environnante, qui a déjà <T> comme paramètre générique. R, dans mon cas, est également une classe en lecture seule. OMI, la disponibilité publique des fonctions Initialize () n'a aucun effet négatif sur l'immuabilité. L'utilisateur de cette classe pourrait placer un autre objet, mais cela ne modifierait pas la collection sous-jacente.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.