Objective-C 对象的使用 (Working with objects)
Objective-C 应用内的主要工作就是对象之间的消息发送. 一些对象是Cocoa或者Cocoa Touch提供的类的实例, 另一些是我们的自定义的对象.
前一篇文章描述了如何定义类的接口和实现, 提及了如何实现方法以响应一个消息. 这篇文章主要讲述如何发送给对象发送一个消息, 包含一些Objective-C的动态特性,动态类型,以及运行时方法决议.
在一个对象可以使用之前, 它必须为它的属性开辟内存空间(Memory allocation)并为内部值做好初始化(Initialization)工作. 这篇文章描述如何连续调用开辟内存空间方法以及初始化方法以保证对象可以被正确地设置.
对象的消息发送与接收
虽然在Objective-C中有很多种不同的方法发送消息. 但是到目前为止, 最常见的使用方括号[]
语法, 示例如下:
[someObject doSomething]; 复制代码
左侧的someObject
代表着消息的接收者(Receiver). 右侧的doSomething
表示调用接收者的方法名称. 当上面这行代码被调用时, 就向 someObject
发送了doSomething
的消息.
前一篇文章描述了如何创建类接口, 如下所示:
@interface XYZPerson : NSObject - (void)sayHello; @end 复制代码
也描述了如何实现一个类, 如下所示:
@implementation XYZPerson - (void)sayHello { NSLog(@"Hello, world!"); } @end 复制代码
Note: 这个示例中使用了Objective-C 字符串语法, @"Hello, world!"
. NSString
类是Objective-C中允许使用语法糖创建实例的类之一. @"Hello, world!"
相当于创建了一个Objective-C string 类型的代表着"Hello,world!"的对象.
假设我们已经有了一个XYZPerson
对象, 我们可以像这样给它发送sayHello
的消息.
[somePerson sayHello]; 复制代码
发送Objective-C消息和调用C函数从概念上很相像. 下图展示了发送sayHello
消息时程序实际的运行流程.
为了指定消息接受者, 理解Objective-C中指针是如何指向对象是很重要的.
使用指针跟踪对象
C和Objective-C 使用变量保存值, 就像其它大多数编程语言一样.
在标准C语言中有定义了很多值变量类型, 包括整型(integers), 浮点型(floating-point numbers), 和字符类型(characters), 它们是这样声明并赋值的:
int someInteger = 42; float someFloatingPointNumber = 3.14f; 复制代码
声明局部变量 -- 在方法或函数中声明的变量, 他们的作用域限制在它们所被定义的方法内部.
- (void)myMethod { int someInteger = 42; } 复制代码
在这个例子中, someInteger
是声明在myMethod
内部的局部变量. 一旦程序执行到}
花括号末尾, someInteger
便不可被访问. 当一个局部的值类型变量(比如int
, 或者float
)的所在作用域结束之后, 它们的值也就不在了.
相反地, Objective-C对象的内存分配有些许不同. 对象通常有更长的生命周期. 对象本身通常需要比存储它的变量活得更久, 所以对象的内存是动态分配/回收的.
Note: 如果用栈(Stack)或者堆(Heap)这种术语来描述的话, 一个局部变量的空间分配在栈上, 而对象空间分配在堆上.
这就需要我们使用保存了对象内存地址的C指针来跟踪它们在内存中的位置:
- (void)myMethod { NSString *myString = // get a string from somewhere... [...] } 复制代码
虽然指针变量myString
的作用域(*
表示它是一个指针)被限制在myMethod内部, 但是它指向的字符串对象在内存中也许生命周期更长一些.
传递对象作为方法参数
如果发送消息的时候需要传递对象作为参数, 我们把对象指针作为参数传入. 前一篇文章描述了单参数方法的声明:
- (void)someMethodWithValue:(SomeType)value; 复制代码
当传入一个NSString
类型的对象时, 是这样的:
- (void)saySomething:(NSString *)greeting; 复制代码
我们可以这样实现:
- (void)saySomething:(NSString *)greeting { NSLog(@"%@", greeting); } 复制代码
greeting
指针就像局部变量一样, 它的作用域被限制在saySomething
方法内部, 虽然它所指向的字符串对象在方法调用前就已经存在了, 而且在方法完成之后还将继续存在.
Note: 和C的标准库函数printf()
相似, NSLog()
使用了格式说明符(format specifiers)来表明占位格式. 在控制台输出的字符串是格式化字符串(第一个参数)中插入对应的字符串(剩余参数)的结果.
Objective-C中有一个额外的占位符%@
, 用来代指一个对象. 在运行时, 这个标识符将会被对象的descriptionWithLocale:
方法(如果它存在)或者description
方法替代. NSObject
类对于description
方法的实现是返回对象所属的类和内存地址, 但是很多Cocoa 和 Cocoa Touch 类重写了这个方法以提供更加有用的信息. 比如NSString
类, description
方法直接返回它所代表的字符串值.
更多NSLog()
,NSString
类的格式符, 请参考 String Format Specifiers.
方法可以返回值
我们可以向方法中传入参数, 方法也可以拥有返回值. 文章中目前为止的每个方法都返回了void
类型的返回值. C的void
关键字表示这个方法没有返回值.
指定返回值类型为int
意味着方法返回一个值类型的整型数值:
- (int)magicNumber; 复制代码
方法的实现使用了C的return
, 表明return
后的值应当在方法结束后传回给调用者:
- (int)magicNumber { return 42; } 复制代码
我们完全可以忽略方法的返回值. 在这里例子里, 虽然magicNumber
除了返回值之外什么事都没有做, 但是这样调用方法不会有任何问题.
[someObject magicNumber]; 复制代码
如果我们确实需要获取返回值, 我们可以声明一个变量并把方法返回值赋予给它:
int interestingNumber = [someObject magicNumber]; 复制代码
我们也可以以同样的方法返回对象, 比如NSString
就提供了一个返回NSString *
的uppercaseString
方法.
- (NSString *)uppercaseString; 复制代码
返回对象的方法和返回数值类型的方法几乎一样, 只是我们需要使用指针来保存结果.
NSString *testString = @"Hello, world!"; NSString *revisedString = [testString uppercaseString]; 复制代码
当上面两行代码调用结束后, revisedString
将会指向一个表示HELLO WORLD!
的字符串.
当我们像这样实现方法, 返回一个对象时:
- (NSString *)magicString { NSString *stringToReturn = // create an interesting string... return stringToReturn; } 复制代码
这个字符串对象仍然存在, 即使stringToReturn
指针的作用域已经结束了.
这里就涉及到Objective-C的内存管理. 这种情况下在堆上创建的对象需要存在足够长的时间, 以保证方法调用者可以正常使用返回值, 但是并不能让这个对象永远存在, 否则就产生了内存泄漏(memory leak). 通常来说, Objective-C编译器的ARC技术(Automatic Reference Count - 自动引用计数)会帮我们管理这些内存.
对象可以给他们自己发送消息
每当我们写方法实现时, 我们都可以获取到一个重要的隐藏值, self
. 从概念上讲, self
是一种获取"接收到这条消息的对象"的方式. 它是一个指针, 就像上面提到过的greeting
对象一样, 可以用来在接收到消息的对象上调用方法.
假设我们需要重构XYZPerson
的实现, 可以在它的sayHello
方法内部使用saySomething:
方法. 这意味着我们以后可以添加更多方法, 比如sayGoodbye
, 这些方法都将通过saySomething:
方法来处理逻辑. 如果我们之后需要在文本框中显示结果, 我们只需要修改saySomething:
一个方法, 不必每个方法都修改了.
@implementation XYZPerson - (void)sayHello { [self saySomething:@"Hello, world!"]; } - (void)saySomething:(NSString *)greeting { NSLog(@"%@", greeting); } @end 复制代码
程序的流程如下图:
对象可以调用父类实现的方法
还有一个很重要的Objective-C 关键字 -- super
. 给super
发送消息是一种调用继承链中父类的方法的方式. 最常见的使用super
关键字的场景就是在重写(override)方法时.
假设我们想要创建一个类似于Person
类的ShoutingPerson
类, 所有的输出都使用大写字母. 我们虽然可以复制整个XYZPerson
类, 然后把对应的实现改成输出大写字母, 但是更简单的方法是使用继承, 然后重写saySomething:
方法, 让它输出大写字母:
@interface XYZShoutingPerson : XYZPerson @end 复制代码
@implementation XYZShoutingPerson - (void)saySomething:(NSString *)greeting { NSString *uppercaseGreeting = [greeting uppercaseString]; NSLog(@"%@", uppercaseGreeting); } @end 复制代码
这个例子声明了一个额外的字符串对象指针uppercaseGreeting
, 之后给greeting
对象发送uppercaseString
消息, 并将该消息的返回值对uppercaseGreeting
进行赋值. 正如我们之前看到的那样,这是一个通过把原字符串转换成全大写的新生成的字符串对象.
由于sayHello
是由XYZPerson
实现的, 并且XYZShoutingPerson
继承自XYZPerson
, 我们也可以在XYZShoutingPerson
实例对象上调用sayHello
方法. 当我们在XYZShoutingPerson
实例对象上调用sayHello
方法时, [self saySomething: ...]
将会使用被重写后的实现, 输出大写字符串, 程序流程如下:
然而我们对于XYZShoutingPerson
的新的实现并不理想. 如果我们之后决定修改XYZPerson
的saySomething:
方法, 想要将输出显示到文本框中, 我们就不得不同时修改XYZShoutingPerson
的saySomething:
方法.
通过修改XYZShoutingPerson
的saySomething:
方法, 使其通过调用父类的实现去处理字符串,可能是一个更好的方式:
@implementation XYZShoutingPerson - (void)saySomething:(NSString *)greeting { NSString *uppercaseGreeting = [greeting uppercaseString]; [super saySomething:uppercaseGreeting]; } @end 复制代码
程序流程如下图:
对象是动态创建的
如本章之前描述的那样, Objective-C对象的内存是动态创建的. 创建对象的第一步就是为对象分配足够的内存空间,这里的分配空间不仅仅是为对象本身类的属性分配空间,还包括为继承链中的所有类中的属性分配空间.
根类NSObject
提供了一个类方法alloc
, 为我们处理了上述流程.
+ (id)alloc; 复制代码
注意到这个方法的返回值类型是id
. 这是Objective-C语言中特殊的关键字, 用于表示"某种对象". 他是指向对象的一个指针, 就像(NSObject )
那样. 但是特殊的一点是, id
不使用星号. 后文会详细描述它.
alloc
方法还有另外一个重要的任务: 清理分配给对象属性的空间,并将它们全部设置为0
. 这样可以避免内存残留之前存储的垃圾信息, 但是这对于完全初始化(initialize)对象来说还是不够的.
我们需要组合alloc
方法和init
方法:
- (id)init; 复制代码
对象使用init
方法以确保它所有的属性在创建时拥有合适的初始值, 这会在下一篇文章中详细说明. //TODO: Add a link.
注意到init
方法的返回值类型也是id
.
如果一个方法返回了一个对象的指针. 可以嵌套(nest)方法调用. 可以通过嵌套调用alloc
init
为一个对象正确地分配内存并初始化:
NSObject *newObject = [[NSObject alloc] init]; 复制代码
上面的例子让变量newObject
指向一个全新创建的NSObject
实例.
嵌套层级中, 最内部的方法最先调用, 因此NSObject
类先收到alloc
消息, 之后返回一个分配了内存空间的NSObject
实例对象, 然后这个返回的实例对象又作为init
消息的消息接收者, 最后返回一个初始化后的对象并对newObject
指针赋值, 如下图所示:
Note: init
方法返回的对象有可能和alloc
方法返回的对象不同. 所以最好像上面那样嵌套调用alloc
, init
.
永远不要像下面这样初始化一个对象后, 不把它赋值给任何变量.
// 不要这样做! NSObject *someObject = [NSObject alloc]; [someObject init]; 复制代码
如果init
方法返回了另外一个对象, someObject
指向的就是分配了内存但是没有经过初始化的NSObject
对象.
向初始化方法中添加参数
一些对象需要在初始化时为其提供必需的值. 比如 NSNumber
对象, 必须在初始化时为其提供一个它所要表示的数值. NSNumber
类定义了一些初始化方法:
- (id)initWithBool:(BOOL)value; - (id)initWithFloat:(float)value; - (id)initWithInt:(int)value; - (id)initWithLong:(long)value; 复制代码
含参的初始化方法调用起来和不含参的初始化方法差不多:
NSNumber *magicNumber = [[NSNumber alloc] initWithInt:42]; 复制代码
类的工厂方法提供了内存分配+初始化的快捷途径
如上文所述, 类可以定义工厂方法. 工厂方法提供了alloc
, init
的快捷方式, 不再需要嵌套调用alloc
,init
方法.
NSNumber
类就定义了和它的初始化方法相对应的一些工厂方法:
+ (NSNumber *)numberWithBool:(BOOL)value; + (NSNumber *)numberWithFloat:(float)value; + (NSNumber *)numberWithInt:(int)value; + (NSNumber *)numberWithLong:(long)value; 复制代码
可以像这样调用工厂方法获取实例对象:
NSNumber *magicNumber = [NSNumber numberWithInt:42]; 复制代码
这个和上面使用alloc``initWithInt
几乎是相同的. 类工厂方法通常来说也只是通过alloc
和相关的init
方法实现的, 只是使用起来更加便捷.
当不需要传入初始化参数时, 使用new
方法创建对象
可以使用new
类方法创建新的类实例对象. 这个方法是由NSObject
类提供的, 子类不需要重写.
它几乎等价于 调用alloc
和无参数的init
方法.
XYZObject *object = [XYZObject new]; // is effectively the same as: XYZObject *object = [[XYZObject alloc] init]; 复制代码
使用字面量语法(Literal Syntax)快速创建对象
一些类允许通过一种更加简洁的字面量语法创建实例对象.
比如我们可以通过@
这种字面量快速创建字符串对象:
NSString *someString = @"Hello, World!"; 复制代码
这和alloc
init
一个NSString
或者使用一个类工厂方法几乎是等价的.
NSString *someString = [NSString stringWithCString:"Hello, World!" encoding:NSUTF8StringEncoding]; 复制代码
NSNumber
类也提供了很多字面量语法:
NSNumber *myBOOL = @YES; NSNumber *myFloat = @3.14f; NSNumber *myInt = @42; NSNumber *myLong = @42L; 复制代码
上述的例子也都和调用alloc
init
或者类工厂方法是等价的.
我们也可以使用表达式创建一个NSNumber
实例:
NSNumber *myInt = @(84 / 2); 复制代码
如上例子中, 会先计算表达式的值, 再通过这个值创建NSNumber
实例.
Objective-C 也支持字面量语法创建NSArray
和NSDictionary
对象. // TODO: add a link
Objective-C是一门动态语言
如之前提及的那样, 我们需要通过指针来跟踪对象的内存. 由于Objective-C 的动态特性, 存储对象的指针的类型无关紧要, 当我们向它发送消息时, Objective-C 总会找到正确的方法并去调用它.
id
类型定义了一个通配对象类型指针. 我们可以在定义对象变量时使用id
关键字, 但是我们会丢失对象的编译时信息.
id someObject = @"Hello, World!"; [someObject removeAllObjects]; 复制代码
上面的例子中, someObject
指向一个NSString
类型的实例对象, 但是编译器并不知道这点. removeAllObjects
消息是由Cocoa 或者 Cocoa Touch 对象(比如 NSMutableArray
)定义的, 所以编译器不会报错. 但是在运行时由于NSString
对象无法响应removeAllObjects
消息, 就会产生运行时异常报错.
重写上述代码使用静态类型:
NSString *someObject = @"Hello, World!"; [someObject removeAllObjects]; 复制代码
此时, 编译器就会报错: 在NSString
的公共接口声明中并不能找到removeAllObjects
.
由于对象的类是在运行时决议的, 在创建时或是使用时指定对象的类别是没有区别的.
XYZPerson *firstPerson = [[XYZPerson alloc] init]; XYZPerson *secondPerson = [[XYZShoutingPerson alloc] init]; [firstPerson sayHello]; [secondPerson sayHello]; 复制代码
虽然firstPerson
和secondPerson
都被静态指定为XYZPerson
类型, 但是在运行时, secondPerson
指向的是XYZShoutingPerson
类型的变量. 当向这两个对象发送sayHello
消息时, 运行时会找到正确的实现. 对于secondPerson
来说, 会使用XYZShoutingPerson
中的sayHello
方法.
判断对象是否相等
如果我们需要判断两个对象是否相等时, 我们需要牢记我们是通过指针获取对象的.
标准C 的 ==
操作符是用来比较两个变量的值是否相等的:
if (someInteger == 42) { // someInteger has the value 42 } 复制代码
当我们比较对象时, ==
操作符是用来比较两个指针是否指向同一个对象的:
if (firstPerson == secondPerson) { // firstPerson is the same object as secondPerson } 复制代码
当我们需要比较两个对象表示的数据是否相同时, 我们需要调用NSObject
提供的类似isEqual:
方法.
如果我们需要比较两个对象表示的值的大小时, 我们不应当使用C的比较操作符<
或者>
. 而应当使用基本Foundation
类型, 如NSNumber
, NSString
, NSDate
提供的compare:
方法:
if ([someDate compare:anotherDate] == NSOrderedAscending) { // someDate is earlier than anotherDate } 复制代码
使用nil
在定义值类型的变量时, 最好总是在定义时就完成初始化操作, 否则他们的初始值可能会包含之前内存中垃圾信息.
BOOL success = NO; int magicNumber = 42; 复制代码
但是这对于对象类型指针来说不是必要的. 如果我们不为对象指定初始值, 编译器会自动为它赋值为nil
XYZPerson *somePerson; // somePerson is automatically set to nil 复制代码
如果我们没有值给对象初始化时, 使用nil
是最安全的. 在Objective-C中给nil
发送消息是完全没问题的. 如果给nil
发送消息, 什么都不会发生.
Note: 如果我们希望获取发送给nil
对象消息的返回值, 如果返回值类型是对象类型, 我们会获取到nil
, 如果是数值类型, 会获取到0
, 如果是BOOL
类型, 会获取到NO
. 返回的结构体的所有成员都被初始化为0
.
如果我们需要检查确认一个对象不为nil
, 可以使用C的!=
操作符:
if (somePerson != nil) { // somePerson points to an object } 复制代码
或者直接提供变量作为判断条件:
if (somePerson) { // somePerson points to an object } 复制代码
如果somePerson
为nil, 它的逻辑值为0(false). 如果它拥有一个地址, 地址值一定不为0
(true).
类似地, 如果我们需要检查对象是否为nil
, 我们可以使用==
, 也可以直接使用!
:
if (somePerson == nil) { // somePerson does not point to an object } 复制代码
if (!somePerson) { // somePerson does not point to an object } 复制代码
练习:
使用上节练习中的项目, 打开
main.m
文件, 找到main()
函数, 和其它C编写的可执行文件一样, 这个函数是应用的入口函数. 使用alloc
init
创建一个新的XYZPerson
实例, 然后调用它的sayHello
方法.Note: 如果编译器没有代码自动提示, 需要先把
XYZPerson
的头文件在main.m
中引入.实现
saySomething:
方法, 重写sayHello
方法, 在sayHello
方法中调用saySomething
方法. 添加一些"打招呼"的方法并在你创建的实例中调用它们.创建一个继承自
XYZShoutingPerson
的新类, 重写saySomething:
方法,使其输出对应的全大写字符, 然后创建一个XYZShoutingPerson
的实例测试是否结果符合预期.实现
XYZPerson
类的person
工厂方法 -- 返回一个正确分配内存并初始化的XYZPerson
实例, 然后替换掉main.m
中使用alloc
init
创建的XYZPerson
实例.Tip: 在类工厂中尝试使用
[[self alloc] init]
来代替[[XYZPerson] alloc] init]
. 在类方法中使用self
,self
相当于类本身. 这样我们在XYZShoutingPerson
类中就不必重写person
方法, 也可以正确地创建实例对象了. 可以通过如下代码测试是否创建了正确类型的实例对象:
XYZShoutingPerson *shoutingPerson = [XYZShoutingPerson person]; 复制代码
创建一个新的
XYZPerson
指针, 不要为其赋值, 使用条件判断检查这个变量是否被自动设置为nil
作者:Onion_Knight
链接:https://juejin.cn/post/7054385949962141703