上一章Objective-C编程快速入门教程请查看:定义类?
使用对象Objective-C应用程序中的大部分工作都是在一个对象生态系统中来回发送消息的结果。这些对象中有些是Cocoa或Cocoa Touch提供的类的实例,有些是你自己类的实例。
前一章描述了定义接口和类实现的语法,包括实现方法的语法,这些方法包含响应消息时要执行的代码。本章解释了如何将这样的消息发送给对象,并介绍了Objective-C的一些动态特性,包括动态类型和在运行时确定应该调用哪个方法的能力。
在使用对象之前,必须使用其属性的内存分配和内部值的任何必要初始化的组合来正确地创建对象。本章描述如何嵌套方法调用来分配和初始化对象,以确保正确配置对象。
对象发送和接收消息虽然在Objective-C中有几种不同的发送消息的方式,但目前为止最常见的是使用方括号的基本语法,比如:
[someObject doSomething];
左边的引用someObject(在本例中)是消息的接收者。右边的消息doSomething是调用该接收者的方法的名称。换句话说,当执行上面这行代码时,someObject将被发送doSomething消息。
前一章描述了如何创建类的接口,如下所示:
@interface XYZPerson : NSObject
- (void)sayHello;
@end
以及如何创建类的实现,像这样
@implementation XYZPerson
- (void)sayHello {
NSLog(@"Hello, world!");
}
@end
注意:这个例子使用了一个Objective-C字符串字面量@“Hello, world!”字符串是Objective-C中的几种类类型之一,它们的创建允许使用简写的字面量语法。指定@“Hello, world!”在概念上等同于说“一个Objective-C字符串对象,它表示字符串Hello, world!”
文本和对象创建将在本章后面的“动态创建对象”中进一步解释。
假设你有一个XYZPerson对象,你可以像这样发送sayHello消息:
[somePerson sayHello];
发送Objective-C消息在概念上很像调用C函数。图2-1显示了sayHello消息的有效程序流。
图2-1基本消息传递流程图解
文章图片
为了指定消息的接收者,理解在Objective-C中指针是如何被用来引用对象的是很重要的。
使用指针来跟踪对象
像大多数其他编程语言一样,C和Objective-C使用变量来跟踪值。
在标准C中定义了许多基本的标量变量类型,包括整数、浮点数和字符,它们的声明和赋值方式如下:
int someInteger = 42;
float someFloatingPointNumber = 3.14f;
局部变量是在方法或函数中声明的变量,如下所示:
- (void)myMethod {
int someInteger = 42;
}
在定义它们的方法范围内受限制。
在这个例子中,someInteger被声明为myMethod中的一个局部变量; 一旦执行到达方法的右大括号,someInteger将不再可访问。当局部标量变量(如int或float)消失时,该值也将消失。
相比之下,Objective-C对象的分配略有不同。对象的生命周期通常比方法调用的简单作用域长。特别是,一个对象通常需要比原来为跟踪它而创建的变量存活更长的时间,所以对象的内存是动态分配和释放的。
注意:如果你习惯于使用栈和堆之类的术语,则会在栈上分配局部变量,而在堆上分配对象。
这需要你使用C指针(它保存内存地址)来跟踪它们在内存中的位置,就像这样:
- (void)myMethod {
NSString *myString = // get a string from somewhere...
[...]
}
虽然指针变量myString的作用域(星号表示它是一个指针)仅限于myMethod的作用域,但它在内存中所指向的实际字符串对象在该作用域之外可能有更长的生存期。例如,它可能已经存在,或者你可能需要在其他方法调用中传递对象。
你可以为方法参数传递对象
如果在发送消息时需要传递对象,则需要为方法参数之一提供对象指针。前一章描述了用单个参数声明方法的语法:
- (void)someMethodWithValue:(SomeType)value;
因此,声明一个接受string对象的方法的语法是这样的:
- (void)saySomething:(NSString *)greeting;
你可以这样实现saySomething:方法:
- (void)saySomething:(NSString *)greeting {
NSLog(@"%@", greeting);
}
greeting指针的行为类似于一个局部变量,它的作用域仅限于saySomething:方法,即使它指向的实际字符串对象在方法被调用之前已经存在,并且在方法完成后仍然存在。
注意:NSLog()使用格式说明符来表示替换标记,就像C标准库printf()函数一样。登录到控制台的字符串是通过插入提供的值(其余的参数)来修改格式字符串(第一个参数)的结果。
在Objective-C中还有一个额外的替换令牌,%@,用来表示一个对象。在运行时,这个说明符将被调用descriptionWithLocale:方法(如果存在的话)或所提供对象上的描述方法的结果所代替。description方法由NSObject实现,以返回对象的类和内存地址,但是许多Cocoa和Cocoa Touch类会覆盖它,以提供更有用的信息。对于NSString, description方法只是返回它所表示的字符串。
有关与NSLog()和NSString类一起使用的可用格式说明符的更多信息,请参见字符串格式说明符。
方法可以返回值
除了通过方法参数传递值之外,方法还可以返回值。到目前为止,本章所示的每个方法都有一个void返回类型。C void关键字意味着一个方法不返回任何东西。
指定int的返回类型意味着该方法返回一个标量整数值:
- (int)magicNumber;
该方法的实现使用了C return语句来表示方法执行完成后应该返回的值,如下所示:
- (int)magicNumber {
return 42;
}
忽略方法返回值的事实是完全可以接受的。在这种情况下,magicNumber方法除了返回一个值之外没有做任何有用的事情,但是像这样调用这个方法并没有错:
[someObject magicNumber];
如果你需要跟踪返回的值,你可以声明一个变量,并将它赋值给方法调用的结果,就像这样:
int interestingNumber = [someObject magicNumber];
【使用对象 – Objective-C编程快速入门教程】你可以以同样的方式从方法中返回对象。例如,NSString类提供了一个大写字符串方法:
- (NSString *)uppercaseString;
它的使用方式与返回标量值的方法相同,尽管你需要使用一个指针来跟踪结果:
NSString *testString = @"Hello, world!";
NSString *revisedString = [testString uppercaseString];
当这个方法调用返回时,edstring将指向一个表示字符HELLO WORLD!的NSString对象。
记住,当实现一个方法来返回一个对象时,像这样:
- (NSString *)magicString {
NSString *stringToReturn = // create an interesting string...
return stringToReturn;
}
当string对象作为返回值传递时,即使stringToReturn指针超出范围,它仍然存在。
在这种情况下,需要考虑一些内存管理问题:返回的对象(在堆上创建的)需要存在足够长的时间,以供方法的原始调用者使用,但不能永久存在,因为这会造成内存泄漏。在大多数情况下,Objective-C编译器的自动引用计数(ARC)特性为你解决了这些问题。
对象可以向自己发送消息
无论何时编写方法实现,你都可以访问一个重要的隐藏值self。从概念上讲,self是指“接收到此消息的对象”。它是一个指针,就像上面的greeting值一样,可以用来调用当前接收对象的方法。
你可能决定通过修改sayHello方法来重构XYZPerson实现,从而使用上面显示的saySomething:方法,从而将NSLog()调用移动到一个单独的方法。这意味着你可以添加更多的方法,比如sayGoodbye,每个调用都将通过saySomething:方法来处理实际的问候过程。如果以后希望在用户界面的文本字段中显示每个问候语,只需修改saySomething:方法,而不必逐个调整每个问候语方法。
使用self调用当前对象上的消息的新实现如下所示:
@implementation XYZPerson
- (void)sayHello {
[self saySomething:@"Hello, world!"];
}
- (void)saySomething:(NSString *)greeting {
NSLog(@"%@", greeting);
}
@end
如果向XYZPerson对象发送此更新实现的sayHello消息,则有效的程序流如图2-2所示。
图2-2消息传递self时的程序流
文章图片
对象可以调用由其超类实现的方法
Objective-C中还有另一个重要的关键字super。向super发送消息是调用由继承链上的超类定义的方法实现的一种方式。super最常见的用法是覆盖一个方法。
假设你想创建一种新型的person类,一个“喊叫的人”类,其中每个问候语都用大写字母显示。你可以复制整个XYZPerson类,并将每个方法中的每个字符串修改为大写,但是最简单的方法是创建一个继承自XYZPerson的新类,然后重写saySomething:方法,这样它就可以用大写显示问候,如下所示:
@interface XYZShoutingPerson : XYZPerson
@end@implementation XYZShoutingPerson
- (void)saySomething:(NSString *)greeting {
NSString *uppercaseGreeting = [greeting uppercaseString];
NSLog(@"%@", uppercaseGreeting);
}
@end
这个例子声明了一个额外的字符串指针,大写的greeting,并将发送原始greeting对象返回的值赋给它。如前所述,这将是通过将原始字符串中的每个字符转换为大写字母构建的新字符串对象。
由于sayHello由XYZPerson实现,并且XYZShoutingPerson设置为从XYZPerson继承,因此你也可以在XYZShoutingPerson对象上调用sayHello。 当你在XYZShoutingPerson上调用sayHello时,对[self saySomething:…]的调用将使用重写的实现并将问候语显示为大写,从而导致有效的程序流程如图2-3所示。
图2-3被覆盖方法的程序流程
文章图片
但是,这个新实现并不理想,因为如果你稍后决定修改saySomething的XYZPerson实现:要在用户界面元素中显示问候语,而不是通过NSLog(),那么你还需要修改XYZShoutingPerson实现。
一个更好的主意是改变saySomething的XYZShoutingPerson版本:调用超类(XYZPerson)实现来处理实际的问候语:
@implementation XYZShoutingPerson
- (void)saySomething:(NSString *)greeting {
NSString *uppercaseGreeting = [greeting uppercaseString];
[super saySomething:uppercaseGreeting];
}
@end
发送一个有效的程序流,现在结果XYZShoutingPerson对象sayHello消息如图2 – 4所示。
图2 – 4 super消息传递时的程序流
文章图片
动态创建对象如前所述,在本章中,内存是一个objective – c的动态分配对象。创建一个对象的第一步是确保足够的内存分配不仅对一个对象的类定义的属性,而且属性上定义的每个超类继承链。
NSObject根类提供了一个类方法,alloc,为你处理这个过程:
+ (id)alloc;
注意,这个方法的返回类型是id。这是Objective-C中一个特殊的关键字,表示“某种对象”。它是一个指向对象的指针,就像(NSObject *),但它的特殊之处在于它不使用星号。本章后面会更详细地描述,Objective-C是一种动态语言。
alloc方法还有一个重要的任务,就是通过将对象属性设置为0来清除分配给它们的内存。这避免了内存中包含以前存储的垃圾的常见问题,但不足以完全初始化对象。
你需要结合调用alloc和调用init,另一个NSObject方法:
- (id)init;
类使用init方法来确保其属性在创建时具有合适的初始值,下一章将对此进行更详细的讨论。
注意,init还返回一个id。
如果一个方法返回一个对象指针,则可以将对该方法的调用嵌套为对另一个方法的调用中的接收者,从而在一条语句中组合多个消息调用。分配和初始化对象的正确方法是将alloc调用嵌套在init调用中,如下所示:
NSObject *newObject = [[NSObject alloc] init];
本例将newObject变量设置为指向新创建的NSObject实例。
最内层的调用首先执行,因此NSObject类被发送给alloc方法,该方法返回一个新分配的NSObject实例。然后,这个返回的对象用作init消息的接收方,该消息本身将返回分配给newObject指针的对象,如图2-5所示。
图2-5嵌套alloc和init消息
文章图片
注意:init返回的对象可能与alloc创建的对象不同,因此最好按如下所示嵌套调用。
如果不重新分配任何指向对象的指针,则永远不要初始化对象。例如,不要这样做:
NSObject *someObject = [NSObject alloc];
[someObject init];
如果对init的调用返回其他对象,则会留下一个指向最初分配但从未初始化的对象的指针。
初始化方法可以接受参数
有些对象需要用所需的值初始化。例如,NSNumber对象必须用它需要表示的数值创建。
NSNumber类定义了几个初始化器,包括:
- (id)initWithBool:(BOOL)value;
- (id)initWithFloat:(float)value;
- (id)initWithInt:(int)value;
- (id)initWithLong:(long)value;
带有参数的初始化方法与普通init方法调用的方式相同——NSNumber对象是这样分配和初始化的:
NSNumber *magicNumber = [[NSNumber alloc] initWithInt:42];
类工厂方法是分配和初始化的替代方法
如前一章所述,类还可以定义工厂方法。工厂方法为传统的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创建对象
还可以使用新类方法创建类的实例。此方法由NSObject提供,不需要在自己的子类中重写。
它实际上是一样的调用alloc和init没有参数:
XYZObject *object = [XYZObject new];
// is effectively the same as:
XYZObject *object = [[XYZObject alloc] init];
字面量提供了简洁的对象创建语法
有些类允许你使用更简洁的字面量语法来创建实例。
你可以创建一个NSString实例,例如,使用一个特殊的字面量表示法,像这样:
NSString *someString = @"Hello, World!";
这与分配和初始化NSString或使用它的类工厂方法是一样的:
NSString *someString = [NSString stringWithCString:"Hello, World!"
encoding:NSUTF8StringEncoding];
NSNumber类也允许多种字面量:
NSNumber *myBOOL = @YES;
NSNumber *myFloat = @3.14f;
NSNumber *myInt = @42;
NSNumber *myLong = @42L;
同样,这些示例实际上与使用相关的初始化器或类工厂方法相同。
你也可以创建一个NSNumber使用框表达式,像这样:
NSNumber *myInt = @(84 / 2);
在本例中,对表达式求值,并使用结果创建一个NSNumber实例。
Objective-C还支持文本来创建不可变的NSArray和NSDictionary对象; 这些将在值和集合中进一步讨论。
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];
意味着编译器现在将生成一个错误,因为removeAllObjects没有在任何它知道的公共NSString接口中声明。
因为对象的类是在运行时确定的,所以在创建或使用实例时分配变量的类型没有区别。要使用本章前面描述的XYZPerson和XYZShoutingPerson类,你可以使用以下代码:
XYZPerson *firstPerson = [[XYZPerson alloc] init];
XYZPerson *secondPerson = [[XYZShoutingPerson alloc] init];
[firstPerson sayHello];
[secondPerson sayHello];
虽然firstPerson和secondPerson都是静态类型的XYZPerson对象,但是secondPerson在运行时将指向XYZShoutingPerson对象。当在每个对象上调用sayHello方法时,将使用正确的实现; 对于第二个人,这意味着XYZShoutingPerson版本。
确定对象的相等性
如果你需要确定一个对象是否与另一个对象相同,那么一定要记住使用的是指针。
使用标准的C等式运算符==来测试两个变量的值是否相等,如下所示:
if (someInteger == 42) {
// someInteger has the value 42
}
当处理对象时,==操作符用于测试两个单独的指针是否指向同一个对象:
if (firstPerson == secondPerson) {
// firstPerson is the same object as secondPerson
}
如果你需要测试两个对象是否代表相同的数据,你需要调用一个类似isEqual:的方法,从NSObject:
if ([firstPerson isEqual:secondPerson]) {
// firstPerson is identical to secondPerson
}
如果需要比较一个对象表示的值是否大于或小于另一个对象,则不能使用标准的C比较操作符> 和< 。相反,基本的基础类型,如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
空值是初始化对象指针最安全的方法,如果你没有其他值可以使用,因为在Objective-C中发送消息给空值是完全可以接受的。如果你发送消息给nil,显然什么都不会发生。
注意:如果你希望从发送给nil的消息中得到一个返回值,那么对于对象返回类型,返回值将为nil,对于数值类型,返回值为0,对于BOOL类型,返回值为NO。返回的结构将所有成员初始化为零。
如果需要检查对象是否为nil(即变量指向内存中的对象),可以使用标准的C不等式运算符:
if (somePerson != nil) {
// somePerson points to an object
}
或者简单地提供变量:
if (somePerson) {
// somePerson points to an object
}
如果somePerson变量为nil,则其逻辑值为0 (false)。如果它有一个地址,它就不是0,所以计算结果为true。
类似地,如果你需要检查一个空变量,你可以使用相等运算符:
if (somePerson == nil) {
// somePerson does not point to an object
}
或者使用C逻辑否定运算符:
if (!somePerson) {
// somePerson does not point to an object
}
练习1、打开main.m文件,从上一章最后的练习中找到,并找到main()函数。与用C编写的任何可执行文件一样,此函数表示应用程序的起点。
使用alloc和init创建一个新的XYZPerson实例,然后调用sayHello方法。
注意:如果编译器没有自动提示你,你将需要导入main.m顶部的头文件(包含XYZPerson接口)。
2、实现本章前面显示的saySomething:方法,并重写sayHello方法来使用它。添加各种其他问候语,并在上面创建的实例中调用它们。
3、为XYZShoutingPerson类创建新的类文件,设置为从XYZPerson继承。
重写saySomething:方法来显示大写的问候语,并在XYZShoutingPerson实例上测试其行为。
4、实现你在前一章中声明的XYZPerson类person factory方法,以返回正确分配和初始化的XYZPerson类实例,然后使用main()中的方法,而不是嵌套的alloc和init。
提示:与其在类工厂方法中使用[[XYZPerson alloc] init],不如尝试使用[[self alloc] init]。
在类工厂方法中使用self意味着引用类本身。
这意味着你不必覆盖XYZShoutingPerson实现中的person方法来创建正确的实例。测试这个通过检查:
XYZShoutingPerson *shoutingPerson = [XYZShoutingPerson person];
5、创建一个新的本地XYZPerson指针,但不包含任何值赋值。
使用分支(if语句)检查变量是否自动赋值为nil。
推荐阅读
- 封装数据 – Objective-C编程快速入门教程
- 定义类 – Objective-C编程快速入门教程
- 入门编程介绍 – Objective-C编程快速入门教程
- Core Foundation编程概念全解
- Objective-C线程技术(线程同步和线程安全)
- Objective-C运行时Runtime完全解读
- iOS内存管理(引用计数、Runloop、AutoreleasePool和引用循环)
- 快速了解iOS内存管理
- 内存管理之(__bridge、__bridge_transfer、__bridge_retained)