En C #, comment instancier un type générique passé dans une méthode?

99

Comment puis-je instancier le type T dans ma InstantiateType<T>méthode ci-dessous?

J'obtiens l'erreur: «T» est un «paramètre de type» mais est utilisé comme une «variable». :

(DÉFILEZ VERS LE BAS POUR UNE RÉPONSE REFACTORÉE)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestGeneric33
{
    class Program
    {
        static void Main(string[] args)
        {
            Container container = new Container();
            Console.WriteLine(container.InstantiateType<Customer>("Jim", "Smith"));
            Console.WriteLine(container.InstantiateType<Employee>("Joe", "Thompson"));
            Console.ReadLine();
        }
    }

    public class Container
    {
        public T InstantiateType<T>(string firstName, string lastName) where T : IPerson
        {
            T obj = T();
            obj.FirstName(firstName);
            obj.LastName(lastName);
            return obj;
        }

    }

    public interface IPerson
    {
        string FirstName { get; set; }
        string LastName { get; set; }
    }

    public class Customer : IPerson
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Company { get; set; }
    }

    public class Employee : IPerson
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int EmployeeNumber { get; set; }
    }
}

RÉPONSE RÉFACTURÉE:

Merci pour tous les commentaires, ils m'ont mis sur la bonne voie, c'est ce que je voulais faire:

using System;

namespace TestGeneric33
{
    class Program
    {
        static void Main(string[] args)
        {
            Container container = new Container();
            Customer customer1 = container.InstantiateType<Customer>("Jim", "Smith");
            Employee employee1 = container.InstantiateType<Employee>("Joe", "Thompson");
            Console.WriteLine(PersonDisplayer.SimpleDisplay(customer1));
            Console.WriteLine(PersonDisplayer.SimpleDisplay(employee1));
            Console.ReadLine();
        }
    }

    public class Container
    {
        public T InstantiateType<T>(string firstName, string lastName) where T : IPerson, new()
        {
            T obj = new T();
            obj.FirstName = firstName;
            obj.LastName = lastName;
            return obj;
        }
    }

    public interface IPerson
    {
        string FirstName { get; set; }
        string LastName { get; set; }
    }

    public class PersonDisplayer
    {
        private IPerson _person;

        public PersonDisplayer(IPerson person)
        {
            _person = person;
        }

        public string SimpleDisplay()
        {
            return String.Format("{1}, {0}", _person.FirstName, _person.LastName);
        }

        public static string SimpleDisplay(IPerson person)
        {
            PersonDisplayer personDisplayer = new PersonDisplayer(person);
            return personDisplayer.SimpleDisplay();
        }
    }

    public class Customer : IPerson
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Company { get; set; }
    }

    public class Employee : IPerson
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int EmployeeNumber { get; set; }
    }
}
Edward Tanguay
la source
+1 pour passer à un meilleur modèle de conception.
Joel Coehoorn
+1 pour un code extrêmement bien saisi, une rareté.
nawfal

Réponses:

131

Déclarez votre méthode comme ceci:

public string InstantiateType<T>(string firstName, string lastName) 
              where T : IPerson, new()

Notez la contrainte supplémentaire à la fin. Créez ensuite une newinstance dans le corps de la méthode:

T obj = new T();    
Joel Coehoorn
la source
4
J'écris en C # depuis des années avec de gros abus de typage générique à mon époque, et je ne savais JAMAIS que vous pouviez définir une contrainte comme celle-ci pour instancier un type générique. Merci beaucoup!
Nicolas Martel
très très gentil!!
Sotiris Zegiannis
Et si AUCUN type spécifié, est-ce possible?
jj
31

Deux façons.

Sans spécifier le type doit avoir un constructeur:

T obj = default(T); //which will produce null for reference types

Avec un constructeur:

T obj = new T();

Mais cela nécessite la clause:

where T : new()
Annakata
la source
1
Le premier attribuera null plutôt que de créer une instance pour les types de référence.
Joel Coehoorn
1
Oui. Vous devez utiliser la réflexion pour créer des types sans constructeur par défaut, la valeur par défaut (T) est null pour tous les types de référence.
Dan C.
1
Oui absolument, inclus par souci d'exhaustivité vraiment.
annakata
13

Pour prolonger les réponses ci-dessus, l'ajout d'une where T:new()contrainte à une méthode générique exigera que T ait un constructeur public sans paramètre.

Si vous voulez éviter cela - et dans un modèle de fabrique, vous forcez parfois les autres à passer par votre méthode de fabrique et non directement par le constructeur - alors l'alternative est d'utiliser Reflection ( Activator.CreateInstance...) et de garder le constructeur par défaut privé. Mais cela vient avec une pénalité de performance, bien sûr.

Dan C.
la source
Ce n'est pas la première fois que les gens votent contre "toutes les autres réponses" :)
Dan C.
J'admets que parfois ne pas voter à la hausse des réponses `` concurrentes '' jusqu'à ce que le crépuscule se soit réglé sur une question: DI suppose que le karma (sans points) les réglera!
Ruben Bartelink
8

vous voulez un nouveau T (), mais vous devrez également ajouter , new()à la wherespécification de la méthode d'usine

Ruben Bartelink
la source
Je l'ai remonté, je l'ai compris, aidé, il semble qu'en général les gens aiment mieux le code posté que les descriptions ici
Edward Tanguay
Merci, le monde a de nouveau du sens!
Ruben Bartelink
correct mais votre réponse est certes un peu courte;)
Lorenz Lo Sauer
4

Un peu vieux mais pour d'autres à la recherche d'une solution, cela pourrait peut-être être intéressant: http://daniel.wertheim.se/2011/12/29/c-generic-factory-with-support-for-private-constructors/

Deux solutions. Un utilisant Activator et un utilisant des Lambdas compilés.

//Person has private ctor
var person = Factory<Person>.Create(p => p.Name = "Daniel");

public static class Factory<T> where T : class 
{
    private static readonly Func<T> FactoryFn;

    static Factory()
    {
        //FactoryFn = CreateUsingActivator();

        FactoryFn = CreateUsingLambdas();
    }

    private static Func<T> CreateUsingActivator()
    {
        var type = typeof(T);

        Func<T> f = () => Activator.CreateInstance(type, true) as T;

        return f;
    }

    private static Func<T> CreateUsingLambdas()
    {
        var type = typeof(T);

        var ctor = type.GetConstructor(
            BindingFlags.Instance | BindingFlags.CreateInstance |
            BindingFlags.NonPublic,
            null, new Type[] { }, null);

        var ctorExpression = Expression.New(ctor);
        return Expression.Lambda<Func<T>>(ctorExpression).Compile();
    }

    public static T Create(Action<T> init)
    {
        var instance = FactoryFn();

        init(instance);

        return instance;
    }
}
Daniel
la source
2

Vous pouvez également utiliser la réflexion pour récupérer le constructeur de l'objet et l'instancier de cette façon:

var c = typeof(T).GetConstructor();
T t = (T)c.Invoke();
pimbrouwers
la source
1

Utilisation d'une classe d'usine pour construire votre objet avec une expression lamba compilée: Le moyen le plus rapide que j'ai trouvé pour instancier un type générique.

public static class FactoryContructor<T>
{
    private static readonly Func<T> New =
        Expression.Lambda<Func<T>>(Expression.New(typeof (T))).Compile();

    public static T Create()
    {
        return New();
    }
}

Voici les étapes que j'ai suivies pour configurer le benchmark.

Créer ma méthode de test de référence:

static void Benchmark(Action action, int iterationCount, string text)
{
    GC.Collect();
    var sw = new Stopwatch();
    action(); // Execute once before

    sw.Start();
    for (var i = 0; i <= iterationCount; i++)
    {
        action();
    }

    sw.Stop();
    System.Console.WriteLine(text + ", Elapsed: {0}ms", sw.ElapsedMilliseconds);
}

J'ai également essayé d'utiliser une méthode d'usine:

public static T FactoryMethod<T>() where T : new()
{
    return new T();
}

Pour les tests, j'ai créé la classe la plus simple:

public class A { }

Le script à tester:

const int iterations = 1000000;
Benchmark(() => new A(), iterations, "new A()");
Benchmark(() => FactoryMethod<A>(), iterations, "FactoryMethod<A>()");
Benchmark(() => FactoryClass<A>.Create(), iterations, "FactoryClass<A>.Create()");
Benchmark(() => Activator.CreateInstance<A>(), iterations, "Activator.CreateInstance<A>()");
Benchmark(() => Activator.CreateInstance(typeof (A)), iterations, "Activator.CreateInstance(typeof (A))");

Résultats sur 1000000 d'itérations:

nouveau A (): 11ms

Méthode d'usine A (): 275 ms

FactoryClass A .Create (): 56 ms

Activator.CreateInstance A (): 235 ms

Activator.CreateInstance (typeof (A)): 157 ms

Remarques : J'ai testé à la fois .NET Framework 4.5 et 4.6 (résultats équivalents).

Thomas
la source
0

Au lieu de créer une fonction pour instancier le type

public T InstantiateType<T>(string firstName, string lastName) where T : IPerson, new()
    {
        T obj = new T();
        obj.FirstName = firstName;
        obj.LastName = lastName;
        return obj;
    }

tu aurais pu le faire comme ça

T obj = new T { FirstName = firstName, LastName = lastname };
TMul
la source
1
Cela ne répond pas à la question posée. Le vrai problème ici était qu'il avait besoin de créer une nouvelle instance de la classe générique. C'était peut-être involontaire, mais il semble que vous disiez que l'utilisation d'un initialiseur résoudrait le problème d'origine, mais ce n'est pas le cas. La new()contrainte est toujours nécessaire sur le type générique pour que votre réponse fonctionne.
Utilisateur
Si vous essayez d'être utile et suggérez que l'initialiseur est un outil utile ici, vous devriez le publier sous forme de commentaire, pas comme une autre réponse.
Utilisateur