Pasar argumentos a C # genérico new () de tipo con plantilla

409

Estoy tratando de crear un nuevo objeto de tipo T a través de su constructor cuando lo agrego a la lista.

Recibo un error de compilación: el mensaje de error es:

'T': no ​​puede proporcionar argumentos al crear una instancia de una variable

¡Pero mis clases tienen un argumento de constructor! ¿Cómo puedo hacer que esto funcione?

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.
   } 
   ...
}
LB.
fuente
2
posible duplicado de Crear instancia de tipo genérico?
nawfal
2
Propuesta para llevar esta funcionalidad al idioma: github.com/dotnet/roslyn/issues/2206
Ian Kemp
En la documentación de Microsoft, vea el Error del compilador CS0417 .
DavidRR
1
La propuesta para llevar esta funcionalidad al idioma se trasladó a: github.com/dotnet/csharplang/issues/769
reducción de la actividad

Respuestas:

410

Para crear una instancia de un tipo genérico en una función, debe restringirla con el indicador "nuevo".

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

Sin embargo, eso solo funcionará cuando desee llamar al constructor que no tiene parámetros. No es el caso aquí. En su lugar, deberá proporcionar otro parámetro que permita la creación de objetos basados ​​en parámetros. Lo más fácil es una función.

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

Entonces puedes llamarlo así

GetAllItems<Foo>(..., l => new Foo(l));
JaredPar
fuente
¿Cómo funcionaría esto cuando se llama internamente desde una clase genérica? He publicado mi código en una respuesta a continuación. No conozco la clase concreta internamente, ya que es una clase genérica. Hay alguna forma de evitar esto. No quiero usar la otra sugerencia de usar la sintaxis del inicializador de propiedades, ya que eso omitirá la lógica que tengo en el constructor
ChrisCa
agregué mi código a otra pregunta stackoverflow.com/questions/1682310/…
ChrisCa el
21
Esta es actualmente una de las limitaciones más molestas de C #. Me gustaría hacer que mis clases sean inmutables: tener solo setters privados haría imposible que la clase esté en un estado no válido por los efectos secundarios. También me gusta usar ese Func y lambda, pero sé que todavía es un problema en el mundo de los negocios, ya que en general los programadores aún no conocen lambdas y esto hace que su clase sea más difícil de entender.
Tuomas Hietanen
1
Gracias. En mi caso, conozco los argumentos del constructor cuando llamo al método, solo necesitaba evitar la limitación del parámetro Tipo de que no se podía construir con parámetros, por lo que utilicé un thunk . El thunk es un parámetro opcional para el método, y solo lo uso si se proporciona: T result = thunk == null ? new T() : thunk(); el beneficio de esto para mí es consolidar la lógica de la Tcreación en un lugar en lugar de crear a veces Tdentro y a veces fuera del método.
Carl G
¡Creo que este es uno de los lugares donde el lenguaje C # decide decir no al programador y dejar de decir que sí todo el tiempo! Aunque este enfoque es una forma un poco incómoda de crear objetos, tengo que usarlo por ahora.
AmirHossein Rezaei
331

en .Net 3.5 y después de que pueda usar la clase activadora:

(T)Activator.CreateInstance(typeof(T), args)
usuario287107
fuente
1
también podríamos usar el árbol de expresión para construir el objeto
Welly Tambunan
44
¿Qué es el args? un objeto[]?
Rodney P. Barbati
3
Sí, args es un objeto [] donde especifica los valores que se proporcionarán al constructor de la T: "nuevo objeto [] {par1, par2}"
TechNyquist
3
ADVERTENCIA: si tiene un constructor dedicado solo por Activator.CreateInstanceesta única cosa, parecerá que su constructor no se utiliza en absoluto, y alguien podría intentar "limpiar" y eliminarlo (para causar un error de tiempo de ejecución en algún tiempo al azar en el futuro). Es posible que desee considerar agregar una función ficticia donde usa este constructor solo para obtener un error de compilación si intenta eliminarlo.
jrh
51

Como nadie se molestó en publicar la respuesta 'Reflexión' (que personalmente creo que es la mejor respuesta), aquí va:

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

Editar: esta respuesta está en desuso debido a .NET 3.5's Activator.CreateInstance, sin embargo, todavía es útil en versiones anteriores de .NET.

James Jones
fuente
Tengo entendido que la mayor parte del éxito en el rendimiento está en adquirir el ConstructorInfo en primer lugar. No confíes en mi palabra sin perfilarlo. Si ese es el caso, simplemente almacenar el ConstructorInfo para su posterior reutilización podría aliviar el impacto en el rendimiento de instancias repetidas a través de la reflexión.
Kelsie
19
Creo que la falta de verificación en tiempo de compilación es más motivo de preocupación.
Dave Van den Eynde
1
@ James Estoy de acuerdo, me sorprendió no ver esto como la "respuesta". De hecho, busqué en esta pregunta esperando encontrar un buen ejemplo fácil (como el suyo) ya que ha pasado tanto tiempo desde que hice la reflexión. De todos modos, +1 de mi parte, pero también +1 en la respuesta del Activador. Investigué qué está haciendo Activator, y resulta que lo que hace es una reflexión muy bien diseñada. :)
Mike
La llamada GetConstructor () es costosa, por lo que vale la pena almacenar en caché antes del bucle. De esta manera, al llamar solo Invoke () dentro del bucle, es mucho más rápido que llamar a ambos o incluso usar Activator.CreateInstance ().
Cosmin Rus
30

Inicializador de objeto

Si su constructor con el parámetro no está haciendo nada además de establecer una propiedad, puede hacerlo en C # 3 o mejor usando un inicializador de objeto en lugar de llamar a un constructor (lo cual es imposible, como se ha mencionado):

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

Con esto, también puede colocar cualquier lógica de constructor en el constructor predeterminado (vacío).

Activator.CreateInstance ()

Alternativamente, puede llamar a Activator.CreateInstance () de esta manera:

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

Tenga en cuenta que Activator.CreateInstance puede tener algunos gastos generales de rendimiento que puede querer evitar si la velocidad de ejecución es una de las principales prioridades y le es posible mantener otra opción.

Tim Lehner
fuente
esto evita Tproteger sus invariantes (dado que Ttiene> 0 dependencias o valores requeridos, ahora puede crear instancias Tque estén en un estado no válido / inutilizable. A menos que Tsea ​​algo muy simple como un modelo de vista DTO, diría que evite esto.
sara
20

Muy antigua pregunta, pero nueva respuesta ;-)

La versión ExpressionTree : (creo que la solución más rápida y limpia)

Como dijo Welly Tambunan , "también podríamos usar el árbol de expresión para construir el objeto"

Esto generará un 'constructor' (función) para el tipo / parámetros dados. Devuelve un delegado y acepta los tipos de parámetros como una matriz de objetos.

Aquí está:

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

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

Uso:

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

ingrese la descripción de la imagen aquí


Otro ejemplo: pasar los tipos como una matriz

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

Vista de depuración de expresión

.Lambda #Lambda1<TestExpressionConstructor.MainWindow+ConstructorDelegate>(System.Object[] $var1) {
    .New TestExpressionConstructor.MainWindow+MyClass(
        (System.Int32)$var1[0],
        (System.String)$var1[1])
}

Esto es equivalente al código que se genera:

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

Pequeño inconveniente

Todos los parámetros de valuetypes se encuadran cuando se pasan como una matriz de objetos.


Prueba de rendimiento 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();

Resultados:

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

Usar Expressionses +/- 8 veces más rápido que invocar ConstructorInfoy +/- 20 veces más rápido que usarActivator

Jeroen van Langen
fuente
¿Tiene alguna idea de qué hacer si desea construir MyClass <T> con el constructor public MyClass (datos T). En este caso, Expression.Convert arroja una excepción y si uso la clase base de restricción genérica para convertir, entonces Expression.New arroja porque la información del constructor es para un tipo genérico
Mason
@Mason (tardó un tiempo en responder ;-)) var myConstructor = CreateConstructor(typeof(MyClass<int>), typeof(int));esto está funcionando bien. No lo sé.
Jeroen van Langen
19

Esto no funcionará en tu situación. Solo puede especificar la restricción de que tiene un constructor vacío:

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

Lo que podría hacer es usar la inyección de propiedades definiendo esta interfaz:

public interface ITakesAListItem
{
   ListItem Item { set; }
}

Entonces podrías alterar tu método para que sea así:

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

La otra alternativa es el Funcmétodo descrito por JaredPar.

Garry Shutler
fuente
sin embargo, esto pasaría por alto cualquier lógica que esté en el constructor que toma los argumentos, ¿verdad? Me gustaría hacer algo como el enfoque de Jared, pero llamo al método internamente dentro de la clase, así que no sé cuál es el tipo concreto ... hmmm
ChrisCa
3
Correcto, esto llama a la lógica del constructor predeterminado T (), luego simplemente establece la propiedad "Elemento". Si está intentando invocar la lógica de un constructor no predeterminado, esto no lo ayudará.
Scott Stafford
7

Debe agregar where T: new () para que el compilador sepa que T está garantizado para proporcionar un constructor predeterminado.

public static string GetAllItems<T>(...) where T: new()
Ricardo
fuente
1
ACTUALIZACIÓN: El mensaje de error correcto es: 'T': no ​​puede proporcionar argumentos al crear una instancia de una variable
LB.
Eso es porque no estás usando un constructor en blanco, le estás pasando un argumento de objeto. No hay forma de que pueda manejar eso sin especificar que el Tipo genérico tiene un nuevo parámetro (objeto).
Min
Luego deberá: 1. Usar la reflexión 2. Pasar el parámetro a un método de inicialización en lugar del constructor, donde el método de inicialización pertenece a una interfaz que implementa su tipo y que se incluye en el lugar donde T: ... declaración. La opción 1 es el impacto más bajo para el resto de su código, pero la opción 2 proporciona la verificación del tiempo de compilación.
Richard
¡No uses la reflexión! Hay otras formas, como se describe en otras respuestas, que le dan el mismo efecto.
Garry Shutler
@Garry: estoy de acuerdo en que la reflexión no es necesariamente el mejor enfoque, pero le permite lograr lo que se requiere con un cambio mínimo en el resto de la base de código. Dicho esto, prefiero el enfoque de delegado de fábrica de @JaredPar.
Richard
7

Si simplemente desea inicializar un campo miembro o propiedad con el parámetro constructor, en C #> = 3 puede hacerlo más fácilmente:

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

Esto es lo mismo que dijo Garry Shutler, pero me gustaría poner una nota adicional.

Por supuesto, puede usar un truco de propiedad para hacer más cosas que solo establecer un valor de campo. Una propiedad "set ()" puede desencadenar cualquier procesamiento necesario para configurar sus campos relacionados y cualquier otra necesidad del objeto en sí, incluida una verificación para ver si se debe realizar una inicialización completa antes de usar el objeto, simulando una construcción completa ( sí, es una solución fea, pero supera la nueva limitación () de M $).

No puedo asegurar si es un agujero planificado o un efecto secundario accidental, pero funciona.

Es muy divertido cómo la gente con EM agrega nuevas características al lenguaje y parece no hacer un análisis completo de los efectos secundarios. Todo lo genérico es una buena evidencia de esto ...

fljx
fuente
1
Ambas restricciones son necesarias. InterfaceOrBaseClass informa al compilador del campo / propiedad BaseMemberItem. Si se comenta la restricción "new ()", se activará el error: Error 6 No se puede crear una instancia del tipo de variable 'T' porque no tiene la restricción new ()
fljx
Una situación que encontré no era exactamente como la pregunta que se hacía aquí, sin embargo, esta respuesta me llevó a donde tenía que ir y parece funcionar muy bien.
RubyHaus
55
Cada vez que alguien menciona a Microsoft como "M $", una pequeña parte de mi alma sufre.
Mathias Lykkegaard Lorenzen
6

Descubrí que recibía un error "no puedo proporcionar argumentos al crear una instancia del parámetro de tipo T", así que necesitaba hacer esto:

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

Si tiene acceso a la clase que va a usar, puede usar este enfoque que yo usé.

Cree una interfaz que tenga un creador alternativo:

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

Haga sus clases con un creador vacío e implemente este método:

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

Ahora usa tus métodos genéricos:

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

Si no tiene acceso, ajuste la clase de destino:

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
fuente
0

Esto es un poco sucio, y cuando digo un poco sucio, puedo decir repugnante, pero suponiendo que pueda proporcionar su tipo parametrizado con un constructor vacío, entonces:

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

Efectivamente le permitirá construir un objeto a partir de un tipo parametrizado con un argumento. En este caso, supongo que el constructor que quiero tiene un único argumento de tipo object. Creamos una instancia ficticia de T utilizando la restricción permitida constructor vacío y luego utilizamos la reflexión para obtener uno de sus otros constructores.

silasdavis
fuente
0

A veces uso un enfoque que se asemeja a las respuestas usando la inyección de propiedades, pero mantiene el código más limpio. En lugar de tener una clase / interfaz base con un conjunto de propiedades, solo contiene un método (virtual) Initialize () que actúa como un "constructor de pobres". Luego puede dejar que cada clase maneje su propia inicialización tal como lo haría un constructor, lo que también agrega una forma conveniente de manejar cadenas de herencia.

Si a menudo me encuentro en situaciones en las que quiero que cada clase de la cadena inicialice sus propiedades únicas, y luego llame al método Initialize () de su padre, que a su vez inicializa las propiedades únicas del padre y así sucesivamente. Esto es especialmente útil cuando se tienen diferentes clases, pero con una jerarquía similar, por ejemplo, objetos de negocios que se asignan a / desde DTO: s.

Ejemplo que usa un diccionario común para la inicialización:

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
fuente
0

Si todo lo que necesita es la conversión de ListItem a su tipo T, puede implementar esta conversión en clase T como operador de conversión.

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

Creo que debe restringir T con una instrucción where para permitir solo objetos con un nuevo constructor.

En este momento acepta cualquier cosa, incluidos los objetos sin él.

klkitchens
fuente
1
Es posible que desee cambiar esta respuesta porque esto se editó en la pregunta después de su respuesta, lo que deja esta respuesta fuera de contexto.
shuttle87