Duck Typing与协议 vs. 继承

本文探讨了Ruby中Duck Typing的概念及其与协议的关系,解释了为何StringIO不是IO的子类,以及如何通过谓词函数来检查对象是否支持特定的API。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在ruby-core邮件列表中,一个关于Ruby中动态类型的问题引发了一系列讨论

\
根据irb中的运行结果:

\u0026gt;\u0026gt; StringIO.is_a?(IO)


\u0026gt;\u0026gt; =\u0026gt;false

对我来说,这看起来不合逻辑。是有意而为之吗?如果是的话,为什么?

\

有一种答案是这样的:

\
因为StringIO没有使用IO作为它的基类实现,所以它不是IO。

不过这又有什么关系呢?如果你使用标准的Duck Typing,这根本就无所谓。使用Duck Typing时,类的层次关系无关紧要。类只不过是Duck Typing的一种实现细节。你也不用关心#is_a?、#respond_to?等方法。只要假定对象提供你要调用的方法,然后调用它们就可以了。

\

原来的问题(以及邮件列表辩论中出现的一些帖子)应该是来自对OOP中继承所担当的角色的混淆。StringIO和IO不需要同样的超类,因为它们之间没有任何共同之处,除一点之外:它们支持的一系列方法。

\

看来这就是Duck Typing的基本想法:让对象可以响应一系列方法就够了,没必要要求它属于某个类型。

\

此处没有什么新概念,也不是Ruby特有的东西。将“方法”这个词换成OOP的术语“消息”,上面的句子就变为:“【……】让对象可以响应一系列消息”。听起来真的很像是某个(网络)协议(Protocol)的概念。毕竟,如果一个系统可以理解并响应诸如“PUT”、“GET”、“POST”等以特定的方式发送给它的消息,就可以说它理解HTTP协议。

\

让我们进一步看看这个想法:某个客户端,比如说Web浏览器,只关心它通信的服务器是否理解HTTP,它才不管服务器是什么类型呢。总之:如果Mosaic浏览器被硬编码为需要另一端的服务器是NCSA或Netscape的HTTP服务器,互联网恐怕就不会像现在这样蓬勃发展了。通过忽略通信另一端的类型,并且只要求以特定的方式回应“GET”消息,Web开发的两端(服务器端与客户端)都可以彼此独立地进行演化。

\

这种方式与协议的类似之处,OOP编程语言的使用者们在很久以前就已经很明白了。SmalltalkObjectiveC,作为动态OOP语言,早就使用“协议”(Protocol)这个词来指代此概念了。

\

协议的概念当然很有用,正好用来给特定的信息定一个名称。这也有助于澄清上述邮件列表中争论的问题。StringIO与IO共享的不是共同的祖先,而是共同的协议。

\

此概念并不仅限于动态语言。在一些静态语言中,比如Java就通过引入接口,而向前跨进了一大步。接口被用来:

\
  • 命名一系列信息 \
  • 静态检查一个类是否实现了这些信息
    \

虽然在一些语言中,比如C++,通过抽象基类和多继承也能做得到,但抽象基类混淆了继承和协议的概念。

\

当然,静态检查的保障是有限的 :接口只能检查一些有特定签名的方法——它不能保证方法的行为,甚或是方法的返回值,甚至也不能保证方法是可以被调用的——比如说在使用Java的Collection API的时候,要小心应对Java的java.lang.UnsupportedOperationException。

\

毫无疑问,接口也有它们自己的问题,比如演化性。改变一个接口,会破坏所有实现了该接口的类。像Eclipse这样的大型项目,会提供API演化的指导建议,其中规定了一个接口在发布之后就不可以再发生变化。这样的状况下,有时会建议大家多使用抽象类而不是接口,因为抽象类可以添加新的函数,但令其实现为空,这样子类就可以,但是不必实现这些新函数。当然,这个问题可以通过定义粒度合适的接口来解决,每个接口对应一个方法,并且通过对已有接口的扩展来搭建完整的接口集合,每个接口对应一定单独的API调用。

\

要解决保证一个对象支持某个协议这个问题,在Ruby中,可以通过将接口检查委托给一个谓语函数(predicate function)来完成。如下示例:

\
\def supports_api?(obj)\ obj.respond_to?(:length) \u0026amp;\u0026amp; obj.respond_to?(:at)\end \\
\

此做法可行,但是同时会带来一个小问题:respond_to?只对定义在一个类中的方法起作用。如果一个对象用method_missing来实现它的部分接口(例如一个代理),虽然对这些方法的调用没有问题,但针对它们调用respond_to?会返回false。这就是说respond_to?检查可以工作,但是它不适合用来替换静态检查(Rick de Natale将这种编码风格称为“Chicken Typing”,这是防御性编程的一种)。

\

另一种静态定义细粒度接口的方式是Scala语言的结构化类型

\
def setElementText(element : {def setText(text : String)}, text : String) = 
{
element.setText(text.trim()
.replaceAll(\"\\
内容概要:本文档主要展示了C语言中关于字符串处理、指针操作以及动态内存分配的相关代码示例。首先介绍了如何实现键值对(“key=value”)字符串的解析,包括去除多余空格和根据键获取对应值的功能,并提供了相应的测试用例。接着演示了从给定字符串中分离出奇偶位置字符的方法,并将结果分别存储到两个不同的缓冲区中。此外,还探讨了常量(const)修饰符在变量和指针中的应用规则,解释了不同类型指针的区别及其使用场景。最后,详细讲解了如何动态分配二维字符数组,并实现了对这类数组的排序释放操作。 适合人群:具有C语言基础的程序员或计算机科学相关专业的学生,尤其是那些希望深入理解字符串处理、指针操作以及动态内存管理机制的学习者。 使用场景及目标:①掌握如何高效地解析键值对字符串并去除其中的空白字符;②学会编写能够正确处理奇偶索引字符的函数;③理解const修饰符的作用范围及其对程序逻辑的影响;④熟悉动态分配二维字符数组的技术,并能对其进行有效的排序和清理。 阅读建议:由于本资源涉及较多底层概念和技术细节,建议读者先复习C语言基础知识,特别是指针和内存管理部分。在学习过程中,可以尝试动手编写类似的代码片段,以便更好地理解和掌握文中所介绍的各种技巧。同时,注意观察代码注释,它们对于理解复杂逻辑非常有帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值