string.IsNullOrEmpty()这个方法算得上是.net中使用频率最高的方法之一。此方法是string的一个静态方法,类似的静态方法在string这个类中还有很多。那么这样的方法作为静态方法是否合理呢?如果我们从面向对象的角度出发,我们会发现这种方案不是十分符合面向对象的设计原则。
什么是对象?对象是拥有数据和行为的结合体。如果说string是一个类,那么string message="hello"这句话就定义了一个string的对象,名称叫做message。
一.让对象自己说话
对象应该是自治的,它拥有自己的行为和数据。如果把对象当作一个生命体,他是可以自己说话的。比如我们把message当作一个人,就可以出现下面的对话:
A:“hi message,你是空的吗?"
B:"不是。"
A:"你的长度是多少啊?"
B:"5"
这样的对话体现了message作为一个对象,拥有自己的行为和数据。
而代码string.IsNullOrEmpty(message) 则描述了以下对话:
A:“Hi string, message是空的吗?”
B:“不是”
很显然,后面的对话借助于string类来完成本应该有message对象自己应该完成的事情。所以我们说这样的设计并没有完全符合面向对象的设计原则。如果说这样的设计还可以说的过去,毕竟string类和message对象是有那么一点关系。那么下面的这种场景则更加不靠谱:
你想对一个字符串实现反转(reverse),翻开string类查看了一番,发现.net并没有此方法,于是你创建了一个StringHelper的类,写下了下面的代码:
public static string Reverse(string originalString) { return something; }
如果代码会说话,则会有如下对话:
A:"hi StringHelper, 把message反转一下"
这样的对话暴露了两个问题:
经过扩展方法的"补救”,代码可以写为:message.IsNullOrEmpty(),这不就是我们想要的结果吗? Ruby作为一门面向对象的语言在设计之初就注意到了这个问题:在irb里输入:String.instance_methods(false)可以看到String的所有实例方法,例如:empty?,:size,:reverse. 在Ruby里可以直接写:message.empty?
实际上,用“补救”一词并不准确,因为任何人都不能在设计之初考虑到对象的所有行为,扩展方法更多的是提供了一种我们扩展用户行为的方案。新型的编程语言:诸如F#,Ruby,swift等均提供了对象扩展的能力。
扩展方法可以完美的解决对象行为的扩展问题。你的项目里有没有诸如**Helper, **Utility之类的类,里面的代码大多可以成为某个对象的扩展方法。
2.模拟中缀运算符
C#中的+、-、*、/ 等运算符均为中缀表达式,比如要使用运算符"+"连接三个字符串: "stringA"+"stringB"+"stringC";
如果使用函数则需要写成:
string.Contact(stringA,string.Contact(stringB,stringC));
后一种写法的问题在于运算的书写顺序与其实际执行顺序相反,因此使用运算符而不使用函数的好处在于“中缀”运算符描述的代码阅读起来更为自然。利用扩展方法可以在一定程度上模拟中缀运算符。
Rectangle里有一个Union的静态方法:public static Rectangle Union(Rectangle a, Rectangle b); 我们很难说Union这个静态方法应该是Rectangle的一个行为。这个方法更多的表现出了两个Rectangle在做Union运算,利用扩展方法:
internal static Rectangle Union(this Rectangle @this,Rectangle notheRectangle) { return Rectangle.Union(@this, notheRectangle); }
可以很自然的写出:var unionRegion = r1.Union(r2).Union(r3);
而不是:var unionRegion = Rectangle.Union(r1, Rectangle.Union(r2, r3));
三、面向语言编程
考虑下面的扩展方法和调用:
public static TimeSpan Days(this int @this) { return TimeSpan.FromDays(@this); } var timeSpan = 3.Days();
在这种场景下,Days()既不是int的行为,也不是运算,而是具有一点面向语言编程的味道。在面向对象编程环境中,有一种编程风格叫做流畅接口(Fluent API),这样的编程场景大多用于类库的API设计,关于Fluent Interface的设计请看使用C#设计Fluent Interface和在C#中使用装饰器模式和扩展方法实现Fluent Interface。
四、泛型扩展方法
扩展方法在本质上是一个静态方法,因此也可以写出泛型扩展,简单举两个例子。
public static T ChangeTo<T>(this object @this) { var value = default(T); value = (T)Convert.ChangeType(@this, typeof (T)); return value; } var numberString = 2.ChangeTo<string>(); var numberBool = 2.ChangeTo<bool>();
例2
public static TValue GetOrAdd<TKey, TValue>(this IDictionary<TKey, TValue> me, TKey key, Func<TValue> constructor) { TValue value; if (me.TryGetValue(key, out value)) { return value; } value = constructor(); me.Add(key, value); return value; } var books = new Dictionary<int, string>(); var book = books.GetOrAdd(1, () => "book1"); var book2 = books.GetOrAdd(1, () => "book1");
这样的扩展方法能使代码更加简洁。
五、一种新的面向对象设计方案
如果说上面的场景仅仅代表代码层面的技巧,那么下面的技巧则是体现了扩展方法在面向对象中的一种的新设计方案。
场景:一辆摩托车和一辆自行车。其中鸣笛(Whistle)和读取实时速度(ReadSpeed)具有相同的实现,而刹车(Brake)和加速(AddSpeed)则各自具有不同的实现方式。
根据这样的场景我们立刻可以设计出这样的继承层次:
为了抽取鸣笛(Whistle)和读取实时速度(ReadSpeed)这两个行为并且公用,我们抽象了一个Vehicle作为抽象类。这样的设计也许是正确的,但是作为一个有经验的OO开发者也许不会马上赞同这个方案。原因有3:
利用扩展方法的方案如下:抽象接口ICanRun,用来实现刹车(Brake)和加速(AddSpped)。将鸣笛(Whistle)和读取实时速度(ReadSpeed)两个公共的实现扩展在了ICanRun接口上面,整个实现非常松耦合。
internal interface ICanRun { void Brake(); void AddSpeed(); } internal class Bicycle : ICanRun { public void Brake() { } public void AddSpeed() { } } internal class Motor : ICanRun { public void Brake() { } public void AddSpeed() { } } internal static class VehicleState { internal static void Whistle(this ICanRun @this) { } internal static void CalculateSpeed(this ICanRun @this) { } }
六:总结
我们现在看微软给出的扩展方法的定义:扩展方法使您能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。这里的“添加”之所以使用引号,是因为并没有真正地向指定类型添加方法。
这个定义更多的是在说明扩展方法的实现方式,而本文则对扩展方法的使用场景做了总结。本文的例子也只能算是扩展方法应用的冰山一角。通过阅读更多优秀的代码,理解别人在使用扩展方法时的意图能使你写出更简洁、更富有表现力的代码。
再谈扩展方法,从string.IsNullOrEmpty()说起
原文:http://www.cnblogs.com/richieyang/p/4869215.html