在Objective-C中,为什么[object doSomething]
?不是[*object doSomething]
,因为你在对象上调用一个方法吗?这意味着你应该取消引用指针?
答案 0 :(得分:112)
答案回到了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函数调用声明调用某些东西(所有这些都用方法语法编写)。
对象引用的执行是因为它给了messenger - objc_msgSend()访问类然后找到方法实现 - 以及那个引用然后成为第一个参数 - 方法的self - 最终会被执行。
如果你真的想深入,start here。但是在完全grokked this之前不要打扰。
答案 1 :(得分:15)
你不应该真的认为这些是指向对象的指针。这是一个历史实现细节,它们是指针,并且你在消息发送语法中使用它们(参见@bbum的答案)。实际上,它们只是“对象标识符”(或引用)。让我们回顾一下,看看概念的基本原理。
Objective-C首先在本书中提出并讨论:Object-Oriented Programming: An Evolutionary Approach。对于现代Cocoa程序员来说,它并不是非常实用,但语言的动机就在那里。
请注意,在本书中,所有对象都被赋予id
类型。你根本没有看到书中更具体的Object *
;当我们谈论“为什么”时,这些只是抽象的漏洞。以下是这本书的内容:
对象标识符必须唯一地标识任何时候系统中共存的对象。它们存储在局部变量中,作为参数传递给消息表达式和函数调用,保存在实例变量(对象内的字段)和其他类型的内存结构中。换句话说,它们可以像基本语言的内置类型一样流畅地使用。
对象标识符如何实际标识对象是一个实现细节,许多选择都是合理的。合理的选择,当然是最简单的选择,也是Objective-C中使用的选择,是使用内存中对象的物理地址作为其标识符。 Objective-C通过在每个文件中生成一个typedef语句使C知道这个决定。这根据C已经理解的另一种类型(即指向结构的指针)定义了一个新类型id。 [...]
id占用固定数量的空间。 [...]此空间与对象本身中私有数据占用的空间不同。
(pp58-59,2nd ed。)
所以你的问题的答案有两个:
严格类型的语法,你说“一个特定于NSString类型的对象”,因此使用NSString *
是一个更现代的变化,基本上是一个实现选择,相当于id
。
如果这似乎是对指针解除引用问题的高度回应,请务必记住,根据语言的定义,Objective-C中的对象是“特殊的”。它们作为结构实现并作为指向结构的指针传递,但它们在概念上是不同的。
答案 2 :(得分:10)
因为objc_msgSend()声明如下:
id objc_msgSend(id theReceiver, SEL theSelector, ...)
答案 3 :(得分:9)
答案 4 :(得分:6)
你永远不会取消引用对象指针,句点。它们被键入为指针而不仅仅是“对象类型”的事实是语言C传承的工件。它完全等同于Java的类型系统,其中始终通过引用访问对象。你永远不会在Java中取消引用一个对象 - 事实上,你不能。您不应该将它们视为指针,因为从语义上讲,它们不是。它们只是对象引用。
答案 5 :(得分:4)
部分原因是你会左右指向空指针异常。允许向nil
发送消息,并且通常完全合法(它什么都不做,不会产生错误)。
但你可以认为它类似于C ++的->
符号:它执行方法并在一段语法糖中取消引用指针。
答案 6 :(得分:4)
我这样说:语言与一系列字母相关联只是一种惯例。设计Objective-C的人决定
[x doSomething];
表示“将doSomething
消息发送到 x指向的对象”。他们这样定义,你按照规则:)
Objective-C的一个特点,与例如C ++,它没有语法来保存对象本身,而不是指向对象的指针。所以,
NSString* string;
没问题,但是
NSString string;
是非法的。如果后者是可能的,则必须有一种方法“将消息capitalizedString
发送到字符串string
”,而不是“将消息capitalizedString
发送到字符串由 string
指出。但实际上,您总是向源代码中的变量指向的对象发送消息。
因此,如果Objective-C的设计者遵循了您的逻辑,那么您必须编写
[*x doSomething];
每次发送消息时......您都会看到*
需要在前导括号[
之后总是,形成组合[*
。在那个阶段,我相信您同意重新设计语言更好,这样您只需要通过更改字母序列[
的含义来编写[*
而不是[x doSomething]
。
答案 7 :(得分:3)
Objective-C中的对象本质上是struct
。 struct
的第一个成员是Class isa
(可以使用struct
确定isa
的总大小。 struct
的后续成员可能包含实例变量。
当您声明一个Objective-C对象时,您总是将其声明为指针类型,因为运行时将您的对象提供给其他方法和函数;如果这些更改了struct
的任何成员(通过修改实例变量等),它们将“应用”到对象的所有引用,而不仅仅是在方法或函数中的本地引用。
答案 8 :(得分:2)
Objective-C运行时可能需要将对象反弹到几个不同的函数,因此它需要对象引用,而不是对象本身。