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

Я пытаюсь создать новый объект типа 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.
   } 
   ...
}

17 ответов

Решение

Чтобы создать экземпляр универсального типа в функции, вы должны ограничить его флагом "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));

В.Net 3.5 и после вы можете использовать класс активатора:

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

Так как никто не удосужился опубликовать ответ "Отражение" (который я лично считаю лучшим ответом), вот так:

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.

Очень старый вопрос, но новый ответ;-)

Версия 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` fucntion 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 раз быстрее, чем Invoking ConstructorInfo и в +/- 20 раз быстрее, чем при использовании Activator

Инициализатор объекта

Если ваш конструктор с параметром ничего не делает, кроме установки свойства, вы можете сделать это в 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 может иметь некоторые потери производительности, которых вы можете избежать, если скорость выполнения является главным приоритетом, а другой вариант доступен для вас.

Это не будет работать в вашей ситуации. Вы можете указать только ограничение на наличие пустого конструктора:

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.

Если вы просто хотите инициализировать поле или свойство элемента с помощью параметра конструктора, в 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 добавляют новые функции в язык и, похоже, не проводят полный анализ побочных эффектов. Все общее является хорошим доказательством этого...

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

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

Я обнаружил, что получаю ошибку "не могу предоставить аргументы при создании экземпляра параметра типа T", поэтому мне нужно было сделать это:

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

Если у вас есть доступ к классу, который вы собираетесь использовать, вы можете использовать этот подход, который я использовал.

Создайте интерфейс, у которого есть альтернативный создатель:

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

Используя функцию статических абстрактных методов интерфейса в C# 11, можно создать обходной путь, используя фабричный шаблон. Например:

      public interface IFactory<TSelf> where TSelf : IFactory<TSelf> {
  static abstract TSelf New(int i, string s);
}

public struct Foo : IFactory<Foo> {

  public static Foo New(int i, string s) {
    return new Foo(i, s);
  }

  public readonly int I;
  public readonly string S;

  public Foo(int i, string s) {
    I = i;
    S = s;
  }
}

public static class Maker {
  public static T Make<T>(int i, string s) where T : IFactory<T> {
    return T.New(i, s);
  }
}

Ограничение этого подхода заключается в том, что его можно использовать только для классов, которыми вы владеете, поскольку вам необходимо реализовать свой конкретный фабричный интерфейс в целевых классах.

Иногда я использую подход, который напоминает ответы с использованием внедрения свойств, но сохраняет код в чистоте. Вместо базового класса / интерфейса с набором свойств, он содержит только (виртуальный) метод 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;
    }
}

Просто чтобы немного расширить ответ на дерево выражений. Если вы хотите избежать списка параметров и типизированных делегатов, вы можете использовать дженерики и старые добрые статические классы для кэширования всех ваших выражений.

Первое использование типа и/или аргументов занимает больше времени из-за компиляции, но впоследствии скорость не имеет себе равных.

      public static class New<T>
{
    private readonly static Type TypeOfT = typeof(T);
    public static T Create()
    {
        return Build.Instantiate();
    }
    public static T Create<Arg1>(Arg1 arg1)
    {
        return Build<Arg1>.Instantiate(arg1);
    }

    public static T Create<Arg1, Arg2>(Arg1 arg1, Arg2 arg2)
    {
        return Build<Arg1, Arg2>.Instantiate(arg1, arg2);
    }

    public static Func<T> Construct()
    {
        return Build.Constructor;
    }

    public static Func<Arg1, T> Construct<Arg1>()
    {
        return Build<Arg1>.Constructor;
    }

    public static Func<Arg1, Arg2, T> Construct<Arg1, Arg2>()
    {
        return Build<Arg1, Arg2>.Constructor;
    }

    private static class Helper
    {
        public static Tuple<NewExpression, ParameterExpression[]> CreateExpressions(Type[] argsTypes)
        {
            var constructorInfo = TypeOfT.GetConstructor(argsTypes);
            var constructorParameters = argsTypes.Select(p => Expression.Parameter(p)).ToArray();
            var expression = Expression.New(constructorInfo, constructorParameters);
            return new Tuple<NewExpression, ParameterExpression[]>(expression, constructorParameters);
        }
    }

    private static class Build
    {
        public static readonly Func<T> Constructor = Expression.Lambda<Func<T>>(Expression.New(typeof(T))).Compile();
        public static T Instantiate() => Constructor.Invoke();
    }

    private static class Build<Arg1>
    {
        public static readonly Func<Arg1, T> Constructor = CreateConstructor();
        public static T Instantiate(Arg1 arg) => Constructor.Invoke(arg);
        private static Func<Arg1, T> CreateConstructor()
        {
            var expressionAndParam = Helper.CreateExpressions(new Type[] { typeof(Arg1) });
            return Expression.Lambda<Func<Arg1, T>>(expressionAndParam.Item1, expressionAndParam.Item2).Compile();
        }
    }

    private static class Build<Arg1, Arg2>
    {
        public static Func<Arg1, Arg2, T> Constructor = CreateConstructor();
        public static T Instantiate(Arg1 arg1, Arg2 arg2) => Constructor.Invoke(arg1, arg2);
        private static Func<Arg1, Arg2, T> CreateConstructor()
        {
            var expressionAndParam = Helper.CreateExpressions(new Type[] { typeof(Arg1), typeof(Arg2) });
            return Expression.Lambda<Func<Arg1, Arg2, T>>(expressionAndParam.Item1, expressionAndParam.Item2).Compile();
        }
    // removed the rest of the code for brevity but you
    // would use the same pattern to continue with as many 
    // expected arguments as you may need
}

Пример использования.

      // create the list with initial capacity
var list = New<List<string>>.Create(10);
        
// constructor for int as a Func
Func<int> integerConstructor = New<int>.Construct();
int x = integerConstructor.Invoke();

MyClass myclass = New<MyClass>.Create("hello", "world");

Если все, что вам нужно, это преобразование из 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);
    } 
    ...
}

Это немного грязно, и когда я говорю "грязно", я имею в виду отвратительное поведение, но предположим, что вы можете снабдить свой параметризованный тип пустым конструктором, тогда:

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, используя разрешенный ограничением пустой конструктор, а затем используем отражение, чтобы получить один из его других конструкторов.

Дополнительная информация о производительности

Тест производительности доступа к базе данных и заполнения класса модели данных с помощью метода выражения Йеруна ван Лангена (см. Выше) и прямого создания экземпляра класса модели данных.

Вывод: метод выражения быстрее.

Результаты:

  1. Тест: прямой экземпляр класса модели данных: записей: 3558, секунд: 1,2746019
  2. Тест: экземпляр со списком чтения метода с параметром типа: записей: 3558, секунд: 0,4878858

Пример кода метода выражения:

      var list = ReadList<DataModel>(SQLStatement, Connection);

Метод ReadList: Примечание: все классы модели данных имеют конструктор с типом параметра SQLDataReader.

      public static List<pDataModel> ReadList<pDataModel>(string pSQLStatement, SqlConnection pConnection) where pDataModel : new()
    {
            // constructor of data model
            var lType = typeof(pDataModel);
            var lParameters = new Type[] { typeof(SqlDataReader) };
            var lDataModelConstructor = CreateConstructor(lType, lParameters);

            // read data
            List<pDataModel> lDataList = new List<pDataModel>();
            using (pConnection)
            {
                SqlCommand lCommand;
                lCommand = new SqlCommand(pSQLStatement, pConnection);
                pConnection.Open();
                SqlDataReader lReader = lCommand.ExecuteReader();


                if (lReader.HasRows)
                {
                    while (lReader.Read())
                    {
                        pDataModel lDataItem = (pDataModel)lDataModelConstructor(lReader);
                        lDataList.Add(lDataItem);
                    }
                }
                lReader.Close();
                pConnection.Close();
            }

            return lDataList;
    }

Пример кода прямого создания экземпляра:

                 List<DataModel> list= new List<DataModel>();
            using (connection)
            {
                SqlCommand command;
                command = new SqlCommand(SQLStatement, connection);
                connection.Open();
                SqlDataReader reader = command.ExecuteReader();
                if (reader.HasRows)
                {
                    while (reader.Read())
                    {
                        list.Add(new DataModel(reader));
                    }
                }
                reader.Close();
                connection.Close();
            }

Я считаю, что вы должны ограничить T оператором where, чтобы разрешить только объекты с новым конструктором.

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

Другие вопросы по тегам