從擴展方法到流暢的程序體驗(一)
今天讓公司的程序員試用了一下還在開發中的代號為"Jumony"的HTML數據綁定引擎,開發人員的一句評價被我視為最高的褒獎。
“感覺這個框架就是,你想到什么就寫什么。”
想到什么就寫什么,在這個越來越強調快速開發的時代,這一點變得越來越重要。我最近經常戲言:“natural code才是王道”,當然,不是說我們要用中文去編程,而是程序應該成為越來越自然的表達。
讓程序員獲得流暢的編程體驗,是將來每一個框架都必須去考慮和實現的事情。隨著.NET Framework 3.5的普及,越來越多的.NET框架開始注重為程序員提供流暢的體驗。為什么是隨著.NET Framework 3.5的普及呢?因為在劣質的語言(Java)上,我們花費大得多的代價,也很難獲得流暢的體驗。
.NET Framework 3.5/C# 3.0增加了大量的新特性,lambda表達式和ExpressionTree自然是很強大的特性,不過在這里我特別想提的是擴展方法。
擴展方法的本質是實現函數的中綴表達式,自從有函數以來,我們就習慣了前綴函數表達式,像這樣:
這樣的形式,對于命令式的程序來說,的確是比較合理的方式,但是如果我們考慮下面這個函數:
就顯得不那么友好,顯然我們喜聞樂見的形式是1 + 2,這與運算符的書寫形式同理,顯然我們不喜歡+ 1 2或是1 2 +這樣的前綴或后綴表達式。并且,這種函數在連起來使用的時候,就是災難:
OO的出現帶來了一個語法的革新,OO認為對象自己可以擁有自己的方法(函數)。我剛剛接觸到OO的時候,最感興趣的就是打開一個文件已經不需要這樣:
fputs( fp, "123", 3 );
fclose( fp );
這些函數都成為了File自己的方法:
file->puts( "123", 3 );
file->close();
我當時為了這個果斷的拋棄了語法過時落后的C而投入了C++的研究之中。我當時甚至連OO是什么都不清楚,只知道結構體現在能夠有自己的函數了,這真是一個不小的進步。
程序設計語言就是這樣,不斷地提高開發效率和帶給程序員更好的書寫體驗。
對象的方法在一定程度上解決了函數中綴表達式的問題,代碼也變得越來越好看,簡單,易懂,結構性強。但很快我們就發現了這種方式的局限性。
對象的類型是一個靜態的東西,一個對象擁有什么方法是設計這個對象的類型的時候決定的。大部分時候這沒有問題,但隨著我們需求的發展,我們發現有一些對象是不能決定自己有些什么方法的。
例如容器對象:decimal[]或是IList或是IEnumerable,我們會希望它擁有一個Sum方法,像這樣:
var sum = data.Sum();
而顯然的,我們不可能為T[]或是IList或是IEnumerable增加這樣一個Sum方法。
這就使得我們不得不這樣來寫代碼:
也許你會覺得這沒什么,但事實上如果這種東西多了,你就會覺得很煩了:
如果是一個復雜的公式,光想想就夠了。
當然,這是擴展方法的其中一個應用場景,也是幾乎不可替代的一個場景。如果不寫擴展方法,我們就不得不專門寫一個DecimalCollection的類型出來。這些架子代碼大量的充斥在我們的代碼中的時候,我們會發現其實我們真正用來表述邏輯和真實意圖的代碼越來越少。
擴展方法的另一個應用場景就是在不同的上下文中,對象可能需要呈現出不同的方法。比如說我們在一個大量需要正則匹配和替換的場景,我們就會希望字符串可以直接調用正則表達式來替換最好:
這是最符合我們預期的,但事實上我們必須寫成:
即使是這樣的代碼:
也是不允許的。
但當我們需要在一個表達式中進行多次正則替換時,代碼就會變成一堆災難。
這里面的原因很多,需要大量的正則替換的場景并不是通用常見場景,當然更重要的原因是,如果要為String實現string Replace( Regex, string )方法,必然導致String產生對Regex的依賴,而String是一個比Regex更為基礎的類型,這種強綁定會帶來很多的問題。例如,Regex必須與String放在同一個程序集中(否則會造成循環引用,因為Regex必然依賴String,事實上String在mscorlib中而Regex在System中)。除此之外,如果我們不想用微軟的正則表達式引擎,那么我們還是不得不退回到原來的丑陋模式。
擴展方法的出現,解決了所有的這些問題,也使得我們的框架代碼變得簡潔,節省了大量的架子代碼。Enumerable所定義的擴展方法只用了很少的代碼就給我們帶來了極大的便利。
為什么會忽然特別想聊擴展方法這個特性,因為我現在做的這個HTML數據綁定引擎中,程序員所調用到的方法大部分都是擴展方法。擴展方法加上接口,讓我幾乎不費力的就為程序員提供了所想即所寫的編程體驗。
不過在這里我不想太多的討論這個東西,來談談擴展方法在其他方面的一些體驗提高。
作為Web開發而言,用Request來接收參數是最普通不過的事情,但Request只能接收字符串,我們不得不寫很多的轉換代碼:
//...
這樣的代碼寫慣了倒也沒什么,但多了的確是個負擔,所以我看到很多程序員為了偷懶,就不作強類型轉換了,這樣帶來非常多的隱患。我提供了一個擴展方法來試圖解決這個問題:
現在代碼比之前要好一些了,因為程序員的思路不會被打斷,而對原有的程序的升級,在后面加一個.ParseTo顯然也不太費事,大家接受度就會比較好。
當然,有程序員A指出,這個什么QueryString實在是太長了,難寫。
那么這樣:
如果是POST傳遞的數據:
來看看數據庫吧,當我們需要從數據庫中取出一個值時,有時候會遇到DBNull的情況。DBNull與null不同,前者是一個合法的值,這個值經常會搞得我們很煩:
當這個表中不存在一條記錄的時候,返回值就會是DBNull。對于這種情況,我們希望對DBNull做一個默認值,如果是null的話,這很好辦:
利用C# 2.0中的??操作符,我們可以很容易辦到這一點,但這對于DBNull卻是無效的。
不過,擴展方法很容易做到這一點:
或是使得下面的丑陋的強制類型轉換語法寫起來順手點:
dataItem.Cast().ToString( "yyyy-MM-dd" );
擴展方法使得框架的編寫者可以用很少的代碼就能實現流暢的編碼感受,我一直認為,一個框架是否好用,并不在于它的功能有多強大,而在于它是否給程序員提供了流暢的編程體驗,和直覺性的代碼書寫。用我的話說就是:“當你第一眼看到這個方法,覺得它會是干什么的,會返回什么,那它就真的是干這個的,也真的會返回你想要的東西”,不要翻手冊或是幫助,甚至不需要借助參數和方法的說明文字,你所想的即它所做的,這才是最重要的。
最后提供我的ParseTo擴展方法的完整實現:
{
public static T ParseTo( this string value )
{
if ( Parser.ParseMethod != null )
return Parser.ParseMethod( value );
throw new NotSupportedException();
}
static WebExtensions()
{
Parser<short>.ParseMethod = short.Parse;
Parser<int>.ParseMethod = int.Parse;
Parser<long>.ParseMethod = long.Parse;
Parser<byte>.ParseMethod = byte.Parse;
Parser<ushort>.ParseMethod = ushort.Parse;
Parser<uint>.ParseMethod = uint.Parse;
Parser<ulong>.ParseMethod = ulong.Parse;
Parser<sbyte>.ParseMethod = sbyte.Parse;
Parser<float>.ParseMethod = float.Parse;
Parser<double>.ParseMethod = double.Parse;
Parser<decimal>.ParseMethod = decimal.Parse;
Parser<bool>.ParseMethod = bool.Parse;
Parser<DateTime>.ParseMethod = DateTime.Parse;
Parser<TimeSpan>.ParseMethod = TimeSpan.Parse;
}
private class Parser
{
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 = (Parser.ParseMethodDelegate) dynamicMethod.CreateDelegate( typeof( Parser.ParseMethodDelegate ) );
}
noParseMethod = true;
return null;
}
set
{
_parseMethod = value;
}
}
}
}
留言列表