En C #, ¿cómo crear una instancia de un tipo genérico pasado dentro de un método?

98

¿Cómo puedo crear una instancia del tipo T dentro de mi InstantiateType<T>método a continuación?

Recibo el error: 'T' es un 'parámetro de tipo' pero se usa como una 'variable'. :

(DESPLAZARSE HACIA ABAJO PARA OBTENER UNA RESPUESTA REFACTADA)

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

RESPUESTA REFACTORADA:

Gracias por todos los comentarios, me pusieron en el camino correcto, esto es lo que quería hacer:

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
fuente
+1 para cambiar a un mejor patrón de diseño.
Joel Coehoorn
+1 para código muy bien escrito, una rareza.
nawfal

Respuestas:

131

Declare su método de esta manera:

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

Observe la restricción adicional al final. Luego crea una newinstancia en el cuerpo del método:

T obj = new T();    
Joel Coehoorn
fuente
4
He estado escribiendo C # durante años con un gran abuso de escritura genérica en mis días, y NUNCA supe que podrías definir una restricción como esta para crear una instancia de un tipo genérico. ¡Muchas gracias!
Nicolas Martel
¡¡muy, muy agradable!!
Sotiris Zegiannis
¿Qué pasa si NO se especifica un tipo, es eso posible?
jj
31

Un par de formas.

Sin especificar el tipo debe tener un constructor:

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

Con un constructor:

T obj = new T();

Pero esto requiere la cláusula:

where T : new()
Annakata
fuente
1
El primero asignará nulo en lugar de crear una instancia para los tipos de referencia.
Joel Coehoorn
1
Sí. Debe usar la reflexión para crear tipos sin un constructor predeterminado, el valor predeterminado (T) es nulo para todos los tipos de referencia.
Dan C.
1
Sí, absolutamente, incluido para completar realmente.
Annakata
13

Para ampliar las respuestas anteriores, agregar where T:new()restricciones a un método genérico requerirá que T tenga un constructor público sin parámetros.

Si desea evitar eso, y en un patrón de fábrica, a veces obliga a los demás a pasar por su método de fábrica y no directamente a través del constructor, entonces la alternativa es usar reflect ( Activator.CreateInstance...) y mantener privado el constructor predeterminado. Pero esto viene con una penalización de rendimiento, por supuesto.

Dan C.
fuente
No es la primera vez que las personas votan en contra de "todas las demás respuestas" :)
Dan C.
Admitiré que a veces no votó de manera burlona las respuestas 'en competencia' hasta que el dusgt se haya resuelto en una pregunta: ¡El karma de adivinar (sin puntos) los resolverá!
Ruben Bartelink
8

desea un nuevo T (), pero también deberá agregar , new()a la whereespecificación para el método de fábrica

Ruben Bartelink
fuente
Lo volví a subir, lo entendí, ayudé, parece que en general a la gente le gusta el código publicado mejor que las descripciones aquí
Edward Tanguay
Gracias, ¡el mundo vuelve a tener sentido!
Ruben Bartelink
correcto, pero su respuesta es ciertamente un poco corta;)
Lorenz Lo Sauer
4

Un poco antiguo pero para otros que buscan una solución, tal vez esto podría ser de interés: http://daniel.wertheim.se/2011/12/29/c-generic-factory-with-support-for-private-constructors/

Dos soluciones. Uno usando Activator y otro usando Lambdas compiladas.

//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
fuente
2

También puede usar la reflexión para obtener el constructor del objeto y crear una instancia de esa manera:

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

Usando una clase de fábrica para construir su objeto con expresión lamba compilada: la forma más rápida que he encontrado para instanciar el tipo genérico.

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

Estos son los pasos que seguí para configurar el punto de referencia.

Crea mi método de prueba de referencia:

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

También intenté usar un método de fábrica:

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

Para las pruebas, he creado la clase más simple:

public class A { }

El guión para probar:

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

Resultados de más de 1000 000 iteraciones:

nuevo A (): 11ms

Método de fábrica A (): 275 ms

FactoryClass A .Create (): 56ms

Activador.CreateInstancia A (): 235ms

Activador.CreateInstancia (tipo de (A)): 157ms

Observaciones : He probado con .NET Framework 4.5 y 4.6 (resultados equivalentes).

Thomas
fuente
0

En lugar de crear una función para crear una instancia del tipo

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

podrías haberlo hecho así

T obj = new T { FirstName = firstName, LastName = lastname };
TMul
fuente
1
Esto no responde a la pregunta que se hace. El verdadero problema aquí era que necesitaba crear una nueva instancia de la clase genérica. Quizás no fue intencionado, pero parece que está diciendo que usar un inicializador resolvería el problema original, pero no es así. La new()restricción sigue siendo necesaria en el tipo genérico para que su respuesta funcione.
Usuario
Si está tratando de ser útil y sugiere que el inicializador es una herramienta útil aquí, debe publicarlo como un comentario, no como otra respuesta.
Usuario