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

问题描述 投票:26回答:6

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

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

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

}

谢谢

c# variadic-templates
6个回答
20
投票

C#泛型与C ++模板不同。 C ++模板是扩展的编译时间,可以使用可变参数模板参数递归使用。 C ++模板扩展实际上是Turing Complete,因此理论上没有限制在模板中可以做什么。

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

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


8
投票

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

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

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

咄咄逼人 - 你怎么会引用那些各种T*来编写通用方法?也许你最好的选择是采取Type[]或类似(取决于具体情况)。


6
投票

我知道这是一个老问题,但是如果您想要做的只是打印这些类型的简单方法,那么无需使用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<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没有得到舍入,添加为double。此代码的问题是如果您在列表中传递一些不兼容的类型,它将会出现错误,因为类型无法一起添加,并且您不会在编译时看到该错误,仅在运行时。

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

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

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

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


4
投票

除了上面提到的那些之外的另一个选择是使用元组<,>和反射,例如:

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

3
投票

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

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参数,但你可能会修改这个代码来处理任意类型的参数。


1
投票

对于模拟,您可以说:

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

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


*:感谢@SLaks提及Tuple<T1, ..., T7, TRest>也以递归方式工作。我注意到它在构造函数和工厂方法上是递归的,而不是它的类定义;和do a runtime type checking类型的最后一个论点的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<>的type参数

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摘要不直接被newed。

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

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

© www.soinside.com 2019 - 2024. All rights reserved.