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
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.