Objective-C & Sprite Kit太空历险记 : 2. 初级训练营——Objective-C基础(下)

时间:2022-06-12 00:18:20

原文  http://www.ituring.com.cn/article/211454

2.7. 布尔类型

O-C教官:本科目是讨论太空船舱门状态的问题,在太空船中,舱门为绿灯时开启,为红灯时关闭,~。

教官,您玩儿呢~

Objective-C & Sprite Kit太空历险记 : 2. 初级训练营——Objective-C基础(下)

我们可看到舱门的两个状态,即开启和关闭,在代码中,我们应该使用一个变量来标识舱门的状态,那么,这个变量应该是什么类型呢?

原来这才是关键的问题~

O-C教官:这种“开/关”、“是/否”、“真/假”一类的数据类型,我们一般会使用布尔(Boolean)类型,也称为逻辑型。在Objective-C中,并没有内置的布尔类型,而是根据不同的平台分别定义为bool或signed char类型的别名,名称为BOOL,其值包括YES和NO。下面的代码演示了BOOL类型的使用。

#import <Foundation/Foundation.h>

int main(int argc, const char *argv[])
{
@autoreleasepool {
BOOL doorIsOpen = NO;
NSLog(@"%i", doorIsOpen);
}
return 0;
}

执行代码,我们会看到输入的内容是0,而不是NO;这是由于YES和NO者是以宏的形式来实现的,所以,我们无法直接显示YES或NO,而是显示它们相应的值,整数形式也就是1(YES)和0(NO)。

2.8. 字符

O-C教官:本科目是信息情报官的入门训练,我们必须掌握文本信息的基本组成,也就是对于字符的处理。

在Objective-C中,字符类型用来操作单个字符,使用char关键字定义,字符内容包含在一对单引号中;在NSLog()函数中,可以使用“%c”格式化显示字符。如下面的代码:

#import <Foundation/Foundation.h>

int main(int argc, const char *argv[])
{
@autoreleasepool {
char chA = 'A';
NSLog(@"%c", chA);
}
return 0;
}

代码会显示大写字母A。

关于char类型的操作,我们经常会将它转换为整数,而这个整数就是字符的ASCII编码。如int ascA = (int)'A';

char类型的另一个常用功能就是组合为字符串,也就是我们所说的C风格字符串,基本质上是一个以“\0”字符结束的char数组。数组的详细内容会在第6章介绍,我们先通过下面的代码简单了解一下这种字符串的使用。

#import <Foundation/Foundation.h>

int main(int argc, const char *argv[])
{
@autoreleasepool {
char hello[] = {'H','e','l','l','o','\0'};
// char hello[] = "Hello\0";
NSLog(@"%s", hello);
}
return 0;
}

在Objective-C项目中,我们还是更建议使用NSString类或NSMutableString类来处理字符串,在第7章会详细讨论。

无论是char类型或是在字符串中,都会有一些特殊的字符,如单引号、双引号用于定义字符或字符串内容,而另外一些则是不可见字符,如ASCII编码为0的字符、换行符等;对于这些字符,我们应该在字符或字符串中使用转义字符来定义,常用的包括:

  • \'表示单引号。
  • \"表示双引号。
  • \n表示换行符。
  • \t表示制度符。
  • \\表示\字符。
  • \0表示ASCII代码为0的字符,在C风格字符串中,都以此字符作为结束符号。

以下为0~127编码的ASCII码表,大家可以在战斗中随时参考。

Objective-C & Sprite Kit太空历险记 : 2. 初级训练营——Objective-C基础(下)

2.9. 指针

O-C教官:相信很多参加过C语言夏令营的队员会对指针操作比较头疼;不过,在我们们控制系统中,指针却又是必不可少的编程工具。

首先,看一下指针与普通变量有什么不同,先看形式上的,如下面的代码。

int numInt = 99;
int *ptInt = &numInt;

首先是numInt变量,它被定义为int数据类型;接下来是*ptInt,它也是int类型的,只不过*符号说明这是一个指针,而指针所指向的位置保存的数据是int类型的,即ptInt是一个int指针。

那么,ptInt指向的位置是哪儿呢?代码中的&numInt,其中的&符号称寻址运算符,&numInt表示获取numInt变量在内存中的地址,而ptInt指向的就是这个地址。

这个地址在哪?大概就是内存中的某个位置吧,我们可以使用如下代码看一下。

NSLog(@"%Li", &numInt);

不过,在代码中,我们几乎不会直接使用这个内存地址,而总是通过指针来操作地址中的数据。下面的代码,我们会显示numInt变量的值,包括使用指针来访问它。

NSLog(@"%i", numInt);
NSLog(@"%i"
, *ptInt);

在这里,我们再次使用了*运算符,它称为间接访问运算符,通过它可以获取指针所指向的内存区域中的数据。

通过上面的操作,我们应该知道ptInt指针指向的就是numInt变量的位置,那么,我们修改其中一个的数据会怎么样呢?下面的代码就演示了这些操作。

int numInt = 99;
int *ptInt = &numInt;
NSLog(@"%i\n", numInt);
NSLog(@"%i\n"
, *ptInt);
numInt = 10;
NSLog(@"%i\n", numInt);
NSLog(@"%i\n"
, *ptInt);

我们可以看到,修改其中一个变量的值后,两个变量获取的值都会变化,这说明它们真的是相同位置的数据,事实证明一切哈!

此外,如果我们需要断开指针与内存位置的关系,可以将指针设置为空指针,即将指针变量设置为NULL值或nil值,如ptInt = NULL。不过,玩失联时要特别注意,因为失联在很多时候并不是令人愉快的事情。

2.10. 自定义函数

O-C教官:前面已经使用了一些函数帮助我们实现功能,接下来,大家必须学会自己创建函数,以便更好的组织代码,以及为整个舰队提供支持。

函数是一种基本的代码封装与组织形式,我们知道,在一艘大型太空船上,只有各个岗位的人员都能够各尽其责,太空船才能够正常运行;而函数对于整个程序来讲,也是这样,必须能够正确地完成本职工作才称得上是一个合格的函数。当然,随着学习的深入,我们也会学习到更多的代码封装与组织方法,如第4章讨论的面向对象编程。

Objective-C & Sprite Kit太空历险记 : 2. 初级训练营——Objective-C基础(下)

我们看到,程序会从main()函数开始,然后会有大量的函数来支援它,这就是函数的基本工作方式。

我们已经使用过内置的函数,如NSLog()函数,不过,我们也可以创建自己的函数。在Objective-C中声明函数,格式如下:

<返回值类型> <函数名>(<参数列表>);

如果我们实现这个函数,就不需要使用分号,而是将函数的代码放在一对花括号{}之间。如下面的代码。

#import <Foundation/Foundation.h>

void sayHello()
{
NSLog(@"Hello!");
}

int main(int argc, const char *argv[])
{
@autoreleasepool {
sayHello();
}
return 0;
}

接下来,我们简单讨论一下函数定义中的几个重要元素。

2.10.1. 返回值类型

返回值类型,也就是函数运行结果的数据类型,在函数中使用return语句返回这个数据;如果函数不需要返回结果,就像上面的代码一样,将返回值类型定义为void。

2.10.2. 函数名

函数名,我们在本书中会使用首字母小写,其它单词首字母大写的形式。函数名应该反映出函数的功能。

2.10.3. 参数

参数列表,在定义函数时,可以没有参数,像上面代码中定义的sayHello()函数,也可以有一个或多个参数,每一个参数都应包含两个基本部分,即参数类型和参数变量,如下面的代码。

#import <Foundation/Foundation.h>

int addInt(int num1, int num2)
{
return num1 + num2;
}

int main(int argc, const char *argv[])
{
@autoreleasepool {
int sum = addInt(6, 4);
NSLog(@"sum = %i", sum);
}
return 0;
}

在我们定义的addInt()函数中,定义了两个参数,分别是num1和num2,它们都是int类型的;而addInt()函数的功能就是返回这两个参数的和。

2.10.4. 指针参数

O-C教官:实训科目,创建一个函数,用于交换两个int变量的值。

Objective-C & Sprite Kit太空历险记 : 2. 初级训练营——Objective-C基础(下)

这个也不难,马上动手,如下面的代码。

void swapInt(int x, int y)
{
int temp = x;
x = y;
y = temp;
}

然后,可以使用如下代码来调用这个函数。

int main(int argc, const char *argv[])
{
@autoreleasepool {
int num1 = 10;
int num2 = 99;
NSLog(@"num1 = %i , num2 = %i", num1, num2);
swapInt(num1, num2);
NSLog(@"num1 = %i , num2 = %i"
, num1, num2);
}
return 0;
}

友情提示,就等着挨教官批吧?上面的代码并不会交换num1和num2的值。

狂汗~!那为什么呢?

因为对于前面的swapInt()函数,当我们将num1和num2变量传递给swapInt()函数时,参数复制了它们的数据,在函数内部,只是在交换数据副本的值,而不是真正地交换num1和num2变量的值。

那么,如何才能真正地修改参数的值呢?答案就是,使用指针参数;下面就是修改后的swapInt()函数。

void swapInt(int *x, int *y)
{
int temp = *x;
*x = *y;
*y = temp;
}

在调用swapInt()函数时,我们同样需要做一些修改,如下面的代码。

swapInt(&num1, &num2);

这样,我们就可以通过swapInt()函数顺利地交换两个int变量的值了。

2.10.5. 静态变量

O-C教官:在Objective-C中,使用static关键字定义静态变量,这种变量可以在代码执行过程中只初始化一次,然后就会一直保存数据的修改。

如下面的代码,我定义了一个fn()函数,其中包含一个静态变量counter,当我们每次调用这个函数时,counter就会加1,然后显示调用的次数。

void fn()
{
static int counter = 0;
counter++;
NSLog(@"第 %i 次调用fn()函数", counter);
}

int main(int argc, const char *argv[])
{
@autoreleasepool {
for(int i = 0; i < 30; i++) {
fn();
}
}
return 0;
}

代码会显示共调用了30次fn()函数,其中的for语句结构属于循环语句结构的一种,下一章会进行相关科目的训练。

在我们定义的fn()函数中,变量counter定义为静态的,它在程序运行过程中只会进行一次初始化,也就是在定义时赋值为0;然后,当我们每次调用fn()函数时,counter变量并不会重新定义,而是保留最新的值。

2.11. 块(block)

O-C教官:块的使用是Objective-C中比较有特点的功能之一,它为我们提供了一种更加灵活地的代码结构,而且你在C语言夏令营是不会看到块的。

Objective-C & Sprite Kit太空历险记 : 2. 初级训练营——Objective-C基础(下)

下面的代码声明了一个名为factory的块,它包括两个int类型的参数,以及int类型的返回值;是的,看上去和函数非常相似,实际上,在Swift中,就是使用了函数类型取代了块的功能。

int(^factory)(int num1, int num2)

当然,这个块并没有什么实际功能,我们经常会将一个块的定义赋值到一个块变量,如下面的代码,我们分别定义了两个块,并赋值给两个块变量(factory1和factory2)。

// 加法工厂
int(^factory1)(int, int) = ^int (int num1, int num2) {
return num1 + num2;
};
// 减法工厂
int(^factory2)(int, int) = ^int (int num1, int num2) {
return num1 - num2;
};

我们可以看到,块变量的格式初看起来可能有些奇怪,它同时包含了块变量名、返回值类型和参数类型;其中,块变量名前需要使用^符号,不过,我们不需要指定参数名称。

在赋值运算符后面则是块的具体实现,此时不使用块名称,但返回值和参数类型必须与块变量的定义相同,并且需要指定参数名称。

接下来,我们看一看如何使用这两个块,如下面的代码。

#import <Foundation/Foundation.h>

int numberFactory(int(^factory)(int,int), int num1, int num2)
{
return factory(num1, num2);
}

int main(int argc , const char * argv[])
{
@autoreleasepool {
// 加法工厂
int(^factory1)(int, int) = ^int (int num1, int num2) {
return num1 + num2;
};
// 减法工厂
int(^factory2)(int, int) = ^int (int num1, int num2) {
return num1 - num2;
};

// 调用工厂块
int num1 = 10;
int num2 = 6;
NSLog(@"%i\n", numberFactory(factory1, num1, num2)); //16
NSLog(@"%i\n", numberFactory(factory2, num1, num2)); //4
}
return 0;
}

代码中,我们首先创建了函数numberFactory(),请注意它的第一个参数,它定义为一个块,其格式为int类型的返回值,以及两个int类型的参数;第二和第三个参数则带入两个int类型的数值。

再看numberFactory()函数中的实现代码,我们可以看到,这个函数的功能是依靠块的执行来处理两个int数据(参数二和参数三)。

再看main()函数中,我们调用了两次numberFactory()函数,不同之处在于第一个参数指定了不同的工厂块,这样也就实现了对两个数值的不同的操作方式,即分别执行了加法运算和减法运算。这就是块的主要功能,我们可以使用相同的定义(块定义)实现不同的功能,也就是加什么块干什么活了!^_^

实际上,如果块只需要使用一次,我们还可以直接定义,如下面的代码就是通过numberFactory()函数进行乘法运算。

int result = numberFactory(
^int (int num1, int num2) { return num1 * num2; }, num1, num2);
NSLog(@"%i", result); //60

在这里,我们只是显示了块的简单使用,在Foundation、Sprite Kit等框架的开发资源中,我们都可以看到块的使用,我们可以在实际应用中逐渐理解块的意义,并能够正确应用。

如果需要在块中修改函数或方法内定义的变量,则必须在变量定义时使用__block标识,先看下面的代码:

int main(int argc, const char * argv[])
{
@autoreleasepool {
int num = 10;
void (^addOne)(void) = ^void(void){
num++; // 出错
};
addOne();
NSLog(@"num = %i", num);
}
return 0;
}

代码执行会出错,我们应该将变量num的定义修改为:

 __block int num = 10;

这样,代码就会正确执行,并显示“num = 11”。

关于块,最后说明的是,如果块真的没有返回值和参数,还可以更简单地定义,如前面的AddOne块就可以写成:

void (^addOne)() = ^{
num++;
};

2.12. 异常处理

警报响起,应急小队出动!

程序执行出错了!怎么办?怎么办?怎么办?重要的事要说三遍!

O-C教官:冷静!冷静!冷静!大家现在只是在训练阶段!接下来,我们就来了解 Objective-C中的异常处理问题。

在Objective-C中,异常的捕捉机制和很多编程语言是不一样的,它是通过指令,而不通过语句结构来实现的;基本应用格式如下:

@try {
// 可能出现异常的代码
}
@catch (NSException *exception) {
// 处理捕捉到的异常
}
@finally {
// 完成清理工作,无论是否有异常出现都会执行
}

如果在@catch块中无法或不需要处理异常,还可以使用@throw指令向代码的上一级结构抛出异常,如果是在main()函数中,就是向系统抛出异常,当然,如果真是这样的话,程序也就挂掉了。

实际上,大多数编程语言在处理异常时都会很明显的性能问题,特别是在代码执行过程中真的出现问题时,Objective-C也是这样,甚至出于性能方面的考虑,Apple官方都不建议这样处理异常。那么,在开发中,我们应该怎么办呢?最好的办法当然是提高代码质量,这样就可以将决大多数可能的异常消灭在开发阶段。

O-C教官:别说不可能,只有完成不可能完成的任务,你才可能成为太空舰队中的一名优秀队员。此外,什么都不是绝对的,软件的性能、正确性、稳定性,包括对于异常情况的处理方法,需要开发人员根据项目的特点综合考虑和权衡。

2.13. 枚举

O-C教官:接下来,各位放松一下,思考一个问题,对于数值数据可以使用整数或浮点数、对于“开/关”类数据可以使用布尔型、对于文本信息可以使用字符或字符串;那么,对于性别这样的数据,使用什么数据类型比较合适?

想正确地处理一类数据,就必须清楚这类数据的值可能有哪些?就说性别吧,大家都知道,主要包括男和女,别告诉我还有雌雄同体,那太那啥了!!不过,还有一个问题,如果有人想对自己的性别保密呢?好吧,那性别数据就有可能有三种基本的数值了,包括:保密、男和女。

那么,我们使用什么类型保存性别数据呢?

理论上讲会有很多方案,比如使用宏,如下面的代码:

#define SexUnknow 0
#define SexMale 1
#define SexFemale 2

或者定义常量,如下面的代码:

const int SexUnknow = 0;
const int SexMale = 1;
const int SexFemale = 2;

如果能够定义一个性别类型就会更加直观了,好的,我们马上使用enum关键字定义枚举类型,如下面的代码就定义了一个性别枚举类型。

enum ESex {Unknow, Male, Female};

然后,我们使用如下代码来使用这个类型。

enum ESex tomSex = Male;

为了更方便地使用枚举类型,我们也可以使用typedef关键字定义它的别名;此时,我们可以单独定义,也可以在定义ESex类型时直接定义。如:

typedef enum ESex Sex;

或:

typedef enum ESex {Unknow, Male, Female} Sex;

然后,我可以直接使用Sex类型来定义变量,如:

Sex tomSex = Male;

当我们需要显示枚举变量(如tomSex)的值时,只能使用整数来显示,其中,枚举类型中定义的第一个成员值为0,第二个成员值为1,第三个成员值为2,以此类推。当然,如果需要自己指定枚举成员的值也是可以的,如下面的代码。

typedef enum ESex {Unknow = 0, Male = 1, Female = 2} Sex;

最后,枚举成员的值也可以不从0开始,但我们并不建议这样使用,除非项目中的数据真的不允许为0。如下面定义的枚举类型。

typedef enum ESpaceGate  {
EarthGate = 300,
MoonGate = 301,
MarsGate = 400,
PlanetIGate = 9000
} SpaceGate;

O-C教官:在使用枚举类型时应注意:(1)总是保留一个未知(Unknow)的项是一项安全措施,这样可以保证在任何时候都能处理意外数据,比如真的出现了雌雄同体^_^。(2)在Objective-C中的枚举值是整数,如果需要保存到文件或数据库中,应使用整数类型。(3)大家自己琢磨吧!

(小声说)教官够坏的!^_^

2.14. 结构

O-C教官:大家知道,在实际应用中有很多数据是相关联的,如日期中的年、月、日;一名太空战士的各种属性,如姓名、编号、年龄、性别等等;对于这些相关联的数据,我们可使用另一种自定义的类型,即结构类型。

结构类型可以将一组相关的数据组合成一个整体,方便代码编写及数据管理,如下面的代码,我们将使用结构定义一个日期类型。

struct SDate {
int year;
int month;
int day;
};
typedef struct SDate Date;

代码的最后一行,我们同样使用typedef定义了SDate结构类型的别名,即Date。下面,我们就可以使用Date来定义日期类型的变量了。

Date today;

定义结构类型变量的同时,我们还可以按结构成员定义的顺序给它们赋值,如下面的代码:

Date today = {2015, 9, 19};

访问结构成员时,我们使用如下格式:

<结构变量>.<成员名称>

如下面的代码,我们修改today变量的内容,并显示。

today.year = 2015;
today.month = 10;
today.day = 28;
NSLog(@"今天是%i%i%i", today.year, today.month, today.day);

对于简单的数据,我们可以使用结构类型定义,但对于复杂的数据管理,如太空船、机甲步兵、生化战士等等,我们应该使用“类”类型进行管理和操作,在第4章会有相关的训练科目。

2.15. 预处理

O-C教官:关于预处理,大家在前面的训练科目中已经多次见到了,如#define、#ifndef、#endif等,它们都有一个明显的特征,那就是这些指令以行作为单位,而且行的结尾没有分号,这是和一般的代码不同的地方。作为初级训练营的最后一个科目,我们将会对常用的预处理指令做一些总结。主要包括:

  • #define和#undef指令
  • 条件编译指令
  • #import指令

2.15.1. #define和#undef指令

#define指令的功能就是定义宏,我们可以定义很简单的宏,简单到只有一个标识名称,如:

#define Debug

这样,就可以根据是否定义了这个宏来判断项目是在调试中,还是正式发布了,我们可以使用#ifdef指令判断一个宏是否已定义,而#ifndef指令则是判断一个宏是否没有定义。

使用#define指令定义的宏,可以使用#undef指令撤消,如:

#undef Debug

#define指令的另一个功能就是模拟定义常量,如:

#define IPhone4 1
#define IPhone5 2
#define IPad 3

此外,我们还可以利用#define定义一些复杂的宏,如:

#define IsEvenNumber(n) (n % 2 == 0)

其中,我们定义的这个宏的功能是判断一个数是否为偶数,它使用起来和函数类似,如:

int num = 100;
if (IsEnenNumber(num)) {
NSLog(@"%i是偶数", num);
}else{
NSLog(@"%i不是偶数"
, num);
}

如果一个#define指令的定义过长,我们可以将它们分行,但需要使用“\”符号放在前一行的结尾处,用以说明下一行与本行是同一指令。

2.15.2. 条件编译指令

使用宏,我们还可以指定在不同的条件下编译不同的代码,此时,我们可以使用#define定义一些与系统平台相关的标识,然后根据这些标识将软件编译成不同的版本。与条件编译相关的指令包括:

  • #ifdef指令,判断一个宏标识是否已定义。
  • #ifndef指令,判断一个宏标识是否没有定义。
  • #endif指令,与#ifdef或#ifndef指令组成一个代码块。
  • #elif和#else指令,可以与#ifdef...#endif组合使用,以判断不同条件下的代码执行情况。

如下面的代码,我们将根据不同的设备类型来编译相应的代码块。

#define IPhone

#ifdef IPhone
// iPhone设备代码
#elif IPad
// iPad设备代码
#else
// 其它类型设备代码
#endif

此外,在头文件中,如前面我们定义的FileTypeDemo.h文件,其中使用了一个模式化的预处理代码,我们再来看一看。

#ifndef __FileTypeDemo_h__
#define __FileTypeDemo_h__

#endif

这三个预处理指令的功能就是在代码文件中防止多次包含头文件,以提高代码的编译效率。

2.15.3. #import指令

#import指令的功能就是引用头文件,我们也已经多次使用,再次说明一下: - 使用<>引用的头文件是系统资源,如Foundation.h文件。 - 使用""引用的头文件是“外部”资源,可能是第三方框架或自定义的头文件。

接下来,#import O-C教官。

O-C教官:祝贺大家完成初级训练营的全部科目,下一步将是军官训练营,如果大家能够顺利完成,到时我就应该叫你们长官了,祝大家好运!

Objective-C & Sprite Kit太空历险记 : 2. 初级训练营——Objective-C基础(下)