Criar instância do tipo genérico cujo construtor requer um parâmetro?

230

Se BaseFruittem um construtor que aceita um int weight, posso instanciar um pedaço de fruta em um método genérico como este?

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

Um exemplo é adicionado atrás dos comentários. Parece que só posso fazer isso se der BaseFruitum construtor sem parâmetros e preencher tudo através de variáveis-membro. No meu código real (não sobre frutas), isso é bastante impraticável.

-Update-
Então parece que não pode ser resolvido por restrições de qualquer forma então. Das respostas, há três soluções candidatas:

  • Padrão de fábrica
  • Reflexão
  • Activator

Costumo pensar que a reflexão é a menos limpa, mas não consigo decidir entre as outras duas.

Boris Callens
fonte
1
BTW: hoje eu provavelmente resolveria isso com a biblioteca IoC de sua escolha.
Boris Callens
Reflexão e Ativador estão realmente intimamente relacionados.
Rob Vermeulen

Respostas:

335

Além disso, um exemplo mais simples:

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

Observe que o uso da nova restrição () em T é apenas para fazer com que o compilador verifique se há um construtor público sem parâmetros em tempo de compilação, o código real usado para criar o tipo é a classe Activator.

Você precisará garantir-se em relação ao construtor específico existente, e esse tipo de requisito pode ser um cheiro de código (ou melhor, algo que você deve tentar evitar na versão atual em c #).

meandmycode
fonte
Como esse construtor está na classe base (BaseFruit), sei que ele terá um construtor. Mas, de fato, se um dia eu decidir que a toranja precisa de mais parâmetros, eu poderia me ferrar. Irá analisar a classe ACtivator. Não ouvi falar disso antes.
Boris Callens 09/04/2009
3
Este funcionou bem. Há também um CreateInstance <T> () procedimento, mas que não tem uma sobrecarga para os parâmetros para alguns rason ..
Boris Callens
20
Não há necessidade de usar new object[] { weight }. CreateInstanceé declarado com params public static object CreateInstance(Type type, params object[] args), para que você possa fazer return (T) Activator.CreateInstance(typeof(T), weight);. Se houver vários parâmetros, passe-os como argumentos separados. Somente se você já possui um enumerável de parâmetros construídos, deve se preocupar em convertê-lo object[]e transmiti-lo para CreateInstance.
ErikE 04/08/2015
2
Isso terá problemas de desempenho que eu li. Use uma lambda compilada. vagifabilov.wordpress.com/2010/04/02/…
David
1
@ RobVermeulen - Eu acho que algo como uma propriedade estática em cada classe Fruit, que contém um Funcque cria a nova instância. Suponha que o Appleuso do construtor seja new Apple(wgt). Em seguida, adicione à Appleclasse esta definição: static Func<float, Fruit> CreateOne { get; } = (wgt) => new Apple(wgt);No Factory defina public static Fruit CreateFruitGiven(float weight, Func<float, Fruit> createOne) { return createOne(weight); } Usage: Factory.CreateFruit(57.3f, Apple.CreateOne);- que cria e retorna um Apple, com weight=57.3f.
ToolmakerSteve 17/03
92

Você não pode usar nenhum construtor parametrizado. Você pode usar um construtor sem parâmetros se tiver uma "where T : new() " restrição.

É uma dor, mas a vida é assim :(

Essa é uma das coisas que eu gostaria de abordar com "interfaces estáticas" . Você seria capaz de restringir T para incluir métodos estáticos, operadores e construtores e depois chamá-los.

Jon Skeet
fonte
2
Pelo menos você pode fazer essas restrições - Java sempre me decepciona.
Marcel Jackwerth
@ JonSkeet: Se eu expus a API com o .NET genérico a ser chamado no VB6.0 ... Ainda funciona?
Roy Lee
@ Roylee: Não faço ideia, mas suspeito que não.
Jon Skeet
Eu acho que as interfaces estáticas podem ser adicionadas por um compilador de linguagem sem alterações no tempo de execução, embora seja bom ter equipes de linguagem coordenadas nos detalhes. Especifique que toda classe que afirma implementar uma interface estática deve conter uma classe aninhada com um nome relacionado à interface, que define uma instância singleton estática de seu próprio tipo. Associado à interface, haveria um tipo genérico estático com um campo de instância que precisaria ser carregado com o singleton uma vez via Reflection, mas poderia ser usado diretamente depois disso.
Supercat 31/03
Uma restrição de construtor parametrizada pode ser tratada da mesma maneira (usando um método de fábrica e um parâmetro genérico para seu tipo de retorno); em nenhum dos casos, nada impediria que o código escrito em um idioma que não suporta esse recurso alegasse implementar a interface sem definir o tipo estático adequado; portanto, o código escrito usando esses idiomas poderia falhar em tempo de execução, mas a reflexão poderia ser evitada no usuário. código.
31514
61

Sim; Mude o seu lugar para estar:

where T:BaseFruit, new()

No entanto, isso funciona apenas com construtores sem parâmetros . Você precisará ter outros meios para definir sua propriedade (definir a propriedade em si ou algo semelhante).

Adam Robinson
fonte
Se o construtor não tiver parâmetros, isso me parece seguro.
PerpetualStudent
Você salvou minha vida. Não consegui restringir T à classe e à nova palavra-chave ().
Genotypek
28

Solução mais simples Activator.CreateInstance<T>()

user1471935
fonte
1
Obrigado pela sugestão, ele chegou onde eu precisava estar. Embora isso não permita que você use um construtor parametrizado. Mas você pode usar a variante não genérica: Activator.CreateInstance (typeof (T), new object [] {...}) onde a matriz de objetos contém os argumentos para o construtor.
Rob Vermeulen
19

Como Jon apontou, essa é a vida para restringir um construtor sem parâmetros. No entanto, uma solução diferente é usar um padrão de fábrica. Isso é facilmente restritivo

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

Ainda outra opção é usar uma abordagem funcional. Passe em um 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
fonte
2
Boa sugestão - embora se você não for cuidadoso você pode acabar no inferno da API Java DOM, com fábricas em abundância :(
Jon Skeet
Sim, esta é uma solução que eu estava pensando. Mas eu esperava algo na linha de restrições. Não acho que então ..
Boris Callens
@boris, infelizmente, a linguagem restrição que você está procurando não existe neste momento no tempo
JaredPar
11

Você pode fazer isso usando a reflexão:

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

EDIT: Adicionado construtor == verificação nula.

EDIT: Uma variante mais rápida usando um cache:

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
fonte
Embora eu não goste da sobrecarga da reflexão, como outros explicaram, é assim que é atualmente. Vendo como esse construtor não será chamado demais, eu poderia ir com isso. Ou a fábrica. Ainda não sei.
Boris Callens 09/04/2009
Atualmente, essa é minha abordagem preferida porque não adiciona mais complexidade no lado da invocação.
Rob Vermeulen
Mas agora eu li sobre a sugestão do Activator, que tem uma repugnância semelhante à da solução de reflexão acima, mas com menos linhas de código :) Vou usar a opção Activator.
Rob Vermeulen
1

Como complemento à sugestão do usuário14191935:

Para instanciar uma classe genérica usando um construtor com um ou mais parâmetros, agora você pode usar a classe Activator.

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

A lista de objetos são os parâmetros que você deseja fornecer. De acordo com a Microsoft :

CreateInstance [...] cria uma instância do tipo especificado usando o construtor que melhor corresponde aos parâmetros especificados.

Há também uma versão genérica de CreateInstance ( CreateInstance<T>()), mas essa também não permite que você forneça parâmetros de construtor.

Rob Vermeulen
fonte
1

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

Eu uso isso desta maneira:

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

Recentemente me deparei com um problema muito semelhante. Só queria compartilhar nossa solução com todos vocês. Eu queria criar uma instância de a Car<CarA>partir de um objeto json usando o qual tinha um enum:

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
fonte
-2

Ainda é possível, com alto desempenho, fazendo o seguinte:

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

e

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

As classes relevantes precisam derivar dessa interface e inicializar de acordo. Observe que, no meu caso, esse código faz parte de uma classe circundante, que já tem <T> como parâmetro genérico. R, no meu caso, também é uma classe somente leitura. Na IMO, a disponibilidade pública das funções Initialize () não tem efeito negativo na imutabilidade. O usuário desta classe poderia colocar outro objeto, mas isso não modificaria a coleção subjacente.

cskwg
fonte