元類別
在面向對象程序設計中,元類(英語:metaclass)是一種實例是類的類。普通的類定義的是特定對象的行為,元類定義的則是特定的類及其實例的行為。不是所有面向對象編程語言都支持元類。在它能做的事情之中,元類可以覆寫任何給定方面類行為的程度是不同的。元類可以通過使類成為頭等對象來實現,在這種情況下元類簡單的就是構造類的一個對象。每個語言都有它自己的元對象協議,給出對象、類和元類如何交互的規則[1]。
Smalltalk-80元類
在Smalltalk中,所有東西都是對象。此外,Smalltalk是基於類的系統,這意味着所有對象都有一個類,它定義這個對象的結構(比如說這個類擁有實例變量),和這個對象所理解的消息。二者在一起蘊含了,在Smalltalk中,類是一個對象,因此類也需要是它的元類的實例。[2]
元類在Smalltalk-80系統中的主要角色,是提供協議來初始化類變量,和建立元類的唯一實例(也就是其對應的類)的初始化實例。
實例聯繫
為了允許類擁有它們自己的方法,和叫作類實例變量它們自己的實例變量,Smalltalk-80為每個類C
介入了它們自己的元類C class
。就像實例方法實際上屬於類一樣,類方法實際上屬於元類。在類中定義實例變量和類變量,而在元類中定義類實例變量。
每個元類在效果上都是單例類。就像連體雙胞胎,類和元類是共生的。元類有一個實例變量thisClass
,它指向它結合的類。平常的Smalltalk類瀏覽器,不將元類展示為單獨的類,轉而允許一起同時編輯類和它的元類。
要得到一個實例的類,需要向它發送消息調用class
方法。類和元類繼承了其超類的name
方法,它返回接收者名字的字符串。例如,轎車對象c
是類Car
的實例,則c class
返回Car
類對象,而c class name
返回'Car'
;依次類推,Car class
返回Car
的元類對象,而Car class name
返回依賴於實現,有的是nil
,即沒有名字,有的是'Car class'
,即用空格分隔的類名字和'class'
。
在早期的Smalltalk-76中,創建新類的方式是向Class
類發送new
消息[3]。在Smalltalk-80中,Class
是元類的基礎類,它是類而不是元類。所有元類都是一個Metaclass
類的實例。Metaclass
類是Metaclass class
的實例,而Metaclass class
作為元類,也是Metaclass
類的實例。
繼承聯繫
在Smalltalk-80中,終端對象是一個整數、一個組件、或一台車等,而類是像Integer
、或Widget
或Car
等這樣的東西,除了Object
之外,所有的類都有一個超類。元類所繼承的元類,就是元類對應的類所繼承的類的元類。
在一個消息被發送到對象的時候,方法的查找開始於它的類。如果沒有找到則在上行超類鏈,停止於Object
而不管找到與否。在一個消息被發送到一個類的時候,類方法查找開始於它的元類,並上行超類鏈至Object class
。直至Object class
,元類的超類層級並行於類的超類層級。在Smalltalk-80中,Object class
是Class
的子類:
Object class superclass == Class.
類方法的查找在元類鏈之後仍可繼續下去,所有元類都是Class
的在繼承層級中的子類,它是所有元類的抽象超類,它描述這些類的一般性質,繼而最終可上溯至Object
。
繼承層級
四個類提供描述新類的設施,下面是它們的繼承層級(起自Object
),和它們提供的主要設施:
Object
,對象類是所有類的基礎類,它為所有對象提供公共的方法,即公共的缺省行為。至少包括了:測試對象的功能比如class
方法,比較對象,對象複製,訪問對象的各部份,打印和存儲對象,錯誤處理。Behavior
,行為類定義了擁有實例的對象所需要的最小狀態,它提供建立一個類的實例的new
方法。特別是,它定義了Smalltalk-80解釋器所用到的狀態,並為編譯方法源代碼提供到編譯器的基本接口,如compile:
等方法。Behavior
描述的這個狀態,包括了一個類層級連接(superclass:
),一個方法字典(methodDictionary:
、addSelector:withMethod:
),和對實例的描述(依據數目和對它們的變量的表示)。儘管一個類的多數設施都規定在Behavior
中,但很多消息不能於此實現,對類的完全描述轉而在它的子類之中提供。ClassDescription
,類描述類為Class
和Metactass
提供了共同的超類。它表現類命名(name
)、類注釋(comment:
)、和命名實例變量(addlnstVarName:
)。特別是,它增加了組織在方法字典中方法(compile:classified:
)和類自身(category:
)的結構。它還提供了在外部串流(文件)上存儲完全的類描述的機制,和記述對類描述的變更的機制。Class
,類類是所有元類的基礎類,從而為所有類提供公共的方法,它定義了初始化類變量的initialize
方法。Class
的實例描述對象的表現和行為,它提供比ClassDescription
更具描述性的設施,特別是,它增加了對類變量名字(addClassVarName:
)和共享的池變量(addSharedPool:
)的表示。它還提供比Behavior
更綜合性的編程支持設施,比如創建一個類的子類的消息:subclass:instanceVariableNames:classVariableNames:poolDictionaries:category:
。Metaclass
,元類類是創建元類的類,它為所有元類提供公共的方法。Metaclass
的關鍵性的消息,是自身初始化消息,這在GNU Smalltalk中依舊保留;一個是發送到Metaclass
自身的消息subclassOf: superMeta
,用來創建元類superMeta
的一個子類;一個是發送到Metaclass
的一個實例的消息,用來建立這個元類的唯一實例,對於建立完全初始化的類,它的每個參數都是需要的:name:environment:subclassOf:instanceVariableNames:shape:classVariableNames:poolDictionaries:category:
。
方法查找次序
下面是方法查找次序的辨析:
- 每個終端對象,在查找方法時,都首先查找自己的類;然後按類繼承鏈上溯,最後不經過
Class
(類類)和Metaclass
(元類類),最終上至Object
(對象類)。 - 每個類,包括
Class
和Metaclass
,在查找查找方法時,首先查找自己的元類;然後按元類繼承鏈上溯,最終經過Object class
(對象元類)而上至Class
;接着按類繼承鏈上溯,不經過與其並列的Metaclass
,最終上至Object
。 - 每個元類,包括
Class class
和Metaclass class
,在查找方法時,因為都是Metaclass
的實例,所以首先查找Metaclass
;然後按類繼承鏈上溯,不經過與其並列的Class
,最終上至Object
。
示意圖
下面是兩個示意圖,二者都是縱向連線表示實例聯繫,而橫向連線表示繼承聯繫。實例聯繫以Metaclass
(元類類)及其元類為頂端,而繼承聯繫以Object
(對象類)及其元類為中心,其中Object class
(對象元類)繼承Class
(類類)是串接元類繼承鏈與類繼承鏈的關鍵環節。前者圖示採用Smalltalk-80藍皮書的樣式(但旋轉了180°),將Metaclass
及其元類放置在最上方的獨立兩行,使得實例聯繫儘量成為樹狀向上匯聚;後者圖示將Metaclass
及其元類放置在最左邊,使得繼承聯繫儘量都在同一行之上。
-
Smalltalk中在類和元類之間的繼承和實例聯繫的示意圖,這裡從左至右,第一列是Metaclass元類和Metaclass(元類類),第二列是Class元類和Class(類類),第三列是ClassDescription元類與Behavior元類、和ClassDescription(類描述類)與Behavior(行為類),第四列是Object元類、Object(對象類)和Object實例,第五列是Foo元類、Foo類和Foo實例,第六列是Bar元類、Bar類和Bar實例。
例子
下列例子展示,從Smalltalk-80派生的Squeak和Pharo的樣例代碼的結構[4],它們的繼承層級的根類實際上是ProtoObject
,ProtoObject
封裝了所有對象都必須擁有的極小化的消息集合,它被設計為引發儘可能多的錯誤,用來支持代理(proxy)定義[5]。例如Smalltalk-80的Object
中,錯誤處理消息doesNotUnderstand:
,和系統原始消息become:
,就轉而在ProtoObject
中定義了。
在示意圖中,縱向的綠色連接,展示繼承聯繫的「子→父」關係(隱含的自下而上),橫向的藍色連接展示實例聯繫的「成員→容器」關係,從x
出的發藍色連接,指向x
的最小實際容器,它是在調用在x
上的方法時查找方法的繼承鏈起點:
r := ProtoObject.
c := Class.
mc := Metaclass.
Object subclass: #A.
A subclass: #B.
u := B new.
v := B new.
|
這個結構由兩個部份構成,用戶部份有四個顯式的對象和類及其兩個隱式的元類:終端對象u
和v
,它們連接到的類A
和B
,它們兩個連接到的右側灰色節點表示的隱式的元類,其他的對象都是內建部份。
Objective-C元類
在Objective-C中的元類,幾乎同於Smalltalk-80的元類,這是因為Objective-C從Smalltalk引進了很多東西。就像Smalltalk,在Objective-C中實例變量和方法是對象的類定義的。類也是對象,因此它是元類的一個實例。
-
在Objective-C中在類和元類之間的繼承和實例聯繫的示意圖。注意Objective-C有多個根類,每個根類都有獨立的層級。這個示意圖只展示了例子根類NSObject的層級。每個其他根類都有類似的層級。
就像Smalltalk,在Objective-C中類方法,簡單的是在類對象上調用的方法,因此一個類的類方法,必須定義為在它的元類中的實例方法。因為不同的類有不同的類方法集合,每個類都必須有它自己單獨的元類。類和元類總是成對創建:運行時系統擁有函數objc_allocateClassPair()
和objc_registerClassPair()
來分別的創建和註冊類-元類對。
元類沒有名字,但是到任何類對象的指針,可以通過泛化類型Class
來提及(類似於用作到任何對象的指針的類型id
)。
元類都是相同的類即根類元類的實例,而根類元類是自身的實例。因為類方法是通過繼承聯繫來繼承的,就像Smalltalk,除了根類元類之外,元類繼承聯繫必須並行於類繼承聯繫(比如說如果類A的父類是類B,則A的元類的父類是B的元類)。
不同於Smalltalk,根類元類繼承自根類自身(通常為使用Cocoa框架的NSObject
)。這確保了所有的元類最終都是根類的子類,從而人們可以將根類的實例方法,它們通常是針對對象有用的實用方法,使用於類對象自身上。
Python元類
r = object
c = type
class M(c): pass
class A(metaclass=M): pass
class B(A): pass
b = B()
|
類的定義,不包括它的實例對象的細節,如它們的字節為單位的大小,它們在內存中的二進制格局,它們是如何分配的,每次建立實例時自動調用的__init__
方法,諸如此類。不只是在建立新實例對象的時候,而且在每次訪問實例對象的任何特性的時候,這些細節都起到作用。在沒有元類的語言中,這些細節是在語言規定中定義的,並且不能被覆寫(override)。
在Python中,元類type
控制着類行為的這些細節,默認定義出的類自身都是type
的實例。新的元類可以很容易定義為type
的子類從而覆寫它,通過向類定義提供「關鍵字參數」metaclass
就可以使用這個新的元類。
>>> type(b)
<class '__main__.B'>
>>> print(type(B), B.__bases__, [*B.__dict__])
<class '__main__.M'> (<class '__main__.A'>,) ['__module__', '__doc__']
>>> print(type(A), A.__bases__, [*A.__dict__])
<class '__main__.M'> (<class 'object'>,) ['__module__', '__dict__', '__weakref__', '__doc__']
>>> print(type(M), M.__bases__, [*M.__dict__])
<class 'type'> (<class 'type'>,) ['__module__', '__doc__']
>>> print(type(c), c.__bases__)
<class 'type'> (<class 'object'>,)
>>> print(type(r), r.__bases__)
<class 'type'> ()
>>> sorted({*r.__dict__} & {*c.__dict__})
['__delattr__', '__dir__', '__doc__', '__getattribute__', '__init__', '__new__', '__repr__', '__setattr__', '__sizeof__']
>>> sorted({*r.__dict__} - {*c.__dict__})
['__class__', '__eq__', '__format__', '__ge__', '__gt__', '__hash__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__reduce__', '__reduce_ex__', '__str__', '__subclasshook__']
>>> sorted({*c.__dict__} - {*r.__dict__})
['__abstractmethods__', '__base__', '__bases__', '__basicsize__', '__call__', '__dict__', '__dictoffset__', '__flags__', '__instancecheck__', '__itemsize__', '__module__', '__mro__', '__name__', '__prepare__', '__qualname__', '__subclasscheck__', '__subclasses__', '__text_signature__', '__weakrefoffset__', 'mro']
例子
考慮下面這個最簡單的Python類:
class Car:
def __init__(self, *args, **kwargs):
self.__dict__.update(kwargs)
def __call__(self, **kwargs):
self.__dict__.update(kwargs)
@property
def description(self):
"""返回这辆车的描述."""
return " ".join(str(value) for value in self.__dict__.values())
>>> new_car = Car(make='Toyota', model='Prius', year=2005, engine='Hybrid')
>>> new_car(color='Green')
>>> new_car.description
'Toyota Prius 2005 Hybrid Green'
上面的例子包含了一些代碼來處理初始化特性,也可以使用元類來完成這種任務:
class AttributeInitType(type):
def __new__(*args, **kwargs):
"""返回创建的实例类."""
cls = type.__new__(*args, **kwargs)
def call(self, **kwargs):
self.__dict__.update(kwargs)
cls.__call__ = call # 为实例类增加__call__方法
return cls
def __call__(cls, *args, **kwargs):
"""返回为实例类创建的实例对象."""
obj = type.__call__(cls, *args) # 以正常缺省方式建立实例对象。
obj.__dict__.update(**kwargs) # 在这个新对象上设置属性。
return obj
這個元類只覆寫實例類和對象創建部份功能。元類行為的所有其他方面仍由type
處理。現在可以重寫類Car
使用這個新元類:
class Car(object, metaclass=AttributeInitType):
def __init__(self, *args): pass # 接收未预期的位置实际参数
@property
def description(self):
"""返回这辆车的描述."""
return " ".join(str(value) for value in self.__dict__.values())
Ruby元類
Ruby通過介入其自稱的特徵類(eigenclass),提煉了Smalltalk-80的元類概念,去除了Metaclass
類,並重新定義了class-of
映射。變更可以圖示如下[8]:
|
→ |
|
特別要注意在Smalltalk的隱含的元類和Ruby類的特徵類之間的對應。Ruby的特徵類模型,使得隱式元類概念完全統一:所有對象x
,都有它自己的元對象,它叫作x
的特徵類,它比x
高一個元層級。高階特徵類通常是純粹概念上的存在,在大多數Ruby程序中,它們不包含任何方法也不存儲任何(其他)數據[9]。
下面的示意圖展示Ruby樣例代碼的核心結構[10]。這裡的灰色節點表示打開A
和v
特徵類後擴張出來的特徵類。
r = BasicObject
c = Class
class A; end
class B < A; end
u = B.new
v = B.new
class << A; end
class << v; end
|
在語言和工具中的支持
下面是支持元類的一些最顯著的編程語言。
- Common Lisp,通過CLOS
- Delphi和受它影響的其他Object Pascal版本
- Groovy
- Objective-C
- Python
- Perl,通過元類pragma,還有Moose
- Ruby
- Smalltalk
- C++(規劃用於C++23)[11]
一些不甚廣泛傳播的語言支持元類,包括OpenJava、OpenC++、OpenAda、CorbaScript、ObjVLisp、Object-Z、MODEL-K、XOTcl和MELDC。其中幾種語言可追溯日期至1990年代早期並具有學術價值[12]。
另見
引用
- ^ Ira R. Forman and Scott Danforth. Putting Metaclasses to Work. 1999. ISBN 0-201-43305-2.
- ^ Alan Kay. The Early History of Smalltalk. [2022-03-12]. (原始內容存檔於2011-04-29).
The most puzzling strange idea – at least to me as a new outsider – was the introduction of metaclasses (really just to make instance initialization a little easier – a very minor improvement over what Smalltalk-76 did quite reasonably already).
Peter’s 1989 comment is typical and true: 「metaclasses have proven confusing to many users, and perhaps in the balance more confusing than valuable.」 In fact, in their PIE system, Goldstein and Bobrow had already implemented in Smalltalk on 「observer language」, somewhat following the view-oriented approach Ihad been advocating and in some ways like the 「perspectives」 proposed in KRL [Goldstein *].
Once one can view an instance via multiple perspectives even 「sem-metaclasses」 like Class Class and Class Object are not really necessary since the object-role and instance-of-a-class-role are just different views and it is easy to deal with life-history issues includeding instantiation. This was there for the taking (along with quite a few other good ideas), but it wsn’t adopted. My guess is that Smalltalk had moved into the final phase I memntioned at the beginning of this story, in which a way of doing things finally gets canonized into an inflexible belief structure. - ^ Learning Research Group. How To Use the Smalltalk-76 System (PDF) (報告). Xerox Palo Alto Research Center. October 1979 [2022-03-13]. (原始內容 (PDF)存檔於2022-04-12).
To define a new class, select a class category in the first pane of the browse window. This selection specifies the category to which the new class will be added, and causes a template to appear in the largest pane of the browse window, the code pane. ……
The template presented in the code pane looks as follows
Class new title: ’NameofClass’
subclassof: Object
fields: ’names of fields’
declare: ’names of class variables’ - ^ The core structure of Smalltalk-80. Object Membership - The Core Structure of Object Technology. [2021-03-29]. (原始內容存檔於2021-05-06).
- ^ ProtoObject. [2022-01-16]. (原始內容存檔於2022-03-12).
- ^ IBM Metaclass programming in Python, parts 1 (頁面存檔備份,存於網際網路檔案館), 2 (頁面存檔備份,存於網際網路檔案館) and 3 (頁面存檔備份,存於網際網路檔案館)
- ^ Artima Forum: Metaclasses in Python 3.0 (part 1 of 2) (頁面存檔備份,存於網際網路檔案館) (part 2 of 2) (頁面存檔備份,存於網際網路檔案館)
- ^ Introduction - Introductory sample. Object Membership - The Core Structure of Object Technology. [2021-03-29]. (原始內容存檔於2021-05-06).
- ^ Paolo Perrotta. Metaprogramming Ruby 2 (PDF). Pragmatic Bookshelf. 2014 [2022-03-30]. ISBN 978-1-94122-212-6. (原始內容 (PDF)存檔於2022-05-15).
- ^ The core structure of Ruby - Eigenclass actuality. Object Membership - The Core Structure of Object Technology. [2021-03-29]. (原始內容存檔於2021-05-06).
- ^ Herb Sutter. Metaclasses (PDF). [2020-09-25]. (原始內容存檔 (PDF)於2020-11-11).
- ^ An implementation of mixins in Java using metaclasses (PDF). [2007-11-27]. (原始內容 (PDF)存檔於2007-10-16).