Simulate variadic templates in C#

前端 未结 6 950
轮回少年
轮回少年 2020-12-09 14:57

Is there a well-known way for simulating the variadic template feature in C#?

For instance, I\'d like to write a method that takes a lambda with an arbitrary set of p

6条回答
  •  无人及你
    2020-12-09 15:45

    For a simulation you can say:

    void MyMethod(Func f) where TSource : Tparams {
    

    where Tparams to be a variadic arguments implementation class. However, the framework does not provide an out-of-box stuff to do that, Action, Func, Tuple, etc., are all have limited length of their signatures. The only thing I can think of is to apply the CRTP .. in a way I've not find somebody blogged. Here's my implementation:


    *: Thank @SLaks for mentioning Tuple also works in a recursive way. I noticed it's recursive on the constructor and the factory method instead of its class definition; and do a runtime type checking of the last argument of type TRest is required to be a ITupleInternal; and this works a bit differently.


    • Code

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

    Note the type parameter for the inherited class C<> in the declaration of

    public class T

    :C>, INode

    {

    is T

    , and the class T

    is nested so that you can do some crazy things such as:

    • Test

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

    Another thing to note is we have two classes named T, the non-nested T:

    public class T

    :C

    {

    is just for the consistency of usage, and I made class C abstract to not directly being newed.

    The Code part above needs to expand ther generic argument to calculate about their length, here are two extension methods it used:

    • Code(extensions)

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

    For this implementation, I choosed not to break the compile-time type checking, so we do not have a constructor or a factory with the signature like params object[] to provide values; instead, use a fluent pattern of method V for mass object instantiation to keep type can be statically type checked as much as possible.

提交回复
热议问题