【问题标题】:Simulate variadic templates in C#在 C# 中模拟可变参数模板
【发布时间】:2011-07-27 13:12:47
【问题描述】:

是否有一种众所周知的方法可以在 C# 中模拟可变参数模板功能?

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

void MyMethod<T1,T2,...,TReturn>(Fun<T1,T2, ..., TReturn> f)
{

}

【问题讨论】:

  • 我想知道您是否可以使用params 的参数(可能是TypedReference 的参数)

标签: c# variadic-templates


【解决方案1】:

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

C# 泛型是直接编译的,使用空的“占位符”表示将在运行时使用的类型。

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

【讨论】:

  • 出于这个原因,甚至不能使用泛型来避免虚函数调用。我现在真的很喜欢 C++。
  • 不正确。 .NET 已具体化泛型。 JITter 在 JIT 时间“扩展”它们,并将在(至少)微不足道的情况下去虚拟化。
【解决方案2】:

没有对泛型类型参数的可变支持(在方法或类型上)。您将不得不添加许多重载。

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

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

重要的是——你甚至会如何参考那些各种T* 来编写一个通用方法?也许你最好的选择是使用Type[] 或类似的(取决于上下文)。

【讨论】:

  • C++0x 给出了关于如何引用这些的答案。实际上,它与 Haskell 并没有太大的不同。您选择一个头部,然后递归传递尾部,例如Foo&lt;T...&gt; Foo<Head, Tail...>
【解决方案3】:

我知道这是一个老问题,但如果您只想做一些简单的事情,比如将这些类型打印出来,您可以很容易地做到这一点,而无需使用元组或任何额外的使用“动态”:

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<T>(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 没有四舍五入,添加为双精度数。这段代码的问题是,如果您在列表中传递了一些不兼容的类型,则会出现错误,因为这些类型无法相加,并且您不会在编译时看到该错误,只会在运行时看到。

因此,根据您的用例,可能还有另一种选择,这就是我一开始说有两种选择的原因。假设您知道可能的类型的选择,您可以创建一个接口或抽象类,并让所有这些类型都实现该接口。例如,以下。抱歉,这有点疯狂。它可能会被简化。

    public interface Applyable<T>
    {
        void Apply(T input);

        T GetValue();
    }

    public abstract class Convertable<T>
    {
        public dynamic value { get; set; }

        public Convertable(dynamic value)
        {
            this.value = value;
        }

        public abstract T GetConvertedValue();
    }        

    public class IntableInt : Convertable<int>, Applyable<int>
    {
        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<int>
    {
        public IntableDouble(double value) : base(value) {}

        public override int GetConvertedValue()
        {
            return (int) value;
        }
    }

    public class IntableString : Convertable<int>
    {
        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<TResult>(ref Applyable<TResult> first, params Convertable<TResult>[] args)
    {
        foreach (var arg in args)
        {                
            first.Apply(arg.GetConvertedValue());  
        }
    }

    static void Main(string[] args)
    {
        Applyable<int> 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();
    }

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

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

【讨论】:

    【解决方案4】:

    除了上面提到的之外,另一种选择是使用 Tuple 和反射,例如:

    class PrintVariadic<T>
    {
        public T Value { get; set; }
    
        public void Print()
        {
            InnerPrint(Value);
        }
    
        static void InnerPrint<Tn>(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<Tuple<
                int, Tuple<
                string, Tuple<
                double, 
                long>>>>();
            v.Value = Tuple.Create(
                1, Tuple.Create(
                "s", Tuple.Create(
                4.0, 
                4L)));
            v.Print();
            Console.ReadKey();
        }
    }
    

    【讨论】:

      【解决方案5】:

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

      public interface ITraversalRoot<TRoot>
      {
          ITraversalSpecification<TRoot> Specify();
      }
      
      public interface ITraverser<TRoot, TCurrent>: ITraversalRoot<TRoot>
      {
          IDerivedTraverser<TRoot, TInclude, TCurrent, ITraverser<TRoot, TCurrent>> AndInclude<TInclude>(Expression<Func<TCurrent, TInclude>> path);
      }
      
      public interface IDerivedTraverser<TRoot, TDerived, TParent, out TParentTraverser> : ITraverser<TRoot, TParent>
      {
          IDerivedTraverser<TRoot, TInclude, TDerived, IDerivedTraverser<TRoot, TDerived, TParent, TParentTraverser>> FromWhichInclude<TInclude>(Expression<Func<TDerived, TInclude>> path);
      
          TParentTraverser ThenBackToParent();
      }
      

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

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

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

      在我的示例中,我正在处理 Funcs 参数,但您可能可以修改此代码以处理任意类型的参数。

      【讨论】:

      • 这可能是强类型 FP 世界所称的 HList。
      • 嗯,这个例子似乎不完整。 ITraversalSpecification 和遍历定义会很好
      【解决方案6】:

      对于模拟,您可以说:

      void MyMethod<TSource, TResult>(Func<TSource, TResult> f) where TSource : Tparams {
      

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


      *:感谢@SLaks 提到Tuple&lt;T1, ..., T7, TRest&gt; 也以递归方式工作。我注意到它在构造函数和工厂方法上是递归的,而不是它的类定义;而TRest类型的最后一个参数的do a runtime type checking必须是ITupleInternal;这有点不同。


      • 代码

        using System;
        
        namespace VariadicGenerics {
            public interface INode {
                INode Next {
                    get;
                }
            }
        
            public interface INode<R>:INode {
                R Value {
                    get; set;
                }
            }
        
            public abstract class Tparams {
                public static C<TValue> V<TValue>(TValue x) {
                    return new T<TValue>(x);
                }
            }
        
            public class T<P>:C<P> {
                public T(P x) : base(x) {
                }
            }
        
            public abstract class C<R>:Tparams, INode<R> {
                public class T<P>:C<T<P>>, INode<P> {
                    public T(C<R> node, P x) {
                        if(node is R) {
                            Next=(R)(node as object);
                        }
                        else {
                            Next=(node as INode<R>).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<TValue> V<TValue>(TValue x) {
                    return new T<TValue>(this, x);
                }
        
                public int GetLength() {
                    return m_expandedArguments.Length;
                }
        
                public C(R x) {
                    (this as INode<R>).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<R>.Value {
                    get; set;
                }
        
                INode INode.Next {
                    get {
                        return null;
                    }
                }
        
                static readonly Type[] m_expandedArguments;
            }
        }
        

      注意声明中继承类C&lt;&gt;的类型参数

      public class T<P>:C<T<P>>, INode<P> {
      

      T&lt;P&gt;,而T&lt;P&gt;类是嵌套的,这样你就可以做一些疯狂的事情,比如:

      • 测试

        [Microsoft.VisualStudio.TestTools.UnitTesting.TestClass]
        public class TestClass {
            void MyMethod<TSource, TResult>(Func<TSource, TResult> f) where TSource : Tparams {
                T<byte>.T<char>.T<uint>.T<long>.
                T<byte>.T<char>.T<long>.T<uint>.
                T<byte>.T<long>.T<char>.T<uint>.
                T<long>.T<byte>.T<char>.T<uint>.
                T<long>.T<byte>.T<uint>.T<char>.
                T<byte>.T<long>.T<uint>.T<char>.
                T<byte>.T<uint>.T<long>.T<char>.
                T<byte>.T<uint>.T<char>.T<long>.
                T<uint>.T<byte>.T<char>.T<long>.
                T<uint>.T<byte>.T<long>.T<char>.
                T<uint>.T<long>.T<byte>.T<char>.
                T<long>.T<uint>.T<byte>.T<char>.
                T<long>.T<uint>.T<char>.T<byte>.
                T<uint>.T<long>.T<char>.T<byte>.
                T<uint>.T<char>.T<long>.T<byte>.
                T<uint>.T<char>.T<byte>.T<long>.
                T<char>.T<uint>.T<byte>.T<long>.
                T<char>.T<uint>.T<long>.T<byte>.
                T<char>.T<long>.T<uint>.T<byte>.
                T<long>.T<char>.T<uint>.T<byte>.
                T<long>.T<char>.T<byte>.T<uint>.
                T<char>.T<long>.T<byte>.T<uint>.
                T<char>.T<byte>.T<long>.T<uint>.
                T<char>.T<byte>.T<uint>.T<long>
                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<byte>.T<char>.T<uint>.T<long>.
                    T<byte>.T<char>.T<long>.T<uint>.
                    T<byte>.T<long>.T<char>.T<uint>.
                    T<long>.T<byte>.T<char>.T<uint>.
                    T<long>.T<byte>.T<uint>.T<char>.
                    T<byte>.T<long>.T<uint>.T<char>.
                    T<byte>.T<uint>.T<long>.T<char>.
                    T<byte>.T<uint>.T<char>.T<long>.
                    T<uint>.T<byte>.T<char>.T<long>.
                    T<uint>.T<byte>.T<long>.T<char>.
                    T<uint>.T<long>.T<byte>.T<char>.
                    T<long>.T<uint>.T<byte>.T<char>.
                    T<long>.T<uint>.T<char>.T<byte>.
                    T<uint>.T<long>.T<char>.T<byte>.
                    T<uint>.T<char>.T<long>.T<byte>.
                    T<uint>.T<char>.T<byte>.T<long>.
                    T<char>.T<uint>.T<byte>.T<long>.
                    T<char>.T<uint>.T<long>.T<byte>.
                    T<char>.T<long>.T<uint>.T<byte>.
                    T<long>.T<char>.T<uint>.T<byte>.
                    T<long>.T<char>.T<byte>.T<uint>.
                    T<char>.T<long>.T<byte>.T<uint>.
                    T<char>.T<byte>.T<long>.T<uint>.
                    T<char>.T<byte>.T<uint>.T<long>, 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<P>:C<P> {
      

      只是为了使用的一致性,我将C类抽象为不直接被newed。

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

      • 代码(扩展)

        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 的流畅模式进行大规模对象实例化,以保持类型可以尽可能地进行静态类型检查。

      【讨论】:

      • 实际上,Tuple&lt;T1, ..., T7, TRest&gt; 是这样工作的。
      • @SLaks:谢谢,很高兴知道,因为我从未使用过T8 .. 将对其进行修改并保留一个不同的实现。
      • 我不明白。这里有什么好处?类型安全在这里很容易被淘汰,只需键入 var crazy = Tparams.V(...).... 如果不重复声明,编译时就不会发现
      • @mrAtari 感谢您的评论。这只是一个不存在的特征的模拟的想法。
      猜你喜欢
      • 2011-02-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-23
      • 2016-12-01
      • 1970-01-01
      • 2013-09-14
      相关资源
      最近更新 更多