一個物件是什麼#
抽象層面:「資料 + 行為」。VM 層面:「實例變數 + 方法表」。
struct Instance {
Object obj;
Class *klass; // 指向自己的類別
Value *fields; // 實例變數陣列
};
struct Class {
Object obj;
char *name;
Class *parent; // superclass,root 為 NULL
HashMap *methods; // method name → Method
HashMap *constants;
int field_count;
};兩層 indirection:instance ➡️ class ➡️ method。每個 method call 都要走這條鏈。
Method dispatch#
obj.foo(x, y) 的執行:
1. 取出 obj 的 class
2. 在 class.methods 找 "foo"
3. 找不到 → 走 class.parent
4. 還找不到 → method_missing 或 NoMethodError
5. 找到 → 用 obj 當 self、x, y 當引數,呼叫 methodbytecode 概念:
LOAD obj
PUSH x
PUSH y
SEND :foo, 2 ;; method name, num argsSEND 的處理:
case OP_SEND: {
Symbol *name = read_symbol();
int nargs = read_byte();
Value receiver = peek(nargs);
Method *m = lookup_method(receiver_class(receiver), name);
if (!m) raise_no_method_error();
call_method(vm, m, receiver, nargs);
break;
}lookup_method 是 method dispatch 的核心。每次 SEND 都做一次 hash lookup ── 這就是動態語言慢的另一大原因。下一章 inline cache 就是來解這個。
繼承的 lookup chain#
class Animal
def name; "anonymous"; end
end
class Dog < Animal
def bark; "woof"; end
end
d = Dog.new
d.bark # 找到 Dog#bark
d.name # Dog 沒有,往 Animal 找Method *lookup_method(Class *cls, Symbol *name) {
while (cls) {
Method *m = hashmap_get(cls->methods, name);
if (m) return m;
cls = cls->parent;
}
return NULL;
}對深的繼承鏈、長 method name ➡️ 多次 hash lookup。每次都付代價。
Method 的內容#
struct Method {
Object obj;
Class *defined_in; // 在哪個 class 定義(重要!與下面 super 有關)
Symbol *name;
Function *func; // 內部還是個 function
bool is_native;
};method 本質上是「綁了 class context 的 function」。call 時:
void call_method(VM *vm, Method *m, Value receiver, int nargs) {
if (m->is_native) {
Value result = m->native(vm, receiver, nargs, vm->sp - nargs);
vm->sp -= nargs + 1;
push(vm, result);
return;
}
// 一般 bytecode:開新 frame
Frame *f = push_frame(vm, m->func, nargs);
f->self = receiver;
}Frame 多一個 self:method body 裡 self 永遠來自這裡。
super 的處理#
class Dog < Animal
def bark
super # 呼叫 Animal#bark(如果有)
end
endsuper 不能簡單地「呼叫 parent class 的 method」── 它必須從當前 method 所在的 class 的 parent 開始找。如果單純說「receiver 是 Dog 的 instance,找它的 parent」是錯的:當前 method 可能定義在更上層。
實作:method 物件記錄 defined_in,super 從 m->defined_in->parent 開始查找。
bytecode:
SUPER :bark, 0 ;; method name + nargshandler 從當前 frame 的 method 的 defined_in 開始 lookup。
Metaclass:類別本身也是物件#
「class 是 object」── 但既然是 object,它的 class 是什麼?答案叫 metaclass:
class Dog; end
Dog.class # => Class
Dog.class.class # => Class(自循環)Smalltalk / Ruby 是「everything is object」的代表。Class 自己也是個 Class,循環指向自身。對 VM 實作意味著:
Class物件本身有klass欄位- 「class 的 method」不是 instance method ── 它是 metaclass 的 method
struct Class {
Object obj; // obj.klass = metaclass
Class *metaclass;
...
};每個 class 自己有獨立 metaclass(singleton class)── 這給 Ruby 帶來 class << self 等語法強大度。
Python / Java 比較簡化:
- Python:
type是所有 class 的 type,但可以自訂 metaclass - Java:class 是 java.lang.Class 的 instance,但不可動態擴展 method
實例變數#
從 instance 角度:
class Dog
def initialize(name)
@name = name
end
end@name 存在哪?
方案 A:HashMap per instance#
struct Instance {
Object obj;
Class *klass;
HashMap *ivars; // 動態,可加新欄位
};讀 @name = hash lookup。慢但極彈性。CPython 的 __dict__ 概念。
方案 B:固定 slot 陣列#
class 編譯期決定有哪些 ivar、給每個分配一個 index:
struct Instance {
Object obj;
Class *klass;
Value fields[]; // 對齊到 class 的 ivar 表
};讀 @name = instance->fields[INDEX_OF_NAME],O(1)。
代價:不能在 runtime 動態加欄位。
方案 C:shape / hidden class(混合)#
V8、JSC、TruffleRuby 都用這個:邏輯上像 hashmap(可動態加),實作上是固定 layout(O(1) 存取)。下一章專門講。
多重繼承 / mixin / interface#
純單繼承(Java、Smalltalk)#
每個 class 一個 parent。簡單但表達力受限。
多重繼承(C++、Python)#
允許多個 parent。lookup 順序成問題:菱形繼承怎麼解?
Python 用 C3 linearization:
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass
D.__mro__ # [D, B, C, A, object]VM 在 class 建立時計算 MRO(Method Resolution Order)並存起來,lookup 時走這條線性鏈而非 tree。
Mixin / module(Ruby)#
module Walker
def walk; "walking"; end
end
class Dog
include Walker
endRuby 把 module 插入 class 的 ancestor chain。lookup 時走 chain:Dog ➡️ Walker ➡️ Object ➡️ …
Interface(Java)#
interface 不帶 method body(早期),實作 class 必須提供。但 Java 8+ 允許 default method。 VM 層面:interface 也是 class 的一種,但 lookup 時要做 invokeinterface 的特殊處理(itable)。
new 與初始化#
d = Dog.new("Rex")兩個分離的步驟:
- allocate:配新 Instance object,連到 class,所有 ivar 初始化為 nil
- initialize:呼叫
Dog#initialize("Rex")
case OP_NEW: {
Class *cls = AS_CLASS(pop());
int nargs = read_byte();
Instance *inst = allocate_instance(cls);
Value receiver = OBJ_VAL(inst);
// 把 receiver 插到引數下面,呼叫 initialize
Value *args = vm->sp - nargs;
memmove(args + 1, args, nargs * sizeof(Value));
*args = receiver;
vm->sp++;
Method *init = lookup_method(cls, sym_initialize);
if (init) call_method(vm, init, receiver, nargs);
push(vm, receiver); // 結果是新 instance
break;
}各語言對「沒寫 initialize 怎麼辦」的處理不同。Ruby、Python 給空實作;C++ 自動產生。
protected / private 怎麼實作#
class Foo
private
def secret; end
end
f = Foo.new
f.secret # NoMethodError: private method實作:method 物件帶 visibility flag。SEND opcode 看 flag:
- private:必須是 implicit
self.method形式 - protected:必須由同類或子類的方法呼叫
- public:任何 caller
JVM bytecode 用 invokespecial、invokevirtual 等不同 opcode 表達 visibility 與 dispatch 規則。
動態增刪方法#
Ruby、Python 允許 runtime 改 class:
class String
def shout
upcase + "!"
end
end
"hi".shout # "HI!"實作:直接修改 class 的 method 表。下次 lookup 看到新 method。
代價:
- inline cache 必須失效
- JIT 編譯過的 code 必須 deoptimize(執行期假設可能不成立)
- 安全沙箱要謹慎(避免污染 base class)
開放與封閉的設計權衡#
| 特性 | 動態(Ruby、Python) | 靜態(Java、C#) |
|---|---|---|
| 可動態加 method | 是 | 否(需 reflection trick) |
| 效能(無 JIT) | 慢(每次 lookup) | 快(編譯期決定 vtable index) |
| 效能(有 JIT) | 接近靜態(inline cache) | 接近原生 |
| 工具支援 | 較難分析 | 編譯期可保證 |
「動態語言比靜態慢」的根源就在這 ── 大部分效能都耗在 method dispatch。
小結#
- 物件 = instance + class(method table 與 ivar layout)
- method dispatch:lookup chain,遍歷 class hierarchy
- super 從當前 method 的定義 class 的 parent 開始找
- ivar 三種策略:hashmap、固定 slot、shape/hidden class
- 繼承多樣:單繼承、多重繼承、mixin、interface ── lookup 都是把它們線性化
- 動態增刪 method 強大但對 cache、JIT 是挑戰
下章看 shape 與 inline cache 怎麼把 dispatch 搞快。