1. 風格糾錯題
修改完的代碼:
修改方法有很多種,現給出一種做示例:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 修改完的代碼,這是第一種修改方法,后面會給出第二種修改方法
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
@end
下面對具體修改的地方,分兩部分做下介紹:硬傷部分和優化部分
。因為硬傷部分沒什么技術含量,為了節省大家時間,放在后面講,大神請直接看優化部分。
優化部分
-
enum 建議使用
NS_ENUM
和NS_OPTIONS
宏來定義枚舉類型,參見官方的 Adopting Modern Objective-C 一文:
//定義一個枚舉
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
(僅僅讓性別包含男和女可能并不嚴謹,最嚴謹的做法可以參考 [這里](https://github.com/ChenYilong/iOSInterviewQuestions/issues/9) 。)
2. age 屬性的類型:應避免使用基本類型,建議使用 Foundation 數據類型,對應關系如下:
```Objective-C
int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
動畫時間 -> NSTimeInterval
同時考慮到 age 的特點,應使用 NSUInteger ,而非 int 。
這樣做的是基于64-bit 適配考慮,詳情可參考出題者的博文《64-bit Tips》。
- 如果工程項目非常龐大,需要拆分成不同的模塊,可以在類、typedef宏命名的時候使用前綴。
- doLogIn方法不應寫在該類中: <p><del>雖然
LogIn
的命名不太清晰,但筆者猜測是login的意思, (勘誤:Login是名詞,LogIn 是動詞,都表示登陸的意思。見: Log in vs. login )</del></p>登錄操作屬于業務邏輯,觀察類名 UserModel ,以及屬性的命名方式,該類應該是一個 Model 而不是一個“ MVVM 模式下的 ViewModel ”:
無論是 MVC 模式還是 MVVM 模式,業務邏輯都不應當寫在 Model 里:MVC 應在 C,MVVM 應在 VM。
(如果拋開命名規范,假設該類真的是 MVVM 模式里的 ViewModel ,那么 UserModel 這個類可能對應的是用戶注冊頁面,如果有特殊的業務需求,比如: -logIn
對應的應當是注冊并登錄的一個 Button ,出現 -logIn
方法也可能是合理的。)
- doLogIn 方法命名不規范:添加了多余的動詞前綴。
請牢記:
如果方法表示讓對象執行一個動作,使用動詞打頭來命名,注意不要使用
do
,does
這種多余的關鍵字,動詞本身的暗示就足夠了。
應為 -logIn
(注意: Login
是名詞, LogIn
是動詞,都表示登陸。 見 Log in vs. login )
-
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中不要用with
來連接兩個參數:withAge:
應當換為age:
,age:
已經足以清晰說明參數的作用,也不建議用andAge:
:通常情況下,即使有類似withA:withB:
的命名需求,也通常是使用withA:andB:
這種命名,用來表示方法執行了兩個相對獨立的操作(從設計上來說,這時候也可以拆分成兩個獨立的方法),它不應該用作闡明有多個參數,比如下面的:
//錯誤,不要使用"and"來連接參數
- (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes;
//錯誤,不要使用"and"來闡明有多個參數
- (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height;
//正確,使用"and"來表示兩個相對獨立的操作
- (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag;
- 由于字符串值可能會改變,所以要把相關屬性的“內存管理語義”聲明為 copy 。(原因在下文有詳細論述:用@property聲明的NSString(或NSArray,NSDictionary)經常使用copy關鍵字,為什么?)
- “性別”(sex)屬性的:該類中只給出了一種“初始化方法” (initializer)用于設置“姓名”(Name)和“年齡”(Age)的初始值,那如何對“性別”(Sex)初始化?
Objective-C 有 designated 和 secondary 初始化方法的觀念。 designated 初始化方法是提供所有的參數,secondary 初始化方法是一個或多個,并且提供一個或者更多的默認參數來調用 designated 初始化方法的初始化方法。舉例說明:
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
//
@implementation CYLUser
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age
sex:(CYLSex)sex {
if(self = [super init]) {
_name = [name copy];
_age = age;
_sex = sex;
}
return self;
}
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age {
return [self initWithName:name age:age sex:nil];
}
@end
上面的代碼中initWithName:age:sex: 就是 designated 初始化方法,另外的是 secondary 初始化方法。因為僅僅是調用類實現的 designated 初始化方法。
因為出題者沒有給出 .m
文件,所以有兩種猜測:1:本來打算只設計一個 designated 初始化方法,但漏掉了“性別”(sex)屬性。那么最終的修改代碼就是上文給出的第一種修改方法。2:不打算初始時初始化“性別”(sex)屬性,打算后期再修改,如果是這種情況,那么應該把“性別”(sex)屬性設為 readwrite 屬性,最終給出的修改代碼應該是:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 第二種修改方法(基于第一種修改方法的基礎上)
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readwrite, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
@end
.h
中暴露 designated 初始化方法,是為了方便子類化 (想了解更多,請戳--》 《禪與 Objective-C 編程藝術 (Zen and the Art of the Objective-C Craftsmanship 中文翻譯)》。)
- 按照接口設計的慣例,如果設計了“初始化方法” (initializer),也應當搭配一個快捷構造方法。而快捷構造方法的返回值,建議為 instancetype,為保持一致性,init 方法和快捷構造方法的返回類型最好都用 instancetype。
- 如果基于第一種修改方法:既然該類中已經有一個“初始化方法” (initializer),用于設置“姓名”(Name)、“年齡”(Age)和“性別”(Sex)的初始值:
那么在設計對應@property
時就應該盡量使用不可變的對象:其三個屬性都應該設為“只讀”。用初始化方法設置好屬性值之后,就不能再改變了。在本例中,仍需聲明屬性的“內存管理語義”。于是可以把屬性的定義改成這樣
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
由于是只讀屬性,所以編譯器不會為其創建對應的“設置方法”,即便如此,我們還是要寫上這些屬性的語義,以此表明初始化方法在設置這些屬性值時所用的方式。要是不寫明語義的話,該類的調用者就不知道初始化方法里會拷貝這些屬性,他們有可能會在調用初始化方法之前自行拷貝屬性值。這種操作多余而且低效。
-
initUserModelWithUserName
如果改為initWithName
會更加簡潔,而且足夠清晰。 -
UserModel
如果改為User
會更加簡潔,而且足夠清晰。 -
UserSex
如果改為Sex
會更加簡潔,而且足夠清晰。 - 第二個
@property
中 assign 和 nonatomic 調換位置。
推薦按照下面的格式來定義屬性
@property (nonatomic, readwrite, copy) NSString *name;
屬性的參數應該按照下面的順序排列: 原子性,讀寫 和 內存管理。 這樣做你的屬性更容易修改正確,并且更好閱讀。這在《禪與Objective-C編程藝術 >》里有介紹。而且習慣上修改某個屬性的修飾符時,一般從屬性名從右向左搜索需要修動的修飾符。最可能從最右邊開始修改這些屬性的修飾符,根據經驗這些修飾符被修改的可能性從高到底應為:內存管理 > 讀寫權限 >原子操作。
硬傷部分
- 在-和(void)之間應該有一個空格
- enum 中駝峰命名法和下劃線命名法混用錯誤:枚舉類型的命名規則和函數的命名規則相同:命名時使用駝峰命名法,勿使用下劃線命名法。
- enum 左括號前加一個空格,或者將左括號換到下一行
- enum 右括號后加一個空格
-
UserModel :NSObject
應為UserModel : NSObject
,也就是:
右側少了一個空格。 -
@interface
與@property
屬性聲明中間應當間隔一行。 - 兩個方法定義之間不需要換行,有時為了區分方法的功能也可間隔一行,但示例代碼中間隔了兩行。
-
-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;
方法中方法名與參數之間多了空格。而且-
與(id)
之間少了空格。
`-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;`方法中方法名與參數之間多了空格:`(NSString*)name` 前多了空格。
`-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;` 方法中 `(NSString*)name`,應為 `(NSString *)name`,少了空格。
- <p><del>doLogIn方法中的
LogIn
命名不清晰:筆者猜測是login的意思,應該是粗心手誤造成的。
(勘誤:Login
是名詞,LogIn
是動詞,都表示登陸的意思。見: Log in vs. login )</del></p>
2. 什么情況使用 weak 關鍵字,相比 assign 有什么不同?
什么情況使用 weak 關鍵字?
在 ARC 中,在有可能出現循環引用的時候,往往要通過讓其中一端使用 weak 來解決,比如: delegate 代理屬性
自身已經對它進行一次強引用,沒有必要再強引用一次,此時也會使用 weak,自定義 IBOutlet 控件屬性一般也使用 weak;當然,也可以使用strong。在下文也有論述:《IBOutlet連出來的視圖屬性為什么可以被設置成weak?》
不同點:
weak
此特質表明該屬性定義了一種“非擁有關系” (nonowning relationship)。為這種屬性設置新值時,設置方法既不保留新值,也不釋放舊值。此特質同assign類似,
然而在屬性所指的對象遭到摧毀時,屬性值也會清空(nil out)。
而assign
的“設置方法”只會執行針對“純量類型” (scalar type,例如 CGFloat 或
NSlnteger 等)的簡單賦值操作。assigin 可以用非 OC 對象,而 weak 必須用于 OC 對象
3. 怎么用 copy 關鍵字?
用途:
- NSString、NSArray、NSDictionary 等等經常使用copy關鍵字,是因為他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary;
- block 也經常使用 copy 關鍵字,具體原因見官方文檔:Objects Use Properties to Keep Track of Blocks:
block 使用 copy 是從 MRC 遺留下來的“傳統”,在 MRC 中,方法內部的 block 是在棧區的,使用 copy 可以把它放到堆區.在 ARC 中寫不寫都行:對于 block 使用 copy 還是 strong 效果是一樣的,但寫上 copy 也無傷大雅,還能時刻提醒我們:編譯器自動對 block 進行了 copy 操作。如果不寫 copy ,該類的調用者有可能會忘記或者根本不知道“編譯器會自動對 block 進行了 copy 操作”,他們有可能會在調用之前自行拷貝屬性值。這種操作多余而低效。你也許會感覺我這種做法有些怪異,不需要寫依然寫。如果你這樣想,其實是你“日用而不知”,你平時開發中是經常在用我說的這種做法的,比如下面的屬性不寫copy也行,但是你會選擇寫還是不寫呢?
@property (nonatomic, copy) NSString *userId;
- (instancetype)initWithUserId:(NSString *)userId {
self = [super init];
if (!self) {
return nil;
}
_userId = [userId copy];
return self;
}
下面做下解釋:
copy 此特質所表達的所屬關系與 strong 類似。然而設置方法并不保留新值,而是將其“拷貝” (copy)。
當屬性類型為 NSString 時,經常用此特質來保護其封裝性,因為傳遞給設置方法的新值有可能指向一個 NSMutableString 類的實例。這個類是 NSString 的子類,表示一種可修改其值的字符串,此時若是不拷貝字符串,那么設置完屬性之后,字符串的值就可能會在對象不知情的情況下遭人更改。所以,這時就要拷貝一份“不可變” (immutable)的字符串,確保對象中的字符串值不會無意間變動。只要實現屬性所用的對象是“可變的” (mutable),就應該在設置新屬性值時拷貝一份。
用
@property
聲明 NSString、NSArray、NSDictionary 經常使用 copy 關鍵字,是因為他們有對應的可變類型:NSMutableString、NSMutableArray、NSMutableDictionary,他們之間可能進行賦值操作,為確保對象中的字符串值不會無意間變動,應該在設置新屬性值時拷貝一份。
該問題在下文中也有論述:用@property聲明的NSString(或NSArray,NSDictionary)經常使用copy關鍵字,為什么?如果改用strong關鍵字,可能造成什么問題?
4. 這個寫法會出什么問題: @property (copy) NSMutableArray *array;
兩個問題:1、添加,刪除,修改數組內的元素的時候,程序會因為找不到對應的方法而崩潰.因為 copy 就是復制一個不可變 NSArray 的對象;2、使用了 atomic 屬性會嚴重影響性能 ;
第1條的相關原因在下文中有論述《用@property聲明的NSString(或NSArray,NSDictionary)經常使用 copy 關鍵字,為什么?如果改用strong關鍵字,可能造成什么問題?》 以及上文《怎么用 copy 關鍵字?》也有論述。
比如下面的代碼就會發生崩潰
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 下面的代碼就會發生崩潰
@property (nonatomic, copy) NSMutableArray *mutableArray;
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 下面的代碼就會發生崩潰
NSMutableArray *array = [NSMutableArray arrayWithObjects:@1,@2,nil];
self.mutableArray = array;
[self.mutableArray removeObjectAtIndex:0];
接下來就會奔潰:
-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
第2條原因,如下:
該屬性使用了同步鎖,會在創建時生成一些額外的代碼用于幫助編寫多線程程序,這會帶來性能問題,通過聲明 nonatomic 可以節省這些雖然很小但是不必要額外開銷。
在默認情況下,由編譯器所合成的方法會通過鎖定機制確保其原子性(atomicity)。如果屬性具備 nonatomic 特質,則不使用同步鎖。請注意,盡管沒有名為“atomic”的特質(如果某屬性不具備 nonatomic 特質,那它就是“原子的”(atomic))。
在iOS開發中,你會發現,幾乎所有屬性都聲明為 nonatomic。
一般情況下并不要求屬性必須是“原子的”,因為這并不能保證“線程安全” ( thread safety),若要實現“線程安全”的操作,還需采用更為深層的鎖定機制才行。例如,一個線程在連續多次讀取某屬性值的過程中有別的線程在同時改寫該值,那么即便將屬性聲明為 atomic,也還是會讀到不同的屬性值。
因此,開發iOS程序時一般都會使用 nonatomic 屬性。但是在開發 Mac OS X 程序時,使用
atomic 屬性通常都不會有性能瓶頸。
5. 如何讓自己的類用 copy 修飾符?如何重寫帶 copy 關鍵字的 setter?
若想令自己所寫的對象具有拷貝功能,則需實現 NSCopying 協議。如果自定義的對象分為可變版本與不可變版本,那么就要同時實現
NSCopying
與NSMutableCopying
協議。
具體步驟:
- 需聲明該類遵從 NSCopying 協議
- 實現 NSCopying 協議。該協議只有一個方法:
- (id)copyWithZone:(NSZone *)zone;
注意:一提到讓自己的類用 copy 修飾符,我們總是想覆寫copy方法,其實真正需要實現的卻是 “copyWithZone” 方法。
以第一題的代碼為例:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 修改完的代碼
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
@end
然后實現協議中規定的方法:
- (id)copyWithZone:(NSZone *)zone {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
return copy;
}
但在實際的項目中,不可能這么簡單,遇到更復雜一點,比如類對象中的數據結構可能并未在初始化方法中設置好,需要另行設置。舉個例子,假如 CYLUser 中含有一個數組,與其他 CYLUser 對象建立或解除朋友關系的那些方法都需要操作這個數組。那么在這種情況下,你得把這個包含朋友對象的數組也一并拷貝過來。下面列出了實現此功能所需的全部代碼:
// .h文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// 以第一題《風格糾錯題》里的代碼為例
typedef NS_ENUM(NSInteger, CYLSex) {
CYLSexMan,
CYLSexWoman
};
@interface CYLUser : NSObject<NSCopying>
@property (nonatomic, readonly, copy) NSString *name;
@property (nonatomic, readonly, assign) NSUInteger age;
@property (nonatomic, readonly, assign) CYLSex sex;
- (instancetype)initWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
+ (instancetype)userWithName:(NSString *)name age:(NSUInteger)age sex:(CYLSex)sex;
- (void)addFriend:(CYLUser *)user;
- (void)removeFriend:(CYLUser *)user;
@end
// .m文件
// .m文件
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
//
@implementation CYLUser {
NSMutableSet *_friends;
}
- (void)setName:(NSString *)name {
_name = [name copy];
}
- (instancetype)initWithName:(NSString *)name
age:(NSUInteger)age
sex:(CYLSex)sex {
if(self = [super init]) {
_name = [name copy];
_age = age;
_sex = sex;
_friends = [[NSMutableSet alloc] init];
}
return self;
}
- (void)addFriend:(CYLUser *)user {
[_friends addObject:user];
}
- (void)removeFriend:(CYLUser *)user {
[_friends removeObject:user];
}
- (id)copyWithZone:(NSZone *)zone {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [_friends mutableCopy];
return copy;
}
- (id)deepCopy {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
copyItems:YES];
return copy;
}
@end
以上做法能滿足基本的需求,但是也有缺陷:
如果你所寫的對象需要深拷貝,那么可考慮新增一個專門執行深拷貝的方法。
【注:深淺拷貝的概念,在下文中有介紹,詳見下文的:用@property聲明的 NSString(或NSArray,NSDictionary)經常使用 copy 關鍵字,為什么?如果改用 strong 關鍵字,可能造成什么問題?】
在例子中,存放朋友對象的 set 是用 “copyWithZone:” 方法來拷貝的,這種淺拷貝方式不會逐個復制 set 中的元素。若需要深拷貝的話,則可像下面這樣,編寫一個專供深拷貝所用的方法:
- (id)deepCopy {
CYLUser *copy = [[[self class] allocWithZone:zone]
initWithName:_name
age:_age
sex:_sex];
copy->_friends = [[NSMutableSet alloc] initWithSet:_friends
copyItems:YES];
return copy;
}
至于如何重寫帶 copy 關鍵字的 setter這個問題,
如果拋開本例來回答的話,如下:
- (void)setName:(NSString *)name {
//[_name release];
_name = [name copy];
}
不過也有爭議,有人說“蘋果如果像下面這樣干,是不是效率會高一些?”
- (void)setName:(NSString *)name {
if (_name != name) {
//[_name release];//MRC
_name = [name copy];
}
}