在c#中模拟可变参数模板

有没有一种众所周知的方法来模拟c#中的可变参数模板function?

例如,我想编写一个带有任意参数集的lambda的方法。 这是我想要的伪代码:

void MyMethod(Fun f) { } 

谢谢

C#generics与C ++模板不同。 C ++模板是扩展的编译时间,可以使用可变参数模板参数递归使用。 C ++模板扩展实际上是图灵完成,所以理论上没有限制在模板中可以做什么。

C#generics是直接编译的,其中包含将在运行时使用的类型的空“占位符”。

要接受带有任意数量参数的lambda,您必须生成大量重载(通过代码生成器)或接受LambdaExpression

generics类型参数(无论是方法还是类型)​​都没有varadic支持。 您将不得不添加大量重载。

varadic支持仅适用于数组,通过params ,即

 void Foo(string key, params int[] values) {...} 

令人沮丧的是 – 你甚至会如何引用那些不同的T*来编写通用方法? 也许你最好的选择是采用Type[]或类似的(取决于上下文)。

我知道这是一个古老的问题,但是如果您想要做的只是打印这些类型的简单操作,那么您可以非常轻松地使用“Tuple”或任何额外的“动态”执行此操作:

 private static void PrintTypes(params dynamic[] args) { foreach (var arg in args) { Console.WriteLine(arg.GetType()); } } static void Main(string[] args) { PrintTypes(1,1.0,"hello"); Console.ReadKey(); } 

将打印“System.Int32”,“System.Double”,“System.String”

如果你想对这些事情采取一些行动,据我所知你有两个选择。 一种方法是信任程序员这些类型可以执行兼容操作,例如,如果您想创建一个方法来求和任意数量的参数。 您可以编写如下方法,说明您希望如何接收结果,并且我认为唯一的先决条件是+操作在这些类型之间起作用:

  private static void AddToFirst(ref T first, params dynamic[] args) { foreach (var arg in args) { first += arg; } } static void Main(string[] args) { int x = 0; AddToFirst(ref x,1,1.5,2.0,3.5,2); Console.WriteLine(x); double y = 0; AddToFirst(ref y, 1, 1.5, 2.0, 3.5, 2); Console.WriteLine(y); Console.ReadKey(); } 

有了这个,第一行的输出将是“9”,因为添加到int,第二行将是“10”,因为.5s没有得到舍入,添加为double。 此代码的问题是如果您在列表中传递了一些不兼容的类型,它将会出现错误,因为类型无法一起添加,并且您不会在编译时看到该错误,仅在运行时。

因此,根据您的使用情况,可能还有另一种选择,这就是为什么我说首先有两个选择。 假设您知道可能类型的选择,您可以创建一个接口或抽象类,并使所有这些类型实现接口。 例如,以下内容。 对不起,这有点疯狂。 它可能是简单的。

  public interface Applyable { void Apply(T input); T GetValue(); } public abstract class Convertable { public dynamic value { get; set; } public Convertable(dynamic value) { this.value = value; } public abstract T GetConvertedValue(); } public class IntableInt : Convertable, Applyable { public IntableInt(int value) : base(value) {} public override int GetConvertedValue() { return value; } public void Apply(int input) { value += input; } public int GetValue() { return value; } } public class IntableDouble : Convertable { public IntableDouble(double value) : base(value) {} public override int GetConvertedValue() { return (int) value; } } public class IntableString : Convertable { public IntableString(string value) : base(value) {} public override int GetConvertedValue() { // If it can't be parsed return zero int result; return int.TryParse(value, out result) ? result : 0; } } private static void ApplyToFirst(ref Applyable first, params Convertable[] args) { foreach (var arg in args) { first.Apply(arg.GetConvertedValue()); } } static void Main(string[] args) { Applyable result = new IntableInt(0); IntableInt myInt = new IntableInt(1); IntableDouble myDouble1 = new IntableDouble(1.5); IntableDouble myDouble2 = new IntableDouble(2.0); IntableDouble myDouble3 = new IntableDouble(3.5); IntableString myString = new IntableString("2"); ApplyToFirst(ref result, myInt, myDouble1, myDouble2, myDouble3, myString); Console.WriteLine(result.GetValue()); Console.ReadKey(); } 

将输出“9”与原始Int代码相同,除了您实际可以传入的唯一值,因为参数是您实际已定义的内容,并且您知道它们将起作用并且不会导致任何错误。 当然,你必须创建新的类,如DoubleableInt,DoubleableString等,以便重新创建10的第二个结果。但这只是一个例子,所以你根本不会尝试添加任何东西取决于您正在编写的代码,您将从最适合您的实现开始。

希望有人可以改进我在这里写的内容或使用它来看看如何在C#中完成。

除了上面提到的那些之外的另一个替代方案是使用元组<,>和reflection,例如:

 class PrintVariadic { public T Value { get; set; } public void Print() { InnerPrint(Value); } static void InnerPrint(Tn t) { var type = t.GetType(); if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Tuple<,>)) { var i1 = type.GetProperty("Item1").GetValue(t, new object[]{}); var i2 = type.GetProperty("Item2").GetValue(t, new object[]{ }); InnerPrint(i1); InnerPrint(i2); return; } Console.WriteLine(t.GetType()); } } class Program { static void Main(string[] args) { var v = new PrintVariadic>>>(); v.Value = Tuple.Create( 1, Tuple.Create( "s", Tuple.Create( 4.0, 4L))); v.Print(); Console.ReadKey(); } } 

我不一定知道这个模式是否有名称,但是我得到了一个递归通用接口的下面的公式,它允许传入无限量的值,返回的类型保留所有传递值的类型信息。

 public interface ITraversalRoot { ITraversalSpecification Specify(); } public interface ITraverser: ITraversalRoot { IDerivedTraverser> AndInclude(Expression> path); } public interface IDerivedTraverser : ITraverser { IDerivedTraverser> FromWhichInclude(Expression> path); TParentTraverser ThenBackToParent(); } 

此处涉及的类型系统没有强制转换或“欺骗”:您可以继续堆叠更多值,并且推断的返回类型会不断存储越来越多的信息。 这是用法的样子:

 var spec = Traversal .StartFrom() // ITraverser .AndInclude(vm => vm.EnvironmentBrowser) // IDerivedTraverser> .AndInclude(vm => vm.Datastore) // IDerivedTraverser> .FromWhichInclude(ds => ds.Browser) // IDerivedTraverser>> .FromWhichInclude(br => br.Mountpoints) // IDerivedTraverser>>> .Specify(); // ITraversalSpecification 

正如您所看到的,在几个链式调用之后,类型签名基本上变得不可读,但只要类型推断起作用并向用户建议正确的类型,这就很好。

在我的例子中,我正在处理Func的参数,但你可能会调整这个代码来处理任意类型的参数。

对于模拟,您可以说:

 void MyMethod(Func f) where TSource : Tparams { 

其中Tparams是一个可变参数实现类。 但是,框架并没有提供开箱即用的东西, ActionFuncTuple等都有其签名的有限长度。 我唯一能想到的就是应用CRTP ..我找不到有人写博客的方式。 这是我的实现:


*:感谢@SLaks提及Tuple也以递归方式工作。 我注意到它在构造函数和工厂方法上是递归的,而不是它的类定义; 并且对 TRest类型的最后一个参数进行运行时类型检查需要是ITupleInternal ; 这有点不同。


  •  using System; namespace VariadicGenerics { public interface INode { INode Next { get; } } public interface INode:INode { R Value { get; set; } } public abstract class Tparams { public static C V(TValue x) { return new T(x); } } public class T

    :C

    { public T(P x) : base(x) { } } public abstract class C:Tparams, INode { public class T

    :C

    >, INode

    { public T(C node, P x) { if(node is R) { Next=(R)(node as object); } else { Next=(node as INode).Value; } Value=x; } public T() { if(Extensions.TypeIs(typeof(R), typeof(C<>.T<>))) { Next=(R)Activator.CreateInstance(typeof(R)); } } public R Next { private set; get; } public P Value { get; set; } INode INode.Next { get { return this.Next as INode; } } } public new T V(TValue x) { return new T(this, x); } public int GetLength() { return m_expandedArguments.Length; } public C(R x) { (this as INode).Value=x; } C() { } static C() { m_expandedArguments=Extensions.GetExpandedGenericArguments(typeof(R)); } // demonstration of non-recursive traversal public INode this[int index] { get { var count = m_expandedArguments.Length; for(INode node = this; null!=node; node=node.Next) { if(--count==index) { return node; } } throw new ArgumentOutOfRangeException("index"); } } R INode.Value { get; set; } INode INode.Next { get { return null; } } static readonly Type[] m_expandedArguments; } }

请注意声明中inheritance的类C<>的type参数

 public class T

:C

>, INode

{

T

,并且类T

是嵌套的,这样你就可以做一些疯狂的事情,比如:

  • 测试

     [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass] public class TestClass { void MyMethod(Func f) where TSource : Tparams { T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T crazy = Tparams // trying to change any value to not match the // declaring type makes the compilation fail .V((byte)1).V('2').V(4u).V(8L) .V((byte)1).V('2').V(8L).V(4u) .V((byte)1).V(8L).V('2').V(4u) .V(8L).V((byte)1).V('2').V(4u) .V(8L).V((byte)1).V(4u).V('2') .V((byte)1).V(8L).V(4u).V('2') .V((byte)1).V(4u).V(8L).V('2') .V((byte)1).V(4u).V('2').V(8L) .V(4u).V((byte)1).V('2').V(8L) .V(4u).V((byte)1).V(8L).V('2') .V(4u).V(8L).V((byte)1).V('2') .V(8L).V(4u).V((byte)1).V('2') .V(8L).V(4u).V('9').V((byte)1) .V(4u).V(8L).V('2').V((byte)1) .V(4u).V('2').V(8L).V((byte)1) .V(4u).V('2').V((byte)1).V(8L) .V('2').V(4u).V((byte)1).V(8L) .V('2').V(4u).V(8L).V((byte)1) .V('2').V(8L).V(4u).V((byte)1) .V(8L).V('2').V(4u).V((byte)1) .V(8L).V('2').V((byte)1).V(4u) .V('2').V(8L).V((byte)1).V(4u) .V('2').V((byte)1).V(8L).V(4u) .V('7').V((byte)1).V(4u).V(8L); var args = crazy as TSource; if(null!=args) { f(args); } } [TestMethod] public void TestMethod() { Func< T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T. T.T.T.T, String> f = args => { Debug.WriteLine(String.Format("Length={0}", args.GetLength())); // print fourth value from the last Debug.WriteLine(String.Format("value={0}", args.Next.Next.Next.Value)); args.Next.Next.Next.Value='x'; Debug.WriteLine(String.Format("value={0}", args.Next.Next.Next.Value)); return "test"; }; MyMethod(f); } } 

另外需要注意的是我们有两个名为T类,非嵌套的T

 public class T

:C

{

只是为了使用的一致性,我把C类抽象为不直接new编辑。

上面的Code部分需要扩展thergenerics参数来计算它们的长度,这里有两种扩展方法:

  • 代码(扩展)

     using System.Diagnostics; using System; namespace VariadicGenerics { [DebuggerStepThrough] public static class Extensions { public static readonly Type VariadicType = typeof(C<>.T<>); public static bool TypeIs(this Type x, Type d) { if(null==d) { return false; } for(var c = x; null!=c; c=c.BaseType) { var a = c.GetInterfaces(); for(var i = a.Length; i-->=0;) { var t = i<0 ? c : a[i]; if(t==d||t.IsGenericType&&t.GetGenericTypeDefinition()==d) { return true; } } } return false; } public static Type[] GetExpandedGenericArguments(this Type t) { var expanded = new Type[] { }; for(var skip = 1; t.TypeIs(VariadicType) ? true : skip-->0;) { var args = skip>0 ? t.GetGenericArguments() : new[] { t }; if(args.Length>0) { var length = args.Length-skip; var temp = new Type[length+expanded.Length]; Array.Copy(args, skip, temp, 0, length); Array.Copy(expanded, 0, temp, length, expanded.Length); expanded=temp; t=args[0]; } } return expanded; } } } 

对于这个实现,我选择不破坏编译时类型检查,所以我们没有像params object[]这样的签名的构造函数或工厂来提供值; 相反,使用方法V的流畅模式进行大规模对象实例化,以保持类型可以尽可能静态地进行类型检查。