缘由
看微博@我就叫Sunny怎么了了招聘一个靠谱的 iOS开发,发现自己有很多知识点需要重新梳理,这里是原文:招聘一个靠谱的iOS开发,感兴趣的可以去看一下。 而需要解决问题,看懂《Effective Objective-C》和《禅与 Objective-C 编程艺术》其实就够了!下面我会针对这两本书不同的模块来一一分析这些问题,也是对自己知识的一次重新梳理~当然有错误的地方希望大家可以给我提出来,感谢~
在这里先抛出来一个问题,下边的写法有什么问题呢,反正我第一次看见是一脸懵逼的.
1. 概念篇(熟悉Object - C的语法)
- 在类的头文件中尽量少引入其他类的头文件,尽量使用 @class 的方式代替 #import 的方式
- 多用字面量语法,少用与之等价的方法
字符串类型
NSString *name = @"多用字面量语法,少用与之等价的方法";
字面数值
NSNumber *intNumber = @1;
字面量数组
NSArray *people = @[@"me",@"you",@"haha"];
NSMutableArray *mutablePeoples = @[@"me",@"you",@"haha"].mutableCopy; // 这个写法有问题吗?
id object1 = @"1";
id object2 = nil;
id object3 = @"3";
NSArray *arrA = [NSArray arrayWithObjects:object1,object2,object3, nil];
NSArray *arrB = @[object1,object2,object3];
由于语法的特殊性,arrA会在object2提前结束,并且不会抛出异常,这样的bug找起来就比较恶心,而arrB在运行期就会直接crash,方便定位问题。
字面量字典
NSDictionary *personData = @{@"firtOne":@"me",
@"secndOne":@"you",
@"thirdOne":@"her",
};
如果采用dictionaryWithObjectsAndKeys也会跟arrayWithObjects有同样的问题
复制代码
- 多用类型常量,少用#define预处理指令
一般UI视图,有一些魔法数字我们一般使用 #define KTopViewHeight 100 这种方式来处理,编译器会将 KTopViewHeight 的字符串提取为常量.但是处理器在预编译的时候会将所有引用到 KTopViewHeight 的一律替换为100,这样也许会在不知不觉中一个不想被你改变的具有相同名字 #define KTopViewHeight 会被改变,当然有些人觉得我我多加一些"盐"来保证不会有重名得了,例如:#define KMainTopViewHeight 100,但是这种做法只是减少了它出现问题的风险,而不是避免了。何不用更加优雅的一种方式呢。
static const int KTopViewHeight = 100;
static const NSString *KTopViewMessage = @"多用类型常量,少用#define预处理指令";
复制代码
而且采用这种方式,下次产品经理发疯,想改变界面中用到所有KTopViewMessage字段,我们只需要整改static const NSString *KTopViewMessage的信息就可以了,而不是去一个一个寻找它。 然而如果不想公开定义的常量,并且为了避免相互引用类造成的常量冲突,我们需要采用下边的方式
@interface WebViewController ()
@end
static const int KTopViewHeight = 100; // 这里为什么要用K来作为前缀呢?
static const NSString *KMessage = @"多用类型常量,少用#define预处理指令"; // 这里为什么要用KWebViewController来作为前缀呢?
@implementation WebViewController
复制代码
使用 static 和 const 同时来修饰一个变量,编译器会像#define一样预处理指令一样,将所有的变量都替换为常量。与之对应的就是全局常量,如果想让外部因为自己定义的一个常量怎么做呢?
#import <UIKit/UIKit.h>
extern NSString *const WebViewNotificationMessage;
@interface WebViewController ()// 头文件.h
@end
#import WebViewController.h // 实现.m
NSString *const WebViewNotificationMessage = @"WebViewNotificationMessage";// const修饰的位置会造成什么不用的影响呢?
@implementation WebViewController
@end
复制代码
- 用枚举表示状态、选项、状态码
下面给大家展示一种写法和它的用法
写法:
typedef NS_ENUM(NSInteger, CYLUserGender) {
CYLUserGenderUnknown = 100,
CYLUserGenderMale,
CYLUserGenderFemale,
CYLUserGenderNeuter
};
用法:
switch (CYLUserGender) {
case CYLUserGenderUnknown:
break;
case CYLUserGenderMale:
break;
case CYLUserGenderFemale:
break;
case CYLUserGenderNeuter:
break;
}
复制代码
如果细心的读者会发现我写的switch语句好像少了一部分,因为我们总是习惯在switch语句中加上default分支,而用枚举定义的状态机,我们最好不要有default分支,这样后续加上一个新的状态,编译器就会发出警告信息。
- 条件语句 条件语句体应该总是被大括号包围。尽管有时候你可以不使用大括号(比如,条件语句体只有一行内容),但是这样做会带 来问题隐患。比如,增加一行代码时,你可能会误以为它是 if 语句体里面的。此外,更危险的是,如果把 if 后面的那行代码 注释掉,之后的一行代码会成为 if 语句里的代码。
// 推荐方式
if (!error) {
return success;
}
// 不推荐这些酷炫的写法~
if (!error)
return success;
if (!error) return success;
复制代码
原因:2014年2月 苹果的 SSL/TLS 实现里面发现了知名的 goto fail 错误。
static OSStatus
SSLVerifySignedServerKeyExchange(SSLContext *ctx, bool isRsa, SSLBuffer signedParams,
uint8_t *signature, UInt16 signatureLen)
{
OSStatus err;
...
if ((err = SSLHashSHA1.update(&hashCtx, &serverRandom)) != 0)
goto fail;
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0)
goto fail;
goto fail;
if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0)
goto fail;
...
fail:
SSLFreeBuffer(&signedHashes);
SSLFreeBuffer(&hashCtx);
return
复制代码
- 尤达表达式
推荐:
if ([myValue isEqual:@42]) { ...
不推荐:
if ([@42 isEqual:myValue]) { ...
复制代码
- nil 和 BOOL 检查
采用下边的方式:
if (myValue == nil) { ...
如果少敲了一个"="
if (myValue = nil) { ...// 这简直是一场灾难,因为从语法结构看来这特么哪里可能会有问题
所以有部分程序员呢采用下边的方式:
// 大部分人的写法,这种方式可以避免不小心少敲了一个=号
if (nil == myValue) { ... // 或许有人会提出这是错的,因为在 nil 作为一个常量的情况下,这样做就像 Yoda 表达式了
推荐方式:
if (someObject) { ...
if (![someObject boolValue]) { ...
if (!someObject) { ...
复制代码
- 避免过多的嵌套
在使用条件语句编程时,代码的左边距应该是一条“黄金”或者“快乐”的大道。 也就是说,不要嵌套 if 语句。使用多个 return 可以避免增加循环的复杂度,并提高代码的可读性。因为方法的重要部分没有嵌套在分支里面,并且你可以很清楚地找到相关的代码。
// 推荐
- (void)someMethod {
if (![someOther boolValue]) {
return;
}
//Do something important
}
// 不推荐:
- (void)someMethod {
if ([someOther boolValue]) {
//Do something important
}
}
复制代码
当然如果你的判断条件很多怎么办,推荐使用下边的方式
BOOL nameContainsSwift = [sessionName containsString:@"Swift"];
BOOL isCurrentYear = [sessionDateCompontents year] == 2014;
BOOL isSwiftSession = nameContainsSwift && isCurrentYear;
if (isSwiftSession) { // 拆开的逻辑看起来总是那么舒服
// Do something very cool
}
复制代码
- 三元运算符
三元运算符 ? 应该只用在它能让代码更加清楚的地方。 一个条件语句的所有的变量应该是已经被求值了的。类似 if 语句,计算多个条件子句通常会让语句更加难以理解。或者可以把它们重构到实例变量里面。
推荐:
result = a > b ? x : y;
不推荐:
result = a > b ? x = c > d ? c : d : y;// 不能仅仅让代码看起来那么"cool",而去忽略下一个阅读你代码的人的感受
复制代码
- 命名
通用的约定: 尽可能遵守 Apple 的命名约定,尤其是和 内存管理规则 (NARC) 相关的地方。
推荐使用长的、描述性的方法和变量名。
控件
推荐:
UIButton *settingsButton;
不推荐:
UIButton *setBut;
-----
常量 推荐看:多用类型常量,少用"#define"预处理指令这一点
推荐:
static NSString * const ZOCCacheControllerDidClearCacheNotification = @"ZOCCacheControllerDidClearCacheNotification";
static const CGFloat ZOCImageThumbnailHeight = 50.0f;
不推荐:
#define CompanyName @"Apple Inc."
#define magicNumber 42
----
方法(驼峰,少用and)
推荐:
- (void)setExampleText:(NSString *)text image:(UIImage *)image;
- (void)sendAction:(SEL)aSelector to:(id)anObject forAllCells:(BOOL)flag;
- (id)viewWithTag:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width height:(CGFloat)height;
不推荐:
- (void)setT:(NSString *)text i:(UIImage *)image;
- (void)sendAction:(SEL)aSelector :(id)anObject :(BOOL)flag;
- (id)taggedView:(NSInteger)tag;
- (instancetype)initWithWidth:(CGFloat)width andHeight:(CGFloat)height;
- (instancetype)initWith:(int)width and:(int)height; // Never do this.
字面值:
NSMutableArray *aMutableArray = [@[] mutableCopy];// 还记得前边这个问题吗?它有问题吗?
上面这种书写方式的效率和可读性的都存在问题。
效率方面,一个不必要的不可变对象被创建后立马被废弃了;虽然这并不会让你的 App 变慢(除非这个方法被频繁调用),但是确实没必要为了少打几个字而这样做。
可读性方面,存在两个问题:第一个问题是当你浏览代码并看见 @[] 的时候,你首先联想到的是 NSArray 实例,但是在这种情形下你需要停下来深思熟虑的检查;另一个问题是,一些新手以他的水平看到你的代码后可能会对这是一个可变对象还是一个不可变对象产生分歧。他/她可能不熟悉可变拷贝构造的含义(这并不是说这个知识不重要)。
当然,不存在绝对的错误,我觉得这个其实无伤大雅(除非你去频繁的去调用)。
我和我最后的倔强~
复制代码
- 类的命名和初始化~~
- 类名 原则:类名应该以三个大写字母作为前缀(双字母前缀为 Apple 的类预留)。尽管这个规范看起来有些古怪,但是这样做可以减少 Objective-C 没有命名空间所带来的问题。 所以你的类的名字应该是长这样子的
OnlyTimeLineModel // 驼峰,不占用Apple的命名空间,一个明确的命名~
复制代码
- Designated 和 Secondary 初始化方法
@implementation OnlyEvent
- (instancetype)initWithTitle:(NSString *)title
date:(NSDate *)date
location:(CLLocation *)location
{
self = [super init];
if (self) {
_title = title;
_date = date;
_location = location;
}
return self;
}
- (instancetype)initWithTitle:(NSString *)title
date:(NSDate *)date
{
return [self initWithTitle:title date:date location:nil];
}
- (instancetype)initWithTitle:(NSString *)title
{
return [self initWithTitle:title date:[NSDate date] location:nil];
}
// 或者你需要一个单例的初始化方式
+ (instancetype)sharedInstance
{
static id sharedInstance = nil;
static dispatch_once_t onceToken = 0;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
@end
复制代码
initWithTitle:date:location: 就是 designated 初始化方法,另外的两个是 secondary 初始化方法。因为它们仅仅是调用类实现的 designated 初始化方法
而针对上边初始化方式,我们的属性其实可以更好的去定义
#import "OnlyEvent.h"
@interface OnlyEvent : NSObject
@property (nonatomic,readonly,copy) NSString *title; // 避免其他类修改属性
@property (nonatomic,readonly,strong) NSDate *date;
@property (nonatomic,readonly,strong) CLLocation *location;
@end
而在.m中
@interface OnlyEvent ()
@property (nonatomic, copy) NSString *title;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) CLLocation *location;
@end
复制代码
- 可变对象
任何可以用一个可变的对象设置的((比如NSString,NSArray,NSURLRequest))属性的内存管理类型必须是 copy 的。
这是为了确保防止在不明确的情况下修改被封装好的对象的值(译者注:比如执行 array(定义为 copy 的 NSArray 实例) = mutableArray,copy 属性会让 array 的 setter 方法为 array = [mutableArray copy], [mutableArray copy] 返回的是不可变的 NSArray 实例,就保证了正确性。用其他属性修饰符修饰,容易在直接赋值的时候,array 指向的是 NSMuatbleArray 的实例,在之后可以随意改变它的值,就容易出错)。
你应该同时避免暴露在公开的接口中可变的对象,因为这允许你的类的使用者改变类自己的内部表示并且破坏类的封装。你可以提供可以只读的属性来返回你对象的不可变的副本
/* .h */
@property (nonatomic, readonly) NSArray *elements
/* .m */
- (NSArray *)elements {
return [self.mutableElements copy];
}
复制代码
- Categories
虽然我们知道这样写很丑, 但是我们应该要在我们的 category 方法前加上自己的小写前缀以及下划线,比如- (id)only_myCategoryMethod。这是必要的,因为如果分类使用了同样的方法会造成不可预计的后果 比较好的方法是在 category 名中使用前缀。
@interface NSDate (OnlyTimeExtensions)// 这里提出一个小问题,分类可以添加属性吗?如果不可以说明原因,如果可以怎么实现呢?
- (NSString *)only_myCategoryMethod;
@end
复制代码
- 利用代码块
一个 GCC 非常模糊的特性,以及 Clang 也有的特性是,代码块如果在闭合的圆括号内的话,会返回最后语句的值
NSURL *url = ({
NSString *urlString = [NSString stringWithFormat:@"%@/%@", baseURLString, endpoint];
[NSURL URLWithString:urlString];
});
复制代码
看到这里你应该对于当开始的那个问题有了一些自己的想法,下面公布相对完美的一个答案
typedef NS_ENUM(NSInteger, CYLUserGender) {
CYLUserGenderUnknown = 100,
CYLUserGenderMale,
CYLUserGenderFemale,
CYLUserGenderNeuter
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readwrite, assign) CYLUserGender sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLUserGender)sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLUserGender)sex;
@end
复制代码
是不是很想吐槽,这特么一个问题牵扯了这么多的事情,而且你特么还没有给全,毕竟现成的答案你也许不会珍惜,剩下的自己去努力的获取吧~也希望你把你找到的分享给我~