var pipeline = new PipelineBuilder<string>
.AddInitial(n => n*12)
.AddStep(n => n.ToString())
.Build();
执行 `pipeline(2)' 应返回“24”。
此外,构建器中的类型参数
<string>
指示结果的类型(最终步骤的返回类型)。
这可能吗?我已经与 C# 的类型系统进行了斗争,但我已经完成了。
我离它越近的是这段代码:
public class PipelineBuilder<TInitial, TResult>
{
private List<Func<dynamic, dynamic>> functions = new();
public PipelineBuilder<TInitial, TOutput, TResult> AddInitial<TOutput>(Func<TInitial, TOutput> step)
{
functions.Add(o => step(o));
return new PipelineBuilder<TInitial, TOutput, TResult>(functions);
}
public PipelineBuilder<TInput, TOutput, TResult> AddStep<TInput, TOutput>(Func<TOutput, TInput> step)
{
functions.Add(a => step(a));
return new PipelineBuilder<TInput, TOutput, TResult>(functions);
}
}
public class PipelineBuilder<TInput, TOutput, TResult>
{
private List<Func<dynamic, dynamic>> functions;
public PipelineBuilder(List<Func<dynamic, dynamic>> functions)
{
this.functions = functions;
}
public PipelineBuilder<TInput, TNewOutput, TResult> AddStep<TNewOutput>(Func<TOutput, TNewOutput> step)
{
functions.Add(a => step(a));
return new PipelineBuilder<TInput, TNewOutput, TResult>(functions);
}
public Func<TInput, TResult> Build()
{
return input =>
{
dynamic result = input;
foreach (var step in functions)
{
result = step(result);
}
return (TResult)(object)result;
};
}
}
这种方法的问题是,我找不到方法将最后一步的输出类型与构建器的返回类型相匹配(
TResult
)。
如果类型不匹配,我得到的只是运行时异常。我什至不知道它们在执行
Build
方法时是否匹配,因为函数是 Func<dynamic, dynamic>
。
你可以通过将 lambda 链接在一起来做一些非常接近你想要的事情,如下所示:
public class PipelineBuilder<TFinal>
{
public PipelineBuilder<TInitial, TCurrent, TFinal> AddInitial<TInitial, TCurrent>(Func<TInitial, TCurrent> step)
=> new PipelineBuilder<TInitial, TCurrent, TFinal> { Step = step };
}
public class PipelineBuilder<TInitial, TCurrent, TFinal>
{
// Represents an intermediate step in the pipeline.
internal Func<TInitial, TCurrent> Step { get; init; }
public PipelineBuilder<TInitial, TNext, TFinal> AddStep<TNext>(Func<TCurrent, TNext> step)
=> new PipelineBuilder<TInitial, TNext, TFinal> { Step = (c) => step(Step(c)) };
}
public static class PipelineBuilderExtensions
{
public static Func<TInitial, TFinal> Build<TInitial, TFinal>(this PipelineBuilder<TInitial, TFinal, TFinal> current)
=> current.Step;
}
然后按如下方式使用:
var pipeline = new PipelineBuilder<string>()
.AddInitial((int n) => n*12) // (int n) is necessary to declare the initial type
.AddStep(n => n.ToString())
.Build();
string result = pipeline(2); // Here I declare result as string to demonstrate that the returned value is of the expected type. Normally one would use var/
Console.WriteLine(result); // Prints 24.
演示小提琴#1 这里。
备注:
在您的初始设计中,编译器无法推断输入参数的类型
n
:
var pipeline = new PipelineBuilder<string>
.AddInitial(n => n*12) // Is n an int, a TimeSpan, a string, or what?
向 lambda 参数添加类型
(int n)
可以解决歧义。
如果当前步骤结果类型不等于最终步骤结果类型,则将找不到扩展方法
.Build()
,并且您将收到编译器错误:
'PipelineBuilder<int, int, string>' does not contain a definition for 'Build' and no accessible extension method 'Build' accepting a first argument of type 'PipelineBuilder<int, int, string>' could be found (are you missing a using directive or an assembly reference?)
Argument 1: cannot convert from 'int' to 'TInitial'
该错误并不是特别具有描述性,但它仍然是一个正确的编译器错误,而不是运行时异常。
演示小提琴 #2 这里。
综上所述,我不推荐这种设计。当最终类型也将由最终
AddStep()
调用定义时,在开始时定义最终结果类型感觉很尴尬且与 LINQ 编程风格不一致。从初始声明中消除 TFinal
使事情变得更加简单:
public class PipelineBuilder
{
public PipelineBuilder<TInitial, TCurrent> AddInitial<TInitial, TCurrent>(Func<TInitial, TCurrent> step)
=> new PipelineBuilder<TInitial, TCurrent> { Step = step };
}
public class PipelineBuilder<TInitial, TCurrent>
{
// Represents an intermediate step in the pipeline.
internal Func<TInitial, TCurrent> Step { get; init; }
public PipelineBuilder<TInitial, TNext> AddStep<TNext>(Func<TCurrent, TNext> step)
=> new PipelineBuilder<TInitial, TNext> { Step = (c) => step(Step(c)) };
public Func<TInitial, TCurrent> Build() => Step;
}
演示小提琴 #3 这里。