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消息时程序实际的运行流程.

Image

为了指定消息接受者, 理解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
复制代码

程序的流程如下图:

Image

对象可以调用父类实现的方法

还有一个很重要的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: ...] 将会使用被重写后的实现, 输出大写字符串, 程序流程如下:

Image

然而我们对于XYZShoutingPerson的新的实现并不理想. 如果我们之后决定修改XYZPersonsaySomething:方法, 想要将输出显示到文本框中, 我们就不得不同时修改XYZShoutingPersonsaySomething:方法.

通过修改XYZShoutingPersonsaySomething:方法, 使其通过调用父类的实现去处理字符串,可能是一个更好的方式:

@implementation XYZShoutingPerson
- (void)saySomething:(NSString *)greeting {
    NSString *uppercaseGreeting = [greeting uppercaseString];
    [super saySomething:uppercaseGreeting];
}
@end
复制代码

程序流程如下图:

Image

对象是动态创建的

如本章之前描述的那样, 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 指针赋值, 如下图所示:

Image

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 也支持字面量语法创建NSArrayNSDictionary对象. // 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];
复制代码

虽然firstPersonsecondPerson都被静态指定为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
    }
复制代码

练习:

  1. 使用上节练习中的项目, 打开main.m 文件, 找到main() 函数, 和其它C编写的可执行文件一样, 这个函数是应用的入口函数. 使用alloc init 创建一个新的XYZPerson实例, 然后调用它的sayHello方法.

    Note: 如果编译器没有代码自动提示, 需要先把XYZPerson的头文件在main.m中引入.

  2. 实现saySomething:方法, 重写sayHello方法, 在sayHello方法中调用saySomething方法. 添加一些"打招呼"的方法并在你创建的实例中调用它们.

  3. 创建一个继承自XYZShoutingPerson的新类, 重写saySomething:方法,使其输出对应的全大写字符, 然后创建一个XYZShoutingPerson的实例测试是否结果符合预期.

  4. 实现XYZPerson类的person工厂方法 -- 返回一个正确分配内存并初始化的XYZPerson实例, 然后替换掉main.m中使用alloc init 创建的XYZPerson实例.

    Tip: 在类工厂中尝试使用[[self alloc] init] 来代替 [[XYZPerson] alloc] init] . 在类方法中使用self , self相当于类本身. 这样我们在XYZShoutingPerson类中就不必重写person方法, 也可以正确地创建实例对象了. 可以通过如下代码测试是否创建了正确类型的实例对象:

XYZShoutingPerson *shoutingPerson = [XYZShoutingPerson person];
复制代码
  1. 创建一个新的XYZPerson指针, 不要为其赋值, 使用条件判断检查这个变量是否被自动设置为nil

参考资料: Working with Objects

猜你喜欢

转载自juejin.im/post/7054385949962141703