Имитировать вариативные шаблоны в С#
Есть ли известный способ моделирования функции вариационного шаблона в С#?
Например, я хотел бы написать метод, который принимает лямбда с произвольным набором параметров. Вот в псевдокоде, что я хотел бы иметь:
void MyMethod<T1,T2,...,TReturn>(Fun<T1,T2, ..., TReturn> f)
{
}
Спасибо
Ответы
Ответ 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
для создания экземпляра массового объекта, чтобы сохранить тип, который можно статически проверять как можно больше.