有没有一种众所周知的方法可以在 C# 中模拟可变参数模板功能?
例如,我想编写一个方法,它采用带有任意参数集的 lambda。这是我想要的伪代码:
void MyMethod<T1,T2,...,TReturn>(Fun<T1,T2, ..., TReturn> f)
{
}
有没有一种众所周知的方法可以在 C# 中模拟可变参数模板功能?
例如,我想编写一个方法,它采用带有任意参数集的 lambda。这是我想要的伪代码:
void MyMethod<T1,T2,...,TReturn>(Fun<T1,T2, ..., TReturn> f)
{
}
C# 泛型与 C++ 模板不同。C++ 模板是扩展的编译时,可以递归地与可变参数模板参数一起使用。C++ 模板扩展实际上是图灵完备的,所以理论上可以在模板中做的事情没有限制。
C# 泛型是直接编译的,使用一个空的“占位符”表示将在运行时使用的类型。
要接受带有任意数量参数的 lambda,您要么必须生成大量重载(通过代码生成器),要么接受LambdaExpression
.
没有对泛型类型参数的可变支持(在方法或类型上)。您将不得不添加很多重载。
可变参数支持仅适用于数组,通过params
,即
void Foo(string key, params int[] values) {...}
重要的是——你甚至会如何参考那些不同T*
的方法来编写通用方法?也许你最好的选择是采取一个Type[]
或类似的(取决于上下文)。
我知道这是一个老问题,但如果你想做的只是一些简单的事情,比如打印这些类型,你可以很容易地做到这一点,而无需使用元组或任何额外的使用“动态”:
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();
}
将输出与原始 Int 代码相同的“9”,除了您实际上可以作为参数传入的唯一值是您实际定义的值,并且您知道它将起作用并且不会导致任何错误。当然,您必须创建新的类,即 DoubleableInt 、 DoubleableString 等。以便重新创建 10 的第二个结果。但这只是一个示例,因此您甚至根本不会尝试添加东西取决于您正在编写的代码,您将从最适合您的实现开始。
希望有人可以改进我在此处编写的内容或使用它来查看如何在 C# 中完成此操作。
除了上面提到的那些之外,另一种选择是使用 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();
}
}
我不一定知道这种模式是否有名称,但我得出了以下递归通用接口的公式,它允许传入无限数量的值,返回的类型保留所有传递值的类型信息。
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
s 参数,但您可能可以调整此代码以处理任意类型的参数。
对于模拟,您可以说:
void MyMethod<TSource, TResult>(Func<TSource, TResult> f) where TSource : Tparams {
在哪里Tparams
成为可变参数实现类。但是,该框架没有提供开箱即用的东西来做到这一点,Action
, Func
,Tuple
等都具有有限的签名长度。我能想到的唯一一件事就是应用 CRTP .. 以我没有找到有人写博客的方式。这是我的实现:
*:感谢@SLaks 提到Tuple<T1, ..., T7, TRest>
也以递归方式工作。我注意到它在构造函数和工厂方法上是递归的,而不是它的类定义;并对type的最后一个参数进行运行时类型检查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> {
is T<P>
,并且该类T<P>
是嵌套的,因此您可以做一些疯狂的事情,例如:
测试
[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
编辑。
上面的代码部分需要扩展通用参数来计算它们的长度,这里有两种扩展方法:
代码(扩展)
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;
}
}
}
对于这个实现,我选择不破坏编译时类型检查,所以我们没有一个构造函数或一个带有签名的工厂params object[]
来提供值;相反,使用流利的方法模式V
进行大量对象实例化以保持类型可以尽可能地进行静态类型检查。