《52个有效方法》笔记1——熟悉Objective-C
前言
若想深刻地理解一门学科,或者学习一门手艺,必须经过严谨的,成系统,成理论地学习。无疑,一本一本地去读关于它们的著作是最好的方式,因为书籍中的文字流露出的思维逻辑是最细腻最连贯的。在浩瀚的文字中你不仅能知其然,且能知其所以然。而碎片化阅读仿佛阅读一片片的小纸片,是散乱的。你得到的信息是不成系统的,不能融会贯通的,感觉就像盲人摸象似是而非,而深度阅读后仿佛站在了上帝视角,以前所有的疑问和谜团也拨云见日,逐渐清晰了。
这几天在阅读《编写高质量iOS与OS X代码的52个有效方法》,发现书中有很多干货,而且有很多重要的细节是以前不曾认真思考的。所以想记录下来。
在类的头文件中尽量少引入其他头文件
如下我们在.h文件中定义类的属性或者定义方法时,需要引入其有关类的头文件(StudentModel.h)。
#import <UIKit/UIKit.h>
#import "StudentModel.h"
@interface YWViewController : UIViewController
@property (nonatomic, strong)StudentModel *studentModel;
- (void)studentStudy:(StudentModel *)studentModel;
@end
但这样不太完美,我们知道#import "StudentModel.h"
在编译时其实是拷贝动作,把导入的StudentModel.h文件也拷贝进了该类进行编译,如果在.h文件中泛滥导入其他文件,无疑会增加编译时间。
其实在该YWViewController.h文件中仅仅需要声明StudentModel是个类就行了,所以在此我们应该使用@class
关键词声明它是一个类,而在YWViewController.m文件中实现定义的方法时我们需要StudentModel
类的细节,所以这时得用#import "StudentModel.h"
导入头文件。
规范的写法应该像下面这样:
YWViewController.h
#import <UIKit/UIKit.h>
@class StudentModel; // 在.h文件中仅声明其确实是一个类
@interface YWViewController : UIViewController
@property (nonatomic, strong)StudentModel *studentModel;
- (void)studentStudy:(StudentModel *)studentModel;
@end
YWViewController.m
#import "YWViewController.h"
#import "StudentModel.h" // 在.m文件中才正式导入文件,因为需要知道该类细节
@interface YWViewController ()
@end
@implementation YWViewController
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (void)studentStudy:(StudentModel *)studentModel
{
NSLog(@"----%@----%@----",studentModel.studentId,studentModel.name);
}
@end
OC中类里的常量和全局常量
- 在OC中定义一个只对类内部有效的常量:
即在.m文件中用static和const关键词同时来修饰,static表示只在该“编译单元”内有效,const表示其为常量,不可修改,若类里对该常量值进行了修改,编译时会报错。而且一般命名时以k开头表示只对类内部有效的常量。
// YWViewController.m文件
#import "YWViewController.h"
static NSString *const kMyURL = @"http://www.jianshu.com/users/8b79c6535a4b/latest_articles";
@interface YWViewController ()
@end
@implementation YWViewController
- (void)viewDidLoad{
[super viewDidLoad];
}
@end
** 千万需要注意的是:const
和*
的位置前后关系有着非常重要的意义,const
修饰的是它右边的部分。**在上面的例子中const
右边的部分是kMyURL
,因此不可变的是kMyURL
。而若const
处在下面这样的位置,则代表就代表另外的意思了!
static NSString const *kMyURL = @"http://www.jianshu.com/users/8b79c6535a4b/latest_articles";
static const NSString *kMyURL = @"http://www.jianshu.com/users/8b79c6535a4b/latest_articles";
** 因为const
修饰的是它后面的部分,而此时它后面的是(* kMyURL )
。"*"
是指针指向符号 ,也就是说此时kMyURL
指向的内存地址不可变,而内存块中保存的内容是可变的。 **
用下面代码来验证:
static NSString const *kUserName = @"wang66";
NSLog(@"%@----%x",kUserName,&kUserName);
kUserName = @"wang77";
NSLog(@"%@----%x",kUserName,&kUserName);
// 2016-04-09 17:57:06.199 OCDemo[1860:994583] wang66----87f6190
// 2016-04-09 17:57:06.199 OCDemo[1860:994583] wang77----87f6190
给kUserName
赋值时编译器没报错,赋值成功,打印的内存地址一样。由此验证上面的结论是正确的。
- 定义一个全局常量:
** 先在.h文件中通过extern关键词声明此全局常量,然后在.m文件中定义该常量。一般命名全局常量时会加上该类的前缀已提高可读性。**
比如在登录成功后我们需要发送一个已登录的通知,这个通知的名字一般得定义成全局常量,其他地方需要判断是否已登录时需要通过该通知名来观察该通知,即需要使用该全局常量。
// YWViewController.h文件
#import <UIKit/UIKit.h>
extern NSString *const YWHasLoginedNotifition; // 先在.h文件中声明此全局常量
@interface YWViewController : UIViewController
@end
// YWViewController.m文件
#import "YWViewController.h"
NSString *const YWHasLoginedNotifition = @"YWHasLoginedNotifition"; // 然后在.m文件中定义该常量
@interface YWViewController ()
@end
@implementation YWViewController
- (void)viewDidLoad{
[super viewDidLoad];
}
@end
关于数组和字典的初始化
数组和字典初始化系统提供了下面几个方法:
方式1:
NSArray *arr1 = [NSArray arrayWithObjects:@"a", @"b", @"c", nil];
NSArray *arr2 = [[NSArray alloc] initWithObjects:@"a", @"b", @"c", nil];
NSDictionary *dict1 = [NSDictionary dictionaryWithObjectsAndKeys:
@"valueA", @"keyA",
@"valueB", @"keyB",
@"valueC", @"keyC", nil];
NSDictionary *dict2 = [[NSDictionary alloc] initWithObjectsAndKeys:
@"valueA", @"keyA",
@"valueB", @"keyB",
@"valueC", @"keyC", nil];
这几种初始化方法是最基本的,但是有更简洁的初始化方式:
方式2:
NSArray *arr3 = @[@"a", @"b", @"c"];
NSDictionary *dict3 = @{@"keyA":@"valueA",
@"keyB":@"valueB",
@"keyC":@"valueC"};
显然这种写法更简洁,而且语义更符合人的思维习惯,更简单易懂(代码1里字典初始化时value在key前,这和人们平时的思维是相反的)。
这两种初始化方法并不仅仅是写法不同而已,需要注意的是通过方式2初始化时,元素不能为空,否则会crash。初始化时放入集合的元素对象必须保证是非空的。
而通过方式1初始化时若有元素是nil,会怎样呢?
NSArray *arr1 = [NSArray arrayWithObjects:@"a", nil, @"c", nil];
for(int i=0; i<arr1.count; i++)
{
NSLog(@"arr1_____%@",arr1[i]);
}
2015-12-12 20:40:36.341 WangDemo[8254:3133600] arr1_____a
可以从上面代码中看到:通过方式1初始化时,若遇到某元素是nil,则就在该处终止,后面的元素不再放入集合中。上面的代码中arr1在初始化时第二个元素是nil,则在此终止初始化,所以最终arr1里只有一个元素@“a”。
另外。
通过方式2创建的数组和字典都是不可变的,若想变成可变的,那你可以拷贝一份使其成为可变数组:
NSMutableArray *mutArr = [@[@"a", @"b", @"c"] mutableCopy];
不过拷贝新份对象和一开始就这样初始化并无多大差异。总之,具体场景具体选择吧!
NSMutableArray *mutArr = [NSMutableArray arrayWithObjects:@"a", @"b", @"c", nil];
善用枚举表示类型,状态、选项,组合等
枚举的写法:
最常见的枚举定义形式是:
typedef enum{
MainList_Ads=0, //轮播广告
MainList_Topic, //专题
MainList_Classify, //课程分类
MainList_Teacher, //推荐老师
MainList_Celebrity, //名师推荐
MainList_Agency, //推荐机构
}MainListType;
不过OC中对这种定义形式稍稍进行了封装。使其可以指明该枚举底层的数据类型,其实默认都是整形的。既然枚举类型是整形的,那在定义属性时就得是这样的:
@property (nonatomic, assign) MainListType mainListType;
指定第一项为0,则后面项的值会递增。其实通过枚举判断类型和定义一个整形变量判断没有区别。枚举好就好在它的语义一看就明白代表什么意思。
typedef NS_ENUM(NSUInteger, MainListType)
{
MainList_Ads=0, //轮播广告
MainList_Topic, //专题
MainList_Classify, //课程分类
MainList_Teacher, //推荐老师
MainList_Celebrity, //名师推荐
MainList_Agency, //推荐机构
};
枚举的组合:
用枚举表示选项时,选项是可以组合多选的。比如自动布局的条件是枚举,而且需要多选组合使用。
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone = 0,
UIViewAutoresizingFlexibleLeftMargin = 1 << 0,
UIViewAutoresizingFlexibleWidth = 1 << 1,
UIViewAutoresizingFlexibleRightMargin = 1 << 2,
UIViewAutoresizingFlexibleTopMargin = 1 << 3,
UIViewAutoresizingFlexibleHeight = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
既然枚举的每项元素要组合使用以表示不同的情况,那我们就得保证元素组合后的唯一性,不能A和B和C和D组合后表示一个值。
那怎么确定枚举每项组合后值的唯一性呢?
普通的枚举每项值是递增的,此时&&或者||的结果是不唯一的。为了保证组合后的唯一性,人类就给每项元素赋值时做了一点改动:枚举每项的值执行按位或操作(第一项是1的0次方,第二项是1的1次方,第三项是1的2次方·····)。
看下面这张图有助于理解:
“对象等同性”判断
有时我们有比较两对象是否相等的需求。首先想到的是通过等号“==”判断,但它** 仅仅比较的是两个指针本身,而不是其所指向的对象。 **即使两个不同的指针,也有可能都指向同一个对象。
然后我们想想两个对象到底怎样就会相等呢?...
** 首先 **,若他俩是同一个指针的话,那不用再费劲去比较了,两个对象毫无疑问是相等的,一个对象可以被多个指针指向,但一个指针不可能指向多个对象吧;
** 然后 **,俩对象若要相等,他俩肯定同属于一种类型吧,若它们的类型不同,则两个对象肯定不相等。
** 最后 **,在同属于一种类型的基础上,两个对象对应的属性得完全相等。
NSObject协议中有两个用于判断对象等同性的关键方法:
- (BOOL)isEqual:(id)object;
- (NSUInteger)hash;
** 通过isEqual:方法判断的两对象若相等,则它们的hash值肯定也是相等的;但两个对象的hash相等,并不能说明他们相等。
对于我们自定义的对象判断等同性,我们可以实现上面两个方法,实现isEqual:方法来定义我们自定义对象比较的逻辑。实现hash方法来定义自定义对象哈希值的算法。编写hash方法时,应该使用计算速度快而且哈希吗碰撞几率低的算法。(hash方法的编写这个日后补充) **
若StudentModel
类的有比较对象等同性的需求,则实现isEqual:方法。
// StudentModel.m
#import "StudentModel.h"
@implementation StudentModel
- (BOOL)isEqual:(id)object
{
if(self == object) return YES; // 若俩指针相同,则肯定是同一个对象。
if([self class] != [object class]) return NO; // 若俩对象不属于同一类型,则肯定不相等。
StudentModel *otherStuModel = (StudentModel *)object;
if(![_studentId isEqualToString:otherStuModel.studentId])
return NO;
if(![_name isEqualToString:otherStuModel.name]) // 若有一个属性不相等,则俩对象不相等
return NO;
return YES;
}
@end
像NSString,NSArray,NSDictionary等系统对象,还提供了诸如isEqualToString:
、isEqualToArray:
等针对这些类特定的比较方法,这样就不用先判断两个对象是否是同一类型了,这样速度更快。我们的自定义对象也完全可以借鉴。
// StudentModel.m
#import "StudentModel.h"
@implementation StudentModel
- (BOOL)isEqual:(id)object
{
if(self == object) return YES;
if([self class] == [object class]){
return [self isEqualToStudentModel:(StudentModel *)object]; // 此时只需要调用isEqualToStudentModel:方法即可
}else{
return [super isEqual:object];
}
}
// 已知俩对象同为StudentModel类型时
- (BOOL)isEqualToStudentModel:(StudentModel *)stuModel
{
if(![_studentId isEqualToString:stuModel.studentId])
return NO;
if(![_name isEqualToString:stuModel.name])
return NO;
return YES;
}
@end
** 注意:** 判断俩对象相等,并不一定都要判断它们对应的每个属性是否都相等,要根据具体情况而定,若在仅通过“主键”等就可以判断对象是否相等的情况下,完全没必要一个一个去判断属性。
容器中可变类的等同性:
关于copy
** 拷贝的目的:改变原对象不影响副本,改变副本不影响原对象。**
并不是说只要拷贝了就一定会生成一个新对象,而要根据上面拷贝的目的
来分析。比如,若一个不可变的对象进行了copy
操作是不会生成新对象的,因为原始对象就是不可变的,拷贝后的对象也是不可变的,两者均不可变,互不干扰,互不影响,并不需要生成一个新的对象,只需要进行指针拷贝
就行了。
同理,若对一个对象进行了mutableCopy
操作,不管原始对象是可变不可变,均会生成新对象。
** 容器类对象无论进行什么拷贝,其元素对象均是指针拷贝。但可以通过归档再解档的方式实现元素对象的神拷贝。
** ||----修饰NSString
型的属性时为什么要用copy
关键字?----||
这篇文章解释得很好:
什么时候用copy,什么时候用strong
** ||----修饰block
属性时为什么要用copy
关键字?----||**
首先这涉及到MRC时代。因为MRC时期,为了防止block内用到的变量提前释放导致程序崩溃,使用copy将block存放到堆中,此时block会对内部变量进行一次retain操作,从而防止意外清空。同时block放入堆中也会带来一个新的问题,self持有block的引用,如果在block中使用self就会产生循环引用,所以不论MRC还是ARC,我们都分别用blcok和weak来修饰self。
这篇文章解释得很全面:
认识copy关键字