Pharo 是一种反射式编程语言。反射既包括能够查询系统自身执行的各个方面,也包括能够修改它。查询能力通常被称为内省,而修改能力则被称为干预。在本章中,我们将介绍 Pharo 内省能力的几个方面:如何访问和修改实例变量的值、如何导航系统,以及如何进行交叉引用。然后,我们将介绍行为反射的一些方面,即如何修改和扩展系统。
18.1 反射简介
反射支持程序能够反思其自身的执行和结构的理念。通过反射,支持程序执行的内部机制(类、方法,甚至调用栈)对开发者是可访问的,并且访问方式与普通程序相同。在 Pharo 中,这意味着这些内部机制被描述为对象,开发者可以向它们发送消息。我们通常将这些支持程序执行的对象称为元对象(metaobjects),以表达它们处于比普通对象更高层次的事实。
从技术上讲,运行时系统的元对象被具体化为普通对象,可以被查询和检查。Pharo 中的元对象包括类、元类、方法字典、编译后的方法,甚至运行时栈、进程等。这种形式的反射也被称为内省,如今许多现代编程语言都支持这一特性(它最初是由 Pharo 的祖先 Smalltalk-80 大力推广的)。
相反,在 Pharo 中,可以修改具体化的元对象,并将这些更改反映回运行时系统(见图 18-1)。这也被称为干预,主要由动态编程语言支持,而静态语言仅以非常有限的程度支持。因此,当人们说 Java 是一种反射语言时,请注意,它是一种内省语言,而不是反射语言。

操纵其他程序(甚至自身)的程序被称为元程序。一种编程语言要具有反射性,它应该同时支持内省和干预。内省是检查定义语言的数据结构(如对象、类、方法和执行栈)的能力。干预是修改这些结构的能力,换句话说,是从程序内部改变语言语义和程序行为的能力。结构反射是关于检查和修改运行时结构,而行为反射是关于修改这些结构的解释方式。
在本章中,我们将主要关注结构反射。我们将通过许多实际示例来探索 Pharo 如何支持内省和元编程。
18.2 内省
使用检查器,你可以查看一个对象,更改其实例变量的值,甚至向它发送消息。
在Playground中求值下面的代码:
w := StPlayground open.
w inspect
这会打开第二个Playground窗口和检查器。检查器显示了新的Playground的内部状态,在左边列出了它的实例变量(announcer,owner,...),在右边列出了被选中的实例变量的值。
现在,选择检查器,点击检查器顶部有注释的Playground区域,输入self bounds: (Rectangle origin: 10@10 corner: 300@300),如图18-2所示,然后如同在一个Playground窗口中那样“Do it”。

你马上就会看到我们创建的Playground视图改变并调整大小。
18.3 访问实例变量
检查器是如何工作的?在 Pharo 中,所有实例变量都是受保护的。理论上,如果类没有定义任何访问器,就无法从另一个对象访问它们。但实际上,检查器可以在不需要访问器的情况下访问实例变量,因为它利用了 Pharo 的反射能力。 类通过名称或数字索引定义实例变量。检查器使用 Object 类定义的方法来访问它们:instVarAt: index 和 instVarNamed: aString 分别用于获取位置 index 或由 aString 标识的实例变量的值。类似地,为了为这些实例变量赋值,它使用 instVarAt:put: 和 instVarNamed:put:。
例如,你可以通过求值下面的表达式来改变第一个工作区的w所绑定的值:
w adapter widget instVarNamed: 'bounds' put: (Rectangle origin: 30@30
corner: 300@300)
单击它以触发刷新,可以查看结果。
实例变量
allInstVarNames方法返回某个类的所有实例变量的名字。
StPlayground allInstVarNames
>>> #(#dependents #announcer #owner #adapter #needRebuild #eventHandler
#application #focusOrder
#contextKeyBindings #windowIcon #aboutText #askOkToClose #titleHolder
#additionalSubpresentersMap
#layout #visible #extent #styles #millerList #model
#lastPageSelectedTabName #withHeaderBar)
下面的代码片段展示了如何显示StPlayground类的任意实例(w)的实例变量的值。
w := StPlayground someInstance.
w class allInstVarNames collect: [ :each | each -> (w instVarNamed: each)]
同样,可以使用select:等迭代器在类的实例上迭代收集具有特定属性的实例。例如,要获得World中直接包含的所有对象,可以尝试以下表达式:
Morph allSubInstances
select: [ :each |
| own |
own := (each instVarNamed: 'owner').
own isNotNil and: [ own isWorldMorph ]]
18.4 关于反射行为
这样的表达式便于调试或构建开发工具,但使用它们来开发常规应用程序是一个坏主意:这些反射方法打破了对象封装的边界。它们破坏了工具导航。它们会使你的代码更加难以理解和维护。有句名言值得记住:“能力越大,责任越大”。你可以做某件事,并不意味着你应该做。
18.5 关于Primitive
instVarAt: 和 instVarAt:put: 都是原语方法,这意味着它们是作为 Pharo 虚拟机的原语操作实现的。如果你查看这些方法的代码,你会看到 pragma <primitive: N>,其中 N 是一个整数。这标记了该方法需要以不同的方式处理。
Object >> instVarAt: index
"Primitive. Answer a fixed variable in an object. ..."
<primitive: 173 error: ec>
self primitiveFailed
只有在原语失败的情况下,才会执行后面的代码。在这个例子中,没有办法实现这个方法,所以整个方法失败了。
还有一些方法也实现在虚拟机中,以提高执行速度。例如SmallInteger上的一些算术运行:
* aNumber
"Primitive. Multiply the receiver by the argument and answer with the
result if it is a SmallInteger. Fail if the argument or the result
if not a
SmallInteger. Essential. No Lookup. See Object documentation
whatIsAPrimitive."
<primitive: 9>
^ super * aNumber
如果此原语失败,例如,如果VM不处理参数类型,则执行Pharo代码。虽然可以修改原语方法的代码,但是要注意,这可能会对Pharo系统的稳定性造成风险。
18.6 查询类和接口
Pharo中的开发工具(系统浏览器、调试器、检查器...)都使用了我们迄今为止看到的反射特性。
下面是一些可能对构建开发工具有用的其他消息:
isKindOf: aClass: 如果接收者是aClass的实例或者其子类的实例,则返回true。例如:
1.5 class
>>> SmallFloat 64
1.5 isKindOf: Float
>>> true
1.5 isKindOf: Number
>>> true
1.5 isKindOf: Integer
>>> false
respondsTo: aSymbol: 如果接收者有一个选择器为aSymbol的方法,则返回true。例如:
1.5 respondsTo: #floor
>>> true "since Number implements floor"
1.5 floor
>>> 1
Exception respondsTo: #,
>>> true "exception classes can be grouped"
小心!
尽管这些特性对于实现开发工具特别有用,但它们通常不适用于典型的应用程序。查询一个对象的类,或者查询它可以理解哪些消息,是典型的存在设计问题的标志,因为它违背了封装的原则。然而,开发工具并不是普通的应用程序,因为它们的领域是软件本身。因此,这些工具有权深入挖掘代码的内部细节。
18.7 简单的代码指标
让我们看看如何利用 Pharo 的内省特性快速提取一些代码指标。代码指标衡量诸如继承层次结构的深度、直接或间接子类的数量、每个类中的方法或实例变量的数量,或者本地定义的方法或实例变量的数量等方面。以下是一些关于 Morph 类的指标,它是 Pharo 中所有图形对象的超类,揭示了它是一个庞大的类,并且处于一个庞大层次结构的根部。这表明它需要进行一些重构!
"inheritance depth"
Morph allSuperclasses size.
>>> 2
"number of methods"
Morph allSelectors size.
>>> 1442
"number of instance variables"
Morph allInstVarNames size.
>>> 6
"number of new methods"
Morph selectors size.
>>> 931
"number of new variables"
Morph instVarNames size
>>> 73
"total subclasses"
Morph allSubclasses size.
>>> 445
"total lines of code!"
Morph linesOfCode.
>>> 5088
在面向对象语言领域中,最有趣的指标之一是扩展了从超类继承的方法的数量。这告诉我们类与其超类之间的关系。在接下来的部分中,我们将看到如何利用对运行时结构的了解来回答此类问题。
18.8 浏览实例
在 Pharo 中,一切都是对象。特别是,类是对象,它们提供了用于导航其实例的有用功能。我们现在要查看的大多数消息都是在 Behavior 中实现的,因此所有类都能理解这些消息。
例如,你可以通过向一个类发送消息 someInstance 来获取它的一个随机实例。
Point someInstance
>>> (-1@-1)
你还可以使用allInstances收集所有的实例,或者使用instanceCount收集内存中活动实例的数量。
ByteString allInstances
>>> #('colection' 'position' ...)
ByteString instanceCount
>>> 58514
String allSubInstances size
>>> 138962
这些方法访问存储在方法内部的实例。由于 Pharo 有大约 130,000 个方法,这样的数字并不完全离谱。
18.9 从方法到实例变量
这些特性在调试应用程序时非常有用,因为你可以要求一个类枚举其具有特定属性的方法。以下是一些通过反射进行代码发现的更有趣且有用的方法。
whichSelectorsAccess: 返回读取或写入由参数所命名的实例变量的所有方法选择器的列表。
whichSelectorsStoreInfo: 返回修改实例变量的值的方法选择器。
whichSelectorsReferTo: 返回发送给定消息的方法的选择器
Point whichSelectorsAccess: 'x'
>>> #(#octantOf: #roundDownTo: #+ #asIntegerPoint #transposed ...)
Point whichSelectorsStoreInfo: 'x'
>>> #(#fromSton: #setX:setY: #setR:degrees: #bitShiftPoint:)
Point whichSelectorsReferTo: #+
>>> #(#+)
下面的消息考虑了继承:
whichClassIncludesSelector: 返回实现给定消息的超类。
unreferencedInstanceVariables 返回在接收者的类及其任意子类中都未使用到的实例变量的列表。
Rectangle whichClassIncludesSelector: #inspect
>>> Object
Rectangle unreferencedInstanceVariables
>>> #()
18.10 关于系统浏览器
SystemNavigation 是一个门面(facade),它支持各种有用的方法,用于查询和浏览系统的源代码。SystemNavigation default 返回一个实例,你可以用它来导航系统。例如:
SystemNavigation default allClassesImplementing: #yourself
>>> an OrderedCollection(Object)
以下信息应该是不言自明的:
SystemNavigation default allSentMessages size
>>> 43985
(SystemNavigation default allUnsentMessageIn: Object selectors) size
>>> 37
SystemNavigation default allUnimplementedCalls size
>>> 335
需要注意的是,已实现但未发送的消息并不一定无用,因为它们可能会被隐式发送(例如,使用 perform:)。然而,已发送但未实现的消息则更有问题,因为发送这些消息的方法在执行时会失败。它们可能是未完成实现、过时 API 或缺失库的标志。在工具实现的测试中,这种情况也很常见。
Point allCallsOn 返回作为接收者显式发送给Point的所有消息。
所有这些功能都集成到了 Pharo 的编程环境中,特别是代码浏览器中。正如我们之前提到的,有一些方便的键盘快捷键可以浏览给定消息的所有实现者(CMD-m)和发送者(CMD-n)。可能不太为人所知的是,有许多这样的预打包查询被实现为 SystemNavigation 类中 query 协议的方法。例如,你可以通过以下代码以编程方式浏览消息 ifTrue: 的所有实现者:
SystemNavigation default browseAllImplementorsOf: #ifTrue:
特别有用的是 browseAllSelect: 和 browseMethodsWithSourceString:matchCase: 方法。以下是浏览系统中所有执行 super 发送的方法的两种不同方式(第一种方式比较粗暴,第二种方式更好,可以消除一些误报):
SystemNavigation default
browseMethodsWithSourceString: 'super'
matchCase: true
SystemNavigation default
browseAllSelect: [ :method | method sendsToSuper ]

18.11 类,方法字典和方法
因为类本身也是对象,所以我们可以象检查其他对象一样检查或研究它们。
执行 Point inspect。在图 18-4 中,检查器显示了类 Point 的结构。你可以看到该类将其方法存储在字典中,并通过选择器对它们进行索引。选择器 #* 指向 Point>>* 的反编译字节码。

让我们考虑类和方法之间的关系。在图 18-5 中,我们看到类和元类有共同的超类 Behavior。这是定义 new 的地方,以及其他一些类的关键方法。每个类都有一个方法字典,它将方法选择器映射到编译后的方法。每个编译后的方法都知道它被安装在哪个类中。在图 18-4 中,我们甚至可以看到该方法的反编译字节码。

我们可以利用类和方法之间的关系来提出关于系统的查询。例如,要发现哪些方法没有覆盖超类方法,我们可以通过下面的方法导航到方法字典:
| aClass |
aClass := SmallInteger.
aClass methodDict keys select: [ :aMethod |
(aClass superclass canUnderstand: aMethod) not ]
>>> an IdentitySet(#threeDigitName #printStringBase:nDigits: ...)
编译后的方法不只是存储方法的字节码。它也是一个对象,为查询系统提供了许多有用的方法。其中一个方法是isAbstract(它回答方法是否发送了subclassResponsibility)。我们可以用它来标识一个类的所有抽象方法。
| aClass |
aClass := Number.
aClass methodDict keys select: [ :aMethod |
(aClass>>aMethod) isAbstract ]
>>> #(#* #asFloat #storeOn:base: #printOn:base: #+ #round: #/ #-
#adaptToInteger:andSend: #nthRoot: #sqrt #adaptToFraction:andSend:)
请注意,这段代码将消息>>发送给一个类,以获取给定选择器的已编译方法。
要浏览给定层次结构中的 super 发送,例如在 Collections 层次结构中,我们可以提出一个更复杂的查询:
class := Collection.
SystemNavigation default
browseMessageList: (class withAllSubclasses gather: [ :each |
each methodDict associations
select: [ :assoc | assoc value sendsToSuper ]
thenCollect: [ :assoc | RGMethodDefinition realClass: each
selector: assoc key ]])
name: 'Supersends of ', class name, ' and its subclasses'
注意我们如何从类导航到方法字典,再到编译后的方法,以识别我们感兴趣的方法。RGMethodDefinition 是编译后方法的轻量级代理,被许多工具使用。CompiledMethod>>methodReference 是一个便捷方法,用于返回编译后方法的方法引用。
(Object>>#=) methodReference selector
>>> #=
18.12 浏览环境
尽管 SystemNavigation 提供了一些有用的方法来以编程方式查询和浏览系统代码,但还有更多方法。集成到 Pharo 中的 Browser 允许我们限制搜索执行的环境。
假设我们有兴趣发现哪些类引用了 Point 类,但仅限于它自己的包中:
在 Point 类上打开一个浏览器。在包面板中点击顶级包 Kernel,然后选择 Scoped View 单选按钮。浏览器现在只显示 Kernel 包及其中的所有类(以及一些具有该包扩展方法的类)。现在,在此浏览器中再次选择 Point 类,右键点击类名并选择 Class refs。这将显示所有引用 Point 类的方法,但仅限于 Kernel 包中的方法。将此结果与没有限制范围的浏览器中的搜索进行比较。
这个范围就是我们所说的 浏览环境(类 RBBrowserEnvironment)。所有其他搜索,例如方法的发送者或方法的实现者,在这个浏览器中也被限制在此环境中。
浏览环境也可以通过编程方式创建。例如,这里我们为 Collection 及其子类创建一个新的 RBBrowserEnvironment,选择执行 super 发送的方法,并浏览生成的环境:
((RBBrowserEnvironment new forClasses: (Collection withAllSubclasses))
selectMethod: [:method | method sendsToSuper])
browse.
请注意,这要比前面使用SystemNavigation的等效示例紧凑得多。
最后,我们可以通过编程方式找到那些发送不同 super 消息的方法,如下所示:
((RBBrowserEnvironment new forClasses: (Collection withAllSubclasses))
selectMethods: [:method |
method sendsToSuper
and: [(method parseTree superMessages includes: method selector)
not]])
在这里,我们要求每个编译后的方法提供其(重构浏览器)解析树,以确定 super 消息是否与方法的选择器不同。查看类 RBProgramNode 的查询协议,了解我们可以对解析树提出的一些问题。

在图18-6中,我们可以看到在Collection层次结构中找到了10个这样的方法,包括Collection>>printNameOn:,它发送super printOn:。当然,我们也可以使用消息浏览器显示结果,但这是另一回事。
18.13 Pragmas:方法注释
Pragma 是一种方法注解,用于指定关于程序的数据,但不参与程序的执行。Pragmas 对它们注解的方法的操作没有直接影响。Pragmas 有多种用途,其中包括:
编译器的信息:Pragmas 可以被编译器用来使方法调用原语函数。该函数必须由虚拟机或外部插件定义。
运行时处理:可以在运行时检查一些编译指示。
一个方法可以声明一个或多个 pragmas,并且 pragmas 必须在任何 Pharo 语句之前声明。在内部,pragma 是一个带有字面参数的静态消息发送。
我们在本章前面介绍原语时简要提到了 pragmas。原语不过是一个 pragma 声明。考虑在 Object>>#instVarAt: 方法中定义的表达式 <primitive: 173 error:ec>。pragma 的选择器是 primitive:error:,其参数是一个直接字面值 173。变量 ec 是一个错误代码,在虚拟机端的实现执行失败时由虚拟机填充。
编译器可能是 pragmas 的最大用户。SUnit 是另一个使用注解的工具。SUnit 能够从测试单元估计应用程序的覆盖率。可能希望从覆盖率中排除某些方法。SplitJointTest 类中的文档方法就是这种情况:
SplitJoinTest class >> documentation
<ignoreForCoverage>
"self showDocumentation"
^ 'This package provides function... "
通过简单地用 pragma <ignoreForCoverage> 注解一个方法,可以控制覆盖范围。
Pragmas 是类 Pragma 的一等对象实例。编译后的方法通过返回 pragmas 数组来响应消息 pragmas。
(SplitJoinTest class >> #documentation) pragmas.
>>> an Array(<ignoreForCoverage)
(SmallFloat64>>#+) pragmas
>>> an Array(<primitive: 541>)
通过向类 Pragma 发送消息 allNamed:in:,我们可以识别定义特定 pragma 的方法。在以下示例中,SplitJoinTest 的类端包含一些用 <ignoreForCoverage> 注解的方法:
Pragma allNamed: #ignoreForCoverage in: SplitJoinTest class
>>> an Array(<ignoreForCoverage> <ignoreForCoverage>)
Pragma 知道它是在哪个方法中定义的(使用method)、方法的名称(selector)、包含该方法的类(methodClass)、其参数的数量(numArgs)、关于杂注参数的文字(hasLiteral:和hasLiteralSuchThap:)。
18.14 访问运行时上下文
我们已经看到了Pharo的内省功能如何让我们查询和探索对象、类和方法。但运行时环境呢?
方法的上下文
事实上,执行方法的运行时上下文在虚拟机中——根本不在镜像中!另一方面,调试器显然可以访问此信息,我们可以像探索任何其他对象一样愉快地探索运行时上下文。这是如何实现的呢?
实际上,调试器并没有什么神奇之处。秘密在于伪变量 thisContext,我们之前只是顺便提到过。每当在运行的方法中引用 thisContext 时,该方法的整个运行时上下文就会被具体化,并作为一系列链式 Context 对象提供给镜像。
我们自己可以很容易地试验这种机制。
更改Integer>>slowFactorial的定义, 插入表达式thisContext inspect. self halt.。如下所示:
Integer >> slowFactorial
"Answer the factorial of the receiver."
self = 0 ifTrue: [ thisContext inspect. self halt. ^ 1 ].
self > 0 ifTrue: [ ^ self * (self - 1) slowFactorial ].
self error: 'Not valid for negative integers'
现在,在Workspace中对3 slowFactorial进行求值。你您应该会同时获得一个调试器窗口和一个检查器,如图18-7所示。

检查 thisContext 可以让你完全访问当前的执行上下文、调用栈、局部临时变量和参数、发送者链以及接收者。欢迎来到穷人的调试器!如果你现在浏览被探索对象的类(即通过在检查器的底部窗格中执行 self browse),你会发现它是类 Context 的一个实例,链中的每个发送者也是如此。
thisContext不用于日常编程,但它对于实现调试器等工具以及访问有关调用堆栈的信息至关重要。您可以求值以下表达式,以发现哪些方法会使用thisContext:
SystemNavigation default browseMethodsWithSourceString: 'thisContext'
matchCase: true
事实证明,最常见的应用程序之一是找出消息的发送者。一个典型的应用包括向开发人员提供更好的信息。让我们举一个例子,按照惯例,发送self subclassResponsibility消息的方法被认为是抽象方法。但是,Object>>subclassResponsibility是如何提供有用的错误消息来指示调用了哪个抽象方法的呢?很简单,通过询问发送者的thisContext。
subclassResponsibility
"This message sets up a framework for the behavior of the class'
subclasses.
Announce that the subclass should have implemented this message."
SubclassResponsibility signalFor: thisContext sender selector
18.15 智能上下文断点
在 Pharo 中设置断点的方法是在方法中的一个有趣点执行 self halt。这将导致 thisContext 被具体化,并在断点处打开一个调试器窗口。不幸的是,这对系统中频繁使用的方法会带来问题。
例如,假设我们想要探索Morph>>openInWorld的执行。在此方法中设置断点是有问题的。
注意下面的实验会打破一切!使用一个新的映像并设置以下断点:
Morph >> openInWorld
"Add this morph to the world."
self halt.
self openInWorld: self currentWorld
请注意,当你尝试打开任何新的 Morph(菜单/窗口/...)时,你的镜像会立即冻结!我们甚至没有得到调试器窗口。一旦我们理解了以下两点,问题就很清楚了:1) Morph>>openInWorld 被系统的许多部分使用,因此在我们与用户界面交互后不久就会触发断点,但 2) 调试器本身在打开窗口时就会发送 openInWorld,从而阻止调试器打开!我们需要的是仅在感兴趣的上下文中条件性地暂停。这正是 Object>>haltIf: 提供的功能。
现在假设我们只想在openInWorld是从MorphTest>>testOpenInWorld的上下文发送的情况下停止。
再次使用新映像启动,并设置以下断点:
Morph >> openInWorld
"Add this morph to the world."
self haltIf: #testOpenInWorld.
self openInWorld: self currentWorld
这一次映像不会冻结。尝试运行MorphTest。它将停止并打开调试器。
MorphTest run: #testOpenInWorld.
这是怎么工作的?让我们看看Object>>haltIf:。它首先调用if:,并将condition传递给Exception类Halt。该方法本身检查condition是否为符号,如果为真,再调用haltIf-CallChain:contains:
Object >> haltIf: condition
<debuggerCompleteToSender>
Halt if: condition.
Halt class >> haltIfCallChain: haltSenderContext contains: aSelector
| cntxt |
cntxt := haltSenderContext.
[ cntxt isNil ] whileFalse: [
cntxt selector = aSelector ifTrue: [ self signalIn:
haltSenderContext ].
cntxt := cntxt sender ]
从thisContext开始,haltIfCallChainContainers:遍历执行堆栈,检查调用方法的名称是否与作为参数传递的名称相同。如果是这种情况,那么它会向自己发出信号,即默认情况下调用调试器的异常。
也可以提供布尔值或布尔块作为haltIf:的参数,但是这种情况很简单,不会用到thisContext。
18.16 截获无法理解的消息
到目前为止,我们主要使用 Pharo 的反射特性来查询和探索对象、类、方法和运行时栈。现在我们将看看如何利用对其系统结构的了解来拦截消息并在运行时修改行为。
当一个对象接收到消息时,它首先在其类的方法字典中查找相应的方法来响应消息。如果没有找到这样的方法,它将继续在类层次结构中查找,直到到达 Object。如果仍然没有找到该消息的方法,对象将向自己发送 doesNotUnderstand: 消息,并将消息选择器作为其参数。然后整个过程重新开始,直到找到 Object>>doesNotUnderstand:,并启动调试器。
但是如果 doesNotUnderstand: 在查找路径中被 Object 的某个子类覆盖了怎么办?事实证明,这是实现某些非常动态行为的便捷方式。一个不理解消息的对象可以通过覆盖 doesNotUnderstand: 来采用替代策略来响应该消息。
这种技术的两个非常常见的应用是:1) 为对象实现轻量级代理,2) 动态编译或加载缺失的代码。这就是我们将在以下部分中描述的内容。
18.17 轻量级代理
为了实现轻量级代理,我们引入一个最小对象作为现有对象的代理。由于代理几乎不会实现自己的任何方法,发送给它的任何消息都会被 doesNotUnderstand: 捕获。通过实现此消息,代理可以在将消息委托给其代理的真实对象之前采取特殊操作。
让我们看看如何实现这一点。我们对LoggingProxy的定义如下:
ProtoObject subclass: #LoggingProxy
instanceVariableNames: 'subject invocationCount'
classVariableNames: ''
package: 'PBE-Reflection'
请注意,我们将ProtoObject子类化,而不是Object,因为我们不希望我们的代理从Object类继承大约450个方法(!)。
Object methodDict size
>>> 479
我们的代理有两个实例变量:它代理的对象(subject)和它拦截的消息计数(count)。我们初始化这两个实例变量,并为消息计数提供一个访问器。最初,subject 变量指向代理对象本身。
LoggingProxy >> initialize
invocationCount := 0.
subject := self.
LoggingProxy >> invocationCount
^ invocationCount
我们简单地拦截所有不理解的消息,将它们打印到 Transcript,更新消息计数,并将消息转发给真实的对象。
LoggingProxy >> doesNotUnderstand: aMessage
Transcript show: 'performing ', aMessage printString; cr.
invocationCount := invocationCount + 1.
^ aMessage sendTo: subject
接下来有点神奇。我们创建一个新的 Point 对象和一个新的 LoggingProxy 对象,然后告诉代理 become: 这个 Point 对象:
point := 1@2.
LoggingProxy new become: point.
这个表达式的作用是将所有指向 point 变量的对象引用交换为代理实例,反之亦然。最重要的是,代理的 subject 实例变量现在将引用 Point!
point invocationCount
>>> 0
point + (3@4)
>>> 4@6
point invocationCount
>>> 1
这在大多数情况下都能很好地工作,但也有一些缺点:
point class
>>> LoggingProxy
实际上,class 方法是在 ProtoObject 中实现的,但即使它是在 Object 中实现的(LoggingProxy 并不继承自 Object),它实际上也不会发送给 LoggingProxy 或其 subject。该消息由虚拟机直接回答。yourself 也从未真正发送过。
其他可能由虚拟机直接解释的消息(取决于接收者)包括:
+- < > <= >= = ~= * / \\ =\=
@ bitShift: // bitAnd: bitOr:
at: at:put: size
next nextPut: atEnd
blockCopy: value value: do: new new: x y
从未发送的选择器,因为它们被编译器内联并转换为比较和跳转字节码:
ifTrue: ifFalse: ifTrue:ifFalse: ifFalse:ifTrue:
and: or:
whileFalse: whileTrue: whileFalse whileTrue
to:do: to:by:do:
caseOf: caseOf:otherwise:
ifNil: ifNotNil: ifNil:ifNotNil: ifNotNil:ifNil:
尝试将这些消息发送给非布尔值通常会导致虚拟机抛出异常,因为它无法对非布尔接收者使用内联调度。你可以通过覆盖接收者中的 mustBeBoolean 或捕获 NonBooleanReceiver 异常来拦截此行为并定义适当的行为。
即使我们可以忽略这些特殊的消息发送,还有另一个无法通过这种方法克服的基本问题:self 发送无法被拦截:
point := 1@2.
LoggingProxy new become: point.
point invocationCount
>>> 0
point rectangle: (3@4)
>>> 1@2 corner: 3@4
point invocationCount
>>> 1
我们的代理在 rectangle: 方法中被两个 self 发送欺骗了:
Point >> rectangle: aPoint
"Answer a Rectangle that encompasses the receiver and aPoint.
This is the most general infix way to create a rectangle."
^ Rectangle
point: self
point: aPoint
尽管可以使用这种技术通过代理拦截消息,但应该意识到使用代理的固有局限性。在 18.19 节中,我们将看到另一种更通用的拦截消息的方法。
18.18 生成缺失的方法
拦截不理解消息的另一个最常见应用是动态加载或生成缺失的方法。考虑一个包含许多方法的非常大的类库。我们可以不加载整个库,而是为库中的每个类加载一个存根。存根知道在哪里可以找到它们所有方法的源代码。存根简单地捕获所有不理解的消息,并动态地按需创建缺失的方法。在某个时候,可以停用此行为,并将加载的代码保存为客户端应用程序所需的最小必要子集。
让我们看看这种技术的一个简单变体,其中一个类可以按需自动为其实例变量添加访问器。以下是逻辑:任何不理解的消息都会被捕获。如果存在与发送的消息同名的实例变量,那么我们要求我们的类为该实例变量编译一个访问器,并重新发送该消息。
Object subclass: #DynamicAccessors
instanceVariableNames: 'x'
classVariableNames: ''
package: 'PBE-Reflection'
DynamicAccessors >> doesNotUnderstand: aMessage
| messageName |
messageName := aMessage selector asString.
(self class instVarNames includes: messageName)
ifTrue: [
self class compile: messageName, String cr, ' ^ ', messageName.
^ aMessage sendTo: self ].
^ super doesNotUnderstand: aMessage
假设类 DynamicAccessors 有一个实例变量 x 但没有预定义的访问器。那么以下代码将动态生成访问器并检索值:
myDA := DynamicAccessors new.
myDA x
>>> nil
让我们逐步了解第一次将消息 x 发送到我们的对象时会发生什么(见图 18-8)。

(1) 我们向 myDA 发送 x,(2) 消息在类中查找,(3) 在类层次结构中未找到。(4) 这导致 self doesNotUnderstand: #x 被发送回对象,(5) 触发新的查找。这次 doesNotUnderstand: 立即在 DynamicAccessors 中找到,(6) 它要求其类编译字符串 'x ^ x'。查找 compile 方法 (7),(8) 最终在 Behavior 中找到,(9-10) 将新编译的方法添加到 DynamicAccessors 的方法字典中。最后,(11-13) 消息被重新发送(使用 sendTo:),这次找到了。
如果原始发送的消息不对应于实例变量,则使用 super 调用 doesNotUnderstand: 的默认行为。
同样的技术可以用于为实例变量生成设置器,或其他类型的样板代码,例如访问者模式中的访问方法。
关于sendTo:消息
Message类上的sendTo:方法实现如下:
Message >> sendTo: receiver
"answer the result of sending this message to receiver"
^ receiver perform: selector withArguments: args
Object>>perform:方法可以用来发送在运行时组成的消息:
5 perform: #factorial
>>> 120
6 perform: ('fac', 'torial') asSymbol
>>> 720
4 perform: #max: withArguments: (Array with: 6)
>>> 6
18.19 作为方法包装器的对象
我们已经看到,在 Pharo 中,编译后的方法是对象,它们支持许多方法,允许程序员查询运行时系统。
可能有点令人惊讶的是,任何对象都可以扮演编译后的方法的角色,并被放入方法字典中。所有这样的对象必须做的就是响应消息 run:with:in: 和其他一些重要消息。使用这种机制,我们可以构建其他间谍工具。以下是原理:我们创建引用原始方法以执行它们的对象,但将方法字典中的方法替换为此类对象(称为方法包装器)。这样,我们可以记录方法是否已执行以及执行的次数。
我们将使用方法包装器的原理进行实验。Pharo 附带了一个名为 ObjectsAsMethodsExample 的简单类来说明这一原理。
定义一个空类 Demo。执行 Demo new answer42 并注意通常会引发的 Message Not Understood 错误。
Object subclass: #Demo
instanceVariableNames: ''
classVariableNames: ''
package: 'PBE-Reflection'
Demo new answer42
现在我们将一个普通对象安装到 Demo 类的方法字典中。
Demo methodDict
at: #answer42
put: ObjectAsMethodsExample new
现在再次尝试打印Demo new answer42的结果。这次我们得到的答案是42。
Demo new answer42
>>> 42
如果我们查看类 ObjectsAsMethodsExample,我们会找到以下方法:
ObjectAsMethodsExample >> run: oldSelector with: arguments in: aReceiver
^ self perform: oldSelector withArguments: arguments
ObjectsAsMethodsExample >> answer42
^ 42
当我们的 Demo 实例接收到消息 answer42 时,方法查找照常进行,但虚拟机会检测到,代替编译后的方法,一个普通的 Pharo 对象正试图扮演这个角色。然后,虚拟机将向该对象发送一个新消息 run:with:in:,并将原始方法选择器、参数和接收者作为参数。由于 ObjectsAsMethodsExample 实现了此方法,它会拦截消息并将其委托给自己。
我们可以通过下面的方法移除伪方法:
Demo methodDict removeKey: #answer42 ifAbsent: []
如果我们仔细查看 ObjectsAsMethodsExample,我们会看到它的超类也实现了一些方法,如 flushcache、methodClass: 和 selector:,但它们都是空的。这些消息可能会发送给编译后的方法,因此需要由假装是编译方法的对象来实现。flushcache 是最重要的方法;其他方法可能由某些工具要求,并且取决于方法是使用 Behavior>>addSelector:withMethod: 还是直接使用 MethodDictionary>>at:put: 安装的。
现在,为了认真使用本节中提出的想法,我们强烈建议使用名为 MethodProxies 的库,该库可在 http://github.com/pharo-contributions/MethodProxies 上获得,因为它更健壮和安全。
18.20 本章总结
反射指的是能够将运行时系统的元对象作为普通对象进行查询、检查甚至修改的能力。
检查器使用
instVarAt:和相关方法来查看对象的私有实例变量。方法
Behavior>>allInstances查询类的实例。消息
class、isKindOf:、respondsTo:等对于收集指标或构建开发工具很有用,但在常规应用程序中应避免使用:它们违反了对象的封装,并使你的代码更难理解和维护。SystemNavigation是一个实用类,包含许多用于导航和浏览类层次结构的有用查询。例如,使用SystemNavigation default browseMethodsWithSourceString: 'pharo' matchCase:true来查找并浏览所有具有给定源代码字符串的方法。(速度慢,但彻底!)每个 Pharo 类都指向一个
MethodDictionary实例,它将选择器映射到CompiledMethod实例。编译后的方法知道它的类,从而形成闭环。RGMethodDefinition是编译后的方法的轻量级代理,提供了额外的便捷方法,并被许多 Pharo 工具使用。RBBrowserEnvironment是重构浏览器基础设施的一部分,提供了比SystemNavigation更精细的系统查询接口,因为查询的结果可以用作新查询的范围。提供了 GUI 和编程接口。thisContext是一个伪变量,它将虚拟机的运行时栈具体化。它主要由调试器用于动态构建栈的交互视图。它对于动态确定消息的发送者也特别有用。可以使用
haltIf:设置上下文断点,它以方法选择器作为参数。haltIf:仅在命名方法作为发送者出现在运行时栈中时才暂停。拦截发送给给定目标的消息的常见方法是使用一个最小对象作为该目标的代理。代理尽可能少地实现方法,并通过实现
doesNotUnderstand:捕获所有消息发送。然后它可以执行一些额外的操作,然后将消息转发给原始目标。发送
become:以交换两个对象的引用,例如代理及其目标。注意,某些消息,如
class和yourself,从未真正发送,而是由虚拟机解释。其他消息,如+、-和ifTrue:,可能会根据接收者由虚拟机直接解释或内联。覆盖
doesNotUnderstand:的另一个典型用途是延迟加载或编译缺失的方法。doesNotUnderstand:无法捕获self发送。
评论