在Alan Kays的“面向对象的定义”中,有部分我不理解的定义:
对我而言,OOP意味着仅消息传递,本地保留和保护以及状态过程的隐藏以及所有事物的极端LateBinding。
但是“ LateBinding”是什么意思?如何在C#之类的语言上应用它?为什么这如此重要?
在Alan Kays的“面向对象的定义”中,有部分我不理解的定义:
对我而言,OOP意味着仅消息传递,本地保留和保护以及状态过程的隐藏以及所有事物的极端LateBinding。
但是“ LateBinding”是什么意思?如何在C#之类的语言上应用它?为什么这如此重要?
Answers:
“绑定”是指将方法名称解析为一段可调用代码的动作。通常,可以在编译时或链接时解析函数调用。使用静态绑定的语言示例是C:
int foo(int x);
int main(int, char**) {
printf("%d\n", foo(40));
return 0;
}
int foo(int x) { return x + 2; }
在这里,调用foo(40)
可以由编译器解决。早期可以进行某些优化,例如内联。最重要的优点是:
另一方面,某些语言将函数解析推迟到最后可能的时刻。Python是一个例子,在这里我们可以即时重新定义符号:
def foo():
""""call the bar() function. We have no idea what bar is."""
return bar()
def bar():
return 42
print(foo()) # bar() is 42, so this prints "42"
# use reflection to overwrite the "bar" variable
locals()["bar"] = lambda: "Hello World"
print(foo()) # bar() was redefined to "Hello World", so it prints that
bar = 42
print(foo()) # throws TypeError: 'int' object is not callable
这是后期绑定的示例。尽管它使严格的类型检查变得不合理(类型检查只能在运行时进行),但它具有更大的灵活性,并允许我们表达无法在静态类型或早期绑定范围内表达的概念。例如,我们可以在运行时添加新功能。
通常在“静态” OOP语言中实现的方法分派介于这两种极端之间:类预先声明了所有支持的操作的类型,因此这些操作是静态已知的,可以进行类型检查。然后,我们可以构建一个指向实际实现的简单查找表(VTable)。每个对象都包含一个指向vtable的指针。类型系统保证我们得到的任何对象都将具有合适的vtable,但是在编译时我们不知道此查找表的值是多少。因此,可以使用对象将函数作为数据传递(OOP和函数编程等效的一半原因)。可以使用支持函数指针的任何语言(例如C)轻松实现Vtable。
#define METHOD_CALL(object_ptr, name, ...) \
(object_ptr)->vtable->name((object_ptr), __VA_ARGS__)
typedef struct {
void (*sayHello)(const MyObject* this, const char* yourname);
} MyObject_VTable;
typedef struct {
const MyObject_VTable* vtable;
const char* name;
} MyObject;
static void MyObject_sayHello_normal(const MyObject* this, const char* yourname) {
printf("Hello %s, I'm %s!\n", yourname, this->name);
}
static void MyObject_sayHello_alien(const MyObject* this, const char* yourname) {
printf("Greetings, %s, we are the %s!\n", yourname, this->name);
}
static MyObject_VTable MyObject_VTable_normal = {
.sayHello = MyObject_sayHello_normal,
};
static MyObject_VTable MyObject_VTable_alien = {
.sayHello = MyObject_sayHello_alien,
};
static void sayHelloToMeredith(const MyObject* greeter) {
// we have no idea what the VTable contents of my object are.
// However, we do know it has a sayHello method.
// This is dynamic dispatch right here!
METHOD_CALL(greeter, sayHello, "Meredith");
}
int main() {
// two objects with different vtables
MyObject frank = { .vtable = &MyObject_VTable_normal, .name = "Frank" };
MyObject zorg = { .vtable = &MyObject_VTable_alien, .name = "Zorg" };
sayHelloToMeredith(&frank); // prints "Hello Meredith, I'm Frank!"
sayHelloToMeredith(&zorg); // prints "Greetings, Meredith, we are the Zorg!"
}
这种方法查找也称为“动态调度”,介于早期绑定和后期绑定之间。我认为动态方法分派是OOP编程的主要定义属性,而其他任何事情(例如封装,子类型等)都是次要的。它使我们能够将多态性引入代码中,甚至无需重新编译就可以在代码中添加新行为!在C示例中,任何人都可以添加新的vtable并将带有该vtable的对象传递给sayHelloToMeredith()
。
尽管这是后期绑定,但这并不是凯推荐的“极端后期绑定”。他使用“通过消息传递进行方法分配”代替了概念模型“通过函数指针进行方法分配”。这是一个重要的区别,因为消息传递更为通用。在此模型中,每个对象都有一个收件箱,其他对象可以在其中放置消息。然后,接收对象可以尝试解释该消息。最著名的OOP系统是WWW。在这里,消息是HTTP请求,服务器是对象。
例如,我可以询问programmers.stackexchange.se服务器GET /questions/301919/
。将此与符号进行比较programmers.get("/questions/301919/")
。服务器可以拒绝此请求或将错误发送给我,或者可以为您解决问题。
消息传递的强大之处在于它可以很好地扩展:不共享数据(仅传输数据),一切都可以异步发生,并且对象可以按自己喜欢的方式解释消息。这使得通过OOP的消息传递系统易于扩展。我可以发送并非所有人都能理解的消息,并且可以返回我的预期结果或错误。该对象无需预先声明它将响应的消息。
这将保持正确性的责任放在消息的接收者上,这种想法也称为封装。例如,如果不通过HTTP消息请求,我将无法从HTTP服务器读取文件。这允许HTTP服务器拒绝我的请求,例如,如果我没有权限。在较小规模的OOP中,这意味着我没有对对象内部状态的读写权限,但必须通过公共方法进行访问。HTTP服务器也不必为我提供文件。它可以是从数据库动态生成的内容。在实际的OOP中,可以切换对象如何响应消息的机制,而无需用户注意。这比“反射”要强,但通常是完整的元对象协议。我上面的C示例无法在运行时更改调度机制。
更改分配机制的能力意味着后期绑定,因为所有消息都是通过用户可定义的代码路由的。这非常强大:给定了元对象协议,我可以添加诸如类,原型,继承,抽象类,接口,特征,多重继承,多调度,面向方面的编程,反射,远程方法调用,将对象等代理到不以这些功能开头的语言。诸如C#,Java或C ++之类的静态语言完全没有这种发展的动力。
后期绑定是指对象之间如何通信。艾伦(Alan)试图实现的理想是使对象尽可能松散地耦合。换句话说,一个对象需要知道可能的最小值,以便与另一个对象进行通信。
为什么?因为这鼓励了独立更改系统各部分的能力,并使其能够有机增长和更改。
例如,在C#中,您可能会为编写obj1
类似的方法obj2.doSomething()
。您可以将其视为obj1
与进行通信obj2
。为此,要在C#中发生,obj1
需要了解一点点obj2
。它将需要了解其类别。它将检查该类是否具有一个称为doSomething
的方法,以及该方法是否具有采用零参数的版本。
现在想象一下您正在通过网络或类似网络发送消息的系统。您可能会写类似Runtime.sendMsg(ipAddress, "doSomething")
。在这种情况下,您不需要了解很多与之通信的机器;它大概可以通过IP与之联系,并且在收到“ doSomething”字符串时会执行某些操作。但是否则,您所知甚少。
现在想象一下对象是如何通信的。您知道一个地址,并且可以使用某种“邮箱”功能将任意消息发送到该地址。在这种情况下,obj1
不需要太多了解obj2
,只需输入地址即可。它甚至不需要知道它能够理解doSomething
。
这几乎是后期绑定的关键。现在,在使用它的语言(例如Smalltalk和ObjectiveC)中,通常会有一些语法糖来隐藏邮箱功能。但是除此之外,想法是相同的。
在C#中,您可以通过有一个Runtime
接受对象ref和字符串并使用反射来查找方法并调用它的类来进行某种复制(它会因参数和返回值而变得复杂,但是尽管如此丑陋)。
编辑:减轻关于后期绑定的含义的一些困惑。在这个答案中,我指的是后期绑定,因为我了解Alan Kay的意思并在Smalltalk中实现了它。现代使用该术语并不是更普遍,它通常是指动态调度。后者涵盖了解析确切方法直到运行时的延迟,但是在编译时仍需要接收者提供一些类型信息。