Passer des arguments à C # générique new () de type modèle

409

J'essaie de créer un nouvel objet de type T via son constructeur lors de l'ajout à la liste.

Je reçois une erreur de compilation: Le message d'erreur est:

'T': ne peut pas fournir d'arguments lors de la création d'une instance d'une variable

Mais mes classes ont un argument constructeur! Comment puis-je faire fonctionner cela?

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.
   } 
   ...
}
KG.
la source
2
doublon possible de Créer une instance de type générique?
nawfal
2
Proposition pour intégrer
Ian Kemp
Dans la documentation de Microsoft, voir Erreur du compilateur CS0417 .
DavidRR
1
La proposition visant à intégrer
réduction de l'activité

Réponses:

410

Afin de créer une instance d'un type générique dans une fonction, vous devez la contraindre avec le "nouveau" indicateur.

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

Cependant, cela ne fonctionnera que si vous souhaitez appeler le constructeur qui n'a pas de paramètres. Pas le cas ici. Au lieu de cela, vous devrez fournir un autre paramètre qui permet la création d'objet basé sur des paramètres. Le plus simple est une fonction.

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

Vous pouvez alors l'appeler ainsi

GetAllItems<Foo>(..., l => new Foo(l));
JaredPar
la source
Comment cela fonctionnerait-il s'il était appelé en interne à partir d'une classe générique? J'ai publié mon code dans une réponse ci-dessous. Je ne connais pas la classe concrète en interne, car c'est une classe générique. Y a-t-il un moyen de contourner cela. Je ne veux pas utiliser l'autre suggestion d'utiliser la syntaxe d'initialisation de propriété car cela contournera la logique que j'ai dans le constructeur
ChrisCa
ajouté mon code à une autre question stackoverflow.com/questions/1682310/…
ChrisCa
21
C'est actuellement l'une des limitations les plus ennuyeuses de C #. Je voudrais rendre mes cours immuables: le fait d'avoir uniquement des setters privés rendrait la classe impossible d'être dans un état invalide par des effets secondaires. J'aime aussi utiliser Func et lambda, mais je sais que c'est toujours un problème dans le monde des affaires car généralement les programmeurs ne connaissent pas encore lambdas et cela rend votre classe plus difficile à comprendre.
Tuomas Hietanen
1
Merci. Dans mon cas, je connais les arguments du constructeur lorsque j'appelle la méthode, j'avais juste besoin de contourner la limitation du paramètre Type selon laquelle il ne pouvait pas être construit avec des paramètres, j'ai donc utilisé un thunk . Le thunk est un paramètre facultatif de la méthode, et je ne l'utilise que s'il est fourni: T result = thunk == null ? new T() : thunk(); l'avantage pour moi est de consolider la logique de la Tcréation en un seul endroit plutôt que de créer parfois à l' Tintérieur et parfois à l'extérieur de la méthode.
Carl G
Je pense que c'est l'un des endroits où le langage C # décide de dire non au programmeur et d'arrêter de dire oui tout le temps! Bien que cette approche soit une façon un peu maladroite de créer un objet mais je dois l'utiliser pour l'instant.
AmirHossein Rezaei
331

dans .Net 3.5 et après, vous pouvez utiliser la classe d'activateur:

(T)Activator.CreateInstance(typeof(T), args)
user287107
la source
1
nous pourrions également utiliser l'arbre d'expression pour construire l'objet
Welly Tambunan
4
Qu'est-ce que args? un objet[]?
Rodney P. Barbati
3
Oui, args est un objet [] dans lequel vous spécifiez les valeurs à fournir au constructeur du T: "nouvel objet [] {par1, par2}"
TechNyquist
3
AVERTISSEMENT: si vous avez un constructeur dédié juste pour le plaisir de Activator.CreateInstancecette chose, il semblerait que votre constructeur ne soit pas utilisé du tout, et quelqu'un pourrait essayer de "nettoyer" et de le supprimer (pour provoquer une erreur d'exécution à dans le futur). Vous voudrez peut-être envisager d'ajouter une fonction fictive dans laquelle vous utilisez ce constructeur afin d'obtenir une erreur de compilation si vous essayez de le supprimer.
2017
51

Étant donné que personne n'a pris la peine de publier la réponse «Réflexion» (que je pense personnellement être la meilleure réponse), voici:

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

Modifier: cette réponse est obsolète en raison de Activator.CreateInstance de .NET 3.5, mais elle est toujours utile dans les anciennes versions de .NET.

James Jones
la source
Ma compréhension est que la plupart des performances sont affectées à l'acquisition du ConstructorInfo en premier lieu. Ne me croyez pas sur parole sans en faire le profil. Si tel est le cas, le simple stockage du ConstructorInfo pour une réutilisation ultérieure pourrait atténuer la perte de performances des instanciations répétées grâce à la réflexion.
Kelsie
19
Je pense que le manque de vérification au moment de la compilation est plus préoccupant.
Dave Van den Eynde
1
@James, je suis d'accord, j'ai été surpris de ne pas voir cela comme la "réponse". En fait, j'ai cherché sur cette question en espérant trouver un bel exemple facile (comme le vôtre) car cela fait si longtemps que je n'ai pas réfléchi. Quoi qu'il en soit, +1 de moi, mais +1 sur la réponse de l'activateur aussi. J'ai regardé ce que fait Activator, et il s'avère que ce qui se produit est une réflexion très bien conçue. :)
Mike
L'appel GetConstructor () est cher, il vaut donc la peine d'être mis en cache avant la boucle. De cette façon, en appelant uniquement Invoke () à l'intérieur de la boucle, c'est beaucoup plus rapide que d'appeler les deux ou même d'utiliser Activator.CreateInstance ().
Cosmin Rus
30

Initialiseur d'objet

Si votre constructeur avec le paramètre ne fait rien d'autre que de définir une propriété, vous pouvez le faire en C # 3 ou mieux en utilisant un initialiseur d'objet plutôt qu'en appelant un constructeur (ce qui est impossible, comme cela a été mentionné):

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

En utilisant cela, vous pouvez également toujours placer n'importe quelle logique de constructeur dans le constructeur par défaut (vide).

Activator.CreateInstance ()

Alternativement, vous pouvez appeler Activator.CreateInstance () comme ceci:

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

Notez qu'Activator.CreateInstance peut avoir une surcharge de performances que vous voudrez peut-être éviter si la vitesse d'exécution est une priorité absolue et qu'une autre option peut être maintenue.

Tim Lehner
la source
cela empêche Tde protéger ses invariants (étant donné qu'il Ta> 0 dépendances ou valeurs requises, vous pouvez maintenant créer des instances Tqui sont dans un état invalide / inutilisable. sauf si Tquelque chose de mort simple comme un modèle de vue DTO och, je dirais éviter cela.
sara
20

Très vieille question, mais nouvelle réponse ;-)

La version ExpressionTree : (je pense que la solution la plus rapide et la plus propre)

Comme l'a dit Welly Tambunan , "nous pourrions également utiliser l'arbre d'expression pour construire l'objet"

Cela générera un «constructeur» (fonction) pour le type / les paramètres donnés. Il renvoie un délégué et accepte les types de paramètres comme un tableau d'objets.

C'est ici:

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

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

Usage:

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

entrez la description de l'image ici


Un autre exemple: passer les types sous forme de tableau

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

C'est équivalent au code qui est généré:

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

Petit inconvénient

Tous les paramètres des types de valeurs sont encadrés lorsqu'ils sont transmis comme un tableau d'objets.


Test de performance simple:

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

Résultats:

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

L'utilisation Expressionsest +/- 8 fois plus rapide que l'invocation de ConstructorInfoet +/- 20 fois plus rapide que l'utilisation de laActivator

Jeroen van Langen
la source
Avez-vous une idée de ce que vous devez faire si vous souhaitez construire MyClass <T> avec le constructeur public MyClass (données T). Dans ce cas, Expression.Convert lève une exception et si j'utilise la classe de base de contrainte générique pour convertir en, puis Expression.New lève parce que les informations du constructeur sont pour un type générique
Mason
@Mason (a pris un certain temps pour répondre ;-)) var myConstructor = CreateConstructor(typeof(MyClass<int>), typeof(int));cela fonctionne bien. Je ne sais pas.
Jeroen van Langen
19

Cela ne fonctionnera pas dans votre situation. Vous ne pouvez spécifier que la contrainte qu'il a un constructeur vide:

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

Ce que vous pourriez faire, c'est utiliser l'injection de propriétés en définissant cette interface:

public interface ITakesAListItem
{
   ListItem Item { set; }
}

Ensuite, vous pouvez modifier votre méthode comme suit:

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'autre alternative est la Funcméthode décrite par JaredPar.

Garry Shutler
la source
cela contournerait toute logique du constructeur qui prend les arguments, non? Je voudrais faire quelque chose comme l'approche de Jared mais j'appelle la méthode en interne au sein de la classe, donc je ne sais pas quel est le type de béton ... hmmm
ChrisCa
3
À droite, cela appelle la logique du constructeur par défaut T (), puis définit simplement la propriété "Item". Si vous essayez d'invoquer la logique d'un constructeur non par défaut, cela ne vous aidera pas.
Scott Stafford
7

Vous devez ajouter où T: new () pour que le compilateur sache que T est garanti pour fournir un constructeur par défaut.

public static string GetAllItems<T>(...) where T: new()
Richard
la source
1
MISE À JOUR: Le message d'erreur correct est: «T»: ne peut pas fournir d'arguments lors de la création d'une instance d'une variable
LB.
C'est parce que vous n'utilisez pas de constructeur vide, vous lui passez un argument d'objet. Il n'y a aucun moyen de gérer cela sans spécifier que le type générique a un nouveau paramètre (objet).
Min
Ensuite, vous devrez soit: 1. Utiliser la réflexion 2. Passer le paramètre dans une méthode d'initialisation au lieu du constructeur, où la méthode d'initialisation appartient à une interface que votre type implémente et qui est incluse dans le où T: ... déclaration. L'option 1 est l'impact le plus faible pour le reste de votre code, mais l'option 2 fournit la vérification du temps de compilation.
Richard
N'utilisez pas la réflexion! Il existe d'autres façons, comme indiqué dans d'autres réponses, qui vous procurent le même effet.
Garry Shutler,
@Garry - Je conviens que la réflexion n'est pas nécessairement la meilleure approche, mais elle vous permet de réaliser ce qui est requis avec un minimum de modifications sur le reste de la base de code. Cela dit, je préfère de loin l'approche des délégués d'usine de @JaredPar.
Richard
7

Si vous souhaitez simplement initialiser un champ membre ou une propriété avec le paramètre constructeur, en C #> = 3, vous pouvez le faire très facilement:

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

C'est la même chose que Garry Shutler a dit, mais j'aimerais ajouter une note supplémentaire.

Bien sûr, vous pouvez utiliser une astuce de propriété pour faire plus de choses que simplement définir une valeur de champ. Une propriété "set ()" peut déclencher tout traitement nécessaire pour configurer ses champs associés et tout autre besoin pour l'objet lui-même, y compris une vérification pour voir si une initialisation complète doit avoir lieu avant que l'objet ne soit utilisé, simulant une construction complète ( oui, c'est une solution de contournement laide, mais elle surmonte la nouvelle limitation de M $).

Je ne peux pas être sûr s'il s'agit d'un trou planifié ou d'un effet secondaire accidentel, mais cela fonctionne.

C'est très drôle de voir comment les personnes atteintes de SEP ajoutent de nouvelles fonctionnalités au langage et ne semblent pas faire une analyse complète des effets secondaires. La chose générique entière en est une bonne preuve ...

fljx
la source
1
Les deux contraintes sont nécessaires. InterfaceOrBaseClass informe le compilateur du champ / propriété BaseMemberItem. Si la contrainte "new ()" est commentée, elle déclenchera l'erreur: Erreur 6 Impossible de créer une instance du type de variable 'T' car elle n'a pas la contrainte new ()
fljx
Une situation que j'ai rencontrée n'était pas exactement comme la question posée ici, mais cette réponse m'a conduit là où je devais aller et cela semble très bien fonctionner.
RubyHaus
5
Chaque fois que quelqu'un mentionne Microsoft comme "M $", une petite partie de mon âme souffre.
Mathias Lykkegaard Lorenzen
6

J'ai trouvé que j'obtenais une erreur «ne peut pas fournir d'arguments lors de la création d'une instance de type paramètre T», j'ai donc dû faire ceci:

var x = Activator.CreateInstance(typeof(T), args) as T;
chris31389
la source
5

Si vous avez accès à la classe que vous allez utiliser, vous pouvez utiliser cette approche que j'ai utilisée.

Créez une interface qui a un créateur alternatif:

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

Créez vos classes avec un créateur vide et implémentez cette méthode:

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

Utilisez maintenant vos méthodes génériques:

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

Si vous n'y avez pas accès, encapsulez la classe cible:

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);
    }
}
Daniel Möller
la source
0

C'est un peu mucky, et quand je dis un peu mucky, je peux vouloir dire révoltant, mais en supposant que vous pouvez fournir votre type paramétré avec un constructeur vide, alors:

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

Vous permettra effectivement de construire un objet à partir d'un type paramétré avec un argument. Dans ce cas, je suppose que le constructeur que je veux a un seul argument de type object. Nous créons une instance fictive de T en utilisant le constructeur vide de contrainte autorisée, puis utilisons la réflexion pour obtenir l'un de ses autres constructeurs.

silasdavis
la source
0

J'utilise parfois une approche qui ressemble aux réponses en utilisant l'injection de propriété, mais garde le code plus propre. Au lieu d'avoir une classe / interface de base avec un ensemble de propriétés, elle ne contient qu'une méthode (virtuelle) Initialize () - qui agit comme un "constructeur du pauvre". Ensuite, vous pouvez laisser chaque classe gérer sa propre initialisation comme le ferait un constructeur, ce qui ajoute également un moyen pratique de gérer les chaînes d'héritage.

Si je me retrouve souvent dans des situations où je veux que chaque classe de la chaîne initialise ses propriétés uniques, puis appelle la méthode Initialize () de son parent, qui à son tour initialise les propriétés uniques du parent, etc. Cela est particulièrement utile lorsque vous avez différentes classes, mais avec une hiérarchie similaire, par exemple des objets métier qui sont mappés vers / depuis DTO: s.

Exemple qui utilise un dictionnaire commun pour l'initialisation:

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;
    }
}
Anders
la source
0

Si tout ce dont vous avez besoin est une conversion de ListItem vers votre type T, vous pouvez implémenter cette conversion dans la classe T comme opérateur de conversion.

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);
    } 
    ...
}
NightmareZ
la source
-4

Je crois que vous devez contraindre T avec une instruction where pour autoriser uniquement les objets avec un nouveau constructeur.

Maintenant, il accepte tout, y compris les objets sans lui.

klkitchens
la source
1
Vous voudrez peut-être modifier cette réponse, car cela a été modifié dans la question après avoir répondu, ce qui laisse cette réponse hors de son contexte.
shuttle87