Przekazywanie argumentów do C # generic new () typu szablonowego


409

Podczas dodawania do listy próbuję utworzyć nowy obiekt typu T za pomocą jego konstruktora.

Otrzymuję błąd kompilacji: Komunikat o błędzie to:

„T”: nie można podać argumentów podczas tworzenia instancji zmiennej

Ale moje klasy mają argument konstruktorski! Jak mogę to zrobić?

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


2
Propozycja przeniesienia tej funkcjonalności na język: github.com/dotnet/roslyn/issues/2206
Ian Kemp

W dokumentacji Microsoft zobacz Błąd kompilatora CS0417 .
DavidRR

1
Propozycję przeniesienia tej funkcjonalności do języka przeniesiono na: github.com/dotnet/csharplang/issues/769
ograniczenie aktywności

Odpowiedzi:


410

Aby utworzyć instancję typu ogólnego w funkcji, musisz ograniczyć ją flagą „new”.

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

Działa to jednak tylko wtedy, gdy chcesz wywołać konstruktor, który nie ma parametrów. Nie w tym przypadku. Zamiast tego musisz podać inny parametr, który pozwala na tworzenie obiektu na podstawie parametrów. Najłatwiejsza jest funkcja.

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

Możesz to tak nazwać

GetAllItems<Foo>(..., l => new Foo(l));

Jak to zadziała, gdy zostanie wywołany wewnętrznie z klasy ogólnej? Mój kod opublikowałem w odpowiedzi poniżej. Nie znam wewnętrznej konkretnej klasy, ponieważ jest to klasa ogólna. Czy jest na to jakiś sposób? Nie chcę używać innej sugestii użycia składni inicjalizującej właściwość, ponieważ
pominie

dodałem mój kod do innego pytania stackoverflow.com/questions/1682310/...
ChrisCa,

21
Jest to obecnie jedno z najbardziej denerwujących ograniczeń języka C #. Chciałbym, aby moje klasy były niezmienne: Posiadanie tylko prywatnych seterów sprawiłoby, że klasa nie byłaby w stanie nieważnym z powodu skutków ubocznych. Lubię też używać tego Funca i lambdy, ale wiem, że wciąż jest to problem w świecie biznesu, ponieważ generalnie programiści nie znają jeszcze lambda, a to sprawia, że ​​twoja klasa jest trudniejsza do zrozumienia.
Tuomas Hietanen,

1
Dzięki. W moim przypadku znam argument (y) konstruktora, gdy wywołuję metodę, musiałem tylko ominąć ograniczenie parametru Type, że nie można go zbudować z parametrami, więc użyłem thunk . Thunk jest opcjonalnym parametrem dla metody i używam go tylko pod warunkiem: T result = thunk == null ? new T() : thunk(); Zaletą tego jest dla mnie utrwalenie logiki Ttworzenia w jednym miejscu zamiast czasem tworzenia Twewnątrz, a czasem poza metodą.
Carl G

Myślę, że jest to jedno z miejsc, w których język C # decyduje się powiedzieć programistowi „nie” i przestać mówić tak przez cały czas! Chociaż takie podejście jest trochę niewygodnym sposobem tworzenia obiektu, ale na razie musze go użyć.
AmirHossein Rezaei

331

w .Net 3.5 i po tym, jak można użyć klasy aktywatora:

(T)Activator.CreateInstance(typeof(T), args)

1
moglibyśmy również użyć drzewa wyrażeń do zbudowania obiektu
Welly Tambunan

4
Co to jest argumenty? obiekt[]?
Rodney P. Barbati

3
Tak, args jest obiektem [], w którym określasz wartości, które mają być dostarczone konstruktorowi T: „nowy obiekt [] {par1, par2}”
TechNyquist


3
OSTRZEŻENIE: Jeśli masz dedykowanego konstruktora tylko ze względu Activator.CreateInstancena tę jedną rzecz, będzie wyglądać, jakby Twój konstruktor w ogóle nie był używany, a ktoś może spróbować „wyczyścić” i usunąć go (aby spowodować błąd w czasie wykonywania jakiś losowy czas w przyszłości). Możesz rozważyć dodanie funkcji zastępczej, w której używasz tego konstruktora, aby uzyskać błąd kompilacji, jeśli spróbujesz go usunąć.
jrh

51

Ponieważ nikt nie zadał sobie trudu opublikowania odpowiedzi „Refleksja” (którą osobiście uważam za najlepszą odpowiedź), oto:

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

Edycja: Ta odpowiedź jest nieaktualna z powodu Activator.CreateInstance .NET 3.5, jednak nadal jest przydatna w starszych wersjach .NET.


Rozumiem, że większość hitów wydajnościowych to przede wszystkim zdobycie ConstructorInfo. Nie wierz mi na słowo bez profilowania. W takim przypadku po prostu zapisanie ConstructorInfo do późniejszego ponownego wykorzystania może złagodzić wydajność powtarzalnych instancji poprzez odbicie.
Kelsie

19
Myślę, że brak kontroli czasu kompilacji jest bardziej powodem do niepokoju.
Dave Van den Eynde,

1
@James Zgadzam się, byłem zaskoczony, że nie widziałem tego jako „odpowiedzi”. W rzeczywistości szukałem tego pytania, oczekując łatwego przykładu (takiego jak twoje), ponieważ minęło już tak dużo czasu od refleksji. W każdym razie +1 ode mnie, ale +1 również w odpowiedzi na Aktywator. Sprawdziłem, co robi Aktywator, i okazuje się, że to, co robi, jest bardzo dobrze skonstruowanym odbiciem. :)
Mike

Wywołanie GetConstructor () jest drogie, dlatego warto buforować przed zapętleniem. W ten sposób, wywołując tylko Invoke () wewnątrz pętli, jest znacznie szybsze niż wywoływanie obu, a nawet użycie Activator.CreateInstance ().
Cosmin Rus

30

Inicjator obiektu

Jeśli twój konstruktor z parametrem nie robi nic poza ustawieniem właściwości, możesz to zrobić w C # 3 lub lepiej, używając inicjatora obiektu zamiast wywoływać konstruktor (co jest niemożliwe, jak wspomniano):

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

Korzystając z tego, zawsze możesz umieścić dowolną logikę konstruktora w domyślnym (pustym) konstruktorze.

Activator.CreateInstance ()

Alternatywnie możesz wywołać Activator.CreateInstance () w następujący sposób:

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

Należy pamiętać, że Activator.CreateInstance może mieć pewne narzuty wydajności , których można uniknąć, jeśli szybkość wykonywania jest najwyższym priorytetem, a inna opcja jest możliwa do utrzymania.


zapobiega Tto ochronie niezmienników (biorąc pod uwagę, że Tma> 0 zależności lub wymagane wartości, możesz teraz tworzyć instancje, Tktóre są w stanie nieprawidłowym / nieużywalnym. chyba że Tjest to coś tak prostego jak DTO och viewmodel, powiedziałbym, że unikaj tego.
sara,

20

Bardzo stare pytanie, ale nowa odpowiedź ;-)

Wersja ExpressionTree : (Myślę, że najszybsze i najczystsze rozwiązanie)

Jak powiedział Welly Tambunan , „możemy również użyć drzewa wyrażeń do zbudowania obiektu”

Spowoduje to wygenerowanie „konstruktora” (funkcji) dla podanego typu / parametrów. Zwraca delegata i akceptuje typy parametrów jako tablicę obiektów.

Oto on:

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

Przykład 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;
    }
}

Stosowanie:

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

wprowadź opis zdjęcia tutaj


Kolejny przykład: przekazywanie typów jako tablic

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

Debuguj widok wyrażenia

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

Jest to równoważne z generowanym kodem:

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

Mały minus

Wszystkie parametry typów wartości są umieszczane w ramkach, gdy są przekazywane jak tablica obiektów.


Prosty test wydajności:

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

Wyniki:

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

Używanie Expressionsjest +/- 8 razy szybsze niż Wywołanie ConstructorInfoi +/- 20 razy szybsze niż używanieActivator


Czy masz wgląd w to, co zrobić, jeśli chcesz zbudować MyClass <T> z konstruktorem public MyClass (dane T). W tym przypadku Expression.Convert zgłasza wyjątek i jeśli użyję do konwersji podstawowej klasy podstawowej ograniczenia, to Expression.New zgłasza, ponieważ informacje o konstruktorze są dla typu ogólnego
Mason

@Mason (odpowiedź zajęła trochę ;-)) var myConstructor = CreateConstructor(typeof(MyClass<int>), typeof(int));to działa dobrze. Nie wiem
Jeroen van Langen

19

To nie zadziała w twojej sytuacji. Możesz jedynie określić ograniczenie, że ma pusty konstruktor:

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

Możesz użyć zastrzyku właściwości, definiując ten interfejs:

public interface ITakesAListItem
{
   ListItem Item { set; }
}

Następnie możesz zmienić swoją metodę na:

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

Inną alternatywą jest Funcmetoda opisana przez JaredPar.


pomijałoby to jakąkolwiek logikę konstruktora, który bierze argumenty, prawda? Chciałbym zrobić coś podobnego do podejścia Jareda, ale
wywołuję

3
Tak, to wywołuje logikę domyślnego konstruktora T (), a następnie po prostu ustawia właściwość „Element”. Jeśli próbujesz wywołać logikę konstruktora innego niż domyślny, to ci nie pomoże.
Scott Stafford,

7

Musisz dodać gdzie T: new (), aby poinformować kompilator, że T zapewnia domyślny konstruktor.

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

1
AKTUALIZACJA: Prawidłowy komunikat o błędzie to: „T”: nie można podać argumentów podczas tworzenia instancji zmiennej
LB.

To dlatego, że nie używasz pustego konstruktora, przekazujesz argument obiektu. Nie ma sposobu, aby sobie z tym poradzić bez określenia, że ​​typ ogólny ma nowy parametr (obiektowy).
Min

Następnie musisz: 1. Użyć refleksji 2. Przekaż parametr do metody inicjalizacji zamiast do konstruktora, gdzie metoda inicjalizacji należy do interfejsu, który implementuje twój typ i który jest zawarty w miejscu, w którym T: ... deklaracja. Opcja 1 ma najmniejszy wpływ na resztę kodu, ale opcja 2 zapewnia sprawdzanie czasu kompilacji.
Richard

Nie używaj refleksji! Istnieją inne sposoby przedstawione w innych odpowiedziach, które zapewniają ten sam efekt.
Garry Shutler

@ Garry - Zgadzam się, że refleksja niekoniecznie jest najlepszym podejściem, ale pozwala osiągnąć to, co jest wymagane, przy minimalnych zmianach w pozostałej części kodu. To powiedziawszy, wolę podejście delegatów fabrycznych od @JaredPar.
Richard

7

Jeśli chcesz po prostu zainicjować pole członka lub właściwość za pomocą parametru konstruktora, w C #> = 3 możesz to zrobić bardzo łatwo:

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

To samo powiedział Garry Shutler, ale chciałbym dodać dodatkową notatkę.

Oczywiście możesz użyć sztuczki właściwości, aby zrobić więcej rzeczy niż tylko ustawienie wartości pola. Właściwość „set ()” może wyzwalać dowolne przetwarzanie potrzebne do ustawienia powiązanych pól i wszelkie inne potrzeby samego obiektu, w tym sprawdzenie, czy pełna inicjalizacja ma nastąpić przed użyciem obiektu, symulując pełną konstrukcję ( tak, jest to brzydkie obejście, ale pokonuje nowe () ograniczenie M $).

Nie mogę się upewnić, czy jest to planowana dziura, czy przypadkowy efekt uboczny, ale działa.

To bardzo zabawne, jak stwardnienie rozsiane dodaje nowe funkcje do języka i wydaje się, że nie wykonuje pełnej analizy skutków ubocznych. Cała ogólna rzecz jest dobrym dowodem na to ...


1
Potrzebne są oba ograniczenia. InterfaceOrBaseClass informuje kompilator o polu / właściwości BaseMemberItem. Skomentowanie ograniczenia „new ()” spowoduje błąd: Błąd 6 Nie można utworzyć instancji zmiennej typu „T”, ponieważ nie ma ograniczenia new ()
fljx

Sytuacja, z którą się spotkałem, nie była dokładnie taka, jak zadawane tutaj pytanie, jednak ta odpowiedź zaprowadziła mnie tam, gdzie powinienem iść, i wydaje się, że działa bardzo dobrze.
RubyHaus,

5
Za każdym razem, gdy ktoś wymienia Microsoft jako „M $”, cierpi niewielka część mojej duszy.
Mathias Lykkegaard Lorenzen

6

Stwierdziłem, że pojawia się błąd „nie można podać argumentów podczas tworzenia wystąpienia parametru typu T”, więc musiałem to zrobić:

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

5

Jeśli masz dostęp do klasy, z której będziesz korzystać, możesz skorzystać z tego podejścia, którego użyłem.

Utwórz interfejs, który ma alternatywnego twórcę:

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

Twórz zajęcia z pustym twórcą i zaimplementuj tę metodę:

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

Teraz użyj ogólnych metod:

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

Jeśli nie masz dostępu, zawiń klasę docelową:

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

0

Jest to trochę paskudne, a kiedy mówię trochę paskudne, mam na myśli zbuntowane, ale zakładając, że możesz dostarczyć sparametryzowany typ pustemu konstruktorowi:

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

Skutecznie pozwoli ci zbudować obiekt ze sparametryzowanego typu z argumentem. W tym przypadku zakładam, że konstruktor, którego chcę, ma jeden argument typu object. Tworzymy atrapę instancji T, używając dozwolonego pustego konstruktora, a następnie używamy odbicia, aby uzyskać jeden z jej innych konstruktorów.


0

Czasami używam podejścia, które przypomina odpowiedzi przy użyciu wstrzykiwania właściwości, ale utrzymuje czystość kodu. Zamiast mieć klasę bazową / interfejs z zestawem właściwości, zawiera on tylko (wirtualną) metodę Initialize () - która działa jak „konstruktor biedaka”. Następnie możesz pozwolić każdej klasie zająć się własną inicjalizacją, tak jak zrobiłby to konstruktor, co dodaje także wygodny sposób obsługi łańcuchów dziedziczenia.

Jeśli często znajduję się w sytuacjach, w których chcę, aby każda klasa w łańcuchu zainicjowała swoje unikalne właściwości, a następnie wywołała metodę nadrzędną Initialize () - metodę, która z kolei inicjuje unikalne właściwości rodzica i tak dalej. Jest to szczególnie przydatne w przypadku różnych klas, ale o podobnej hierarchii, na przykład obiektów biznesowych odwzorowanych na / z DTO: s.

Przykład, który używa wspólnego słownika do inicjalizacji:

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

0

Jeśli wszystko, czego potrzebujesz, to konwersja z ListItem na typ T, możesz zaimplementować tę konwersję w klasie T jako operator konwersji.

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

-4

Uważam, że musisz ograniczyć T za pomocą instrukcji where, aby zezwolić tylko na obiekty z nowym konstruktorem.

Teraz akceptuje wszystko, w tym przedmioty bez niego.


1
Możesz zmienić tę odpowiedź, ponieważ została ona edytowana w pytaniu po udzieleniu odpowiedzi, co pozostawia tę odpowiedź poza kontekstem.
transfer87
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.