元類別
在物件導向程式設計中,元類別(英語: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
|
圖示還展示了Ruby中特徵類的惰性求值。v
對象可以有它的特徵類,作為向v
增加「單例方法」的結果而被求值(被分配)。
在語言和工具中的支援
[編輯]下面是支援元類別的一些最顯著的程式語言。
- 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]。
Logtalk是Prolog的物件導向擴充,它也支援元類別。
資源描述框架(RDF)和統一建模語言(UML)二者都支援元類別。
另見
[編輯]參照
[編輯]- ^ 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).