惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
V
V2EX
爱范儿
爱范儿
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Martin Fowler
Martin Fowler
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
有赞技术团队
有赞技术团队
宝玉的分享
宝玉的分享
Last Week in AI
Last Week in AI
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
罗磊的独立博客
小众软件
小众软件
Vercel News
Vercel News
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
V
Visual Studio Blog
J
Java Code Geeks
P
Proofpoint News Feed
MongoDB | Blog
MongoDB | Blog
B
Blog
美团技术团队
量子位

酷 壳 – CoolShell

感染新冠的经历 | 酷 壳 - CoolShell 从一次经历谈 TIME_WAIT 的那些事 | 酷 壳 - CoolShell 网络数字身份认证术 | 酷 壳 - CoolShell 我做系统架构的一些原则 | 酷 壳 - CoolShell 源代码特洛伊木马攻击 | 酷 壳 - CoolShell Go编程模式 : 泛型编程 | 酷 壳 - CoolShell 如何做一个有质量的技术分享 | 酷 壳 - CoolShell Go 编程模式:k8s Visitor 模式 | 酷 壳 - CoolShell Go编程模式:Pipeline | 酷 壳 - CoolShell Go编程模式:委托和反转控制 | 酷 壳 - CoolShell Go 编程模式:Go Generation | 酷 壳 - CoolShell Go编程模式:Map-Reduce | 酷 壳 - CoolShell Go 编程模式:错误处理 | 酷 壳 - CoolShell Go编程模式:切片,接口,时间和性能 | 酷 壳 - CoolShell 百度为什么掉队了 | 酷 壳 - CoolShell 程序员如何把控自己的职业 | 酷 壳 - CoolShell 计时攻击 Timing Attacks | 酷 壳 - CoolShell Rust语言的编程范式 | 酷 壳 - CoolShell HTTP的前世今生 | 酷 壳 - CoolShell 记一次Kubernetes/Docker网络排障 | 酷 壳 - CoolShell 可视化编程 | 酷 壳 - CoolShell 程序的本质复杂性和元语言抽象 | 酷 壳 - CoolShell 伙伴分配器的一个极简实现 | 酷 壳 - CoolShell C++11的Lambda使用一例:华容道求解 | 酷 壳 - CoolShell C++面试中string类的一种正确写法 | 酷 壳 - CoolShell C++模板”>>”编译问题与词法消歧设计 | 酷 壳 - CoolShell 数据即代码:元驱动编程 | 酷 壳 - CoolShell 数据的游戏:冰与火 | 酷 壳 - CoolShell 7个示例科普CPU Cache | 酷 壳 - CoolShell 加班与效率 | 酷 壳 - CoolShell
对象的消息模型 | 酷 壳 - CoolShell
Todd · 2011-08-15 · via 酷 壳 – CoolShell

[ ———— 感谢 Todd 同学 投递本文,原文链接 ———— ]

C++对象模型

话题从下面这段C++程序说起,你认为它可以顺利执行吗?

//C++
class A {
    public:
        void Hello(const std::string& name) {
           std::cout << "hello " << name;
         }
};
int main(int argc, char** argv)
{
    A* pa = NULL; //!!
    pa->Hello("world");
    return 0;
}

试试的确可以顺利运行输出hello world,奇怪吗?其实并不奇怪,根据C++对象模型,类的非虚方法并不会存在于对象内存布局中,实际上编译器是把Hello方法转化成了类似这样的全局函数:

void A_Hello_xxx(A * const this, const std::string& name) {
    std::cout << “hello “ << name;
}

对象指针其实是作为第一个参数被隐式传递的,pa->Hello(“world”)实际上是调用的A_Hello_xxx(pa, “world”),而恰好A_Hello_xxx内部没有使用pa,所以这段代码得以顺利运行。

对象的消息模型

如果是研究C++对象模型,上面的讨论可以到此为止,不过这里我想从另一个层面来继续探讨这个问题。OOP的先驱人物Alan Kay在总结Smalltalk的OO特征时强调:

Smalltalk is not only NOT its syntax or the class library, it is not even about classes. I’m sorry that I long ago coined the term “objects” for this topic because it gets many people to focus on the lesser idea. The big idea is “messaging”.

也就是说相比类和对象的概念来讲,他认为对象交互的消息模型是OOP更为本质的特征,因为消息关注的是对象间的接口和交互,在构建大的系统的时候重要的不是对象/模块的内部状态,而是它们的交互。根据消息模型,牛.吃(草) 的语义是发送一条消息给“牛”,消息的类型是“吃”,消息的内容是“草”。如果按照严格的消息模型,那么上面那段C++代码应解释为向一个NULL对象发送Hello消息,这显然是不应该顺利执行的。类似的代码如果是在Java或C#中则会抛出空引用异常,所以Java和C#的设计更符合消息模型。

不过,Java和C#中也并非完全符合消息模型,来看一个经典的封装问题:

//C#

public class Account {
    private int _amount;

    public void Transfer(Account acc, int delta) {
        acc._amount += delta;
        this._amount -= delta;
    }
    …
}

上面定义了一个Account类,问题在于为什么在这个类的Transfer方法中可以直接访问另一个对象acc的私有成员_amount呢?这是不是有破坏封装的嫌疑呢?这个问题经典的答案是:并不破坏封装,封装是划分了基于类的静态的代码边界,使得类的private代码修改不影响外界,而不是对于动态对象的保护。这个解释当然是合理的,不过正如上面C++代码的解释属于C++对象模型范畴,这个解释则属于基于类的静态类型OOP语言的范畴。消息模型强调了对象内部状态的保护,只能通过消息改变其状态,而对象内部是否真的具有_amout这样一个私有成员对其他任何对象(即使同类对象)都是未知的。

如果要严格遵守消息模型实现对象内部状态的保护应该怎么做呢?我们来看一个例子,定义一个集合类,包括:1.集合对象的构造函数;2.In方法:判断元素是否存在;3.Join方法:对两个集合做交集;4.Union方法:对两个集合做并集。下面是一种Javascript实现:

//Javascript

//集合类Set的构造函数
function Set() {
    var _elements = arguments;
    //In方法:判断元素e是否在集合中
    this.In = function(e) {
        for (var i = 0; i < _elements.length; ++i) {
            if (_elements[i] == e) return true;
        }
        return false;
    };
}

//Join方法:对两个集合求交集
Set.prototype.Join = function(s2) {
    var s1 = this;
    var s = new Set();
    s.In = function(e) { return s1.In(e) && s2.In(e); }
    return s;
};

//Union方法:对两个集合求并集
Set.prototype.Union = function(s2) {
    var s1 = this;
    var s = new Set();
    s.In = function(e) { return s1.In(e) || s2.In(e); }
    return s;
};

var s1 = new Set(1, 2, 3, 4, 5);
var s2 = new Set(2, 3, 4, 5, 6);
var s3 = new Set(3, 4, 5, 6, 7);
assert(false == s1.Join(s2).Join(s3).In(2));
assert(true == s1.Join(s2).Uion(s3).In(7));

如果是在静态类型OOP语言中,要实现集合类的Join或Union,我们多半会像上面Account的例子一样直接对s2内部的_elements进行操作,而上面这段Javascript定义的Set关于对象s2的访问完全是符合消息模型的基于接口的访问。要实现消息模型Javascript的prototype机制并非必须的,真正的关键在于函数式的高级函数和闭包特性。从这个例子我们也可以体会到函数式的优点不仅在于无副作用,函数的可组合性也是函数式编程强大的原因。

Method Missing

接下来我们还要进行深度历险,让我们思考一下如果发送一条对象不能识别的消息会怎样?这种情况在C++、Java、C#等静态类型语言中会得到一个方法未定义的编译错误,如果是在Javascript中则会产生运行时异常。比如,s1.count()会产生一个运行时异常:Object #<Set> has no method ‘count’。

在静态类型语言这个问题很少受到重视,但在动态类型语言中却大有文章,来看下面的例子:
//Ruby

builder = Builder::XmlMarkup.new
xml = builder.books {|b|
    b.book :isbn => "14134" do
        b.title "Revelation Space"
        b.author "Alastair Reynolds"
    end
    b.book :isbn => "53534" do
        b.title "Accelerando"
        b.author "Charles Stross"
    end
}

上面这段很DSL的Ruby代码创建了这样一个XML文件对象:


<books>
    <book isbn="14134">
        <title>Revelation Space</title>
        <author>Alastair Reynolds</author>
    </book>
    <book isbn="53534">
        <title>Accelerando</title>
        <author>Charles Stross</author>
    </book>
</books>

builder.books, b.book, b.title都是对象方法调用,由于XML的元素名是任意的,所以不可能事先定义这些方法,类似的代码如果是在Javascript中就是no method异常。那为什么上面的Ruby代码可以正确执行呢?其实只要理解了消息模型就很容易想明白,只需要定义一个通用的消息处理方法,所有未明确定义的消息都交给它来处理就行了,这就是所谓的Method Missing模式:

class Foo
    def method_missing(method, *args, &block)
        …
    end
end

Method Missing除了对实现DSL很重要外,还可用于产生更好地调试和错误信息,把参数嵌入到方法名中等场合。目前,Ruby、Python、Groovy几种语言对Method Missing都有很好的支持,甚至在C# 4.0中也可以利用动态特性实现。

总结

本文主要介绍了对象的消息模型的特征,并比较了C++对象模型,Java、C#等基于类的静态类型语言中的对象模型与严格消息模型的差异,最后探讨了Method Missing相关话题。

参考

(全文完)

Loading...