C#中是否有带参数约束的通用构造函数?

210

在C#中,您可以像这样对泛型方法进行约束:

public class A {

    public static void Method<T> (T a) where T : new() {
        //...do something...
    }

}

当你指定 T 需要一个不需要参数的构造函数时,我想知道是否有一种方法可以添加约束条件,例如 "存在一个带有 float[,] 参数的构造函数?"

以下代码无法编译:

public class A {

    public static void Method<T> (T a) where T : new(float[,] u) {
        //...do something...
    }

}

绕过也是有用的吗?
10个回答

172

正如你所发现的,你无法这样做。

作为一种解决方法,我通常提供一个可以创建类型为 T 的对象的委托:

public class A {

    public static void Method<T> (T a, Func<float[,], T> creator) {
        //...do something...
    }

}

66
参数化构造函数的限制条件是否因为某种合理的原因而不存在,还是只是尚未被添加到该语言中? - Dave Cousineau
49
同意,我们应该有new(float, double)new(string)等。 - SliverNinja - MSFT
46
并不是每个类都有无参数构造函数,如果您定义了带参数的构造函数并且没有重新定义默认构造函数,则没有默认构造函数。 - Johnny5
28
这就是泛型类型约束的全部意义。你需要一个派生自某个类并包含特定参数构造函数的类。 - Spook
17
@bc3tech,从技术上讲,你的观点并不完全正确。如果一个基类没有默认构造函数,你必须提供一个调用其中一个基类构造函数的构造函数,但你不必提供匹配的构造函数。这里有一个微妙的区别... - ghigad
显示剩余6条评论

59
使用反射创建通用对象时,类型仍然需要声明正确的构造函数,否则将抛出异常。只要它们匹配其中一个构造函数,您可以传递任何参数。
以这种方式使用,您无法在模板中对构造函数设置约束。如果缺少构造函数,则需要在运行时处理异常,而不是在编译时获取错误。
// public static object CreateInstance(Type type, params object[] args);

// Example 1
T t = (T)Activator.CreateInstance(typeof(T));
// Example 2
T t = (T)Activator.CreateInstance(typeof(T), arg0, arg1, arg2, ...);
// Example 3
T t = (T)Activator.CreateInstance(typeof(T), (string)arg0, (int)arg1, (bool)arg2);

48

没有这样的构造体。您只能指定空构造约束。

我使用lambda方法解决了这个问题。

public static void Method<T>(Func<int,T> del) {
  var t = del(42);
}

应用场景

Method(x => new Foo(x));

有没有办法将Method内创建Foo进行抽象化? - wingerse
如果Method的用户执行了Method(x => new Foo());,有什么办法可以确保lambda表达式是这样的吗? - wingerse
在这种情况下提供委托的好处是什么,而不是返回int并让使用者进行包装?感觉这只是多余的样板文件而已。 - Josh

20

这里有一个解决方法,我个人认为非常有效。如果你考虑泛型参数化构造函数约束是什么,它实际上是一种类型与具有特定签名的构造函数之间的映射关系。你可以通过使用字典来创建自己的映射关系。将它们放在静态"工厂"类中,你可以创建不同类型的对象,而不必每次都担心构建构造函数lambda:

public static class BaseTypeFactory
{
   private delegate BaseType BaseTypeConstructor(int pParam1, int pParam2);

   private static readonly Dictionary<Type, BaseTypeConstructor>
   mTypeConstructors = new Dictionary<Type, BaseTypeConstructor>
   {
      { typeof(Object1), (pParam1, pParam2) => new Object1(pParam1, pParam2) },
      { typeof(Object2), (pParam1, pParam2) => new Object2(pParam1, pParam2) },
      { typeof(Object3), (pParam1, pParam2) => new Object3(pParam1, pParam2) }
   };

然后在你的通用方法中,例如:

   public static T BuildBaseType<T>(...)
      where T : BaseType
   {
      ...
      T myObject = (T)mTypeConstructors[typeof(T)](value1, value2);
      ...
      return myObject;
   }

1
我现在正在使用这个,我认为这是一个很好的模式。它与工厂模式非常配合。谢谢! - Matthew
这可以扩展为基于其他数据创建类型。当解析类似IFF的文件时,我经常使用这种构造类型的方式。我更喜欢在类型本身上定义静态构造函数,这样我的字典条目最终看起来像 ["CELL"] = Cell.CreateInstance, ["WRLD"] = World.CreateInstance, ... - user502255

9

我认为这是最干净的解决方案,可以对对象构造方式进行约束。它并不完全在编译时检查。当你同意使类的实际构造函数具有与IConstructor接口相同的签名时,就像对构造函数强加了约束一样。由于显式接口实现,正常使用对象时Constructor方法是隐藏的。

using System.Runtime.Serialization;

namespace ConsoleApp4
{
    class Program
    {
        static void Main(string[] args)
        {
            var employeeWorker = new GenericWorker<Employee>();
            employeeWorker.DoWork();
        }
    }

    public class GenericWorker<T> where T:IConstructor
    {
        public void DoWork()
        {
            T employee = (T)FormatterServices.GetUninitializedObject(typeof(T));
            employee.Constructor("John Doe", 105);
        }
    }

    public interface IConstructor
    {
        void Constructor(string name, int age);
    }

    public class Employee : IConstructor
    {
        public string Name { get; private set; }
        public int Age { get; private set; }

        public Employee(string name, int age)
        {
            ((IConstructor)this).Constructor(name, age);
        }

        void IConstructor.Constructor(string name, int age)
        {
            Name = name;
            Age = age;
        }
    }
}

+1 是因为它提供了一些编译时的安全性,而其他工具则没有提供这种安全性,同时也提供了其他工具不支持的接口支持。 - Josh

9
目前,您只能指定无参构造函数的构造函数约束。

3

如果您想保持构造函数的参数完整,C#维护人员建议使用以下推荐解决方案:间接调用构造函数:

            i = (TService)Activator.CreateInstance(typeof(TService), new object[] {arg});

TService 是一个泛型,拥有参数化的构造函数,我想保留它。

如果您想了解这个方法的工作原理,请查看: https://learn.microsoft.com/en-us/dotnet/api/system.activator.createinstance?view=net-5.0#system-activator-createinstance(system-type-system-object-)

并且 C# 的维护人员已经进行了讨论: https://github.com/dotnet/csharplang/discussions/769


请注意,Activator.CreateInstance 的速度相当慢。 - kofifus

3

你可以创建一个带有类型约束的通用类,这里我选择了结构体和类,用于分别处理值类型和引用类型。

这样一来,你的构造函数就对值类型进行了约束。

class MyGenericClass<T, X> where T :struct where X: class 
{
    private T genericMemberVariableT;
    private X genericMemberVariableX;
    public MyGenericClass(T valueT, X valueX)
    {
        genericMemberVariableT = valueT;
        genericMemberVariableX = valueX;
    }

    public T genericMethod(T genericParameter)
    {
        Console.WriteLine("Parameter type: {0}, value: {1}", typeof(T).ToString(), genericParameter);
        Console.WriteLine("Return type: {0}, value: {1}", typeof(T).ToString(), genericMemberVariableT);
        Console.WriteLine("Return type: {0}, value: {1}", typeof(X).ToString(), genericMemberVariableX);
        return genericMemberVariableT;
    }

    public T genericProperty { get; set; }
}

实现:

        MyGenericClass<int, string> intGenericClass = new MyGenericClass<int, string>(10, "Hello world");
        int val = intGenericClass.genericMethod(200);

1
作为替代方案(从C# 9+开始),您可以定义一个带有“init”属性的接口,就像构造函数的参数一样。 其中一个主要优点是它适用于结构体或类。
using System;
                    
public class Program
{
    public interface ITest
    {
        int a { init; }
    }
    public struct Test : ITest{
        public int a { private get; init; }
        public int b => a;
    }   
    public static T TestFunction<T>() where T: ITest, new() {
        return new(){ a = 123 };
    }
    public static void Main()
    {
        var t = TestFunction<Test>();
        Console.WriteLine($"Hello World: {t.b}"); // Prints: Hello World: 123
    }
}

1
截至C# 11 / .NET 7,可以通过在包含必要参数的静态抽象工厂创建方法上应用约束,然后在所有相关类型中实现该接口来实现此目标。
例如,首先定义以下接口:
public interface ICreatable<TArgument, TResult>
{
    public abstract static TResult Create(TArgument arg);
}

然后,在您的Method<T>中,如果您希望T具有一个接受2D浮点数组的静态工厂方法,可以按照以下方式约束它:
public class A 
{
    public static void Method<T> (T a) where T : ICreatable<float[,], T>
    {
        var t = T.Create(new [,] { { 1f, 2f }, {3f, 4f} });
        //...do something...        
    }
}

当然,你传入 Method<T> 的任何类型都需要实现 ICreatable<float[,], T>,例如如下所示:
public partial class Matrix2DFloat : ICreatable<float[,], Matrix2DFloat>
{
    readonly float[,] array;
    public Matrix2DFloat(float[,] array) => this.array = array ?? throw new ArgumentNullException(nameof(array));

    #region ICreatable<float[,], Matrix2DFloat> Members

    public static Matrix2DFloat Create(float[,] arg) => new Matrix2DFloat(arg);

    #endregion
}

演示fiddle here

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