【发布时间】:2011-01-12 10:49:49
【问题描述】:
在 Objective-C 中,为什么是 [object doSomething]?难道不是[*object doSomething],因为你在对象上调用一个方法吗?这意味着你应该取消引用指针?
【问题讨论】:
标签: objective-c pointers
在 Objective-C 中,为什么是 [object doSomething]?难道不是[*object doSomething],因为你在对象上调用一个方法吗?这意味着你应该取消引用指针?
【问题讨论】:
标签: objective-c pointers
答案可以追溯到 Objective-C 的 C 根源。 Objective-C 最初是作为 C 的编译器预处理器编写的。也就是说,Objective-C 并没有被编译,而是直接转换为 C 然后编译。
从id 类型的定义开始。它被声明为:
typedef struct objc_object {
Class isa;
} *id;
也就是说,id 是指向第一个字段类型为 Class 的结构的指针(它本身是指向定义类的结构的指针)。现在,考虑NSObject:
@interface NSObject <NSObject> {
Class isa;
}
注意NSObject的布局和id指向的类型的布局是一样的。这是因为,实际上,Objective-C 对象的实例实际上只是一个指向结构的指针,该结构的第一个字段(始终是指针)指向包含该实例方法的类(以及一些其他元数据) )。
当您继承 NSObject 并添加一些实例变量时,出于所有意图和目的,您只需创建一个新的 C 结构,其中包含您的实例变量作为该结构中的插槽,并连接在所有超类的实例变量的插槽上。 (现代运行时的工作方式略有不同,因此超类可以附加 ivars 而无需重新编译所有子类)。
现在,考虑这两个变量之间的区别:
NSRect foo;
NSRect *bar;
(NSRect 是一个简单的 C 结构——不涉及 ObjC)。 foo 是使用堆栈上的存储创建的。一旦堆栈框架关闭,它将无法生存,但您也不必释放任何内存。 bar 是对 NSRect 结构的引用,该结构很可能是使用 malloc() 在堆上创建的。
如果你试着说:
NSArray foo;
NSArray *bar;
编译器会抱怨第一个,说类似Objective-C 中不允许基于堆栈的对象。换句话说,所有Objective-C 对象必须从堆中分配(或多或少——有一两个例外,但它们对于本次讨论比较深奥),因此,你总是通过堆上所述对象的地址来引用一个对象;您总是在使用指向对象的指针(id 类型实际上只是指向任何旧对象的指针)。
回到该语言的 C 预处理器根源,您可以将每个方法调用转换为等效的 C 行。例如,以下两行代码是相同的:
[myArray objectAtIndex: 42];
objc_msgSend(myArray, @selector(objectAtIndex:), 42);
类似地,这样声明的方法:
- (id) objectAtIndex: (NSUInteger) a;
相当于这样声明的C函数:
id object_at_index(id self, SEL _cmd, NSUInteger a);
而且,查看objc_msgSend(),第一个参数被声明为id 类型:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);
这正是您不使用*foo 作为方法调用目标的原因。通过上述形式进行转换——对[myArray objectAtIndex: 42] 的调用被转换为上面的 C 函数调用,然后它必须调用具有等效 C 函数调用声明的东西(全部以方法语法装饰)。
对象引用被执行是因为它让信使——objc_msgSend() 可以访问类,然后找到方法实现——以及该引用然后成为方法的第一个参数——self最终被执行。
如果你真的想深入,start here。但是在您完全拥有grokked this 之前不要打扰。
【讨论】:
您不应该真的将这些视为指向对象的指针。这是一种历史实现细节,它们是指针,并且您在消息发送语法中使用它们(请参阅@bbum 的答案)。事实上,它们只是“对象标识符”(或引用)。让我们稍微回顾一下,看看概念上的基本原理。
Objective-C 在本书中首次提出和讨论:Object-Oriented Programming: An Evolutionary Approach。对于现代 Cocoa 程序员来说,它并不是非常实用,但该语言的动机就在那里。
请注意,本书中所有对象的类型都为id。你在书中根本看不到更具体的Object *s;当我们谈论“为什么”时,这些只是抽象的泄漏。这本书是这样说的:
对象标识符必须在任何时候唯一标识系统中可能共存的尽可能多的对象。它们存储在局部变量中,在消息表达式和函数调用中作为参数传递,保存在实例变量(对象内的字段)和其他类型的内存结构中。换句话说,它们可以像基础语言的内置类型一样流畅地使用。
对象标识符如何实际识别对象是一个实现细节,许多选择都是合理的。一个合理的选择,当然是最简单的选择之一,也是 Objective-C 中使用的选择,是使用内存中对象的物理地址作为其标识符。 Objective-C 通过在每个文件中生成一个 typedef 语句来让 C 知道这个决定。这根据 C 已经理解的另一种类型定义了一个新类型 id,即指向结构的指针。 [...]
一个 id 占用固定数量的空间。 [...] 这个空间与对象本身的私有数据占用的空间不同。
(pp58-59,第 2 版)
所以你的问题的答案是双重的:
你说“一个特定类型的对象 NSString”并因此使用 NSString * 的严格类型语法是一种更现代的变化,基本上是一种实现选择,相当于 id。
如果这似乎是对指针解引用问题的高尚回答,请务必记住,根据语言的定义,Objective-C 中的对象是“特殊的”。它们作为结构实现并作为指向结构的指针传递,但它们在概念上是不同的。
【讨论】:
因为objc_msgSend() 是这样声明的:
id objc_msgSend(id theReceiver, SEL theSelector, ...)
【讨论】:
【讨论】:
你永远不会取消引用对象指针,句号。它们被键入为指针而不仅仅是“对象类型”这一事实是该语言的 C 遗产的产物。它完全等同于 Java 的类型系统,其中对象总是通过引用来访问。在 Java 中你永远不会取消引用一个对象——事实上,你不能。您不应该将它们视为指针,因为从语义上讲,它们不是。它们只是对象引用。
【讨论】:
部分原因是您会左右出现空指针异常。向nil 发送消息是允许的,而且通常是完全合法的(它什么都不做,也不会产生错误)。
但您可以将其视为类似于 C++ 的-> 表示法:它执行方法并取消引用一个语法糖中的指针。
【讨论】:
nil 发送消息是我如此喜欢Objective-C 的原因之一。能够使用 if([myString length]) 测试非空字符串或使用 if([myArray count]) 测试非空数组,这比在查询之前必须明确确保对象有效。
我会这样说:一种语言与一系列字母相关的只是一种约定。设计 Objective-C 的人认为
[x doSomething];
表示“将doSomething 消息发送到由 x 指向的对象”。他们是这样定义的,你遵守规则:)
Objective-C 的一个特点,与例如C++,是它没有语法来保存对象本身,而不是指向对象的指针。所以,
NSString* string;
没问题,但是
NSString string;
是非法的。如果后者是可能的,则必须有一种方法可以“将消息capitalizedString 发送到字符串string”,而不是“将消息capitalizedString 发送到字符串指向的 string"。但实际上,您总是向源代码中的变量指向的对象发送消息。
所以,如果 Objective-C 的设计者遵循了你的逻辑,你将不得不编写
[*x doSomething];
每次发送消息...您会看到,* 需要始终出现在前括号 [ 之后,形成 [* 的组合。在那个阶段,我相信你同意最好重新设计语言,这样你只需写[而不是[*,通过改变字母序列[x doSomething]的含义。
【讨论】:
Objective-C 中的对象本质上是struct。 struct 的第一个成员是Class isa(struct 的总大小可以使用isa 确定)。 struct 的后续成员可能包括实例变量。
当你声明一个 Objective-C 对象时,你总是将它声明为指针类型,因为运行时会将你的对象提供给其他方法和函数;如果这些更改了struct 的任何成员(通过修改实例变量等),它们将“应用”到对您的对象的所有引用,而不仅仅是在方法或函数中的本地引用。
【讨论】:
Objective-C 运行时可能需要将对象反弹到几个不同的函数,因此它需要对象引用,而不是对象本身。
【讨论】: