2011-07-27 17 views
23

¿Existe una forma bien conocida de simular la característica de plantilla variadic en C#?Simular plantillas variadic en C#

Por ejemplo, me gustaría escribir un método que tome una lambda con un conjunto arbitrario de parámetros. Esto es en pseudo código lo que me gustaría tener:

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

} 

Gracias

+1

Me pregunto si hay algo que podría hacer con (tal vez un argumento 'params' de) [' TypedReference'] (http://stackoverflow.com/questions/4764573/why-is-typedreference-rehind-the-scenes -s-tan-rápido-y-seguro-casi mágico) – AakashM

Respuesta

17

Los genéricos de C# no son lo mismo que las plantillas de C++. Las plantillas de C++ son compilaciones ampliadas y se pueden usar de forma recursiva con argumentos de plantilla variadic. La expansión de la plantilla de C++ es en realidad Turing completa, por lo que no hay límite teórico para lo que se puede hacer en las plantillas.

Los genéricos C# se compilan directamente, con un "marcador de posición" vacío para el tipo que se utilizará en el tiempo de ejecución.

Para aceptar una lambda que tome cualquier cantidad de argumentos, tendrá que generar muchas sobrecargas (a través de un generador de código) o aceptar un LambdaExpression.

+0

Buena explicación –

7

No hay soporte para varadic argumentos de tipo genérico (en cualquiera de los métodos o tipos). Deberá agregar muchas sobrecargas.

apoyo varadic sólo está disponible para las matrices, a través de params, es decir

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

improtantly - ¿cómo incluso referirse a los diversos T* escribir un método genérico? Tal vez su mejor opción es tomar un Type[] o similar (dependiendo del contexto).

+5

C++ 0x da una respuesta acerca de cómo se referiría a esos. No es muy diferente a Haskell, en realidad. Usted toma una cabeza y pasa la cola recursivamente, p. 'Foo ' <- plantilla primaria, 'Foo ' <- una implementación real. Las plantillas C++ tienen mucho en común con haskell y la coincidencia de patrones. –

4

Otra alternativa además de los mencionados anteriormente es usar Tupla <,> y la reflexión, por ejemplo:

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

Sé que esto es una vieja pregunta, pero si todo lo que queremos hacer es algo tan sencillo como imprimir esos tipos out, se puede hacer esto con mucha facilidad y sin tupla o nada extra usando 'dinámico':

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

imprimirá "System.Int32", "System.Double", "System.String"

Si desea realizar alguna acción en estas cosas, por lo que sé, tiene dos opciones. Una es confiar en el programador para que estos tipos puedan realizar una acción compatible, por ejemplo, si quiere hacer un método para Sumar cualquier cantidad de parámetros. Se puede escribir un método como el siguiente dicho cómo desea recibir el resultado y el único requisito que supongo sería que la operación + trabaja entre estos tipos:

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

Con esto, la salida de la primera línea haría ser "9" porque se agrega a un int, y la segunda línea sería "10" porque los .5s no se redondearon, agregando como un doble. El problema con este código es que si pasa algún tipo incompatible en la lista, tendrá un error porque los tipos no se pueden agregar juntos, y no verá ese error en tiempo de compilación, solo en el tiempo de ejecución.

Por lo tanto, dependiendo de su caso de uso, podría haber otra opción y es por eso que dije que al principio había dos opciones. Suponiendo que conozca las opciones para los posibles tipos, podría hacer una interfaz o clase abstracta y hacer que todos esos tipos implementen la interfaz. Por ejemplo, lo siguiente. Lo siento, esto es un poco loco.Y probablemente puede ser simplfied.

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

salida será "9" el mismo que el código original Int, excepto los únicos valores que realmente puede pasar como parámetros son cosas que en realidad se han definido y que sabemos que va a trabajar y no causar ningún error. Por supuesto, tendrías que hacer nuevas clases, es decir, DoubleableInt, DoubleableString, etc. para volver a crear el segundo resultado de 10. Pero esto es solo un ejemplo, por lo que ni siquiera estarías intentando agregar cosas. dependiendo del código que está escribiendo y simplemente comenzaría con la implementación que le sirvió mejor.

Esperemos que alguien pueda mejorar lo que escribí aquí o usarlo para ver cómo se puede hacer esto en C#.

2

No necesariamente sé si hay un nombre para este patrón, pero llegué a la siguiente formulación para una interfaz genérica recursiva que permite pasar una cantidad ilimitada de valores, con la información del tipo de retención del tipo devuelto para todos los valores pasados

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

No hay colada o "trampas" del sistema de tipos involucrado aquí: usted puede mantener la colocación sobre las más valores y el tipo de retorno inferido sigue almacenando más y más información. Esto es lo que el uso se ve así:

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> 

Como se puede ver el tipo de firma se convierte básicamente ilegible próximo, después de unas cuantas llamadas encadenadas, pero esto está muy bien, siempre y cuando las obras inferencia de tipos y sugiere el tipo correcto para el usuario .

En mi ejemplo, me ocupo de los argumentos de Func, pero es posible que adapte este código para tratar con argumentos de tipo arbitrario.

+0

Esto es probablemente lo que el mundo de FP fuertemente tipado llamaría HList. –

1

Para una simulación que puede decir:

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

donde Tparams para ser una clase de implementación argumentos variadic. Sin embargo, el marco no proporciona un producto listo para usar, Action, Func, Tuple, etc., todos tienen una longitud limitada de sus firmas. Lo único que se me ocurre es aplicar el CRTP ... de una manera que no he encontrado a alguien blogueado. Aquí está mi aplicación:


*: Gracias por mencionar @SLaks Tuple<T1, ..., T7, TRest> también trabaja de manera recursiva. Me di cuenta de que es recurrente en el constructor y el método de fábrica en lugar de su definición de clase; y do a runtime type checking del último argumento de tipo TRest debe ser un ITupleInternal; y esto funciona de forma un poco diferente.


  • Código

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

Nota el parámetro de tipo para la clase heredada C<> en la declaración de

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

es T<P>, y la clase T<P> está anidada para que pueda hacer algunas cosas locas como:

  • prueba

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

Otra cosa a destacar es que tenemos dos clases llamado T, la no anidada T:

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

es solo por la consistencia del uso, e hice clase C resumen para no ser directamente new ed.

El Código parte superior necesita ampliar Ther argumento genérico para el cálculo de su longitud, aquí hay dos métodos de extensión que utiliza:

  • Código (extensiones)

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

Para esta implementación, decidí no romper la comprobación de tipos en tiempo de compilación, por lo que no tenemos un constructor o una fábrica con la firma como params object[] para proporcionar valores; en su lugar, use un patrón fluido del método V para la creación de instancias masivas de objetos para mantener el tipo de letra estáticamente comprobado tanto como sea posible.

+1

En realidad, así es como funciona 'Tuple '. – SLaks

+0

@SLaks: Gracias y bueno saber que nunca he usado 'T8' ... lo revisaré y lo dejaré en una implementación diferente. –

Cuestiones relacionadas