我那时什么也不懂!我应该根据她的行为,而不是根据她的话来判断她。她使我的生活芬芳多彩,我真不该离开她跑出来。我本应该猜出在她那令人爱怜的花招后面所隐藏的温情。花是多么自相矛盾!我当时太年青,还不懂得爱她。
——圣埃克絮佩里
摘自《小王子》
摘要
如果扩展方法(Extension Method)可以动态选择要扩展的对象……
* * * * * *
这里有个非常有趣的设计。Quackable和Squeakable接口拥有相同函数签名的抽象函数WhoAmI()和Quack()。当Marllard Duck(以及Redhead Duck)同时实现了Quackable和Squeakable接口时,就被同时混入了两个版本的Quack()方法。这样就可以在运行期动态决定是使用Quackable.Quack()还是Squeakable.Quack()了。
有什么感觉?是不是很乱很强大?
感觉很强大,是因为Mallard Duck就像变相怪杰,换个面具(interface),行为就完全不同了,而且是动态的哟。以前,这种“运行期动态改变对象行为”的功能只能用Strategy才能实现。虽然背后的运作原理不同,但这个设计仍然可以给人一种duck.Quack()方法的行为被“动态”改变了的感觉。
它可以作为Strategy的替代品么?不,决不能。Strategy表现出来的语义是“Mallard Duck有一种鸣叫的方法——嘎嘎叫或吱吱叫”(Strategy的例子可以参考上一篇)。再来看一下图一,Mallard Duck同时实现了Quackable和Squeakable接口,所表现出来的语义是“Mallard Duck既能嘎嘎叫同时又能吱吱叫”,这可就和我们的本意相悖了。
Strategy模式是把对象的行为提炼成显式的概念,一大好处是你只要看一看有哪些类实现了QuackBehavior接口,就知道一共有多少种鸣叫的方法了。这个在图一的设计里是完全体现不出来的。
难道说Strategy就是Strategy,天上地下独一无二的Strategy?
难道说我们一不小心又弄了个垃圾设计?
在急着把它扔进垃圾箱之前,不妨再作一个设想:如果可以在运行期动态决定让Mallard Duck实现哪个接口又会如何?
这个设计与图一的设计只有一点点不同:Mallard Duck(以及Redhead Duck)在编译期既不实现Quackable接口也不实现Squeakable接口。这样,在一开始的时候(编译期)Mallard Duck没有鸣叫的能力,但是有鸣叫的潜力(因为实现了WhoAmI()函数)。在运行期,通过让Mallard Duck动态实现Quackable接口,使它被动态混入QuackModule.Quack()函数,就具有了嘎嘎叫的能力;或动态实现Quackable接口,使它被动态混入Squeakable.Quack(), 就具有了吱吱叫的能力。

但是它是怎么实现的?呵呵,是使用了CodeProject上的一个开源类库NDuck。我知道很多人一听到开源项目或第三方的东东就会头痛了。不过请相信我,NDuck真的是超级小巧超级简便的——小巧到全部源代码才几百行,简便到无需任何配置只要引用一个NDuck.dll文件就可以使用了。
附录
1. Duck Typing 简介
Duck Typing是一种基于动态类型的编程方式。在这种方式里,将根据一个对象当前所具有的方法和属性集来决定有效语义,而不是根据这个对象所继承的类。(这个我喜欢。王侯将相,宁有种乎?) Duck Typing这名称来源于Duck Test。
在Duck Typing里,我们只关心对象是否实现了需要被使用的那些方面,而不是对象本身的类型。就像我喜欢张曼玉这种类型的,但却不是非张曼玉或张曼玉的女儿不娶。
在C#这样的静态类型语言里,我们可能会定义这样一个用于判断字符串是否为空的函数IsEmpty().
bool IsEmpty(String arg)
{
return arg.Length <= 0;
}
虽然数组或Stream对象都有Length属性,却不能使用这个函数。要想判断它们是否为空,我们必须再写两个IsEmpty()函数。
bool IsEmpty(Array arg)
{
return arg.Length <= 0;
}
bool IsEmpty(Stream arg)
{
return arg.Length <= 0;
}
而在Ruby这种动态类型的语言里,只需定义一个IsEmpty()函数就行了。
2. NDuck 项目简介
不论你是否喜欢Duck Typing,应该都会同意在上面那个C#的例子中写3个几乎一模一样的IsEmpty()函数很是累赘。有什么方法可以只写一个函数么?
C#提供编译期的类型检查,但是也提供了绕过类型检查的方法——反射。我们可以让IsEmpty()函数接收Object类型的参数,然后使用反射获得参数的Length属性。
另一种方法是定义一个HasLength接口,让IsEmpty()针对这个接口编程。
然后就可以这样使用IsEmpty()了。
是的,要知道HasLength接口拥有哪些函数还是要使用反射,还有动态生成、编译代码,这些工作肯定会耗费一些时间。但是NDuck保证这些工作只会进行一次,然后这些动态生成的代理类就会被缓存起来以备后用。所以,如果你的程序是用于心脏起搏器或控制火箭发射,我不建议你使用它,其它情况则不必太担心。
参考文献
Thomas et al, 孙勇等 译, Programming Ruby 中文版。电子工业出版社,2007.
Russ Olsen, Design Patterns in Ruby. Addison-Wesley, 2007.
Guenter Prossliner, DuckTyping: Runtime Dynamic Interface Implementation. codeproject, 2006.
Duck typing. Wikipedia. (可能需要使用代理)

——圣埃克絮佩里
摘自《小王子》
摘要
如果扩展方法(Extension Method)可以动态选择要扩展的对象……
* * * * * *
这里有个非常有趣的设计。Quackable和Squeakable接口拥有相同函数签名的抽象函数WhoAmI()和Quack()。当Marllard Duck(以及Redhead Duck)同时实现了Quackable和Squeakable接口时,就被同时混入了两个版本的Quack()方法。这样就可以在运行期动态决定是使用Quackable.Quack()还是Squeakable.Quack()了。

有什么感觉?是不是很乱很强大?
感觉很强大,是因为Mallard Duck就像变相怪杰,换个面具(interface),行为就完全不同了,而且是动态的哟。以前,这种“运行期动态改变对象行为”的功能只能用Strategy才能实现。虽然背后的运作原理不同,但这个设计仍然可以给人一种duck.Quack()方法的行为被“动态”改变了的感觉。
它可以作为Strategy的替代品么?不,决不能。Strategy表现出来的语义是“Mallard Duck有一种鸣叫的方法——嘎嘎叫或吱吱叫”(Strategy的例子可以参考上一篇)。再来看一下图一,Mallard Duck同时实现了Quackable和Squeakable接口,所表现出来的语义是“Mallard Duck既能嘎嘎叫同时又能吱吱叫”,这可就和我们的本意相悖了。
Strategy模式是把对象的行为提炼成显式的概念,一大好处是你只要看一看有哪些类实现了QuackBehavior接口,就知道一共有多少种鸣叫的方法了。这个在图一的设计里是完全体现不出来的。
难道说Strategy就是Strategy,天上地下独一无二的Strategy?
难道说我们一不小心又弄了个垃圾设计?
在急着把它扔进垃圾箱之前,不妨再作一个设想:如果可以在运行期动态决定让Mallard Duck实现哪个接口又会如何?
源代码下载:GeekDesign.rar (VS2008控制台工程)
动态实现接口这个设计与图一的设计只有一点点不同:Mallard Duck(以及Redhead Duck)在编译期既不实现Quackable接口也不实现Squeakable接口。这样,在一开始的时候(编译期)Mallard Duck没有鸣叫的能力,但是有鸣叫的潜力(因为实现了WhoAmI()函数)。在运行期,通过让Mallard Duck动态实现Quackable接口,使它被动态混入QuackModule.Quack()函数,就具有了嘎嘎叫的能力;或动态实现Quackable接口,使它被动态混入Squeakable.Quack(), 就具有了吱吱叫的能力。

源代码下载: DuckTyping.rar(VS2008控制台工程)
怎么样?虽然只进行了一点点改变,感觉却完全不同了。当然,本例的“动态混入不同的算法”和Strategy的“动态替换不同的算法”还是有一些差别。但是不管怎么说,我们的设计工具箱里又多了件超灵活的利器。但是它是怎么实现的?呵呵,是使用了CodeProject上的一个开源类库NDuck。我知道很多人一听到开源项目或第三方的东东就会头痛了。不过请相信我,NDuck真的是超级小巧超级简便的——小巧到全部源代码才几百行,简便到无需任何配置只要引用一个NDuck.dll文件就可以使用了。
附录
1. Duck Typing 简介
Duck Typing是一种基于动态类型的编程方式。在这种方式里,将根据一个对象当前所具有的方法和属性集来决定有效语义,而不是根据这个对象所继承的类。(这个我喜欢。王侯将相,宁有种乎?) Duck Typing这名称来源于Duck Test。
Duck Test
If a bird looks like a duck, swims like a duck and quacks like a duck, then it's probably a duck..
如果一只鸟外表像鸭子,游起泳来像鸭子并且叫起来像鸭子,那么它可能就是鸭子。
If a bird looks like a duck, swims like a duck and quacks like a duck, then it's probably a duck..
如果一只鸟外表像鸭子,游起泳来像鸭子并且叫起来像鸭子,那么它可能就是鸭子。
在Duck Typing里,我们只关心对象是否实现了需要被使用的那些方面,而不是对象本身的类型。就像我喜欢张曼玉这种类型的,但却不是非张曼玉或张曼玉的女儿不娶。
在C#这样的静态类型语言里,我们可能会定义这样一个用于判断字符串是否为空的函数IsEmpty().
bool IsEmpty(String arg)
{
return arg.Length <= 0;
}
虽然数组或Stream对象都有Length属性,却不能使用这个函数。要想判断它们是否为空,我们必须再写两个IsEmpty()函数。
bool IsEmpty(Array arg)
{
return arg.Length <= 0;
}
bool IsEmpty(Stream arg)
{
return arg.Length <= 0;
}
而在Ruby这种动态类型的语言里,只需定义一个IsEmpty()函数就行了。
# 判断arg是否为空
def is_empty?(arg)
return arg.length <= 0
end
s1 = 'abc' # 字符串
s2 = '' # 空字符串
a = [1,3,5] # 数组
puts is_empty?(s1) # => false
puts is_empty?(s2) # => true
puts is_empty?(a) # => false
def is_empty?(arg)
return arg.length <= 0
end
s1 = 'abc' # 字符串
s2 = '' # 空字符串
a = [1,3,5] # 数组
puts is_empty?(s1) # => false
puts is_empty?(s2) # => true
puts is_empty?(a) # => false
2. NDuck 项目简介
不论你是否喜欢Duck Typing,应该都会同意在上面那个C#的例子中写3个几乎一模一样的IsEmpty()函数很是累赘。有什么方法可以只写一个函数么?
C#提供编译期的类型检查,但是也提供了绕过类型检查的方法——反射。我们可以让IsEmpty()函数接收Object类型的参数,然后使用反射获得参数的Length属性。
static bool IsEmpty(Object arg)
{
// 通过反射取得arg的Length属性的值
int length = (int)arg.GetType().GetProperty("Length").GetValue(arg, null);
return length <= 0;
}
这个IsEmpty()函数可以适用于任何拥有Length属性的对象。但是千万别把它用于你的程序,它会让程序慢得像蜗牛。而且使用反射又麻烦又不直观。我宁肯去微软总部示威游行,要求把C#改成动态语言。{
// 通过反射取得arg的Length属性的值
int length = (int)arg.GetType().GetProperty("Length").GetValue(arg, null);
return length <= 0;
}
另一种方法是定义一个HasLength接口,让IsEmpty()针对这个接口编程。
public interface HasLength
{
int Length { get; }
}
static bool IsEmpty(HasLength arg)
{
return arg.Length <= 0;
}
这个版本的IsEmpty()函数适用于任何实现了HasLength接口的对象。但是我们没法修改String、Array和Stream的源代码,怎能让它们实现HasLength接口呢?还记得那句经典的“Any problem in computer science can be solved with another layer of indirection.”么?NDuck实现动态实现接口的方法就是动态创建一个代理类Duck0,让这个代理类实现HasLength接口并作为String的代理。例如{
int Length { get; }
}
static bool IsEmpty(HasLength arg)
{
return arg.Length <= 0;
}
string s = "abc";
HasLength s1 = DuckTyping.Implement<HasLength>(s);
将动态创建一个代理类Duck0HasLength s1 = DuckTyping.Implement<HasLength>(s);
public class Duck0 : HasLength
{
private string _obj;
public Duck0(string obj)
{
this._obj = obj;
}
int HasLength.Length
{
get
{
return this._obj.Length;
}
}
}
{
private string _obj;
public Duck0(string obj)
{
this._obj = obj;
}
int HasLength.Length
{
get
{
return this._obj.Length;
}
}
}

然后就可以这样使用IsEmpty()了。
static void Main(string[] args)
{
string s = "abc";
int[] a = new int[] { };
HasLength s1 = DuckTyping.Implement<HasLength>(s);
HasLength a1 = DuckTyping.Implement<HasLength>(a);
Console.WriteLine(IsEmpty(s1)); // 输出: False
Console.WriteLine(IsEmpty(a1)); // 输出: True
Console.WriteLine(s1.GetType().Name); // 输出: Duck0
}
{
string s = "abc";
int[] a = new int[] { };
HasLength s1 = DuckTyping.Implement<HasLength>(s);
HasLength a1 = DuckTyping.Implement<HasLength>(a);
Console.WriteLine(IsEmpty(s1)); // 输出: False
Console.WriteLine(IsEmpty(a1)); // 输出: True
Console.WriteLine(s1.GetType().Name); // 输出: Duck0
}
是的,要知道HasLength接口拥有哪些函数还是要使用反射,还有动态生成、编译代码,这些工作肯定会耗费一些时间。但是NDuck保证这些工作只会进行一次,然后这些动态生成的代理类就会被缓存起来以备后用。所以,如果你的程序是用于心脏起搏器或控制火箭发射,我不建议你使用它,其它情况则不必太担心。
参考文献
Thomas et al, 孙勇等 译, Programming Ruby 中文版。电子工业出版社,2007.
Russ Olsen, Design Patterns in Ruby. Addison-Wesley, 2007.
Guenter Prossliner, DuckTyping: Runtime Dynamic Interface Implementation. codeproject, 2006.
Duck typing. Wikipedia. (可能需要使用代理)

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· 字符编码:从基础到乱码解决
· Open-Sora 2.0 重磅开源!