传递参数给C#泛型new()的模板类型

479

当向列表中添加元素时,我试图通过其构造函数创建类型为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
可能是创建泛型类型的实例?的重复问题。 - nawfal
2
将此功能添加到语言中的建议: https://github.com/dotnet/roslyn/issues/2206 - Ian Kemp
1
在微软的文档中,请参阅编译器错误 CS0417 - DavidRR
2
将此功能纳入语言的提案已移至:https://github.com/dotnet/csharplang/issues/769 - reducing activity
17个回答

448
为了在函数中创建泛型类型的实例,您必须使用“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
将我的代码添加到另一个问题中: https://dev59.com/nXI-5IYBdhLWcg3wxbjv - ChrisCa
1
谢谢。在我的情况下,当我调用方法时,我知道构造函数的参数,我只需要绕过Type参数的限制,因为它不能用参数构造,所以我使用了一个thunk。thunk是该方法的可选参数,只有在提供时才会使用:T result = thunk == null ? new T() : thunk(); 对我来说,这样做的好处是将T的创建逻辑集中在一个地方,而不是有时在方法内部创建T,有时在方法外部创建T。 - Carl G

372

在 .Net 3.5 及以上版本中,您可以使用 Activator 类:

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

1
我们也可以使用表达式树来构建对象。 - Welly Tambunan
6
args是什么?是一个object[]吗? - Rodney P. Barbati
6
是的,args 是一个 object[] 数组,其中你可以指定要提供给 T 类构造函数的值:"new object[]{ par1, par2 }"。 - Niki Romagnoli
51
警告性的结论是,Activator.CreateInstance<T>执行相同任务所需的时间大约是new T()的11倍,而委托需要大约1.5倍的时间。如果您有性能问题,请先测量以查看瓶颈在哪里。是的,上述时间可能表明Activator.CreateInstance具有比动态构建的委托更多的开销,但在您的代码库中,在达到这个优化级别之前(甚至不必达到),可能会有更大的问题需要解决。 - user585968
11
警告:如果你只是为了让 Activator.CreateInstance 这一个功能而创建了一个专门的构造函数,那它看起来就好像你的构造函数根本没有被使用过,有人可能会试图“清理”并删除它(导致将来某个随机时间运行时出现错误)。你可能需要考虑添加一个虚拟函数,在其中使用这个构造函数,这样如果你尝试删除它,就会得到一个编译错误。 - jrh
显示剩余3条评论

53

既然没有人费心回答“反射”问题(我个人认为这是最好的答案),那么就让我来回答吧:

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

编辑:由于.NET 3.5的Activator.CreateInstance,此答案已过时,但在旧版.NET中仍然有用。


我的理解是,大部分性能损失都在获取ConstructorInfo时发生。如果没有进行分析,请不要轻信我的话。如果是这种情况,简单地存储ConstructorInfo以供后续重复实例化通过反射来缓解性能损失。 - Kelsie
1
@James 我同意,我很惊讶没有看到这个作为“答案”。实际上,我搜索了这个问题,期望找到一个漂亮简单的例子(像你的一样),因为我已经很久没有做反射了。无论如何,我给你+1,但也给Activator的答案+1。我研究了一下Activator在做什么,结果发现它所做的是一些非常精心设计的反射。 :) - Mike
GetConstructor()调用很昂贵,因此在循环之前进行缓存是值得的。这样,仅在循环内调用Invoke(),比同时调用两者甚至使用Activator.CreateInstance()要快得多。 - Cosmin Rus
1
除了性能问题之外,这并不是类型安全的,因为所有内容都在运行时解析。如果classType构造函数发生更改,例如变成IFoo列表而不是具体的Foo列表,则编译器将无法提供任何帮助。事情将继续编译,但你最终会在运行时崩溃。 - Orion Edwards

38

�常�的问题,但是有新的答案 😉

表达�树版本:(我认为这是最快和最干净的解决方案)

�Welly Tambunan所说的那样,“我们也�以使用表达�树��建对象�

这将为给定类�/�数生�一个'�造函数'(函数)。它返�一个委托,并将�数类�作为对象数组��。

以下是代�:

// 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


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

小缺陷

所有值类型参数在传递时都会被装箱为对象数组。


简单性能测试:

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

使用 表达式 的速度比调用 ConstructorInfo 快约 8倍,比使用 Activator 快约 20倍


1
你对如何使用构造函数 public MyClass(T data) 构造 MyClass<T> 有什么想法吗? 这种情况下,Expression.Convert 会抛出异常,如果我使用泛型约束基类进行转换,那么 Expression.New 也会抛出异常,因为构造函数信息是针对泛型类型的。 - Mason
1
@Mason(回答有点慢;-))var myConstructor = CreateConstructor(typeof(MyClass<int>), typeof(int)); 这个代码正常运行。我也不知道。 - Jeroen van Langen

34

对象初始化器

如果您的构造函数除了设置属性之外没有做任何其他事情,您可以在 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 可能存在性能开销,如果执行速度是最重要的,并且有另一种可维护的选择,则可能需要避免使用它。

1
这会导致 T 无法保护其不变量(假设 T 具有 >0 个依赖项或所需值,则现在可以创建处于无效/不可用状态的 T 实例。除非 T 是像 DTO 或视图模型这样简单的东西,否则我建议避免这种情况。 - sara
这也需要属性不被封装。 - Guney Ozsan

20

这在你的情况下不起作用。你只能指定它具有空构造函数的约束:

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

你可以通过定义以下接口来使用属性注入:

```

public interface IMyService

{

    void DoSomething();

}

```

然后在需要使用的类中声明该接口的属性,并使用相应的依赖注入框架将其注入。这样,您就可以在类中使用IMyService接口的方法了。
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 });
   } 
   ...
}

另一个选择是JaredPar所描述的Func方法。

这是否会绕过接受参数的构造函数中的任何逻辑呢? 我想做一些像Jared那样的事情,但是在类内部调用方法,所以不知道具体的类型是什么... 嗯 - ChrisCa
3
对的,这会调用T()默认构造函数的逻辑,然后简单地设置属性“Item”。如果您尝试调用非默认构造函数的逻辑,这将无济于事。 - Scott Stafford

8
如果您只是想使用构造函数参数初始化成员字段或属性,在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. 
   }  
   ... 
} 

这是Garry Shutler说过的同一件事,但我想加上一个附加说明。
当然,您可以使用属性技巧来完成更多的操作,而不仅仅是设置字段值。 属性“set()”可以触发任何处理需要设置其相关字段和对象本身所需的任何其他内容,包括检查是否需要在使用对象之前进行完整初始化,模拟完整构造(是的,这是一种丑陋的解决方法,但它克服了M $的new()限制)。
我不能确定它是计划中的漏洞还是意外的副作用,但它确实起作用。
很有趣的是,微软的人们似乎并没有对新功能进行全面的副作用分析。 整个通用事物就是这方面的很好证据...

1
两个约束都是必需的。InterfaceOrBaseClass 让编译器知道字段/属性 BaseMemberItem。如果注释 "new()" 约束,将触发以下错误:错误6:无法创建变量类型为'T'的实例,因为它没有new()约束。 - fljx

7
我发现我遇到了一个错误:“无法在创建类型参数T的实例时提供参数”,因此我需要执行以下操作:
var x = Activator.CreateInstance(typeof(T), args) as T;

7

您需要添加 where T: new(),以便让编译器知道 T 一定提供了默认构造函数。

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

2
更新:正确的错误信息是:“T”:在创建变量实例时无法提供参数。 - LB.
这是因为您没有使用空构造函数,而是将一个对象参数传递给它。如果不指定泛型类型具有新的(对象)参数,它就无法处理它。 - Min
那么你需要:
  1. 使用反射
  2. 将参数传递到初始化方法中,而不是构造函数中,在这种情况下,初始化方法属于一个接口,你的类型实现了该接口,并且该接口包含在 where T: ... 声明中。
选项1对代码的其余部分影响最小,但选项2提供了编译时检查。
- Richard
不要使用反射!其他回答中概述的方法可以达到相同的效果。 - Garry Shutler
@Garry - 我同意反射并不一定是最好的方法,但它确实允许你在最小程度更改代码库的情况下实现所需的功能。话虽如此,我更喜欢 @JaredPar 的工厂委托方法。 - Richard
如果确实需要传递参数,则使用反射,否则请使用@richards的示例。例如,CurrentDataStore [UnitOfWorkDataStore.DATA_CONTEXT_KEY] = new T(); 可以完美地创建一个新的EF数据上下文。 - Nickz

7
如果您可以访问要使用的类,您可以使用我使用过的这种方法。创建一个具有替代创建者的接口:
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);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接