iOS 有效编写高质量Objective-C方法(一)

1,749 阅读7分钟

欢迎大家关注我的公众号,我会定期分享一些我在项目中遇到问题的解决办法和一些iOS实用的技巧,现阶段主要是整理出一些基础的知识记录下来

文章也会同步更新到我的博客:
ppsheep.com

之前有读过Effective Objective-C 2.0 觉得受益匪浅,决定再读一遍,应该会有不一样的感受,这里就将阅读的过程记录下来,供自己查阅,也供大家参考。

在类的头文件中尽量少引用其他头文件

与C和C++一样,OC也是使用头文件(.h)和实现文件(.m)来分隔代码。
代码看上去是这样:

//PPSTeacher.h
#import <Foundation/Foundation.h>

@interface PPSTeacher : NSObject
@property (nonatomic, copy) NSString *name;
@end


//PPSTeacher.m
#import "PPSTeacher.h"

@implementation PPSTeacher
//具体实现
@end

在OC中 每个类都需要引入Foundation.h。如果在该类本身不引用,那么就需要引用与其超类所对应的基本头文件,比如我们常见的UIViewController 通常继承UIViewControlelr的子类都需要引入UIKit.h 因为在每个用到的控件都会用到UIKit中的大部分内容,在需要用到的那些控件中都已经引入了Foundation.h 所以实际上 还是引入了Foundatiuon框架

好我们现在需要再创建一个学生类 PPSStudent ,每位老师有一位学生,那么我们的代码可能就需要这么写了

#import <Foundation/Foundation.h>
#import "PPSStudent.h"

@interface PPSTeacher : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) PPSStudent *student;

@end

这样写当然不会有什么问题,但是我们想想,在Teacher中,我只需要知道有学生这么一个类就好,我不想知道他到底能干什么,我也不关心。OC提供了这样一种方法,叫做“向前声明”:

@class PPSStudent;

#import <Foundation/Foundation.h>

@class PPSStudent;
@interface PPSTeacher : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) PPSStudent *student;

@end

在实现文件中我们需要引入 #import "PPSStudent.h",因为在实现过程中,我们需要知道PPSStudent能做哪些事情

#import "PPSTeacher.h"
#import "PPSStudent.h"

@implementation PPSTeacher
//实现
@end

这样做的好处有两个:

  • 能够缩短编译器的编译时间
  • 还能够避免循环引用

避免循环引用:

之前是每个老师拥有一个学生,我们再加上逻辑每个学生需要一个老师

如果按照之前直接#import的引入方式

PPSTeacher.h

#import <Foundation/Foundation.h>
#import "PPSStudent.h"

@interface PPSTeacher : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, strong) PPSStudent *student;

@end

PPSStudent.h

#import <Foundation/Foundation.h>
#import "PPSTeacher.h"

@interface PPSStudent : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *name;

- (void)addTeacher:(PPSTeacher *)teacher;

@end

如果我们是按照这种方式去引用了 那么就会造成循环引用,编译是会报错的
在编译PPSStudent时 import了PPSTeacher,然后编译器编译PPSTeacher,编译PPSTeacher时,又发现了PPSStudent, 这样就造成了引用循环。

多用字面量,少用与之等价的方法

使用字面量,能够使代码简洁易读

字面量数值

常规的方法我们需要

NSNumber *number = [NSNumber numberWithInt:1];

使用字面量,我们只需要

NSNumber *number = @1;

并且NSNumber实例表示的所有数据类型都可以使用字面量:

NSNumber *number = @1;
NSNumber *number = @2.5f;
NSNumber *number = @23.312121;
NSNumber *number = @YES;
NSNumber *number = @'a';

字面量数组

一般的创建数组方法:

NSArray *arr = [NSArray arrayWithObjects:@"1",@"2", nil];

使用字面量

NSArray *arr = @[@"1",@"2"];

字面量字典

一般创建方法:

NSDictionary *dic = [NSDictionary dictionaryWithObjectsAndKeys:@"key1",@"object1",@"key2",@"object2", nil];

这样会给我们带来困惑,key和value完全不能一样区分开来

使用字面量:

NSDictionary *dic = @{
                          @"key1"   :   @"value1",
                          @"key2"   :   @"value2",
                      };

使用字面量要点

  • 对于字符串、数值、数组、字典,应尽量使用字面量创建
  • 访问数组或字典,应尽量使用下标发来访问 例如:arr[1] dic[@"key1"]
  • 创建字面量时,需要保证值中没有nil对象,否则会报异常

多用类型常量,少用#define预处理指令

首先讨论不需要对外开放使用的常量

编写代码时,我们经常会定义一些常量。例如,要写一个UIView视图类,此视图显示出来后开始播放动画,播放完成后消失。那么我们可能就会想把这个动画的播放时间,提取为一个常量,一般来说,我们会写成这样:

当然是在.m中定义,因为不需要对外开放

#define ANIMATION_DURATION 0.3

这样定义有一个坏处,就是我们并不知道这个常量 是一个什么类型的常量,也不知道他究竟是干什么的,有一个办法比这种预处理指令更好

static const NSTimeInterval kAnimationDuration = 0.3;

按照此方法定义的常量表明了他的类型为NSTimeInterval,有助于其他团队成员理解代码,并且有助于编写开发文档,如果有更多的常量定义,那么这种方法就更能展现他的优势

对于常量的命名,一般用法是:

如果常量只是作用于当前的编译单元(就是当前的.m实现类),那么应该在常量的名称前加上k

如果常量还要作用于外部,需要以当前的类名为前缀

常量一定要用static const两个一起定义,因为我们本来就是希望它是一个常量,不能够被更改

还有一个原因,因为我们常量只作用于当前的.m类,如果不加上static,那么编译器在编译我们当前的类时,会给它加上一个外部符号(external symbol),如果其他类也定义了一个相同的同名变量,那么编译器就会报错

需要对外开放的常量

这种情况,一般我们比较常见的是,在当前类中需要完成一项操作,需要发送一个全局通知(NSNotificationCenter),用以通知他人,在派发通知时,我们需要用到当前的一个常量字符串,在外部,接收者也需要知道这样一个字符串

我们通常这样定义

在头文件中:(假定当前的类名是PPSView)

extern NSString *const PPSViewNotofication;

在实现文件中

NSString *const PPSViewNotofication = @"PPSViewNotofication";

这样定义的话,在引入该头文件的文件中,当编译器知道extern关键字时,就能明白,在全局符号表中需要一个PPSViewNotofication的符号,编译器无需知道这个符号的定义,当链接成二进制文件后,就能找到这个常量。

在对象内部尽量直接访问实例变量

在对象之外,我们知道总是通过属性(property)来对实例变量进行操作,那么在实例内部应该怎么做呢? 强烈建议在除了在懒加载中,其他情况下,都应该是:

在读取变量时,都应该采用直接访问的形式(_变量名),在设置实例变量时通过属性来设置

我们来看个例子:

当前有个PPSPerson类

//PPSPerson.h
@interface PPSPerson : NSObject

@property (nonatomic, copy) NSString *firstName;
@property (nonatomic, copy) NSString *lastName;

- (NSString *)fullName;

- (void)setFullName:(NSString *)funllName;

@end

//PPSPerson.m
@implementation PPSPerson

-(NSString *)fullName{
    return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];
}

-(void)setFullName:(NSString *)funllName{
    NSArray *components = [funllName componentsSeparatedByString:@" "];//通过空格 将firstName 和lastName分割开来
    self.firstName = components[0];
    self.lastName = components[1];
}

@end

在上面的代码中,我们都通过了点语法,来存取相关的实例变量,现在假设我们不经过存取方法,而是直接访问实例变量:

@implementation PPSPerson

-(NSString *)fullName{
    return [NSString stringWithFormat:@"%@ %@",_firstName,_lastName];
}

-(void)setFullName:(NSString *)funllName{
    NSArray *components = [funllName componentsSeparatedByString:@" "];//通过空格 将firstName 和lastName分割开来
    _firstName = components[0];
    _lastName = components[1];
}

@end

这两种写法有以下几个区别:

  • 由于不经过Objective-C的“方法派发”(后面会讲到),所以直接访问实例变量的速度当然比较快。编译器所生成的代码会直接访问保存对象实例变量的那块内存
  • 直接访问实例变量时,不会调用其“设置方法”,这就绕过了为相关属性所定义的“内存管理语义”。比如,如果在ARC下直接访问一个声明为copy的属性,那么并不会拷贝该属性,只会白柳新值并释放旧值
  • 如果直接访问,不会触发KVO,当然这具体还是需要看需求
  • 通过属性访问有助于排查与之相关的错误,可以再set和get方法中新增断点调试

之前讲的懒加载方法,就必须使用属性来访问了,否则实例变量永远不会初始化

要点

  • 在对象内部读取数据时,应该直接通过实例变量来读,而写入数据时,则应该通过属性来写
  • 在初始化及dealloc方法中,总是应该通过实例变量来读写数据
  • 在懒加载中应该通过属性来读取数据