再谈扩展方法,从string.IsNullOrEmpty()说起
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反转一下"
这样的对话暴露了两个问题:
- 耦合了StringHelper类,使用者必须要知道存在这样的一个类,使用者知道的太多。
- 反转自己是对象自己的行为,但自己并没有实现。
经过扩展方法的"补救”,代码可以写为: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 anotheRectangle)
{
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:
- 因为要公用代码就立即设计类的继承关系不具有说服力,特别是在设计初期,抽象出的Vehicle并不一定准确,很多公用的行为放在Vehicle里久而久之违反了SRP(单一职责),进一步违反OCP(开放封闭原则)
- 面向对象编程中有一个指导性的原则叫做:使用组合而非继承。这个原则告诉我们组合比继承更加灵活,没有十足的把握不要使用继承。
- 只有非常确定继承关系,并且继承关系符合LSP(里氏替换原则)时,才会认为这个抽象类设计的没有问题。
利用扩展方法的方案如下:抽象出接口ICanRun,用来实现刹车(Brake)和加速(AddSpeed)。将鸣笛(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 ReadSpeed(this ICanRun @this)
{ }
}
六:利用扩展方法写出混搭风格的代码
我们现在看微软对扩展方法的定义:扩展方法使您能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。这里的“添加”之所以使用引号,是因为并没有真正地向指定类型添加方法。
这个定义更多的是在说明扩展方法的实现方式,但是也会带来一个误解:在有源代码的情况下无需使用扩展方法。通过上面几种场景我们可以看出扩展方法的存在不仅仅是为了“添加”方法,灵活使用扩展方法有助于写出简洁、富有表达力的代码。比较典型的案例为:owin katana,asp.net core 1.0,这两个项目中大量使用了扩展方法,这种混搭风格的代码提高了代码的扩展性,易于阅读。
举个例子,刚开始我们有这样的一个设计:
public interface ISprite
{
void Move();
void Stop();
void Speak();
} public class Sprite:ISprite
{
public void Move()
{
throw new NotImplementedException();
} public void Stop()
{
throw new NotImplementedException();
} public void Speak()
{
throw new NotImplementedException();
}
}
接口ISprite定义的一尘不染,定义了Sprite应该具有的核心能力,这样的代码对于阅读者而言一目了然。不过随着业务的发展,我们需要对Sprite序列化和反序列化,还要给Sprite添加一些属性等待各种需求。这些能力应该是Sprite对象应该具备的,定义在Sprite类中无可厚非,但是这样一来Sprite类急剧膨胀,阅读性也变的差了起来,阅读序列化和反序列化这样的代码几乎不会增加阅读者对Sprite类的理解,所以我们完全可以将这些能力分别归类扩展在Sprite上:
public static class SpriteSerilization
{
public static byte[] Serilize(this ISprite sprite)
{
return null;
} public static ISprite Deserilize(this byte[] bytes, string name)
{
return null;
}
} public static class SpriteClassifier
{
public static bool IsBad(this ISprite sprite)
{
return true;
} public static bool IsGood(this ISprite sprite)
{
return false;
}
}
这两个扩展类帮我们对Sprite不太核心的能力做了分类,SpriteSerilization和SpriteClassifier对能力做了归类。在我来看这样的设计更利于阅读和维护,你觉得呢?
再谈扩展方法,从string.IsNullOrEmpty()说起的更多相关文章
- 再谈Java方法传参那些事
把一个变量带进一个方法,该方法执行结束后,它的值有时会改变,有时不会改变.一开始会觉得--“好神奇呀”.当我们了解java内存分析的知识后,一切都是那么简单明了了--“哦,这么回事呀”.但是今天的上机 ...
- C# 五、谈扩展方法的理解
http://www.cnblogs.com/zhaopei/p/5678842.html
- .NET 扩展方法 (二)
上一篇随笔 .NET 扩展方法 (一) 已经对 扩展方法有了大致的介绍,这篇算是一个补充,让我们来看一下扩展方法的几个细节: 一.扩展方法具有继承性 当使用扩展方法扩展一个类型的时候,其也扩展了派生类 ...
- [置顶] C#扩展方法 扩你所需
通过前面的学习,了解到:使用扩展方法,可以向现有类型“添加”方法.本文将使用扩展方法来对系统类型,自定义类型及接口进行方法扩展,一睹扩展方法的风采. 1.使用扩展方法来扩展系统类型 String是c# ...
- LinQ—扩展方法
概述 本节主要解说扩展方法,涉及LinQ的详细知识不多. 扩展方法的描写叙述 .net framework为编程人员提供了非常多的类,非常多的方法,可是,不论.net framework在类中为我们提 ...
- .NET 简单的扩展方法使用。
写代码时,我们经常会碰到dll中提供的方法,不够用或者不好用的情况.而且我们也不方便去更改dll本身的源码. 这时候我们可以使用.NET提供的"扩展方法"去解决这个问题. 下面我写 ...
- C#扩展方法入门
扩展方法被定义为静态方法,但它们是通过实例方法语法进行调用的. 它们的第一个参数指定该方法作用于哪个类型,并且该参数以 this 修饰符为前缀. 仅当你使用 using 指令将命名空间显式导入到源代码 ...
- 转:扩展方法(C# 编程指南)
扩展方法使你能够向现有类型“添加”方法,而无需创建新的派生类型.重新编译或以其他方式修改原始类型.扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用.对于用 C# 和 Visual ...
- 从扩展方法到匿名方法再到LINQ
1.首先我们应该知道什么是扩展方法: 扩展方法使您能够向现有类型“添加”方法,而无需创建新的派生类型.重新编译或以其他方式修改原始类型. 扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样 ...
随机推荐
- 【如何快速的开发一个完整的iOS直播app】(采集篇)
原文转自:袁峥Seemygo 感谢分享.自我学习 前言 在看这篇之前,如果您还不了解直播原理,请查看这篇文章如何快速的开发一个完整的iOS直播app(原理篇) 开发一款直播app,首先需要采集主 ...
- 如何用Maven创建web项目(具体步骤)
版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] 使用eclipse插件创建一个web project 首先创建一个Maven的Project如下图 我们勾选上Creat ...
- Ceph剖析:数据分布之CRUSH算法与一致性Hash
作者:吴香伟 发表于 2014/09/05 版权声明:可以任意转载,转载时务必以超链接形式标明文章原始出处和作者信息以及版权声明 数据分布是分布式存储系统的一个重要部分,数据分布算法至少要考虑以下三个 ...
- vs2015里给c#添加轮廓折叠功能
轮廓折叠,在看代码时非常有用的功能,可以暂时屏蔽大段不相关的if/else分支 记得以前老版vs里是自带这功能的,c++和c#都有 但不知为啥最新的2015里c#没有了,搜了下原来是要装插件: htt ...
- openfire+strophe
其实是关于strophe的使用的,因为openfire那部分我并没有安装,项目过程中是直接访问的已经部署好的服务器了. 关于使用strophe+ openfire完成 即时通讯,做到及时的信息交互,主 ...
- ServiceStack.OrmLite中的一些"陷阱"(2)
注:此系列不是说ServiceStack.OrmLite的多个陷阱,这仅仅个人认为是某一个陷阱(毕竟我踩坑了)而引发的思考. 前文说到了项目需要使用两种不同的数据库语言,虽说前文问题已基本解决了,但是 ...
- UI基础之UITableViewController相关
1> 设置类型为Group - (instancetype)initWithStyle:(UITableViewStyle)style { return [super initWithStyle ...
- 广义Euler常数
对于区间(a,b)内f''(x)>0 那么在该区间内函数的一阶导数对应切线在该区间内只与f(x)在切点相交 1. f''(x)>0那么可知 f'(x)在该区间内是单调增的 以下图为例,过( ...
- 汇总常用的jQuery操作Table tr td方法
虽然现在DIV+CSS进行页的布局大行其道,但是很多地方使用table还是有很多优势,用table展示数据是比较方便的,下面汇总了jQuery操作Table tr td常用的方法,熟记这些操作技巧,下 ...
- LightOJ Beginners Problems 部分题解
相关代码请戳 https://coding.net/u/tiny656/p/LightOJ/git 1006 Hex-a-bonacci. 用数组模拟记录结果,注意取模 1008 Fibsieve's ...