元类
在面向对象程序设计中,元类(英语: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).