Подтвердить что ты не робот

Имитировать вариативные шаблоны в С#

Есть ли известный способ моделирования функции вариационного шаблона в С#?

Например, я хотел бы написать метод, который принимает лямбда с произвольным набором параметров. Вот в псевдокоде, что я хотел бы иметь:

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

}

Спасибо

4b9b3361

Ответ 1

Генерирование С# не совпадает с шаблонами С++. Шаблоны С++ расширены compiletime и могут быть использованы рекурсивно с аргументами вариационного шаблона. Расширение шаблона С++ на самом деле является Turing Complete, поэтому теоретически не ограничено тем, что можно сделать в шаблонах.

Генераторы С# скомпилированы напрямую с пустым "заполнителем" для типа, который будет использоваться во время выполнения.

Чтобы принять лямбда, принимающую любое количество аргументов, вам нужно будет либо генерировать много перегрузок (через генератор кода), либо принять LambdaExpression.

Ответ 2

Нет поддержки varadic для аргументов универсального типа (по любым методам или типам). Вам придется добавить много перегрузок.

Поддержка varadic доступна только для массивов, через params, т.е.

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

Совершенно - как вы могли бы обратиться к тем различным T*, чтобы написать общий метод? Возможно, ваш лучший вариант - взять Type[] или аналогичный (в зависимости от контекста).

Ответ 3

Другой альтернативой, кроме упомянутых выше, является использование Tuple <, > и отражения, например:

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

Я знаю, что это старый вопрос, но если все, что вы хотите сделать, это что-то простое, как распечатать эти типы, вы можете сделать это очень легко без Tuple или чего-нибудь дополнительного с помощью "dynamic":

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 не округлились, добавив в качестве двойника. Проблема с этим кодом заключается в том, что если вы передадите некоторый несовместимый тип в списке, у него будет ошибка, потому что типы не могут быть добавлены вместе, и вы не увидите эту ошибку во время компиляции, только во время выполнения.

Итак, в зависимости от вашего варианта использования может быть другой вариант, поэтому я сказал, что сначала есть два варианта. Предполагая, что вы знаете варианты возможных типов, вы можете создать интерфейс или абстрактный класс и реализовать все эти типы интерфейса. Например, следующее. Извините, это немного с ума. И это, вероятно, может быть упрощено.

    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. Но это всего лишь пример, поэтому вы даже не пытались бы вообще добавить что-либо в зависимости от того, какой код вы пишете, и вы только начинаете с реализации, которая послужила вам лучшим.

Надеюсь, кто-то может улучшить то, что я написал здесь, или использовать его, чтобы посмотреть, как это можно сделать на С#.

Ответ 5

Я не обязательно знаю, есть ли имя для этого шаблона, но я пришел к следующей формулировке для рекурсивного универсального интерфейса, который позволяет передавать неограниченное количество значений с возвращаемой информацией о типе сохраняемого типа для всех переданные значения.

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>

Как вы можете видеть, подпись типа становится практически нечитаемой почти после нескольких цепочечных вызовов, но это нормально, пока работает вывод типа и предлагает правильный тип пользователю.

В моем примере я имею дело с аргументами Func, но вы могли бы, вероятно, адаптировать этот код для обработки аргументов произвольного типа.

Ответ 6

Для моделирования вы можете сказать:

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

где Tparams - класс реализации переменных аргументов. Тем не менее, структура не предоставляет готовые вещи для этого, Action, Func, Tuple и т.д., Все имеют ограниченную длину своих подписей. Единственное, что я могу придумать, это применить CRTP.. таким образом, что я не нашел кого-то в блоге. Здесь моя реализация:


*: Спасибо @SLaks за упоминание Tuple<T1, ..., T7, TRest> также работает рекурсивным образом. Я заметил, что он рекурсивный для конструктора и метода factory вместо определения его класса; и выполнить проверку типа времени выполнения последнего аргумента типа 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<> в объявлении

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

есть T<P>, а класс T<P> вложен так, что вы можете делать некоторые сумасшедшие вещи, такие как:

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

Еще одно замечание: у нас есть два класса с именем T, не вложенные T:

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

- это просто согласованность использования, и я сделал абстрактное выражение класса C не непосредственно new ed.

В приведенной выше части кода необходимо развернуть общий аргумент, чтобы рассчитать их длину, вот два метода расширения, которые он использовал:

  • Код (расширения)

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

Для этой реализации я решил не прерывать проверку типа компиляции, поэтому у нас нет конструктора или factory с сигнатурой типа params object[] для предоставления значений; вместо этого используйте свободную модель метода V для создания экземпляра массового объекта, чтобы сохранить тип, который можно статически проверять как можно больше.