Передача аргументів до C # generic new () шаблонного типу


409

Я намагаюся створити новий об’єкт типу T через його конструктор при додаванні до списку.

Я отримую помилку компіляції: Повідомлення про помилку:

'T': не може надати аргументи під час створення екземпляра змінної

Але мої класи мають аргумент конструктора! Як я можу змусити цю роботу?

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
Пропозиція перекласти
Ian Kemp

У документації Microsoft див. Помилка компілятора CS0417 .
DavidRR

1
Пропозицію перевести цю функціональність на мову було переміщено на сторінку: github.com/dotnet/csharplang/isissue/769
зменшення активності

Відповіді:


410

Щоб створити екземпляр загального типу у функції, потрібно обмежити його новим прапором.

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

Однак це спрацює лише тоді, коли ви захочете викликати конструктор, у якого немає параметрів. Тут не так. Натомість вам доведеться надати інший параметр, який дозволяє створювати об’єкт на основі параметрів. Найпростіша - функція.

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

Потім ви можете назвати це так

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

Як би це працювало, коли викликали внутрішньо із загального класу? Я опублікував свій код у відповіді нижче. Я не знаю внутрішньо конкретного класу, оскільки це загальний клас. Чи є спосіб цього обійти. Я не хочу використовувати іншу пропозицію використання синтаксису ініціалізатора властивостей, оскільки це обійде логіку, яку я маю в конструкторі
ChrisCa

додав свій код на інше питання stackoverflow.com/questions/1682310 / ...
ChrisCa

21
Наразі це одне з найбільш дратівливих обмежень C #. Я хотів би зробити свої класи непорушними: Наявність лише приватних сетерів зробило б неможливим перебування класу в недійсному стані через побічні ефекти. Мені також подобається використовувати цю функцію та лямбда, але я знаю, що це все ще є проблемою у світі бізнесу, оскільки програмісти ще не знають лямбда, і це робить ваш клас важче зрозуміти.
Туомас Хіетанен

1
Дякую. У моєму випадку я знаю аргументи (-ла) конструктора, коли я викликаю метод, мені просто потрібно було обійти обмеження параметра "Тип" про те, що він не може бути побудований з параметрами, тому я використав тис . Thunk - необов'язковий параметр методу, і я використовую його лише за умови, що це передбачено: T result = thunk == null ? new T() : thunk(); Перевага цього для мене полягає в консолідації логіки Tстворення в одному місці, а не іноді створюючи Tвсередині, а іноді поза методом.
Карл Г

Я думаю, що це одне з місць, коли мова C # вирішує сказати ні програмісту і перестати говорити так весь час! Хоча цей підхід є дещо незручним способом створення об'єкта, але це я мушу зараз його використовувати.
AmirHossein Rezaei

331

в .Net 3.5 і після того, як ви могли використовувати клас активатора:

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

1
ми також могли б використовувати дерево виразів для створення об'єкта
Welly Tambunan

4
Що таке арги? об’єкт []?
Родні П. Барбаті

3
Так, args - це об'єкт [], де ви вказуєте значення, які слід надати конструктору T: "новий об'єкт [] {par1, par2}"
TechNyquist


3
ПОПЕРЕДЖЕННЯ. Якщо у вас є виділений конструктор лише заради Activator.CreateInstanceцієї однієї речі, це буде виглядати так, що ваш конструктор взагалі не використовується, і хтось може спробувати "очистити" та видалити його (щоб викликати помилку виконання під час якийсь випадковий час у майбутньому). Ви можете розглянути можливість додавання фіктивної функції, де ви використовуєте цей конструктор, щоб отримати помилку компіляції, якщо ви спробуєте її видалити.
jrh

51

Оскільки ніхто не заважав публікувати відповідь "Роздум" (що я особисто вважаю найкращою відповіддю), ось що:

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

Редагувати: Ця відповідь застаріла через Activator.CreateInstance .NET 3.5, проте вона все ще корисна у старих версіях .NET.


Я розумію, що більша частина хітів у першу чергу полягає в придбанні ConstructorInfo. Не сприймайте моє слово за нього, не проробляючи його. Якщо це так, просто зберігання ConstructorInfo для подальшого повторного використання може полегшити показник продуктивності повторних миттєвостей за допомогою роздумів.
Келсі

19
Я думаю, що відсутність перевірки часу компіляції викликає більше занепокоєння.
Дейв Ван ден Ейнде

1
@James Я згоден, я був здивований, що не сприйняв це як "відповідь". Насправді я шукав це питання, сподіваючись знайти приємний простий приклад (як ваш), оскільки минуло так довго, як я задумався. У будь-якому разі +1 від мене, але +1 також на відповідь Активатора. Я вивчив, чим займається Активатор, і виявилося, що те, що робить, - це дуже добре розроблене відображення. :)
Майк

Виклик GetConstructor () коштує дорого, тому варто кешувати перед циклом. Таким чином, викликаючи лише Invoke () всередині циклу, це набагато швидше, ніж викликати обидва або навіть використовувати Activator.CreateInstance ().
Космін Русь

30

Ініціалізатор об'єктів

Якщо ваш конструктор з параметром нічого не робить, крім встановлення властивості, ви можете зробити це в C # 3 або краще, використовуючи ініціалізатор об'єктів, а не викликати конструктор (що неможливо, як уже згадувалося):

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

Використовуючи це, ви завжди можете помістити будь-яку логіку конструктора і в конструктор за замовчуванням (порожній).

Activator.CreateInstance ()

Крім того, ви можете викликати Activator.CreateInstance () так:

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

Зауважте, що Activator.CreateInstance може мати деяку накладну ефективність, якої ви можете уникнути, якщо швидкість виконання є першочерговим пріоритетом, а інший варіант дозволений для вас.


це заважає Tзахищати його інваріанти (враховуючи, що Tмає> 0 залежностей або потрібні значення, тепер ви можете створювати екземпляри, Tякі знаходяться в недійсному / непридатному стані. Якщо не Tє щось мертве просте, як перегляд моделей DTO och, я б сказав, уникайте цього.
Сара

20

Дуже давнє запитання, але нова відповідь ;-)

Версія ExpressionTree : (я думаю, що найшвидше і найчистіше рішення)

Як сказав Уеллі Тамбунан , "ми також могли б використовувати дерево виразів для створення об'єкта"

Це створить 'конструктор' (функцію) для заданого типу / параметрів. Він повертає делегата і приймає типи параметрів як масив об'єктів.

Ось:

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

Приклад 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;
    }
}

Використання:

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

введіть тут опис зображення


Інший приклад: передача типів у вигляді масиву

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 Expression

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

Це еквівалентно створеному коду:

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

Невеликий зворотній бік

Усі параметри valuetypes поля, коли вони передаються як масив об'єктів.


Простий тест на працездатність:

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

Результати:

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

Використання Expressionsв +/- 8 разів швидше, ніж виклик ConstructorInfoта +/- в 20 разів швидше, ніж використанняActivator


Чи маєте ви якусь інформацію про те, що робити, якщо ви хочете сконструювати MyClass <T> із конструктором public MyClass (дані T). У цьому випадку Expression.Convert видає виняток, і якщо для перетворення я використовую базовий клас загального обмеження, то Expression.New кидає, оскільки інформація про конструктор призначена для загального типу
Мейсон

@ Мейсон (потрібен час, щоб відповісти ;-)) var myConstructor = CreateConstructor(typeof(MyClass<int>), typeof(int));це прекрасно працює. Не знаю.
Джероен ван Ланген

19

Це не спрацює у вашій ситуації. Ви можете лише вказати обмеження, що він має порожній конструктор:

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

Що ви можете зробити, це використовувати введення властивостей, визначивши цей інтерфейс:

public interface ITakesAListItem
{
   ListItem Item { set; }
}

Тоді ви можете змінити свій метод таким:

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

Інша альтернатива - Funcметод, описаний JaredPar.


це обмине будь-яку логіку, яка є в конструкторі, який бере аргументи, правда? Я хотів би зробити щось на кшталт підходу Джареда, але я називаю метод всередині класу, тому не знаю, що таке конкретний тип ...
гммм

3
Правильно, це викликає логіку конструктора за замовчуванням T (), а потім просто встановлює властивість "Item". Якщо ви намагаєтеся використати логіку конструктора, який не використовується за замовчуванням, це вам не допоможе.
Скотт Стаффорд,

7

Вам потрібно додати, де T: new (), щоб повідомити компілятору, що T гарантовано забезпечує конструктор за замовчуванням.

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

1
ОНОВЛЕННЯ: Правильне повідомлення про помилку: 'T': не може надати аргументи під час створення екземпляра змінної
LB.

Це тому, що ви не використовуєте порожній конструктор, ви передаєте йому аргумент об'єкта. Ні в якому разі це не вдається вирішити, не вказуючи, що загальний тип має новий (об'єктний) параметр.
Хв

Тоді вам потрібно буде: 1. Використовувати відображення 2. Передати параметр методу ініціалізації замість конструктора, де метод ініціалізації належить до інтерфейсу, який реалізує ваш тип і який включений у T: ... декларація. Варіант 1 є найменшим впливом для решти коду, але варіант 2 забезпечує перевірку часу компіляції.
Річард

Не використовуйте рефлексію! Є й інші способи, викладені в інших відповідях, які дають вам такий же ефект.
Гаррі Шутлер

@Garry - Я погоджуюся, що рефлексія не обов'язково є найкращим підходом, але це дозволяє вам досягти необхідного з мінімальними змінами на іншій частині коду. З цього приводу я дуже віддаю перевагу підходу делегата заводу від @JaredPar.
Річард

7

Якщо ви просто хочете ініціалізувати поле або властивість члена за допомогою параметра конструктора, у C #> = 3 це можна зробити дуже простіше:

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

Це те саме, що сказав Гаррі Шутлер, але я хотів би поставити додаткову записку.

Звичайно, ви можете використовувати властивість фокус, щоб зробити більше матеріалів, ніж просто встановити значення поля. Властивість "set ()" може запустити будь-яку обробку, необхідну для встановлення відповідних полів та будь-яку іншу потребу в самому об'єкті, включаючи перевірку, чи має відбутися повна ініціалізація до того, як об'єкт буде використаний, імітуючи повну структуру ( так, це потворне вирішення, але воно долає нове () обмеження M $).

Я не можу бути впевнений, якщо це запланована діра чи випадковий побічний ефект, але це працює.

Дуже смішно, як люди з MS додають нові функції до мови і, здається, не роблять повного аналізу побічних ефектів. Вся родова річ є хорошим свідченням цього ...


1
Обидва обмеження потрібні. InterfaceOrBaseClass робить компілятора обізнаним про поле / властивість BaseMemberItem. Якщо коментується обмеження "new ()", воно спричинить помилку: Помилка 6 Неможливо створити екземпляр типу змінної 'T', оскільки у нього немає нового () обмеження
fljx

Ситуація, з якою я стикався, була не зовсім такою, як запитання, яке мені тут задавали, однак ця відповідь привела мене туди, куди мені потрібно поїхати, і, здається, працює дуже добре.
RubyHaus

5
Щоразу, коли хтось згадує Microsoft як "M $", крихітна частина моєї душі страждає.
Mathias Lykkegaard Lorenzen

6

Я виявив, що я отримую помилку "не можу надати аргументи під час створення екземпляра параметра типу T", тому мені потрібно було це зробити:

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

5

Якщо у вас є доступ до класу, який ви будете використовувати, ви можете використовувати такий підхід, який я використовував.

Створіть інтерфейс, який має альтернативного творця:

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

Складіть свої заняття з порожнім творцем і реалізуйте цей метод:

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

Тепер використовуйте свої загальні методи:

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

Якщо у вас немає доступу, оберніть цільовий клас:

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

Це на кшталт мукі, і коли я кажу, що це мікі, я можу означати бунт, але припустимо, що ви можете надати свій параметризований тип порожнім конструктором, тоді:

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

Ефективно дозволить вам сконструювати об’єкт із параметра, що параметризується, з аргументом. У цьому випадку я припускаю, що у конструктора, який я хочу, є єдиний аргумент типу object. Ми створюємо фіктивний екземпляр T, використовуючи обмежений дозволений порожній конструктор, а потім використовуємо відображення для отримання одного з інших його конструкторів.


0

Я іноді використовую підхід, який нагадує відповіді, використовуючи властивість введення властивостей, але зберігає код чистішим. Замість того, щоб мати базовий клас / інтерфейс із набором властивостей, він містить лише (віртуальний) метод Initialize (), який діє як "конструктор бідної людини". Тоді ви можете дозволити кожному класу обробляти його власну ініціалізацію так само, як конструктор, що також додає зручний спосіб обробки ланцюгів успадкування.

Якщо часто опиняюсь у ситуаціях, коли я хочу, щоб кожен клас у ланцюжку ініціалізував свої унікальні властивості, а потім викликав батьківський метод Initialize () - метод, який у свою чергу ініціалізує унікальні властивості батьків тощо. Це особливо корисно при наявності різних класів, але з подібною ієрархією, наприклад, бізнес-об'єкти, відображені в / з DTO: s.

Приклад, який використовує загальний словник для ініціалізації:

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

Якщо все, що вам потрібно, це перетворення з ListItem у тип T, ви можете здійснити цю конверсію в T класі як оператор перетворення.

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

Я вважаю, що вам доведеться обмежувати T за допомогою заяви, де дозволено лише об'єкти з новим конструктором.

Тепер він приймає будь-що, включаючи об'єкти без нього.


1
Ви можете змінити цю відповідь, оскільки це питання було змінено на запитання після того, як ви відповіли, що не відповідає цій відповіді поза контекстом.
shuttle87
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.