2011-07-27 16 views
23

Czy istnieje znany sposób symulacji funkcji szablonu variadic w języku C#?Symulować szablony variadic w C#

Na przykład, chciałbym napisać metodę, która przyjmuje lambdę z dowolnym zbiorem parametrów. Oto w kodzie pseudo co chciałbym mieć:

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

} 

Dziękuję

+1

Zastanawiam się, czy jest coś, co można zrobić (być może argument "params") ["TypedReference"] (http://stackoverflow.com/questions/4764573/why-is-typedreference-behind-the-scenes -to-tak-szybko-i-safe-prawie-magiczne) – AakashM

Odpowiedz

17

C# generics nie są takie same jak szablony C++. Szablony C++ są rozszerzone podczas kompilacji i mogą być używane rekursywnie z argumentami szablonu variadic. Rozszerzenie szablonu C++ jest w rzeczywistości Turing Complete, więc teoretycznie nie ma ograniczeń co do tego, co można zrobić w szablonach.

C# generics są kompilowane bezpośrednio, z pustym "placeholder" dla typu, który będzie używany w czasie wykonywania.

Aby zaakceptować dowolną liczbę argumentów lambda, trzeba wygenerować wiele przeciążeń (przez generator kodu) lub zaakceptować LambdaExpression.

+0

Dobre wyjaśnienie –

7

Nie ma varadic poparcie dla ogólnych argumentów typu (po obu metod i typy). Będziesz musiał dodać wiele przeciążeń.

varadic wsparcie jest dostępne tylko dla tablic, poprzez params, tj

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

Improtantly - jak można nawet odnieść się do tych różnych T* napisać metody rodzajowe? Być może najlepszym wyjściem jest pobranie Type[] lub podobnej (w zależności od kontekstu).

+5

C++ 0x daje odpowiedź o tym, jak byś się do nich odnosił. W zasadzie nie przypomina to wcale Haskella. Wybierasz głowę i rekurencyjnie przekazujesz ogon, np. 'Foo ' <- szablon podstawowy, 'Foo ' <- faktyczna implementacja. Szablony C++ mają wiele wspólnego z haskellami i dopasowywaniem wzorców. –

4

Inną alternatywą oprócz tych wymienionych powyżej jest wykorzystanie krotki <,> i refleksji, na przykład:

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

wiem, jest to stara sprawa, ale jeśli wszystko, co chcesz zrobić, to coś prostego jak druku te typy out, można to zrobić bardzo łatwo, bez krotki lub coś ekstra za pomocą 'dynamiczny':

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

wypisze "System.Int32", "System.Double", "System.String"

Jeśli chcesz wykonać pewne działanie w tych sprawach, o ile wiem, masz dwie możliwości. Jednym z nich jest zaufanie programistom, że te typy mogą wykonywać zgodne działania, na przykład, jeśli chcesz stworzyć metodę Sumowania dowolnej liczby parametrów. Można napisać metodę jak poniżej mówiąc w jaki sposób chcesz otrzymać wynik, a jedynym warunkiem Chyba byłoby to, że operacja + działa pomiędzy tymi typami:

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

z tym, wyjście na pierwszej linii będzie być "9", ponieważ dodanie do int, a druga linia będzie "10", ponieważ .5s nie zostały zaokrąglone, dodając jako podwójne. Problem z tym kodem polega na tym, że jeśli przekażesz jakiś niekompatybilny typ na liście, wystąpi błąd, ponieważ typy nie mogą zostać dodane razem, a błąd nie pojawi się w czasie kompilacji, tylko w środowisku wykonawczym.

Tak więc, w zależności od przypadku użycia może być inna opcja, dlatego powiedziałem, że na początku były dwie możliwości. Zakładając, że znasz wybory możliwych typów, możesz utworzyć interfejs lub klasę abstrakcyjną i sprawić, że wszystkie te typy implementują interfejs. Na przykład poniżej. Przepraszam, to trochę szalone.I prawdopodobnie można go uprościć.

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

wyjście będzie „9” tak samo jak oryginalny kod INT, z wyjątkiem jedynie wartościami rzeczywiście można przekazać jako parametry są rzeczy, które rzeczywiście zostały zdefiniowane i wiesz zadziała i nie powoduje żadnych błędów. Oczywiście, musiałbyś stworzyć nowe klasy, takie jak DoubleableInt, DoubleableString, etc., aby ponownie utworzyć drugi wynik 10. Ale to tylko przykład, więc nawet nie próbowałbyś dodawać rzeczy w ogóle w zależności od tego, jaki kod piszesz, a dopiero zaczynasz od implementacji, która służyła Ci najlepiej.

Mam nadzieję, że ktoś może poprawić to, co tu napisałem, lub użyć go, aby zobaczyć, jak można to zrobić w języku C#.

2

Nie wiem, czy istnieje nazwa tego wzorca, ale przyszedłem do następującego sformułowania dla rekurencyjnego interfejsu generycznego, który pozwala na przekazanie nieograniczonej ilości wartości wraz z informacją o typie zwracanego typu dla wszystkie przekazane wartości.

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

Nie ma odlew albo „oszustwo” systemu typu zaangażowanych tutaj: można zachować układania na więcej wartości i wywnioskować typ zwracany utrzymuje przechowywania coraz więcej informacji. Oto co użycie wygląda następująco:

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> 

Jak widać podpis typu staje się w zasadzie nieczytelny blisko po kilku przykuty połączeń, ale to jest w porządku tak długo, jak dzieła wnioskowania typu i sugeruje odpowiedni rodzaj użytkownikowi .

W moim przykładzie mam do czynienia z argumentami Func s, ale można prawdopodobnie dostosować ten kod, aby poradzić sobie z argumentami dowolnego typu.

+0

Jest to prawdopodobnie to, co silnie typowany świat FP miałby nazywać HList. –

1

Dla symulacji można powiedzieć:

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

gdzie Tparams być o zmiennej liczbie argumentów argumenty klasa realizacja. Jednak framework nie zapewnia gotowych rzeczy do wykonania, wszystkie są ograniczone pod względem długości ich podpisów. Jedyne, co mogę wymyślić to zastosowanie CRTP .. w sposób, w jaki nie znalazłem kogoś bloga. Oto moja realizacja:


*: Dziękuję @SLaks dla wspomnieć Tuple<T1, ..., T7, TRest> działa również w sposób rekurencyjnego. Zauważyłem, że jest rekursywny dla konstruktora i metody fabryki zamiast definicji klasy; i do a runtime type checking ostatniego argumentu typu TRest musi być ITupleInternal; a to działa nieco inaczej.


  • Kod

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

Uwaga parametr typu dla klasy dziedziczonej C<> w deklaracji

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

jest T<P>, a klasa T<P> jest zagnieżdżony, dzięki czemu można zrobić kilka szalonych rzeczy, takich jak:

  • test

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

Inną rzeczą, aby pamiętać, mamy dwie klasy o nazwie T, non-zagnieżdżona T:

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

jest po prostu dla spójności użycia i zrobiłem klasy C streszczenie, aby nie bezpośrednio być new wyd.

Powyższy Kod część musi poszerzyć ther rodzajowe argumentu, aby obliczyć o ich długości, tu są dwie metody przedłużania on używany:

  • Code (rozszerzenia)

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

Dla tej implementacji postanowiłem nie przerywać sprawdzania typu podczas kompilacji, więc nie mamy konstruktora ani fabryki z podpisem jak params object[], aby podać wartości; zamiast tego użyj płynnego wzorca metody do tworzenia obiektów masowych, aby typ danych mógł być statycznie sprawdzany tak bardzo, jak to możliwe.

+1

Tak właśnie działa 'Tuple '. – SLaks

+0

@SLaks: Dzięki i dobrze wiedzieć, nigdy nie użyłem 'T8' .. zrewiduję i zostawię inną implementację. –