¿Crear instancia de tipo genérico cuyo constructor requiere un parámetro?

230

Si BaseFruittiene un constructor que acepta un int weight, ¿puedo crear una instancia de una fruta en un método genérico como este?

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

Se agrega un ejemplo detrás de los comentarios. Parece que solo puedo hacer esto si doy BaseFruitun constructor sin parámetros y luego lo relleno todo a través de las variables miembro. En mi código real (no sobre fruta) esto es poco práctico.

-Update-
Entonces parece que no puede resolverse por restricciones de ninguna manera entonces. De las respuestas hay tres soluciones candidatas:

  • Patrón de fábrica
  • Reflexión
  • Activador

Tiendo a pensar que la reflexión es la menos limpia, pero no puedo decidir entre las otras dos.

Boris Callens
fuente
1
Por cierto: hoy probablemente resolvería esto con la biblioteca de IoC de elección.
Boris Callens
La reflexión y el activador están realmente estrechamente relacionados.
Rob Vermeulen el

Respuestas:

335

Además, un ejemplo más simple:

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

Tenga en cuenta que el uso de la restricción new () en T es solo para hacer que el compilador verifique un constructor público sin parámetros en tiempo de compilación, el código real utilizado para crear el tipo es la clase Activator.

Deberá asegurarse con respecto al constructor específico existente, y este tipo de requisito puede ser un olor a código (o más bien algo que debería tratar de evitar en la versión actual en c #).

meandmycode
fuente
Como este constructor está en la clase base (BaseFruit), sé que tendrá un constructor. Pero, de hecho, si un día decido que la fruta base necesita más parámetros, podría estar jodido. Sin embargo, investigará la clase ACtivator. No lo escuché antes.
Boris Callens
3
Este funcionó bien. También hay un procedimiento CreateInstance <T> (), pero que no tiene una sobrecarga para los parámetros de algún rason ..
Boris Callens
20
No hay necesidad de usar new object[] { weight }. CreateInstancese declara con params, public static object CreateInstance(Type type, params object[] args)por lo que puedes hacerlo return (T) Activator.CreateInstance(typeof(T), weight);. Si hay varios parámetros, páselos como argumentos separados. Solo si ya tiene una construcción de parámetros enumerables, debería molestarse en convertirla object[]y pasarla a CreateInstance.
ErikE
2
Esto tendrá problemas de rendimiento que he leído. Use una lambda compilada en su lugar. vagifabilov.wordpress.com/2010/04/02/…
David
1
@RobVermeulen: creo que es algo así como una propiedad estática en cada clase de Fruit, que contiene una Funcque crea la nueva instancia. Supongamos que el Appleuso del constructor es new Apple(wgt). Luego agregue a la Appleclase esta definición: static Func<float, Fruit> CreateOne { get; } = (wgt) => new Apple(wgt);En la Fábrica, defina public static Fruit CreateFruitGiven(float weight, Func<float, Fruit> createOne) { return createOne(weight); } Uso: Factory.CreateFruit(57.3f, Apple.CreateOne);- que crea y devuelve un Apple, con weight=57.3f.
ToolmakerSteve
92

No puede usar ningún constructor parametrizado. Puede usar un constructor sin parámetros si tiene una " where T : new()" restricción.

Es un dolor, pero así es la vida :(

Esta es una de las cosas que me gustaría abordar con "interfaces estáticas" . Luego podrá restringir a T para que incluya métodos estáticos, operadores y constructores, y luego llamarlos.

Jon Skeet
fuente
2
Al menos PUEDES hacer tales restricciones: Java siempre me decepciona.
Marcel Jackwerth
@ JonSkeet: Si expuse la API con .NET genérico para que se llame en VB6.0 ... ¿Sigue funcionando?
Roy Lee
@Roylee: No tengo idea, pero sospecho que no.
Jon Skeet
Creo que un compilador de idiomas podría agregar interfaces estáticas sin cambios en el tiempo de ejecución, aunque sería bueno tener equipos de idiomas coordinados en los detalles. Especifique que cada clase que pretende implementar una interfaz estática debe contener una clase anidada con un nombre relacionado con la interfaz particular, que define una instancia de singleton estático de su propio tipo. Asociado con la interfaz sería un tipo genérico estático con un campo de instancia que necesitaría cargarse con el singleton una vez a través de Reflection, pero podría usarse directamente después de eso.
supercat
Una restricción de constructor parametrizada podría manejarse de la misma manera (usando un método de fábrica y un parámetro genérico para su tipo de retorno); en ninguno de los dos casos evitaría que el código escrito en un lenguaje que no admitiera dicha característica afirmara implementar la interfaz sin definir el tipo estático adecuado, por lo que el código escrito usando dichos lenguajes podría fallar en el tiempo de ejecución, pero se podría evitar Reflection en el usuario código.
supercat
61

Si; cambia tu lugar donde estar:

where T:BaseFruit, new()

Sin embargo, esto solo funciona con constructores sin parámetros . Tendrá que tener otros medios para configurar su propiedad (configurar la propiedad en sí o algo similar).

Adam Robinson
fuente
Si el constructor no tiene parámetros, esto me parece seguro.
PerpetualStudent
Me has salvado la vida. No pude restringir T a la clase y la palabra clave new ().
Genotypek
28

Solución más simple Activator.CreateInstance<T>()

usuario1471935
fuente
1
Gracias por la sugerencia, me llevó a donde necesitaba estar. Aunque esto no le permite usar un constructor parametrizado. Pero podría usar la variante no genérica: Activator.CreateInstance (typeof (T), new object [] {...}) donde la matriz de objetos contiene los argumentos para el constructor.
Rob Vermeulen
19

Como Jon señaló, esta es la vida para restringir un constructor sin parámetros. Sin embargo, una solución diferente es usar un patrón de fábrica. Esto es fácilmente restringible

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

Otra opción más es utilizar un enfoque funcional. Pase en un método de fábrica.

public void AddFruit<T>(Func<int,T> factoryDel) where T : BaseFruit { 
  BaseFruit fruit = factoryDel(weight); /* new Apple(150); */
  fruit.Enlist(fruitManager);
}
JaredPar
fuente
2
Buena sugerencia, aunque si no tienes cuidado, puedes terminar en el infierno de la API DOM de Java, con muchas fábricas :(
Jon Skeet
Sí, esta es una solución que estaba considerando. Pero esperaba algo en la línea de las restricciones. Supongo que no entonces ..
Boris Callens
@boris, desafortunadamente el lenguaje de restricción que estás buscando no existe en este momento
JaredPar
11

Puedes hacerlo usando la reflexión:

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

EDITAR: Constructor agregado == verificación nula.

EDITAR: una variante más rápida usando un caché:

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) });
}
mmmmmmmm
fuente
Aunque no me gusta la sobrecarga de la reflexión, como otros han explicado, esta es la forma en que está actualmente. Al ver cómo este constructor no se llamará demasiado, podría ir con esto. O la fabrica. No lo se todavía.
Boris Callens
Actualmente, este es mi enfoque preferido porque no agrega más complejidad en el lado de la invocación.
Rob Vermeulen
Pero ahora he leído acerca de la sugerencia del Activador, que tiene una maldad similar a la solución de reflexión anterior, pero con menos líneas de código :) Voy a ir a la opción Activador.
Rob Vermeulen
1

Como una adición a la sugerencia de user1471935:

Para crear una instancia de una clase genérica utilizando un constructor con uno o más parámetros, ahora puede usar la clase Activador.

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

La lista de objetos son los parámetros que desea suministrar. De acuerdo con Microsoft :

CreateInstance [...] crea una instancia del tipo especificado utilizando el constructor que mejor coincide con los parámetros especificados.

También hay una versión genérica de CreateInstance ( CreateInstance<T>()) pero esa tampoco le permite proporcionar parámetros de constructor.

Rob Vermeulen
fuente
1

Creé este método:

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

Lo uso de esta manera:

public class A 
{
    public int PROP1 {get; set;}
}

public class B : A
{
    public int PROP2 {get; set;}
}

Código:

A instanceA = new A();
instanceA.PROP1 = 1;

B instanceB = new B();
instanceB = ConvertParentObjToChildObj<A,B>(instanceA);
Diocleziano Carletti
fuente
0

Recientemente me encontré con un problema muy similar. Solo quería compartir nuestra solución con todos ustedes. Quería crear una instancia de a Car<CarA>partir de un objeto json usando que tenía una enumeración:

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) });
farshid
fuente
-2

Todavía es posible, con un alto rendimiento, haciendo lo siguiente:

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

y

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

Las clases relevantes tienen que derivar de esta interfaz e inicializarse en consecuencia. Tenga en cuenta que, en mi caso, este código es parte de una clase circundante, que ya tiene <T> como parámetro genérico. R, en mi caso, también es una clase de solo lectura. En mi opinión, la disponibilidad pública de las funciones Initialize () no tiene ningún efecto negativo sobre la inmutabilidad. El usuario de esta clase podría poner otro objeto, pero esto no modificaría la colección subyacente.

cskwg
fuente