Clozure Common Lisp 帮助文档-第14章 Objective-C 桥(中文版)

风格不统一 提交于 2019-12-06 21:24:43

Clozure Common Lisp 帮助文档-第14章 Objective-C 桥(中文版)

=== 原文地址: 网络: http://ccl.clozure.com/ccl-documentation.html
原文标题: Clozure CL Documentation 翻译者: FreeBlues 2013-08-03

===

目录

14 Objective-C 桥 the Objective-C Bridge

特别说明: 本章节我也是边学边翻, 不少地方没有理解, 导致翻译过来的中文读着也是磕磕绊绊, 欢迎各位朋友指正,谢谢!

Mac OS X 的 API 使用名为 Objective-C 的语言,这是在 C 的基础上增加了一些仿照 Smalltalk 的面向对象的扩展。 Objective-C 的桥可以从 Lisp 使用 Objective-C 的对象和类,并在 Lisp 中定义可以用于 Objective-C 的类。

Objective-C 和 Cocoa 桥的最终目的是使得 Cocoa(Mac OS X上的标准用户界面框架)易于从 Clozure CL 中使用 ,以支持在 Mac OS X(在任何平台上,支持 Objective-C 语言,如 GNUstep)上开发 GUI 应用程序和集成开发环境(IDE) 。最终的目标,比以前更为接近,是把 Cocoa 完全集成到CLOS 中。

当前的版本提供类似 Lisp 语法和命名约定的 Objective-C 的基本操作,以及自动类型处理和编译时的消息有效性检查。在用 Cocoa 工作时它也提供了一些便利功能。

14.1 版本 1.2 里的变化 Changes in 1.2

1.2版的 Clozure CL 导出了大多数本章中描述的最有用的符号,在以前的版本中,其中大部分在 CCL 包中是 private 的。

有一些新的读取宏(reader macro),使它比以前更方便于引用类的符号用于 Objective-C 的桥。对于这些读取宏的完整说明,请参阅13.12 外部函数接口字典 FFI Dictionary,尤其是项目开始时,关于读取宏的描述。

在以前的版本,32位版本的 Clozure CL 使用32位的浮点数和整数数据结构描述的几何形状,字体大小和度量,等等。64位版本的 Clozure CL 在适当情况下使用64位值。

Objective-C 桥定义类型 NS:CGFLOAT 作为当前平台上的首选浮点类型类的 Lisp 类型,并定义常量 NS:+CGFLOAT+。 在 DarwinPPC32 中,外部类型 :cgfloat,:<NSUI>nteger, 以及 :<NSI>nteger 被 Objective-C 桥分别定义为(32位浮点,32位无符号整数,32位有符号整数); 这些类型在64位的接口被定义为64位变体。

现在每个 Objective-C 类被正确命名,无论是从 NS 包(在一个接口文件中声明中预定义的类的情况下)或提供的名称在 DEFCLASS 形式导出的名称(通过 :MetaClass NS:+NS-OBJECT)从 Lisp 定义类。 类的 Lisp 名字现在自称为一个“静态”变量(仿佛通过 DEFSTATIC,在“4.8 静态变量”一节)和类对象作为它的价值。换句话说:

(send (find-class 'ns:ns-application) 'shared-application)

(send ns:ns-application 'shared-application)

是等价的. (既然绑定一个“静态”变量是不合法的, 它可能有必要进行重命名一些东西, 这样使一些名字碰巧跟 Objective-C 类名冲突的无关变量都不这样做)

14.2 使用 Objective-C 的类 Using Objective-C Classes

最标准的 CLOS 类的类被命名为 STANDARD-CLASS. 在 Objective-C 的对象模型, 每个类是一个(通常是唯一的) metaclass 的一个实例, 它本身就是一个 “base” metaclass(通常该类的 metaclass 被名为 “NSObject”). 因此, Objective-C 类被命名为 "NSWindow" 并且 "Objective-C" 类 "NSArray" 是(sole) 它们的同样名为 "NSWindow" 和 "NSArray" 的不同的 meataclass 的实例. (在 Objective-C 世界里, 像实例分配一样指定类的行为, 是更为常见和有用的)

当 Clozure CL 首次加载含 Objective-C 类的外部库时, 它会识别它们包含的类. 外部类名, 如 "NSWindow" 通过桥的转换规则被映射到一个 "NS" 包里的外部符号--NS:NS-WINDOW. 一个类似的变形会发生在 metaclass 名上, 前面加上一个 "+", 产生类似于这样的形式 NS:+NS-WINDOW.

这些被集成到 CLOS 里的类, 如 metaclass 是类 OBJC:OBJC-METACLASS 的一个实例并且该类也是 metaclass 的一个实例. SLOT-DESCRIPTION metaobjects 被每一个实例变量所创建, 并且类和 metaclass 通过一些非常相似的“标准” CLOS 类初始化协议(与别不同的是,这些类已被分配)

当你 (require "COCOA") 会在当前花费几秒钟来执行所有这些初始化. 加快的念头可以想想,但这是绝不可能加快的.

当该过程完成后, CLOS 清楚认识几百个新的 Objective-C 类和它们的元类. Clozure CL 的运行时系统可以可靠地识别 Objective-C 类 MACPTRs 为类对象, 并且能(相当可靠但启发式地)识别这些类(虽然这里有复杂的因素, 见下文)的实例. SLOT-VALUE 能被用来访问(以及小心地设置) Objective-C 实例中的实例变量. 为了演示这些, 请看下面示例:

? (require "COCOA")
"COCOA"
NIL
?

接着, 在等待时间稍微长一点的一个 Cocoa listener 窗口出现后,激活 Cocoa listener 并且这样做:

? (describe (ccl::send ccl::*NSApp* 'key-window))
#<HEMLOCK-LISTENER-FRAME <HemlockListenerFrame: 0x10a8f20> (#x10A8F20)>
Class: #<OBJC:OBJC-CLASS GUI::HEMLOCK-LISTENER-FRAME (#x152340)>
Wrapper: #<CLASS-WRAPPER GUI::HEMLOCK-LISTENER-FRAME #x302000C3FF7D>
Instance slots
GUI::ECHO-AREA-BUFFER: #<Hemlock Buffer "Echo Area 1">
GUI::ECHO-AREA-STREAM: NIL
NS:ISA: #<OBJC:OBJC-CLASS GUI::HEMLOCK-LISTENER-FRAME (#x152340)>
NS:_NEXT-RESPONDER: #<HEMLOCK-LISTENER-WINDOW-CONTROLLER <HemlockListenerWindowController: 0x1478b60> (#x1478B60)>
NS:_FRAME: #<NS-RECT 892 X 784 @ 575,60 (#x10A8F30) #x302000EE923D>
NS:_CONTENT-VIEW: #<NS-VIEW <NSView: 0x146a460> (#x146A460)>
NS:_DELEGATE: #<HEMLOCK-LISTENER-WINDOW-CONTROLLER 	<HemlockListenerWindowController: 0x1478b60> (#x1478B60)>
NS:_FIRST-RESPONDER: #<HEMLOCK-TEXT-VIEW <HemlockTextView: 0x637df0>
    Frame = {{0.00, 0.00}, {892.00, 3439.00}}, Bounds = {{0.00, 0.00}, {892.00, 3439.00}}
    Horizontally resizable: NO, Vertically resizable: YES
    MinSize = {892.00, 727.00}, MaxSize = {10000000.00, 10000000.00}
 (#x637DF0)>
NS:_LAST-LEFT-HIT: #<HEMLOCK-TEXT-VIEW <HemlockTextView: 0x637df0>
    Frame = {{0.00, 0.00}, {892.00, 3439.00}}, Bounds = {{0.00, 0.00}, {892.00, 3439.00}}
    Horizontally resizable: NO, Vertically resizable: YES
    MinSize = {892.00, 727.00}, MaxSize = {10000000.00, 10000000.00}
 (#x637DF0)>
NS:_LAST-RIGHT-HIT: #<A Null Foreign Pointer>
NS:_COUNTERPART: #<A Null Foreign Pointer>
NS:_FIELD-EDITOR: #<A Null Foreign Pointer>
NS:_WIN-EVENT-MASK: -1071906816
NS:_WINDOW-NUM: 5967
NS:_LEVEL: 0
NS:_BACKGROUND-COLOR: #<NS-COLOR NSCalibratedWhiteColorSpace 1 1 (#x519EC0)>
NS:_BORDER-VIEW: #<NS-VIEW <NSThemeFrame: 0x10265c0> (#x10265C0)>
NS:_POSTING-DISABLED: 0
NS:_STYLE-MASK: 15
NS:_FLUSH-DISABLED: 0
NS:_RESERVED-WINDOW-1: 0
NS:_CURSOR-RECTS: #<A Foreign Pointer #x149FF00>
NS:_TRECT-TABLE: #<A Foreign Pointer #x103BD20>
NS:_MINI-ICON: #<A Null Foreign Pointer>
NS:_UNUSED: 1
NS:_DRAG-TYPES: #<A Null Foreign Pointer>
NS:_REPRESENTED-URL: #<A Null Foreign Pointer>
NS:_SIZE-LIMITS: #<A Null Foreign Pointer>
NS:_FRAME-SAVE-NAME: #<NS-MUTABLE-STRING "Untitled" (#x17DB3C0)>
NS:_REG-DRAG-TYPES: #<NS-MUTABLE-SET {(
    "NeXT font pasteboard type",
    NSStringPboardType,
    "NSColor pasteboard type",
    "Apple URL pasteboard type",
    "Apple PNG pasteboard type",
    "Apple HTML pasteboard type",
    "NeXT ruler pasteboard type",
    "Apple PDF pasteboard type",
    "public.url",
    WebURLsWithTitlesPboardType,
    "Apple PICT pasteboard type",
    "NeXT RTFD pasteboard type",
    "NeXT Encapsulated PostScript v1.2 pasteboard type",
    "NeXT TIFF v4.0 pasteboard type",
    NSFilenamesPboardType,
    "CorePasteboardFlavorType 0x6D6F6F76",
    "NeXT Rich Text Format v1.0 pasteboard type"
)} (#x634FE0)>
NS:_W-FLAGS: #<A Foreign Pointer (:* (:STRUCT :__W<F>LAGS)) #x10A9000>
NS:_DEFAULT-BUTTON-CELL: #<A Null Foreign Pointer>
NS:_INITIAL-FIRST-RESPONDER: #<NS-VIEW <NSView: 0x146a460> (#x146A460)>
NS:_AUXILIARY-STORAGE: #<A Foreign Pointer (:*
                                            (:STRUCT
                                             :<NSW>INDOW<A>UXILIARY))#x1028320>
GUI::ECHO-AREA-VIEW: #<ECHO-AREA-VIEW <EchoAreaView: 0x1476580>
    Frame = {{0.00, 0.00}, {876.00, 20.00}}, Bounds = {{0.00, 0.00}, {876.00, 20.00}}
    Horizontally resizable: YES, Vertically resizable: NO
    MinSize = {876.00, 20.00}, MaxSize = {10000000.00, 10000000.00}
 (#x1476580)>
GUI::PANE: #<TEXT-PANE <TextPane: 0x51aa40> (#x51AA40)>
?  

这样发出一个消息, 要求关键窗口, 就是具有输入焦点的窗口(经常在最前面), 然后描述它. 正如我们可以看到的, NS:NS-WINDOWS 有很多有趣的槽。

14.3 实例化 Objective-C 对象 Instantiating Objective-C Objects

实现一个 Objective-C 类(不论该类是否被预定义或由应用程序定义过)的一个实例,包括通过类和作为参数的一组 initargs 来调用 MAKE-INSTANCE. 正如 STANDARD-CLASS,实现一个包括初始化(用 INITIALIZE-INSTANCE)一个通过 ALLOCATE-INSTANCE 分配的对象的实例。

例如,您可以像这样创建一个 ns:ns-number :

? (make-instance 'ns:ns-number :init-with-int 42)
#<NS-CF-NUMBER 42 (#x85962210)>

奇怪, 译者的环境是这个结果:
? (make-instance 'ns:ns-number :init-with-int 42)
#<A Null Foreign Pointer>
? 

如果你用 Objective-C 来编写, 如何做到这一点是值得期待的:

[[NSNumber alloc] initWithInt: 42]

分配一个 Objective-C 类的一个实例包括发送给这个类一个 “alloc” 消息, 然后利用这些作为“初始化”消息被发送到新分配的实例的不对应插槽 initargs 的 initargs. 所以,上面的例子可以被做得更冗长:

? (defvar *n* (ccl::send (find-class 'ns:ns-number) 'alloc))
*N*
? 

? (setq *n* (ccl::send *n* :init-with-int 42))
#<NS-NUMBER 42 (#x2A83)>
? 

setq 一样是很重要的,这是一个初始化决定替换对象并返回新对象,而不是修改现有对象的场景. 事实上,如果你略去 setq,然后尝试查看 *N* 的值,Clozure CL 将冻结. 很少有理由来这么做; 这只是表明这是怎么回事。

你见过一个 Objective-C 初始化方法并非必须返回跟它所传递的相同的对象. 事实上, 它根本没必要必须返回任何对象; 在这种情况下,初始化失败,make-instance 返回nil。

在某些特殊情况下, 如从一个 .nib 文件加载一个 ns:ns-window-controller, 传递实例本身作为初始化方法的参数之一对你来说可能是必要的. 它是这样的:

? (defvar *controller*
      (make-instance 'ns:ns-window-controller))
*CONTROLLER*
?

? (setq *controller*
      (ccl::send *controller*
      :init-with-window-nib-name #@"DataWindow"
      :owner *controller*))
#<NS-WINDOW-CONTROLLER <NSWindowController: 0xc283d00> (#xC283D00)>
? 

此示例不带 initargs 调用 (make-instance). 当你这么做,对象仅仅被分配,没有被初始化。然后它发送 “init” 消息来手工初始化。

有一种替代的 API 用于实例化 Objective-C 类. 你可以调用 OBJC:MAKE-OBJC-INSTANCE, 把 Objective-C 类名作为一个字符串传递给它. 在以前的版本中, 在类没有定义任何 Lisp 槽的情况下, OBJC:MAKE-OBJC-INSTANCE 能比 OBJC:MAKE-INSTANCE 实现更高的效率, 现在不再是那样了. 现在您可以把 OBJC:MAKE-OBJC-INSTANCE 和 OBJC:MAKE-INSTANCE 看做完全等效,除非你可以传递类名字符串--有利于这种场景:在某种程度上类名不太常见。

14.4 调用 Objective-C 方法 Calling Objective-C Methods

在 Objective-C 中, 方法被称为"消息", 并且有一种特定的语法用来给一个对象发送一条消息:

[w alphaValue]
[w setAlphaValue: 0.5]
[v mouse: p inRect: r]

第一行发送方法 "alphaValue" 给对象 w, 不带参数. 第二行发送方法 "setAlphaValue", 带参数 0.5. 第三行发送方法 "mouse:inRect:" - 是的,就是这长长的一串内容- 带着参数 p 和 r.

在 Lisp 中, 同样的三行如下:

(send w 'alpha-value)
(send w :set-alpha-value 0.5)
(send v :mouse p :in-rect r)

请注意,当一个方法没有参数,它的名字是一个普通的符号(不要紧的符号是什么样的包,只是它的名称被选中)。当一个方法的参数,其名称的一部分是一个关键字,关键字的值交替。

这两行语句打破这些规则,都将导致在错误消息:

(send w :alpha-value)
(send w 'set-alpha-value 0.5)

除了(send),你也可以调用(send-super),具有相同的接口。 CLOS(call-next-method)它具有大致相同的目的,当你用(send-super),该消息被超类处理。这可以用来在原来实行的一种方法,被子类中的一个方法屏蔽(shadow)时。

14.4.1. Objective-C 方法调用的类型强制 Type Coercion for Objective-C Method Calls

Clozure CL 的 FFI 处理很多 Lisp 和外部数据之间的公共约定, 如拆箱(unboxing)浮点参数和装箱(boxing)浮点结果. 桥增加了一些更自动化的约定:

NIL 等价于 (%NULL-PTR) , 对于任何需要一个指针的消息参数

T/NIL 等价于 #$YES/#$NO , 对于任何布尔参数

被任何返回 BOOL 类型的方法返回的一个 #$YES/#$NO 将会被自动转换为 T/NIL.

14.4.2. 返回结构的方法 Methods which Return Structures

一些 Cocoa 方法返回小结构, 比如用于 表示点, 区域, 大小和范围. 当用 Objective-C 写代码时, 编译器隐藏了实现的细节. 不幸的是, 在用 Lisp 写代码时我们必须对它们知道得更清楚明白.

返回结构的方法被以一种特别的方式调用; 调用者为结果分配空间, 并且把一个指针当做一个附加的参数传递给这个方法. 这被称为一个结构返回, 或者 STRET. 不要瞪我, 这些名字不是我起的. :)

这里有一个 Objective-C 对此的简单使用. 第一行发送 "bounds" 消息给 v1, 它返回一个 rectangle. 第二行发生 "setBounds" 消息给 v2, 传递同样的作为一个参数的 rectangle.

NSRect r = [v1 bounds];
[v2 setBounds r];

在 Lisp 中, 我们必须明确地分配内存, 由 rlet 最轻易,最安全地完成. 我们这么做:

(rlet ((r :<NSR>ect))
	(send/stret r v1 'bounds)
	(send v2 :set-bounds r))

rlet 分配了存储(但是没有初始化), 并且确认当我们处理时它会被释放掉. 它绑定变量 r 来指向它. send/stret 的调用就像一个正常的 send 调用, 除了 r 会被传递给一个附加的第一个参数. 第三行, 调用 send , 不需要做任何特别的事, 因为把一个结构作为一个参数来传递没有什么复杂的.

为了让 STRETs 更易于使用, 桥提供了两个约定.

首先, 你可以在一步内使用宏 slet 和 slet* 来分配和初始化局部变量为外部结构. 下面的示例可以写的更简洁:

(slet ((r (send v1 'bounds)))
	(send v2 :set-bounds r))

其次, 当一个 send 调用被创建在另一个内部时, 内部的那个有一个围绕它的隐含的SLET. 因此, 代码实际可以这么写:

(send v1 :set-bounds (send v2 'bounds))

有一些由 Objective-C 编译器提供于约定的虚拟函数(pseudo-functions), 可以使对象成为特定的类型. 下面是目前桥所支持的: NS-MAKE-POINT, NS-MAKE-RANGE, NS-MAKE-RECT, and NS-MAKE-SIZE.

These pseudo-functions can be used within an SLET initform: 这些虚拟函数可以被用于一个 SLET 的 initform 内:

(slet ((p (ns-make-point 100.0 200.0)))
      (send w :set-frame-origin p))

或者在一个 send 调用内:

(send w :set-origin (ns-make-point 100.0 200.0))

然而, 既然这里没有实际函数, 一个类似下面的调用是不会工作的:

(setq p (ns-make-point 100.0 200.0))

为了从对象中提取字段, 这里也有一些约定宏: NS-MAX-RANGE, NS-MIN-X, NS-MIN-Y, NS-MAX-X, NS-MAX-Y, NS-MID-X, NS-MID-Y, NS-HEIGHT, and NS-WIDTH.

注意, 这里还有一种用于方法内部的 send-super/stret. 就像 send-super, 会忽略任何在一个子类里被屏蔽的方法, 然后调用属于它的父类的方法的版本.

14.4.3. 参数数量可变的消息 Variable-Arity Messages

Cocoa 中有一些消息的参数个数可变. 或许最常见的例子包括格式化字符串:

[NSClass stringWithFormat: "%f %f" x y]

在 Lisp 中, 将会被写作:

(send (find-class 'ns:ns-string)
      :string-with-format #@"%f %f"
      (:double-float x :double-float y))

注意, 必须指出变量(在这个例子里是 :double-float)的外部类型, 因为编译器无法通过正常途径来知道这些类型.(你可能认为它可以分析格式化字符串, 但是这种情况仅仅在格式化字符串没有在运行时中被确定时才有效)

因为 Objective-C 的运行时系统没有提供消息是参数数量可变的任何信息, 它们必须被明确地声明. Cocoa 中标准的参数数量可变消息在桥中会被预声明. 如果你需要声明一个新的参数数量可变消息, 请使用: (DEFINE-VARIABLE-ARITY-MESSAGE "myVariableArityMessage:").

14.4.4. 优化 Optimization

即使获取了足够的信息, 桥在优化消息发送方面仍然工作得极其艰难. 在它工作时这有两个原因. 任一种原因, 一个消息发送应该差不多和用 Objective-C 书写代码效率相近.

第一个原因是当消息和接收器的类都在编译时可知. 一般地, 知道接收器的类的仅有办法是如果你或者使用 DECLARE 或者使用一个 THE 形式来声明它. 例如:

(send (the ns:ns-window w) 'center)

注意, 在 Objective-C 中是没有办法去命名一个类的类. 因而桥提供了一个声明 @METACLASS. "NSColor" 的一个实例的类型是 ns:ns-color. 类 "NSColor" 的类型是 (@metaclass ns:ns-color):

(let ((c (find-class 'ns:ns-color)))
  (declare ((ccl::@metaclass ns:ns-color) c))
  (send c 'white-color))

其他允许优化的原因是仅仅当消息在编译时可知, 但是它的类型签名是唯一的. 在当前超过 6000 个由 Cocoa 提供的消息中, 仅仅有 50 个拥有非唯一的类型签名.

关于一个带有类型签名的消息不是唯一的例子是 SET. 它返回 VOID 给 NSColor, 但是返回 ID 给 NSSet. 为了优化带有非唯一类型签名的消息发送, 接收器的类必须在编译时被声明.

如果类型签名是非唯一或者消息是未知, 在编译时, 那么一个更慢得运行时调用必须被使用.

当接收器的类未知, 桥的优化能力依赖于一个它所维护的类型签名表. 当第一个被加载, 桥通过扫描每个 Objective-C 类的每个方法来初始化这个表. 当新方法在此之后被定义, 这个表必须被更新. 这些都是当你在 Lisp 中定义方法时自动发生的. 在任何其他重要的改变之后, 比如加载一个外部框架, 你需要执行如下命令来重建这个表:

? (update-type-signatures)

因为 send 以及和它相关的 send-super, send/stret, 还有 send-super/stret 都是宏, 所以它们不能被 funcall 和 apply 调用, 或者当做一个参数传递给函数.(译者注: fuuncall 和 apply 只能调用函数, 不能调用宏)

为了解决这个问题, 这里有一些等效函数: %send, %send-super, %send/stret, 和 %send-super/stret. 不过, 这些函数应该仅被用于宏不被使用的场景, 因为它们无法被优化.

14.5 定义 Objective-C 类 Defining Objective-C Classes

你能定义你自己的外部类, 可以被传递给外部函数; 你在 Lisp 中实现的方法将被作为回调函数提供给外部代码.

你也可以定义已存类的子类, 在 Lisp 实现你的子类, 哪怕父类是在 Objective-C 也可以. 类似的一个子类是 CCL::NS-LISP-STRING. 创建 NS-WINDOW-CONTROLLER 的子类也是相当有用的.

我们能用 MOP 来定义新 Objective-C 类, 但是我们不得不做的事情却有些滑稽: 我们想要在一个 DEFCLASS 选项里使用的 :METACLASS 一般不存在--直到我们已经创建了那个类(还记得 Objective-C 类有, 为了参数的缘故, 唯一并且私有的 metaclass) 我们能排出丑陋的解决方法: 通过指定一个已知的
Objective-C metaclass 对象名, 当做 DEFCLASS :METACLASS 对象的值; 根类 NS:NS-OBJECT, NS:+NS-OBJECT 的 metaclass, 做一个好的选择. 创建一个 NS:NS-WINDOW(为了简单起见,不定义任何新的插槽) 的子类, 我们可以这么写:

(defclass example-window (ns:ns-window)
  ()
  (:metaclass ns:+ns-object))

这样将会创建一个新的 Objective-C 类, 被命名为 EXAMPLE-WINDOW, 它的 metaclass 是名为 +EXAMPLE-WINDOW 的类. 该类将会成为一个类型为 OBJC:OBJC-CLASS 的对象, 并且 metaclass 将的类型将为 OBJC:OBJC-METACLASS. EXAMPLE-WINDOW 将成为 NS-WINDOW 的一个子类.

14.5.1 定义带外部槽的类 Defining classes with foreign slots

如果在一个 Objective-C 类里定义的槽的格式包含关键字 :FOREIGN-TYPE, 这个槽就是一个"外部槽"(例如一个 Objective-C 实例变量). 要知道以任何方式重定义一个 Objective-C 类而导致它的外部槽发生变化都是错误的, 当你尝试这么做时 Clozure CL 不会做任何事来保持一致性.

:FOREIGN-TYPE initarg 的值应该是一个外部类型指示符. 例如, 如果我们想(出于一些原因)定义一个 NS:NS-WINDOW 的子类, 可以保持追踪他接收到的(需要一个实例变量来保持这个信息)关键事件的数目, 我们可以这么写:

(defclass key-event-counting-window (ns:ns-window)
  ((key-event-count :foreign-type :int
                    :initform 0
                    :accessor window-key-event-count))
  (:metaclass ns:+ns-object))

外部槽一般是 SLOT-BOUNDP, 并且上述的 initform 是冗余: 外部槽被初始化为二进制 0

14.5.2 定义带 Lisp 槽的类 Defining classes with Lisp slots

一个 Objective-C 类里定义的槽的格式不包含关键字 :FOREIGN-TYPE initarg, 定义了一个漂亮得多的 Lisp 槽, 碰巧被关联到 "一个外部类的实例". 例如:

(defclass hemlock-buffer-string (ns:ns-string)
  ((hemlock-buffer :type hi::hemlock-buffer
                   :initform hi::%make-hemlock-buffer
                   :accessor string-hemlock-buffer))
  (:metaclass ns:+ns-object))

正如人们所期望, 这里有内存管理含义: 我们不得不维护介于一个 MACPTR 和一个 lisp 对象(它的槽)集合之间的关联, 只要这个 Objective-C 实例存在, 我们不得不确定 Objective-C 实例的存在(没有被调用 -dealloc 方法)当 lisp 试着去把它当做一个当它依然可能被引用而不能被 "deallocated" 的第一类对象去思考. 关联一个或多个 lisp 对象到一个外部实例上往往是很有用的; 如果你"手动"去做这些, 你不得不面对很多类似的内存管理问题.

14.6 定义 Objective-C 方法 Defining Objective-C Methods

在 Objective-C 中, 不像在 CLOS 中, 每个方法输入一些特定类. 对于你这或许不是一个奇怪的概念, 因为 C++ 和 Java 也做同样的事. 当你使用 Lisp 去定义 Objective-C 方法时, 只可能定义一些属于 Objective-C 的类并且已经在 Lisp 中被定义好的方法.

你可以使用两种不同宏的任一种来定义 Objective-C 类的方法. define-objc-method 接受一个包含一个消息选择器名和一个类名的二元素列表, 和一个形式体. objc:defmethod 表面上类似于普通的 CLOS 宏 defmethod, 但它实际在 Objective-C 类上新建方法时, 会受到跟使用 define-objc-method 新建方法一样的限制.

14.6.1 使用 define-objc-method 函数 Using define-objc-method

如同在章节 "14.4. Calling Objective-C Methods" 中所描述, Objective-C 的方法名被分成小块, 每一块跟着一个参数. 所有参数的类型都必须被确切地声明.

认真考虑一些例子, 用来说明 define-objc-method 的使用. 让我们定义一个类来在它们中使用

(defclass data-window-controller (ns:ns-window-controller)
  ((window :foreign-type :id :accessor window)
   (data :initform nil :accessor data))
  (:metaclass ns:+ns-object))

关于这个类没什么特别的地方. 它继承自 ns:ns-window-controller. 它有两个槽(slots): window 是一个外部槽, 保存在 Objective-C 世界中; data 是一个普通槽, 储存在 Lisp 世界中.

这里有一个实例关于如何定义一个不带任何参数的方法:

(define-objc-method ((:id get-window)
                     data-window-controller)
    (window self))

这个方法的返回值是外部类型 :id, 被用于所有的 Objective-C 对象. 方法名为 get-window. 方法的形式体是单独一行 (window self). 变量 self , 在形式体内部, 被绑定到接收消息的实例上. 对 window 的调用使用 CLOS 的访问器(acessor)来获取 window 字段的值.

Here's an example that takes a parameter. Notice that the name of the method without a parameter was an ordinary symbol, but with a parameter, it's a keyword: 这里有一个带参数的示例. 注意不带参数的方法名是一个普通符号, 但是一旦带一个参数, 它就变成一个关键字了:

(define-objc-method ((:id :init-with-multiplier (:int multiplier))
                     data-window-controller)
  (setf (data self) (make-array 100))
  (dotimes (i 100)
    (setf (aref (data self) i)
          (* i multiplier)))
  self)

对使用类的 Objective-C 代码来说, 这个方法的名字是 initWithMultiplier:. 参数名是 multiplier, 它的类型是 :int. 方法形式体做了一些毫无意义的事情. 然后返回了 self, 因为这是一个初始化方法.

这里是一个带有多于一个参数的示例:

(define-objc-method ((:id :init-with-multiplier (:int multiplier)
                          :and-addend (:int addend))
                     data-window-controller)
  (setf (data self) (make-array size))
  (dotimes (i 100)
    (setf (aref (data self) i)
          (+ (* i multiplier)
             addend)))
  self)

对于 Objective-C 代码来说, 方法名是 initWithMultiplier:andAddend:. 两个参数的类型都是 :int; 第一个参数名是 multiplier, 第二个参数名是 addend. 这个方法再次返回 self.

接下来是一个不返回任何值的方法. 也被称为 "空方法"(void method). 也就是其他方法返回值类型为 :id, 这个方法返回值类型为 :void

(define-objc-method ((:void :take-action (:id sender))
                     data-window-controller)
  (declare (ignore sender))
  (dotimes (i 100)
    (setf (aref (data self) i)
          (- (aref (data self) i)))))

这个方法在 Objective-C 中被称为 takeAction:. 作为将被当做 Cocoa 行为使用的这种方法的约定是, 它们使用一个对象负责的用于触发该行为的参数, 所以它明确地忽略它,以避免编译器警告. 如同约定所承诺, 这类方法不返回任何值.

还有其他的语法, 如这里所示. 下面两种方法定义时等效的:

(define-objc-method ("applicationShouldTerminate:"
                     "LispApplicationDelegate")
    (:id sender :<BOOL>)
    (declare (ignore sender))
    nil)

(define-objc-method ((:<BOOL>
                      :application-should-terminate sender)
                       lisp-application-delegate)
    (declare (ignore sender))
    nil)

14.6.2 使用 objc:defmethod 函数 Using objc:defmethod

宏 OBJC:DEFMETHOD 可被用来定义 Objective-C 方法. 在某些方面, 它表面上看起来很像 CL:DEFMETHOD

它的语法是:

(OBJC:DEFMETHOD name-and-result-type 
               ((receiver-arg-and-class) &rest other-args) 
      &body body)

name-and-result-type 或者是一个 Objective-C 消息名, 它对应的方法是返回一个 :ID 类型的值, 或者是一个列表, 包含一个带有不同外部结果类型的方法的 Objective-C 消息名和一个外部类型描述符.

receiver-arg-and-class 是一个二元素列表, 列表的第一个元素是一个变量名, 第二个元素是一个Objective-C 类或 metaclass 的 Lisp 名. 接收器(receiver)变量名可以是任何可被绑定的 Lisp 变量名, 不过 SELF 可能是一个合理的选择. 接收器(receiver)变量被声明为"不可设置"(unsettable); 例如, 在方法定义形式体内试图去修改接收器的值是一个错误.

other-args 或者是变量名(参数类型为 :ID), 或者是第一个元素为一变量名第二个元素为一外部类型指示符的二元素列表.

认真思考这个示例:

(objc:defmethod (#/characterAtIndex: :unichar)
    ((self hemlock-buffer-string) (index :<NSUI>nteger))
  ...)

方法 characterAtIndex:, 当被一个 HEMLOCK-BUFFER-STRING 类的对象通过一个类型为 :<NSU>integer 的附加参数调用时, 会返回一个类型为 :unichar 的值.

除一些指针类型以外的参数 :ID(如, 指针, 按值传递的记录)都表示为类型的外部指针,因此更高级别的类型检查访问器(accessors),可用于类型的参数 :ns-rect, :ns-point, 依此类推。

在通过 OBJC:DEFMETHOD 定义的方法体内, 局部函数 CL:CALL-NEXT-METHOD 被定义. 它不像 CL:CALL-NEXT-METHOD 在一个 CLOS 方法内使用那么普遍, 而是有一些相同的语义. 它接受包含方法的 other-args 列表并且调用在接收者的类的带有接收者和其他被提供参数的父类的实例上所调用的包含方法的版本。(传递当前方法的参数到下一个方法的惯例, 已经足够通用了, OBJC:DEFMETHODs 中的 CALL-NEXT-METHOD 应该大致也能做这个如果它没有收到任何参数.)

一个通过 OBJC:DEFMETHOD 定义的方法返回一个结构 "通过值" 能够通过返回一个以 MAKE-GCABLE-RECORD 新建的记录来实现, 能够通过返回一个以 CALL-NEXT-METHOD 新建的值来实现, 或者通过其他类似的手段. 在幕后, 可以是一个预分配好的记录类型实例(用来支持本地结构返回约定), 和通过将会被拷贝到这个内部记录实例中的方法体所返回的任意值. 在一个通过被声明返回一个结构类型的 OBJC:DEFMETHOD 所定义的方法体内部, 局部宏 OBJC:RETURNING-FOREIGN-STRUCT 可以被用于访问内部结构.

例如:

(objc:defmethod (#/reallyTinyRectangleAtPoint: :ns-rect) 
  ((self really-tiny-view) (where :ns-point))
  (objc:returning-foreign-struct (r)
    (ns:init-ns-rect r (ns:ns-point-x where) (ns:ns-point-y where)
                        single-float-epsilon single-float-epsilon)
    r))

如果 OBJC:DEFMETHOD 新建一个新的方法, 接着的效果是它会显示一条消息. 这些消息对于捕获方法定义里的名字的错误很有用. 另外附带地, 如果一个 OBJC:DEFMETHOD 形式体中通过修改它的类型签名重定义了一个方法, Clozure CL 的状况系统将会升起一个持续的错误.

14.6.3 方法重定义约束 Method Redefinition Constraints

Objective-C 没有像 Lisp 一样, 在构思时就被设计成在运行时支持重定义. 因此对于你如何和何时能够替换一个 Objective-C 方法的定义有一些限制. 目前, 如果你违反规则, 什么都不会崩溃, 但是程序的行为将会变得让人疑惑; 所以别那么做.

Objective-C 方法可以被重定义在运行时, 但是它们的签名不会改变. 这就是说, 参数的类型和返回值的类型不得不保持原样. 关于这一点的原因是修改签名会修改用于调用方法的选择器(selector)

当一个方法已经在一个类中被定义, 并且你把它定义在一个子类里, 屏蔽了原始的方法, 它们必须拥有相同类型的签名. 不存在这样的限制,不过,如果这两个类是不相关的, 并且方法恰好具有相同的名称。

14.7 加载框架 Loading Frameworks

在 Mac OS X 中, 一个框架是一个包含着一个或多个共享库, 连同形如 C 和 Objective-C 头文件的元数据的结构化目录. 有时, 框架可以包含附加项, 如可执行文件.

加载一个框架意味着打开共享库并且处理所有的声明, 因此 Clozure CL 能随之调用入口点并使用它们的数据结构. 为此目的, Clozure CL 提供了函数 OBJC:LOAD-FRAMEWORK

(OBJC:LOAD-FRAMEWORK framework-name interface-dir)

framework-name 是一个命名框架的字符串(例如, "Foundation", 或 "Cocoa"), interface-dir 是一个关键字, 用来命名和被命名框架相关联的接口数据库集合(例如, :foundation, 或 :cocoa)

假设被命名框架的接口数据库已经存在于标准搜索路径中, OBJC:LOAD-FRAMEWORK 通过搜索 OSX 的标准框架搜索路径找到并初始化框架束(bundle). 加载被命名框架可能新建 Objective-C 类和方法, 增加外部类型描述和入口点, 并且调整 Clozure CL 的分发函数.

如果你想使用的一个框架的接口数据库不存在, 你需要新建它们. 关于新建接口数据库的更多信息, 请参考 "13.5.2. Creating new interface directories"

14.8 如何把 Objective-C 名字映射到 Lisp 符号上 How Objective-C Names are Mapped to Lisp Symbols

对于 Cocoa 类, 消息等有一个标准的命名约定集. 只要遵循这个约定, 桥就能相对好地在 Objective-C 和 Lisp 名字之间自动转换.

例如, "NSOpenGLView" 变成 ns:ns-opengl-view; "NSURLHandleClient" 变成 ns:ns-url-handle-client; and "nextEventMatchingMask:untilDate:inMode:dequeue:" 变成 (:next-event-matching-mask :until-date :in-mode :dequeue). 多么拗口啊.

如果想了解一个给定的 Objective-C 或者 Lisp 名字如何被桥转换, 你可以使用下面的函数:

(ccl::objc-to-lisp-classname string)
(ccl::lisp-to-objc-classname symbol)
(ccl::objc-to-lisp-message string)
(ccl::lisp-to-objc-message string)
(ccl::objc-to-lisp-init string)
(ccl::lisp-to-objc-init keyword-list)

当然了, 对于任何命名约定总会出现例外. 如果你遇到任何看起来像是缺陷的转换问题, 请通过邮件列表告诉我们. 否则, 桥提供了两种处理例外的方法:

首先, 你可以把一个字符串当做 MAKE-OBJC-INSTANCE 的类名和 SEND 的消息来传递. 这些字符串将会被当做 Objective-C 名字不做转换直接解释. 对于一次性的例外来说这种方法很有用. 例如:

(ccl::make-objc-instance "WiErDclass")
(ccl::send o "WiErDmEsSaGe:WithARG:" x y)

其次, 你可以为你的例外定义一条特别的转换规则. 这对于你需要在代码中多次用到的例外名字很有用. 例如

(ccl::define-classname-translation "WiErDclass" wierd-class)
(ccl::define-message-translation "WiErDmEsSaGe:WithARG:" (:weird-message :with-arg))
(ccl::define-init-translation "WiErDiNiT:WITHOPTION:" (:weird-init :option))

Objective-C 命名的一般规则是名字中每个有意义的单词以大写字母开头(除了第一个单词). 如果按照字面意思使用这条规则, "NSWindow" 将会被错误地转换为 N-S-WINDOW. 在 Objectve-C 中 "NS" 是一个特殊的单词, 不应该被拆分成单个的大写字母. 就像 "URL", "PDF", "OpenGL" 等一样. 在 Cocoa 中使用的大多数常见的特殊单词已经在桥中定义好了, 但是你可以按照下述方式定义一些新的:

(ccl::define-special-objc-word "QuickDraw")

注意在 SEND 中的像 (SEND V :MOUSE P :IN-RECT R) 之类的消息关键字可能看起来像一个 Lisp 函数调用中的关键字参数, 但它们实际不是. 所有关键字必须出现并且顺序明显. 无论 (:IN-RECT :MOUSE) 还是 (:MOUSE) 都不会转换成 "mouse:inRect:"

另外, 作为一个特殊例外, 一个 "init" 前缀在 initializer 关键字中是可选的, 因此 (MAKE-OBJC-INSTANCE 'NS-NUMBER :INIT-WITH-FLOAT 2.7) 也能被表述成 (MAKE-OBJC-INSTANCE 'NS-NUMBER :WITH-FLOAT 2.7)

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!