在本小节中,笔者会详细讲解下ObjC的类的相关内容,包括创建、构造、方法、属性以及属性读取等相关知识,先来看下类的组成:
类的创建
ObjC是在C语言基础上扩展的,在编写OS软件时可以混用两种语言。但它们之间是有区别的,C是面向过程的语言,而ObjC则是定位成一种OOP语言,所以本文档中基本是以ObjC语言为主,少量的会用C来编写(原因是C的性能会好一些)。
在objc中只要看到@符号,就可以把它看成是对C语言的扩展;
此处先讲一点ObjC面向对象的代码编写知识,后续会再详细描述OOP内容。OOP语言中的对象一般统称为类,ObjC也不例外,定义一个ObjC类时比java语言要麻烦一些,ObjcC类需要以下部分的组合;
在Xcode中新建类的方法如下,右键选择项目文件夹,然后选择 New File,比如创建一个名为Fraction的类。
创建完成生会生成两个源文件,这种源码格式需要适应,后面还有一种更简单的方式,会在本小节最后面介绍下。
类对象声明@interface
用关键字@interface来定义,这个关键字定义新类时首先需要告诉编译器类此类来自何处,这种设计方式是一个好的习惯,尤其是在一些更高级的语言中比如java不会限制源文件的名称必须一致,ObjC语言所有类的超类是NSObject,如果无特殊设计,一般自定义的类都会继承NSObject。然后在此接口中就可以定义属性和方法了,语法格式如下:
/*语法格式*/
#import <Foundation/Foundation.h>
@interface newClassName: parentClassName{ //一般为NSObject
//propertyDeclarations
}
//methodDeclarations
@end // newClassName
demo示例,下列代码中的NS_ASSUME_NONNULL_BEGIN是可有可无的,其它的符号会在后续详细说明。
//demo示例
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface Fraction : NSObject
{//属性定义
@private
int size;
int length;
}
//方法定义,此处通常是顶格写的
- (void) print;
- (void) setNumerator: (int) n;
- (void) setDenominator: (int) d;
@end
NS_ASSUME_NONNULL_END
类对象实现@implementation
类实现用@implementation关键字来指定,它与interface是配套的。语法如下:
//语法格式
@implementation newClassName{
memberDeclarations; //类变量
}
methodDefinitions; //方法实现
@end
demo示例
@implementation Fraction {
int numerator;
int denominator;
}
- (void) print {
NSLog(@"%i / %i", numerator, denominator);
}
- (void) setNumerator:(int) n {
numerator = n;
}
- (void) setDenominator:(int) d {
denominator = d;
}
@end
需要注意:
- 定义@implemention与@interface的文件名称相同;
- implementation可以不实现全部的interface中定义的函数声明,但未实现的方法调用在运行时会报异常;
- 如果在.m中选择性的实现在.h中定义的方法,在调用时可以使用respondsToSelector等函数来进行安全检查;
类对象调用
此示例中我们用main函数来调用上述自定义的类对象
#import "Fraction.h"
int main(int argc, char* argv[]){
@autoreleasepool {
Fraction *myFraction; //表明myFraction是一个对象的引用(指针)
myFraction = [Fraction alloc]; //alloc表示要分配新的内存空间,返回存储数据的引用
myFraction = [myFraction init]; //初始化类
//上述三行代码可以合并为一行代码,即 Fraction *myFraction = [[Fraction alloc] init]
[myFraction setNumerator:1]; //函数调用
[myFraction setDenominator:3];
[myFraction print];
}
return 0;
}
也可以改成如下的简单测试方法调用
#import "Fraction.h"
void fractionTest(){
Fraction *fraction = [[Fraction alloc] init];
[fraction setNumerator:1];
[fraction print];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
fractionTest();
}
return 0;
}
构造函数
类可以使用new和alloc init的方式来初始化对象,推荐后者, 在NSObject提供的init函数用于类对象的初始化,类似于java中的构造函数的概念,所以默认所有的自定义对象都有一个init方法(可以覆写)。
//alloc申请一块内存空间,同时也会设置一些属性的默认值保证类可用
//init可以有多个用于初始化类属性
Fraction *fraction = [[Fraction alloc] init];
覆写默认构造函数
(instancetype) init{ //instancetype也可换成id
if(self = [super init] }{ //调用父类的初始化方法
//个性化代码
NSLog(@"FRACTION INIT");
}
return self; //返回实例对象
}
自定义多构造函数
同时也支持多构造函数,比如Foundation框架中提供的NSArray类。
- (instancetype)initWithString:(NSString *)aString;
- (instancetype)initWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
- (instancetype)initWithFormat:(NSString *)format arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0);
//用myInitArrayValue变量初始化,即initWithArray是个有参数的方法
NSArray *myArray = [[NSArray alloc] initWithArray:myInitArrayValue];
自定义的类也可以仿写成NSArray那样类似initXXX的初始化方法,如下:
@interface Fraction:NSObject
- (instancetype) initWith: (int)n;
- (Fraction *) initWith: (int)n;
@end
//覆写超类的初始化代码,这块代码可不写
@implemention Fraction
(instancetype) init{
if(self = [super init] }{ //调用父类的初始化方法
//个性化代码
NSLog(@"FRACTION INIT");
}
return self; //返回实例对象
}
//方式1:
- (Fraction *) initWith: (int)n {
self = [super init]; //此处也可以调用其它的初始化方法
if(self){
[self setTo:n over:3]; //执行初始化方法
}
return self;
}
//方式2:
- (instancetype) initWith: (int)n {
self = [super init]; //此处也可以调用其它的初始化方法
if(self){
[self setTo:n over:3]; //执行初始化方法
}
return self;
}
@end
//测试程序
int main(int argc, const char * argv[]) {
@autoreleasepool {
Fraction *a = [[Fraction alloc] initWith:3];
}
return 0;
}
上述代码中初始化方法的返回值一般会用(id)或(instancetype)来代替,id是一个关键字可简单认为它是一个通用的对象类型,类似泛型的概念,所以一般会用改成如下写法:
- (id) initWith: (int)n {
self = [super init];
if(self){
[self setTo:n over:3]; //执行初始化方法
}
return self;
}
属性定义
全局变量 extern 关键字
定义在类声明和类实现部分都可以(这样本类和其它类全可以访问了),但一般会定义在类声明部分,下面是一个例子说明在Fraction.m中定义一个全局变量,需遵循两点规定:
- 在所有的方法和函数之外声明;
- 一般变量名为g开头,表示这是一个全局变量;
#import "Fraction.h"
//全局变量
int gGlobalVar = 5;
int main(int argc, const char * argv[]) {
NSLog(@"%i", gGlobalVar);
}
这种全局变量一般用于跨类使用,如果在其它文件中使用,不需要import引用就可以直接用,但需要用到extern关键字声明一下,它表示告诉编译器要访问其它文件中定义的全局变量,比如在另一个类Foo中访问
- (instancetype) init {
self = [super init];
if(self){
NSLog(@"FRACTION INIT");
extern int gGlobalVar;
NSLog(@"FRACTION INIT %i", gGlobalVar); //5
}
return self;
}
一个好的实践是把全局变量定义在一个单独的类中或定义的main.m函数中;
不可变变量 const 关键字
有此修饰的变量的值不能被改变,相当于java中的final的作用。
const char *words[4] = {"java", "c", "objc"};
for (int i=0; i<sizeof(words); i++){
NSLog(@"%s is %lu length", words[i], strlen(words[i]));
}
静态变量 static 关键字
ObjC中的static变量是和java不太一样,在ObjC中没有static只能在static方法中使用的限制。ObjC中的静态变量主要是为了弥补全局变量的安全缺陷而增加的,因为全局变量作用域太大,如果只希望在本类中使用全局变量,则可以在原基础上加上static来修饰。
另外一个用处就是用static修饰的变量的值不会被释放,其属于常驻内存的一个常量。
#import "Fraction.h"
//作用域只即于本类的全局变量
static int gGlobalVar = 5;
int main(int argc, const char * argv[]) {
NSLog(@"%i", gGlobalVar);
}
私有变量 @private 标识
除以使用上述private标识,还可以使用public, protected, package这三个,这些其实并不是作用域的概念,这些标识和变量的内存位置有关,因为类的成员变量是存储在结构体中的。这些标识指的是能否通过指针的方式直接操作内存的结构体。
@interface Fraction : NSObject
{//属性定义
@private
int size;
int length;
}
比如一个Fraction类定义了一个numerator成员,并用@public来修饰,则就可以通过下面的方式来访问这个变量
Fraction *myFract = [[Fraction alloc] init];
myFract->numerator;
方法中的临时局部变量
局部变量其实就是定义在方法体中的变量,因作用域只局限于此方法
- (void) setName{
int i = 1; //此变量就是局部变量
}
属性存取
类属性的存取就是属性的getter和setter方法,一般不建议直接通过属性名称的方式来设置和读取属性的值,最好封装到一个方法中,这样能获取最大的便利。在ObjC中其有一个特定的规范,比如:
int tire //属性名
- (void) setTire:(int)tire; //设置方法,加set前缀
- (int) tire; //读取方法,不加get前缀
不加get前缀,这是一个不成文的规定,因为Cocoa框架中的API带get前缀的函数表示返回传入参数的指针值。
通用实现
这种实现方式比较通用,但会写大量的代码。在现代的编程中可以使用新的特性,如果是比较老的应用只能用这种方法。
@interface AllWeatherRadial : NSObject
{
float pressure;
float treadDepth
}
- (float) pressure;
- (void) setPressure: (float *) pressure;
- (float) treadDepth;
- (void) setTreadDepth: (float *) treadDepth;
@end // AllWeatherRadial end
@implementation AllWeatherRadial
- (float) pressure{
return pressusre
}
- (void) setPressure: (float *) pressure{
pressusre = pressusre;
}
- (float) treadDepth{
return treadDepth;
}
- (void) setTreadDepth: (float *) treadDepth{
treadDepth = treadDepth;
}
@end // AllWeatherRadial end
使用注解@property
@property是一种新的编译器功能,它意味着声明了一个新对象的属性,编译器还会自己生成getter和setter方法,用这种特性简化上面的代码如下:
#import <Foundation/Foundation.h>
@interface AllWeatherRadial : NSObject
@property float rainHandling;
@property float snowHandling;
@end
#import "AllWeatherRadial.h"
@implementation AllWeatherRadial
/*这两行代码不是必须的,它只是一种get/set预生成器指令,但在Xcode4.5版本后可以不写*/
//@synthesize rainHandling;
//@synthesize snowHandling;
- (NSString *) description{
NSString *desc;
desc = [[NSString alloc] initWithFormat: @"AllWeatherRadial: / %.1f / %.1f",
[self rainHandling],
[self snowHandling]];
return (desc);
}
@end
测试代码
#import <Foundation/Foundation.h>
#import "AllWeatherRadial.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
AllWeatherRadial *item = [[AllWeatherRadial alloc] init];
[item rainHandling];
[item setRainHandling:2.2];
NSLog(@"%@", item);
}
return 0;
}
@property指令详细
因为@property是一种自动生成代码的指令,所以也需要有一些灵活的配置。比如添加一个内存回收机制,是否允许生成代码等,以下是这引指令常用的修饰符,可多个同时使用。
@property (readonly, nonatomic) NSString *name;
- weak/strong:引用类型,常用
- nonatomic:线程不安全,常用
- atomic:线程安全,系统会生成互斥的getter和setter方法,常用
- readonly:只读属性,不会生成setter方法
- readwrite:默认
- assign:默认编译器
- nullable:是否可为空
- retain:保留和内容释放有关
- null_resettable、null_unspecified:老版本遗留的不太使用了,变量引用相关
- setter/getter:自定义方法名,如@property (setter=isHidden) BOOL hidden;这样编译器就会自动生成一个名为isHidden的方法做为setter方法;
如果使用了@property指令定义属性,则在实现类中可用以下方式访问
@interface AllWeatherRadial : NSObject
@property float rainHandling;
@end
@implementation AllWeatherRadial
- (void) propertyGetter{
[self rainHandling]; //调用rainHandling()方法
self.rainHandling; //调用rainHandling()方法
_rainHandling; //直接读取属性
}
@end
@synthesize指令详细
如果不想用上面带下划线(直接读取属性)的方式调用,就需要用到 @synthesize 关键字重新声明一次。
- 有一点需要注意的是,使用这个指令时,属性的名称不要以new、alloc、copy、init开头;
- 使用了@property指令后,省略了@synthesize时,则编译器会默认把property声明的属性名称最前面加上下划线,比如name变成_name;
@interface AllWeatherRadial : NSObject
@property float rainHandling;
@end
@implementation AllWeatherRadial
@synthesize rainHandling
- (void) propertyGetter{
rainHandling; //直接读取属性,这样就不用前面加_了
}
@end
@dynamic指令
这个指令比较简单,就是阻止自动生成getter/setter方法
#import <Foundation/Foundation.h>
@interface AllWeatherRadial : NSObject
@property float rainHandling;
@property float snowHandling;
@end
//---------------------
@implementation AllWeatherRadial
@dynamic rainHandling;
- (NSString *) description{
NSString *desc;
desc = [[NSString alloc] initWithFormat: @"AllWeatherRadial: / %.1f / %.1f",
[self rainHandling], //这处会报错,因为上面阻止生成了存取方法
[self snowHandling]];
return (desc);
}
@end
方法定义
在objc中会区分类方法和实例方法,比如alloc就是一个类方法,而init就是一个实例方法。
- -:减号定义实例方法,必须实例化类后才能使用;
- +:加号用来定义类方法,相当于static方法,可以不实例化类直接使用;
消息:接收者+方法名+参数三部分组成,在ObjC中方法调用有时称为消息发送,消息的格式其实就是[NSObject alloc]这样的格式,前面是接收者,后面是方法名
语法格式
注意参数的定义格式,参数名称这个东西这点和python比较类似。
无参函数
有参函数:建议的是第二种格式。
- 有参数时需要在方法名称后加一个冒号:,否则直接以分号或{}结尾即可;多个参数以冒号:相连;
- (int) n,必须要指定其类型,参数名称任意,例如:- (int) getNumerator;
- ObjC语言中如果参数或变量是一个引用类型,则需要加一个*号,例如:- (NSString *) getNumerator: (NSString *) val;
返回类型:如果有返回值,需要在方法实现的最后加一个return语句;
返回值
- 返回值为基本数据,则直接用 () 包装即可,比如 (int);
- 返回值为指针(对象),则需要用( *)包装,比如(NSString *);
示例程序
// 无返回值,无参数
- (void) createLabel{
}
//有返回值 ,无参数
- (BOOL) createLabel{
return YES;
}
- (NSTextField *) createLabel{
NSTextField *label = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 10, 280, 20)];
return label;
}
//无返回值,有一个参数
- (void) formRichLabel:(NSTextField *) richTextLabel{
}
//无返回值,有多个参数,参数名+类型,第一个参数不需要指定参数名
- (void)addAttribute: (NSAttributedStringKey)name
value:(id)value
range:(NSRange)range{
}
//用const关键字,定义为不可变参数
- (void) setTo: (const int) n over:(int) d
事实上,参数名是可选的,如下实现与上述等价,但是如果定义了名称则所有的实现和调用都需要带名称。
//方法定义
- (void) setTo: (int) n :(int) d
私有方法
严格来讲这并不是私有方法,因为ObjC没有私有这一定义,只是为了一些编程方便,设置在实现类中的一些私有方法。多数充当测试和工具类使用。
#import <Foundation/Foundation.h>
NSString *bool2Str(BOOL y){
if (y == YES){
return @"yes";
}else{
return @"no";
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
NSLog(@"Hello, World!\n");
printf("Hello, World!\n");
NSLog(@"%d = %@", 1, bool2Str(1));
NSLog(@"%d = %@", YES, bool2Str(YES));
}
return 0;
}
类的描述
在上面的程序中,如果通过NSLog()函数可以打印一个类的实例,一般只会打印一个内存地址出来,如果想变成一个可读性更强的文案就需要覆写NSObject对象提供的description方法(此方法和java的toString类似)。
descritpion 方法
在ObjC中NSLog(@"%@)调用的就是对象的description方法,此方法直接在实现中写即可,不需要在接口中定义,因为它是在NSObject中实现的,下面例子演示了自定义类的描述信息的方法,注意下面两种方式实现的不同
+ (NSString *)description{
return @"aaa";
}
NSLog(@"%@", s);
NSLog(@"%@", [Rectangle description]);
/*
<Square: 0x10110d950>
aaa
*/
- (NSString *)description{
return @"aaa";
}
Rectangle *s = [[Square alloc] init];
NSLog(@"%@", s);
NSLog(@"%@", [Rectangle description]);
/*
aaa
Rectangle
*/
推荐的方式是声明为 - ,因为它会打印出更多信息
- (NSString *)description{
NSString *str = [[NSString alloc] initWithFormat:@"size=%d length=%d", size, length];
return str;
}
/*
size=5 length=6
Rectangle
*/
class 方法
在ObjC中也有和java一样的一个class方法,比如得到当前类的类型:
[Complex class] //Complex是一个自定义的类
if([ob1 class] == [ob2 class]) //比较两个类是否是同一类型
sizeof 内存大小
这个方法可以打印当前对象、变量等占用的内存大小,单位为byte。
//基本数据大小
int a[3] = {1,2,4};
sizeof(a);//~~12
//对象大小
sizeof(*myFract);