Passando argumentos para C # genérico new () do tipo de modelo

409

Estou tentando criar um novo objeto do tipo T através de seu construtor ao adicionar à lista.

Estou recebendo um erro de compilação: A mensagem de erro é:

'T': não é possível fornecer argumentos ao criar uma instância de uma variável

Mas minhas classes têm um argumento construtor! Como posso fazer isso funcionar?

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.
   } 
   ...
}
LIBRA.
fonte
2
possível duplicado de Criar instância do tipo genérico?
Nawfal
2
Proposta para obter essa funcionalidade para a língua: github.com/dotnet/roslyn/issues/2206
Ian Kemp
Na documentação da Microsoft, consulte Erro do compilador CS0417 .
DavidRR
1
A proposta para obter essa funcionalidade no idioma foi movida para: github.com/dotnet/csharplang/issues/769
reduzindo a atividade em

Respostas:

410

Para criar uma instância de um tipo genérico em uma função, você deve restringi-la com o sinalizador "novo".

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

No entanto, isso só funcionará quando você desejar chamar o construtor que não possui parâmetros. Não é o caso aqui. Em vez disso, você precisará fornecer outro parâmetro que permita a criação de objeto com base em parâmetros. O mais fácil é uma função.

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

Você pode então chamar assim

GetAllItems<Foo>(..., l => new Foo(l));
JaredPar
fonte
Como isso funcionaria quando chamado internamente de uma classe genérica? Publiquei meu código em uma resposta abaixo. Não conheço a classe concreta internamente, pois é uma classe genérica. Existe uma maneira de contornar isso. Eu não quero usar a outra sugestão de usar propriedade initialiser sintaxe que irá ignorar a lógica que eu tenho no construtor
ChrisCa
adicionei meu código a outra pergunta stackoverflow.com/questions/1682310/…
ChrisCa
21
Atualmente, essa é uma das limitações mais irritantes do C #. Eu gostaria de tornar minhas aulas imutáveis: ter apenas setters privados tornaria impossível a classe estar em um estado inválido por efeitos colaterais. Também gosto de usar o Func e o lambda, mas sei que ainda é um problema no mundo dos negócios, pois geralmente os programadores ainda não conhecem o lambdas e isso torna sua classe mais difícil de entender.
Tuomas Hietanen
1
Obrigado. No meu caso, conheço o (s) argumento (s) do construtor quando chamo o método, só precisava contornar a limitação do parâmetro Type de que ele não podia ser construído com parâmetros, então usei um thunk . O thunk é um parâmetro opcional para o método, e eu o uso apenas se fornecido: T result = thunk == null ? new T() : thunk(); O benefício disso para mim é consolidar a lógica da Tcriação em um só lugar, em vez de criar algumas vezes Tdentro e outras fora do método.
Carl G
Eu acho que esse é um dos lugares que a linguagem C # decide dizer não ao programador e parar de dizer sim o tempo todo! Embora essa abordagem seja uma maneira um pouco estranha de criar objetos, eu tenho que usá-la por enquanto.
AmirHossein Rezaei 17/04/19
331

no .Net 3.5 e depois que você pudesse usar a classe ativator:

(T)Activator.CreateInstance(typeof(T), args)
user287107
fonte
1
nós também podemos usar árvore de expressão para construir o objeto
Welly Tambunan
4
O que é args? um objeto[]?
Rodney P. Barbati
3
Sim, args é um objeto [] onde você especifica os valores a serem fornecidos ao construtor de T: "new object [] {par1, par2}"
TechNyquist
3
AVISO: Se você tiver um construtor dedicado apenas para Activator.CreateInstanceisso, parecerá que seu construtor não foi usado e alguém poderá tentar "limpar" e excluí-lo (para causar um erro de execução em algum tempo aleatório no futuro). Você pode considerar adicionar uma função fictícia onde você usa esse construtor apenas para obter um erro de compilação se tentar excluí-lo.
Jrh #
51

Como ninguém se preocupou em postar a resposta 'Reflexão' (que eu pessoalmente acho que é a melhor resposta), aqui vai:

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 resposta foi descontinuada devido ao Activator.CreateInstance do .NET 3.5, no entanto, ainda é útil em versões mais antigas do .NET.

James Jones
fonte
Meu entendimento é que a maior parte do impacto no desempenho está na aquisição do ConstructorInfo em primeiro lugar. Não aceite minha palavra sem criar um perfil. Se for esse o caso, simplesmente armazenar o ConstructorInfo para reutilização posterior pode aliviar o impacto no desempenho de instâncias repetidas por meio de reflexão.
Kelsie
19
Eu acho que a falta de verificação em tempo de compilação é mais motivo de preocupação.
Dave Van den Eynde
1
@ James Concordo, fiquei surpreso ao não ver isso como a "resposta". Na verdade, pesquisei nessa questão esperando encontrar um exemplo fácil e agradável (como o seu), já que faz tanto tempo desde que eu refleti. De qualquer forma, +1 de mim, mas +1 no Ativador também respondem. Analisei o que o Activator está fazendo, e acontece que o que faz é uma reflexão muito bem projetada. :)
Mike
A chamada GetConstructor () é cara, portanto vale a pena armazenar em cache antes do loop. Dessa forma, chamando apenas Invoke () dentro do loop, é muito mais rápido do que chamar os dois ou até mesmo usar Activator.CreateInstance ().
Cosmin Rus
30

Inicializador de objeto

Se o seu construtor com o parâmetro não estiver fazendo nada além de definir uma propriedade, você pode fazer isso no C # 3 ou melhor usando um inicializador de objeto em vez de chamar um construtor (o que é impossível, como já foi 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
   } 
   ...
}

Usando isso, você sempre pode colocar qualquer lógica de construtor no construtor padrão (vazio) também.

Activator.CreateInstance ()

Como alternativa, você pode chamar Activator.CreateInstance () assim:

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

Observe que Activator.CreateInstance pode ter alguma sobrecarga de desempenho que você pode evitar se a velocidade de execução for uma prioridade e outra opção puder ser mantida por você.

Tim Lehner
fonte
isso impede a Tproteção de seus invariantes (dado que Ttem> 0 dependências ou valores obrigatórios, agora você pode criar instâncias Tque estão em um estado inválido / inutilizável T.
Sara #
20

Pergunta muito antiga, mas nova resposta ;-)

A versão ExpressionTree : (acho que a solução mais rápida e mais limpa)

Como Welly Tambunan disse, "também podemos usar a árvore de expressão para construir o objeto"

Isso irá gerar um 'construtor' (função) para o tipo / parâmetros fornecidos. Ele retorna um delegado e aceita os tipos de parâmetro como uma matriz de objetos.

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

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

insira a descrição da imagem aqui


Outro exemplo: passando os tipos como uma 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");

DebugView of Expression

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

Isso é equivalente ao código que é gerado:

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

Pequena desvantagem

Todos os parâmetros de tipos de valor são colocados em caixa quando são passados ​​como uma matriz de objetos.


Teste de desempenho simples:

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 Expressionsé +/- 8 vezes mais rápido que Invocar ConstructorInfoe +/- 20 vezes mais rápido que usar oActivator

Jeroen van Langen
fonte
Você tem alguma idéia sobre o que fazer se deseja construir MyClass <T> com o construtor public MyClass (dados T). Neste caso, Expression.Convert lança uma exceção e se eu usar a classe base restrição genérica para converter, em seguida, Expression.New joga porque a informação construtor é para um tipo genérico
Mason
@Mason (demorou um pouco para responder ;-)) var myConstructor = CreateConstructor(typeof(MyClass<int>), typeof(int));isso está funcionando bem. Eu não sei.
Jeroen van Langen
19

Isso não vai funcionar na sua situação. Você só pode especificar a restrição de que ele tenha um construtor vazio:

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

O que você pode fazer é usar a injeção de propriedade, definindo esta interface:

public interface ITakesAListItem
{
   ListItem Item { set; }
}

Então você pode alterar seu método para ser o seguinte:

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

A outra alternativa é o Funcmétodo descrito por JaredPar.

Garry Shutler
fonte
isso ignoraria qualquer lógica que esteja no construtor que aceita os argumentos, certo? Eu gostaria de fazer a abordagem de algo como Jared, mas estou chamando o método internamente dentro da classe, então não sei o que o tipo concreto é ... hmmm
ChrisCa
3
Certo, isso chama a lógica do construtor padrão T () e simplesmente define a propriedade "Item". Se você estiver tentando invocar a lógica de um construtor não padrão, isso não ajudará.
Scott Stafford
7

Você precisa adicionar onde T: new () para informar ao compilador que T é garantido para fornecer um construtor padrão.

public static string GetAllItems<T>(...) where T: new()
Richard
fonte
1
UPDATE: A mensagem de erro correta é: 'T': não é possível fornecer argumentos ao criar uma instância de uma variável
LB.
Isso porque você não está usando um construtor em branco, está passando um argumento para ele de objeto. Não há como lidar com isso sem especificar que o Type genérico tenha um novo parâmetro (objeto).
Min
Você precisará: 1. Usar reflexão 2. Passar o parâmetro para um método de inicialização em vez do construtor, onde o método de inicialização pertence a uma interface que seu tipo implementa e que está incluída no local onde T: ... declaração. A opção 1 é o menor impacto para o restante do seu código, mas a opção 2 fornece a verificação do tempo de compilação.
840 Richard
Não use reflexão! Existem outras maneiras descritas em outras respostas que obtêm o mesmo efeito.
Garry Shutler
@ Garry - eu concordo que a reflexão não é necessariamente a melhor abordagem, mas permite que você consiga o que é necessário com uma alteração mínima no restante da base de código. Dito isto, prefiro a abordagem de delegado da fábrica da @JaredPar.
840 Richard Richard
7

Se você deseja simplesmente inicializar um campo ou propriedade membro com o parâmetro construtor, em C #> = 3, você pode fazer isso muito mais facilmente:

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

É a mesma coisa que Garry Shutler disse, mas eu gostaria de colocar uma nota adicional.

É claro que você pode usar um truque de propriedade para fazer mais coisas do que apenas definir um valor de campo. Uma propriedade "set ()" pode acionar qualquer processamento necessário para configurar seus campos relacionados e qualquer outra necessidade do próprio objeto, incluindo uma verificação para verificar se uma inicialização completa deve ocorrer antes do objeto ser usado, simulando uma construção completa ( sim, é uma solução feia, mas supera a nova limitação de M $ ().

Não posso ter certeza se é um buraco planejado ou um efeito colateral acidental, mas funciona.

É muito engraçado como as pessoas com esclerose múltipla adicionam novos recursos ao idioma e parecem não fazer uma análise completa dos efeitos colaterais. A coisa toda genérica é uma boa evidência disso ...

fljx
fonte
1
Ambas as restrições são necessárias. InterfaceOrBaseClass torna o compilador ciente do campo / propriedade BaseMemberItem. Se a restrição "new ()" for comentada, ele disparará o erro: Erro 6 Não é possível criar uma instância do tipo de variável 'T' porque ela não possui a nova restrição ()
fljx
Uma situação que encontrei não era exatamente como a pergunta feita aqui, no entanto, essa resposta me levou aonde eu precisava ir e parece funcionar muito bem.
RubyHaus 12/08
5
Sempre que alguém menciona a Microsoft como "M $", uma pequena parte da minha alma sofre.
Mathias Lykkegaard Lorenzen
6

Descobri que estava recebendo um erro "não é possível fornecer argumentos ao criar uma instância do tipo parâmetro T", então eu precisava fazer isso:

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

Se você tiver acesso à classe que usará, poderá usar esta abordagem que eu usei.

Crie uma interface que tenha um criador alternativo:

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

Faça suas aulas com um criador vazio e implemente este método:

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

Agora use seus métodos genéricos:

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

Se você não tiver acesso, agrupe a classe 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
fonte
0

Isso é meio bobo, e quando digo meio bobo, posso significar revoltante, mas supondo que você possa fornecer seu tipo parametrizado com um construtor vazio, então:

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

Permite efetivamente construir um objeto a partir de um tipo parametrizado com um argumento. Neste caso, estou assumindo que o construtor que eu quero tem um único argumento do tipo object. Criamos uma instância fictícia de T usando o construtor vazio de restrição permitido e depois usamos a reflexão para obter um de seus outros construtores.

silasdavis
fonte
0

Às vezes, uso uma abordagem semelhante às respostas usando injeção de propriedade, mas mantém o código mais limpo. Em vez de ter uma classe / interface base com um conjunto de propriedades, ele contém apenas um método (virtual) Initialize () - que atua como um "construtor pobre". Em seguida, você pode permitir que cada classe lide com sua própria inicialização, como faria um construtor, o que também adiciona uma maneira conveniente de lidar com cadeias de herança.

Se muitas vezes me encontro em situações em que quero que cada classe na cadeia inicialize suas propriedades exclusivas e chame o método Initialize () dos pais, que por sua vez inicializa as propriedades exclusivas dos pais e assim por diante. Isso é especialmente útil quando se tem classes diferentes, mas com uma hierarquia semelhante, por exemplo, objetos de negócios que são mapeados para / de DTO: s.

Exemplo que usa um dicionário comum para inicialização:

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

Se tudo o que você precisa é de conversão de ListItem para seu tipo T, você pode implementar essa conversão na classe T como operador de conversão.

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

Eu acredito que você precisa restringir T com uma instrução where para permitir apenas objetos com um novo construtor.

Agora, ele aceita qualquer coisa, incluindo objetos sem ele.

cozinhas
fonte
1
Você pode alterar essa resposta porque ela foi editada na pergunta depois que você respondeu, o que deixa essa resposta fora de contexto.
shuttle87