从扩展方法到流畅的程序体验

发布时间:2026/7/5 2:13:06
从扩展方法到流畅的程序体验 今天让公司的程序员试用了一下还在开发中的代号为Jumony的HTML数据绑定引擎开发人员的一句评价被我视为最高的褒奖。“感觉这个框架就是你想到什么就写什么。”想到什么就写什么在这个越来越强调快速开发的时代这一点变得越来越重要。我最近经常戏言“natural code才是王道”当然不是说我们要用中文去编程而是程序应该成为越来越自然的表达。让程序员获得流畅的编程体验是将来每一个框架都必须去考虑和实现的事情。随着.NET Framework 3.5的普及越来越多的.NET框架开始注重为程序员提供流畅的体验。为什么是随着.NET Framework 3.5的普及呢因为在劣质的语言如Java等上我们花费大得多的代价也很难获得流畅的体验。.NET Framework 3.5/C# 3.0增加了大量的新特性lambda表达式和ExpressionTree自然是很强大的特性不过在这里我特别想提的是扩展方法。扩展方法的本质是实现函数的中缀表达式自从有函数以来我们就习惯了前缀函数表达式像这样Console.Write( Hello world! );这样的形式对于命令式的程序来说的确是比较合理的方式但是如果我们考虑下面这个函数Add( 1, 2 );就显得不那么友好显然我们喜闻乐见的形式是1 2这与运算符的书写形式同理显然我们不喜欢 1 2或是1 2 这样的前缀或后缀表达式。并且这种函数在连起来使用的时候就是灾难Add( ... Add( Add( Add( 1, 2 ), 3 ), 4 ) ... );OO的出现带来了一个语法的革新OO认为对象自己可以拥有自己的方法函数。我刚刚接触到OO的时候最感兴趣的就是打开一个文件已经不需要这样FILE* fp fopen( C:\\Temp.txt, w );fputs( fp, 123, 3 );fclose( fp );这些函数都成为了File自己的方法file-open( C:\\Temp.txt, w );file-puts( 123, 3 );file-close();我当时为了这个果断的抛弃了语法过时落后的C而投入了C的研究之中。我当时甚至连OO是什么都不清楚只知道结构体现在能够有自己的函数了这真是一个不小的进步。程序设计语言就是这样不断地提高开发效率和带给程序员更好的书写体验。对象的方法在一定程度上解决了函数中缀表达式的问题代码也变得越来越好看简单易懂结构性强。但很快我们就发现了这种方式的局限性。对象的类型是一个静态的东西一个对象拥有什么方法是设计这个对象的类型的时候决定的。大部分时候这没有问题但随着我们需求的发展我们发现有一些对象是不能决定自己有些什么方法的。例如容器对象decimal[]或是IListdecimal或是IEnumerabledecimal我们会希望它拥有一个Sum方法像这样IEnumerabledecimal data GetData();var sum data.Sum();而显然的我们不可能为T[]或是IListT或是IEnumerableT增加这样一个Sum方法。这就使得我们不得不这样来写代码var sum Math.Sum( data );也许你会觉得这没什么但事实上如果这种东西多了你就会觉得很烦了:var average Math.Sum( data ) / Enumerable.Count( data );如果是一个复杂的公式光想想就够了。当然这是扩展方法的其中一个应用场景也是几乎不可替代的一个场景。如果不写扩展方法我们就不得不专门写一个DecimalCollection的类型出来。这些架子代码大量的充斥在我们的代码中的时候我们会发现其实我们真正用来表述逻辑和真实意图的代码越来越少。扩展方法的另一个应用场景就是在不同的上下文中对象可能需要呈现出不同的方法。比如说我们在一个大量需要正则匹配和替换的场景我们就会希望字符串可以直接调用正则表达式来替换最好str.Replace( \.html$, .aspx );这是最符合我们预期的但事实上我们必须写成Regex.Replace( \.html$, str, .aspx );即使是这样的代码str.Replace( new Regex( \.html$ ), .aspx );也是不允许的。但当我们需要在一个表达式中进行多次正则替换时代码就会变成一堆灾难。这里面的原因很多需要大量的正则替换的场景并不是通用常见场景当然更重要的原因是如果要为String实现string Replace( Regex, string )方法必然导致String产生对Regex的依赖而String是一个比Regex更为基础的类型这种强绑定会带来很多的问题。例如Regex必须与String放在同一个程序集中否则会造成循环引用因为Regex必然依赖String事实上String在mscorlib中而Regex在System中。除此之外如果我们不想用微软的正则表达式引擎那么我们还是不得不退回到原来的丑陋模式。扩展方法的出现解决了所有的这些问题也使得我们的框架代码变得简洁节省了大量的架子代码。Enumerable所定义的扩展方法只用了很少的代码就给我们带来了极大的便利。为什么会忽然特别想聊扩展方法这个特性因为我现在做的这个HTML数据绑定引擎中程序员所调用到的方法大部分都是扩展方法。扩展方法加上接口让我几乎不费力的就为程序员提供了所想即所写的编程体验。不过在这里我不想太多的讨论这个东西来谈谈扩展方法在其他方面的一些体验提高。作为Web开发而言用Request来接收参数是最普通不过的事情但Request只能接收字符串我们不得不写很多的转换代码int userId int.Parse( Reuqest.QueryString[userId] );//...这样的代码写惯了倒也没什么但多了的确是个负担所以我看到很多程序员为了偷懒就不作强类型转换了这样带来非常多的隐患。我提供了一个扩展方法来试图解决这个问题int userId Request.QueryString[userId].ParseToint();现在代码比之前要好一些了因为程序员的思路不会被打断而对原有的程序的升级在后面加一个.ParseTo显然也不太费事大家接受度就会比较好。当然有程序员A指出这个什么QueryString实在是太长了难写。那么这样int userId Request.HttpGetint( userId );如果是POST传递的数据int userId Request.HttpPostint( userId );当然我们还可以做很多有用的扩展。来看看数据库吧当我们需要从数据库中取出一个值时有时候会遇到DBNull的情况。DBNull与null不同前者是一个合法的值这个值经常会搞得我们很烦(int) DbUtility.Scaler( SELECT MAX(ID) FROM Users );当这个表中不存在一条记录的时候返回值就会是DBNull。对于这种情况我们希望对DBNull做一个默认值如果是null的话这很好办(int) (DbUtility.Scalar( SELECT MAX(ID) FROM Users ) ?? 0);利用C# 2.0中的??操作符我们可以很容易办到这一点但这对于DBNull却是无效的。不过扩展方法很容易做到这一点int id DbUtility.Scalar( SELECT MAX(ID) FROM Users ).IfNull( 0 );或是使得下面的丑陋的强制类型转换语法写起来顺手点((DateTime) dataItem).ToString( yyyy-MM-dd );dataItem.CastDateTime().ToString( yyyy-MM-dd );扩展方法使得框架的编写者可以用很少的代码就能实现流畅的编码感受我一直认为一个框架是否好用并不在于它的功能有多强大而在于它是否给程序员提供了流畅的编程体验和直觉性的代码书写。用我的话说就是“当你第一眼看到这个方法觉得它会是干什么的会返回什么那它就真的是干这个的也真的会返回你想要的东西”不要翻手册或是帮助甚至不需要借助参数和方法的说明文字你所想的即它所做的这才是最重要的。最后提供我的ParseTo扩展方法的完整实现public static class WebExtensions { public static T ParseToT( this string value ) { if ( ParserT.ParseMethod ! null ) return ParserT.ParseMethod( value ); throw new NotSupportedException(); } static WebExtensions() { Parsershort.ParseMethod short.Parse; Parserint.ParseMethod int.Parse; Parserlong.ParseMethod long.Parse; Parserbyte.ParseMethod byte.Parse; Parserushort.ParseMethod ushort.Parse; Parseruint.ParseMethod uint.Parse; Parserulong.ParseMethod ulong.Parse; Parsersbyte.ParseMethod sbyte.Parse; Parserfloat.ParseMethod float.Parse; Parserdouble.ParseMethod double.Parse; Parserdecimal.ParseMethod decimal.Parse; Parserbool.ParseMethod bool.Parse; ParserDateTime.ParseMethod DateTime.Parse; ParserTimeSpan.ParseMethod TimeSpan.Parse; } private class ParserT { public delegate T ParseMethodDelegate( string value ); private static bool noParseMethod false; private static ParseMethodDelegate _parseMethod; public static ParseMethodDelegate ParseMethod { get { if ( _parseMethod ! null ) return _parseMethod; if ( noParseMethod ) return null; var method typeof( T ).GetMethod( Parse, new Type[] { typeof( string ) } ); if ( method ! null (method.Attributes MethodAttributes.Static) ! 0 ) { DynamicMethod dynamicMethod new DynamicMethod( typeof( T ).FullName _Parse, typeof( T ), new Type[] { typeof( string ) } ); var il dynamicMethod.GetILGenerator(); il.Emit( OpCodes.Ldarg_0 ); il.EmitCall( OpCodes.Call, method, null ); il.Emit( OpCodes.Ret ); return _parseMethod (ParserT.ParseMethodDelegate) dynamicMethod.CreateDelegate( typeof( ParserT.ParseMethodDelegate ) ); } noParseMethod true; return null; } set { _parseMethod value; } } } }