在c#中模拟可变模板

本文关键字:模拟 | 更新日期: 2023-09-27 18:03:47

是否有一种众所周知的方法来模拟c#中的可变模板功能?

例如,我想写一个方法,它接受一个带有任意一组参数的lambda。下面是我想要的伪代码:

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

在c#中模拟可变模板

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

c#泛型是直接编译的,带有一个空的"占位符",用于在运行时使用的类型。

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

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

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

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

重要的是——你如何引用那些不同的T*来写一个泛型方法?也许您最好的选择是采取Type[]或类似的(取决于上下文)。

我知道这是一个老问题,但如果你想做的只是简单的事情,比如打印这些类型,你可以很容易地做到这一点,而不需要Tuple或使用'dynamic':

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

将打印"系统。Int32", "System. "双"、"系统。字符串"

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

    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",因为.5没有被四舍五入,添加为双精度。这段代码的问题是,如果你在列表中传递一些不兼容的类型,它会有一个错误,因为类型不能加在一起,你不会在编译时看到这个错误,只有在运行时。

所以,根据你的用例,可能有另一个选择,这就是为什么我说一开始有两个选择。假设您知道可能的类型的选择,您可以创建一个接口或抽象类,并让所有这些类型实现接口。例如:抱歉,这有点疯狂。而且它可能可以被简化。

    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#中完成这一点。

除了上面提到的方法之外,另一种方法是使用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();
    }
}

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

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>

可以看到,在几个链式调用之后,类型签名基本上变得不可读,但只要类型推断有效并向用户建议正确的类型,这就没有问题。

在我的例子中,我处理的是Func的参数,但是你可以改编这段代码来处理任意类型的参数。

对于模拟,您可以说:

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

其中Tparams为可变参数实现类。然而,框架并没有提供现成的东西来做到这一点,Action, Func, Tuple等,都是有长度限制的签名。我唯一能想到的就是应用CRTP。以一种我从未见过的方式下面是我的实现:


*:感谢@SLaks提到Tuple<T1, ..., T7, TRest>也以递归方式工作。我注意到它是递归的构造函数和工厂方法,而不是它的类定义;并对TRest类型的最后一个参数进行运行时类型检查,要求为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<>的类型参数
public class T<P>:C<T<P>>, INode<P> {

T<P>,并且T<P>类是嵌套的,因此您可以做一些疯狂的事情,例如:

    测试
  • [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类抽象为不直接被new编辑。

上面的代码部分需要展开它们的泛型参数来计算它们的长度,下面是它使用的两个扩展方法:

  • 代码(扩展)

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