为URL生成设计流畅接口(Fluent Interface)
2009-11-03 09:43 by 老赵, 18858 visits昨天我比较了三种URL生成方式的性能,并对结果进行了分析。从结果中我们得知使用Lambda表达式生成URL的性能最差,而且差到了难以接受的地步。经过分析,我们发现其中光“构造表达式树”这个阶段就占了接近30%的开销。虽然表达式树的节点是有些多,但是.NET中创建对象其实非常快,我实在没想到它会占这么高的比例。因此,我们需要这种做法进行方向性的调整,减少对象创建的数目。
但是,既然我们要尽可能地保留“静态检查”的优势,因此这里的关键是如何设计出既美观好用,又高效的做法。例如,我在上一篇文章最后留下的做法是这样的:
// API签名 public static string Action(this UrlHelper helper, Expression template, params object[] args) // 使用方式 private readonly static Expression<...> ToPostTemplate = (c, blog, post) => c.Post(blog, post); public static string ToPost(this UrlHelper helper, Blog blog, Post post) { return helper.Action(ToPostTemplate, blog, post); }
我要求为每个Action都指定一个表达式“模板”,它是一个单例,既可以作为解析,又可用于缓存。它也不会创建额外对象,因此可以缓解性能问题。但是,它的缺点也很明显:
- 必须单独创建一个表达式,因此使用比较麻烦(无法直接在视图中调用)。
- 表达式模板的签名非常麻烦,无法利用C#编译器的类型推导能力。
- 使用params object[]指定Action的参数,仍有些“弱类型”的意味。
因此,这个方式其实并不理想。不过,在文章的评论中,Ivony...老大给了我一些提示,然后再讨论中,我们总结出了一个似乎不错的API。这个API首先利用了C#编译器的一个特性:可以将一个方法直接转化为相同签名的委托对象,而不需要使用new进行显式创建。例如:
HomeController controller = new HomeController(); Func<Blog, Post, ActionResult> action = controller.Post;
如第2行代码,其完整的写法应该使用new关键字,配合委托的类型,把方法包装为一个委托。但是,从C# 2.0起编译器就允许我们使用省略的写法。于是接下来,我们便要设法利用C# 3.0中那微弱的类型推导能力进行API设计了。从以前的一次讨论中,我们了解了C# 3.0编译器类型推导的一些“盲点”,例如:一个泛型方法只有通过传入的参数,才能确定泛型参数的具体类型。它使得以下代码无法编译通过:
// 定义 static void Do<T1, T2>(Func<T1, T2, ActionResult> action) // 调用(类型推导失败) Do(controller.Post);
除非通过其他两个参数来明确T1和T2的类型:
// 方法定义 static void Do<T1, T2>(Func<T1, T2, ActionResult> action, T1 arg1, T2 arg2) // 调用(编译成功) Post post = null; Blog blog = null; HomeController controller = new HomeController(); Do(controller.Post, blog, post);
可惜委托的返回值还是必须指明ActionResult类型,它无法推导出来。不过,这似乎也基本满足我们的需求。那么,我们又如何为Do方法提供controller的类型信息呢?要知道这样的API是行不通的:
public static string Action<TController, T1, T2>(this UrlHelper helper, ...)
因为C#编译器不允许在方法调用时指定部分参数(如单独指定TController而让T1,T2由类型推导获得),因此我们必须将Controller类型的指定工作,与剩下的泛型参数分开:
public static class UrlHelperExtensions { public static ActionOf<TController> Of<TController>(this UrlHelper helper) where TController : new() { return new ActionOf<TController>(helper); } } public class ActionOf<TController> { public ActionOf(UrlHelper urlHelper) { this.m_urlHelper = urlHelper; } public UrlHelper m_urlHelper; }
在ActionOf类中已经明确了TController泛型类型,它只要负责推导后续的参数即可,因此我们定义出这样的方法:
public string Action<T1, T2>(Func<TController, Func<T1, T2, ActionResult>> action, T1 arg1, T2 arg2)
请注意现在Action方法第一个参数的类型:这是一个委托对象,接受TController类型作为参数,返回另一个委托对象——它不需要使用new进行显式创建。这个委托对象返回ActionResult类型,并接受T1,T2两个参数——这两个参数的类型又可以由Action方法的后续参数确定下来。因此,最后的调用方法大概是这样的:
Url.Of<HomeController>().Action(c => c.Post, blog, post)
这行代码可读性也不错,我们可以这样理解:“URL of HomeController’s action ‘Post’ with parameter ‘blog’ & ‘post’...”,因此它也可以算是一个流畅接口(Fluent Interface)。这个调用方式在我看来还是挺美观的,而且有明确的静态类型检查,属于比较理想的API。至于在Action方法内部,我们可以通过传入一个TController类型的对象来得到一个委托,这个委托对象的Method属性便是那个Action的MethodInfo——至于它的参数,便是blog和post了:
public class ActionOf<TController> where TController : new() { private static TController prototype = new TController(); public string Action<T1, T2>(Func<TController, Func<T1, T2, ActionResult>> action, T1 arg1, T2 arg2) { return Action(action(prototype).Method, arg1, arg2); } public static string Action(MethodInfo methodInfo, params object[] args) { ... } }
在这段代码中,我要求TController类型有个默认的构造函数,这样便于我们构造一个TController来“获取委托”,并得到它的Method信息。在实际使用过程中,我们不能强求每个Controller类型都满足这个条件,因此我打算使用Emit来动态生成TController的子类。使用Emit的好处在于,我们生成的动态类型可以绕开C#编译器的限制,不调用任何基类的构造函数,这样可以创建一个无用的对象而不会触及基类的任何逻辑,恰好满足我们的需求。
当然,这种流畅接口和Lambda表达式相比还是有些缺点,如:
- 获得IDE的智能提示效果不佳。
- 需要为参数数目不同的Action方法准备不同的重载。
- 对类型要求严格,而构造Lambda表达式可以如普通C#代码那样的“宽松”
关于最后一点可能值得用代码示例来说明。如这样的Action方法:
public ActionResult Detail(long id) { ... }
而构造URL时:
int id = 0; helper.Action<HomeController>(c => c.Detail(id)); // Lambda Expression helper.Of<HomeController>().Action(c => c.Detail, (long)id); // Fluent Interface
在流畅接口中将id强制转型为long的操作是不可以省略的,因为ActionOf<HomeController>.Action方法需要根据泛型类型来推断出c.Detail的签名。如果传入一个int类型的id,则编译器便会告知我们“c.Detail不符合Func<int, ActionResult>类型”,进而编译失败。而使用Lambda表达式时,C#编译器会自动对id进行“提升(Lift)”操作,把int转化为long。不过这点在实际使用过程中应该不会成为问题。
使用流畅接口生成URL时还是会创建一些对象,例如ActionOf对象,action委托对象、调用action后得到的另一个委托对象等等,可能还需要包括公用的Action(MethodInfo, params object[])方法所需要的对象数组。不过对象数量还是要比Lambda表达式少一些,而且不像Expression类型的那些工厂方法包含一些逻辑,因此在性能是有所提高的。这里我进行了一个简单的性能测试,得到的结果为:
Lambda Expression Time Elapsed: 9,353ms CPU Cycles: 20,339,339,141 Gen 0: 373 Gen 1: 0 Gen 2: 0 Fluent Interface Time Elapsed: 3,041ms CPU Cycles: 6,614,088,228 Gen 0: 97 Gen 1: 0 Gen 2: 0
可见,使用流畅接口的方式,在构造URL的第1阶段(构造对象)可以得到超过2/3的性能提升,而如果参数数量多的话差距应该会更为明显。
您觉得这个方式如何?可以给出您的看法吗?
相关文章
- 各种URL生成方式的性能对比
- 各种URL生成方式的性能对比(结论及分析)
- 为URL生成设计流畅接口(Fluent Interface)
- URL生成方式性能优化结果
- Route组件GetVirtualPath方法性能优化结果
先占沙发再看