Передача аргументов в 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
Предложение
Ян Кемп

В документации Microsoft см. Ошибка компилятора CS0417 .
DavidRR

1
Предложение о переводе этой функциональности на язык было перенесено на: github.com/dotnet/csharplang/issues/769
снижение активности

Ответы:


410

Чтобы создать экземпляр универсального типа в функции, вы должны ограничить его флагом «new».

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 #. Я хотел бы сделать мои классы неизменными: наличие только частных установщиков сделало бы невозможным пребывание класса в недопустимом состоянии из-за побочных эффектов. Мне также нравится использовать этот Func и лямбду, но я знаю, что это все еще проблема в мире бизнеса, так как обычно программисты еще не знают лямбды, и это затрудняет понимание вашего класса.
Туомас Хиетанен

1
Спасибо. В моем случае я знаю аргумент (ы) конструктора при вызове метода, мне просто нужно было обойти ограничение параметра Type, что он не может быть создан с параметрами, поэтому я использовал thunk . 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: «new object [] {par1, par2}»
TechNyquist


3
ВНИМАНИЕ: Если у вас есть специальный конструктор только для Activator.CreateInstanceэтой цели, похоже, ваш конструктор вообще не используется, и кто-то может попытаться «очистить» и удалить его (чтобы вызвать ошибку времени выполнения в какое-то случайное время в будущем). Возможно, вы захотите добавить фиктивную функцию, где вы используете этот конструктор, просто чтобы вы получили ошибку компиляции, если попытаетесь удалить ее.
17

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
@ Джеймс Я согласен, я был удивлен, не увидев это как «ответ». На самом деле, я искал этот вопрос, ожидая найти хороший простой пример (например, ваш), так как прошло много времени с тех пор, как я размышлял. В любом случае, +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, я бы сказал, избегайте этого.
Сара

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 data). В этом случае Expression.Convert генерирует исключение, и если я использую базовый класс универсального ограничения для преобразования в, тогда Expression.New выбрасывает, потому что информация конструктора для универсального типа
Mason

@ Мейсон (потребовалось время, чтобы ответить ;-)) 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. Передать параметр в метод инициализации вместо конструктора, где метод инициализации принадлежит интерфейсу, который реализует ваш тип и который включен в where T: ... декларация. Вариант 1 - самый низкий эффект для остальной части вашего кода, но вариант 2 обеспечивает проверку времени компиляции.
Ричард

Не используйте отражение! Существуют и другие способы, описанные в других ответах, которые дают вам тот же эффект.
Гарри Шатлер

@ Гарри - я согласен, что рефлексия не обязательно лучший подход, но он позволяет вам достичь того, что требуется, с минимальными изменениями в остальной части кода. Тем не менее, я очень предпочитаю подход фабричного делегата от @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», поскольку у него нет ограничения new ()
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 оператором where, чтобы разрешить только объекты с новым конструктором.

Теперь он принимает все, включая объекты без него.


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