面向对象程序设计中,元类(英语:metaclass)是一种实例是的类。普通的类定义的是特定对象的行为,元类定义的则是特定的类及其实例的行为。不是所有面向对象编程语言都支持元类。在它能做的事情之中,元类可以覆写任何给定方面类行为的程度是不同的。元类可以通过使类成为头等对象来实现,在这种情况下元类简单的就是构造类的一个对象。每个语言都有它自己的元对象协议,给出对象、类和元类如何交互的规则[1]

Smalltalk-80元类

Smalltalk中,所有东西都是对象。此外,Smalltalk是基于类的系统,这意味着所有对象都有一个类,它定义这个对象的结构(比如说这个类拥有实例变量),和这个对象所理解的消息。二者在一起蕴含了,在Smalltalk中,类是一个对象,因此类也需要是它的元类的实例。[2]

元类在Smalltalk-80系统中的主要角色,是提供协议来初始化类变量,和建立元类的唯一实例(也就是其对应的类)的初始化实例。

实例联系

为了允许类拥有它们自己的方法,和叫作类实例变量它们自己的实例变量英语Instance variable,Smalltalk-80为每个类C介入了它们自己的元类C class。就像实例方法实际上属于类一样,类方法实际上属于元类。在类中定义实例变量英语Instance variable类变量英语Class variable,而在元类中定义类实例变量。

每个元类在效果上都是单例类。就像连体双胞胎,类和元类是共生的。元类有一个实例变量thisClass,它指向它结合的类。平常的Smalltalk类浏览器英语class browser,不将元类展示为单独的类,转而允许一起同时编辑类和它的元类。

要得到一个实例的类,需要向它发送消息调用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、或WidgetCar等这样的东西,除了Object之外,所有的都有一个超类。元类所继承的元类,就是元类对应的类所继承的类的元类。

在一个消息被发送到对象的时候,方法的查找开始于它的类。如果没有找到则在上行超类链,停止于Object而不管找到与否。在一个消息被发送到一个类的时候,类方法查找开始于它的元类,并上行超类链至Object class。直至Object class,元类的超类层级并行于类的超类层级。在Smalltalk-80中,Object classClass的子类:

Object class superclass == Class.

类方法的查找在元类链之后仍可继续下去,所有元类都是Class的在继承层级中的子类,它是所有元类的抽象超类,它描述这些类的一般性质,继而最终可上溯至Object

继承层级

四个类提供描述新类的设施,下面是它们的继承层级(起自Object),和它们提供的主要设施:

  • Object,对象类是所有类的基础类,它为所有对象提供公共的方法,即公共的缺省行为。至少包括了:测试对象的功能比如class方法,比较对象,对象复制,访问对象的各部分,打印和存储对象,错误处理。
    • Behavior,行为类定义了拥有实例的对象所需要的最小状态英语State (computer science),它提供建立一个类的实例的new方法。特别是,它定义了Smalltalk-80解释器所用到的状态,并为编译方法源代码提供到编译器的基本接口,如compile:等方法。Behavior描述的这个状态,包括了一个类层级连接(superclass:),一个方法字典(methodDictionary:addSelector:withMethod:),和对实例的描述(依据数目和对它们的变量的表示)。尽管一个类的多数设施都规定在Behavior中,但很多消息不能于此实现,对类的完全描述转而在它的子类之中提供。
      • ClassDescription,类描述类为ClassMetactass提供了共同的超类。它表现类命名(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(对象类)。
  • 每个类,包括ClassMetaclass,在查找查找方法时,首先查找自己的元类;然后按元类继承链上溯,最终经过Object class(对象元类)而上至Class;接着按类继承链上溯,不经过与其并列的Metaclass,最终上至Object
  • 每个元类,包括Class classMetaclass class,在查找方法时,因为都是Metaclass的实例,所以首先查找Metaclass;然后按类继承链上溯,不经过与其并列的Class,最终上至Object

示意图

下面是两个示意图,二者都是纵向连线表示实例联系,而横向连线表示继承联系。实例联系以Metaclass(元类类)及其元类为顶端,而继承联系以Object(对象类)及其元类为中心,其中Object class(对象元类)继承Class(类类)是串接元类继承链与类继承链的关键环节。前者图示采用Smalltalk-80蓝皮书的样式(但旋转了180°),将Metaclass及其元类放置在最上方的独立两行,使得实例联系尽量成为树状向上汇聚;后者图示将Metaclass及其元类放置在最左边,使得继承联系尽量都在同一行之上。

例子

下列例子展示,从Smalltalk-80派生的SqueakPharo的样例代码的结构[4],它们的继承层级的根类实际上是ProtoObjectProtoObject封装了所有对象都必须拥有的极小化的消息集合,它被设计为引发尽可能多的错误,用来支持代理(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.
Thumb

这个结构由两个部分构成,用户部分有四个显式的对象和类及其两个隐式的元类:终端对象uv,它们连接到的类AB,它们两个连接到的右侧灰色节点表示的隐式的元类,其他的对象都是内置部分。

Objective-C元类

Objective-C中的元类,几乎同于Smalltalk-80的元类,这是因为Objective-C从Smalltalk引进了很多东西。就像Smalltalk,在Objective-C中实例变量和方法是对象的类定义的。类也是对象,因此它是元类的一个实例。

就像Smalltalk,在Objective-C中类方法,简单的是在类对象上调用的方法,因此一个类的类方法,必须定义为在它的元类中的实例方法。因为不同的类有不同的类方法集合,每个类都必须有它自己单独的元类。类和元类总是成对创建:运行时系统拥有函数objc_allocateClassPair()objc_registerClassPair()来分别的创建和注册类-元类对。

元类没有名字,但是到任何类对象的指针,可以通过泛化类型Class来提及(类似于用作到任何对象的指针的类型id)。

元类都是相同的类即根类元类的实例,而根类元类是自身的实例。因为类方法是通过继承联系来继承的,就像Smalltalk,除了根类元类之外,元类继承联系必须并行于类继承联系(比如说如果类A的父类是类B,则A的元类的父类是B的元类)。

不同于Smalltalk,根类元类继承自根类自身(通常为使用Cocoa框架的NSObject)。这确保了所有的元类最终都是根类的子类,从而人们可以将根类的实例方法,它们通常是针对对象有用的实用方法,使用于类对象自身上。

Python元类

Python中,内置的类type是元类[6][7]

 
r = object
c = type
class M(c): pass

class A(metaclass=M): pass

class B(A): pass

b = B()
Thumb

类的定义,不包括它的实例对象的细节,如它们的字节为单位的大小,它们在内存中的二进制格局,它们是如何分配的,每次建立实例时自动调用的__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-80
隐式
元类
终端
对象
Ruby
类的
特征类
特征类的
特征类
终端
对象
终端对象的
特征类

特别要注意在Smalltalk的隐含的元类和Ruby类的特征类之间的对应。Ruby的特征类模型,使得隐式元类概念完全统一:所有对象x,都有它自己的元对象,它叫作x的特征类,它比x高一个元层级。高阶特征类通常是纯粹概念上的存在,在大多数Ruby程序中,它们不包含任何方法也不存储任何(其他)数据[9]

下面的示意图展示Ruby样例代码的核心结构[10]。这里的灰色节点表示打开Av特征类后扩张出来的特征类。

 
r = BasicObject
c = Class
class A; end
class B < A; end
u = B.new
v = B.new

class << A; end
class << v; end
Thumb

图示还展示了Ruby中特征类的惰性求值v对象可以有它的特征类,作为向v增加“单例方法”的结果而被求值(被分配)。

在语言和工具中的支持

下面是支持元类的一些最显著的编程语言

一些不甚广泛传播的语言支持元类,包括OpenJava英语OpenJava、OpenC++、OpenAda、CorbaScript英语CorbaScript、ObjVLisp、Object-Z英语Object-Z、MODEL-K、XOTcl英语XOTcl和MELDC。其中几种语言可追溯日期至1990年代早期并具有学术价值[12]

Logtalk英语LogtalkProlog的面向对象扩展,它也支持元类。

资源描述框架(RDF)和统一建模语言(UML)二者都支持元类。

另见

引用

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.