在面向对象程序设计中,元类(英语: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]。
Logtalk是Prolog的面向对象扩展,它也支持元类。
另见
引用
Wikiwand in your browser!
Seamless Wikipedia browsing. On steroids.
Every time you click a link to Wikipedia, Wiktionary or Wikiquote in your browser's search results, it will show the modern Wikiwand interface.
Wikiwand extension is a five stars, simple, with minimum permission required to keep your browsing private, safe and transparent.