@inerface的11條規(guī)范寫法

總結(jié)一些interface聲明時的規(guī)范,相關(guān)宏的介紹,定義方法時有用的修飾符,編寫注釋的規(guī)范,最終寫出一個合格的頭文件。

  • 1.讀寫權(quán)限
    • 1.1實例變量的@public,@protected,@private關(guān)鍵字
    • 1.2屬性的readonly,readwrite關(guān)鍵字
  • 2.前向聲明
  • 3.只暴露必要的接口和實現(xiàn)
    • 3.1不要暴露任何只在類內(nèi)部使用的私有方法
    • 3.2不要在頭文件里聲明類內(nèi)部遵循的protocol
  • 4.nullability說明
  • 5.定義枚舉
    • 5.1 NS_ENUM
    • 5.2 NS_OPTIONS
    • 5.3 字符串枚舉
  • 6.使用extern向外部提供只讀常量
  • 7.向子類和category提供父類的私有屬性
  • 8.標(biāo)明designated initializer
  • 9.API版本控制
    • 9.1 available
    • 9.2 unavailable
    • 9.3 deprecated
  • 10.額外的修飾符
    • 10.1泛型
    • 10.2 NS_REQUIRES_SUPER
    • 10.3 NS_NOESCAPE
  • 11.寫注釋
    • 11.1單行注釋
    • 11.2多行注釋
    • 11.3枚舉注釋
    • 11.4幾個注釋約定

1.讀寫權(quán)限

.h文件里的聲明是用于暴露給外部的接口,而類內(nèi)部的私有方法、私有屬性和實例變量,應(yīng)該放到.m文件的interface extension里。

1.1 實例變量的@public,@protected,@private關(guān)鍵字

這3個關(guān)鍵字用于修飾實例變量,不能用于修飾屬性。當(dāng)錯誤地使用了實例變量時,Xcode會報錯提示。

關(guān)鍵字 說明
@private 作用范圍只能在自身類
@protected 作用范圍在自身類和繼承自己的子類,什么都不寫,默認(rèn)是此屬性。
@public 作用范圍最大,在任何地方。

示例代碼:

//SearchManager.h
@interface SearchManager : NSObject {
    @public    NSInteger *state;
    @public    NSInteger *timeout;
    @protected id *searchAPI;
    @private   id _privateIvar;
}
@end

由于會暴露私有變量,并且沒有@property的一些高級關(guān)鍵字,很少在頭文件里聲明實例變量。優(yōu)先使用@property。

1.2 屬性的readonly,readwrite關(guān)鍵字

頭文件中的屬性是用于描述這個對象的一系列特性集合。
聲明@property時,在.h里使用readonly,讓外部只有讀的權(quán)限,在.m里使用readwrite,使內(nèi)部擁有讀寫權(quán)限。

示例代碼:

//SearchManager.h
@interface SearchManager : NSObject
@property (nonatomic, readonly) NSInteger * state;
@end
//SearchManager.m
@interface SearchManager : NSObject
@property (nonatomic, readwrite) NSInteger * state;
@end

2.前向聲明

當(dāng)在@interface的接口里用到了其他類,不要在.h里直接導(dǎo)入類的頭文件,這樣會讓使用此頭文件的地方也導(dǎo)入這些不必要的其他頭文件。正確的做法是使用關(guān)鍵字@class進(jìn)行前向聲明。當(dāng)然,如果是繼承了父類,還是需要import父類的頭文件。
示例代碼:

//SearchManager.h
#import "SearchManagerBase.h"http://導(dǎo)入父類的頭文件

@class LocationModel;//前向聲明LocationModel類

typedef void(^LocationSearchCompletionHandler)(LocationModel *location, NSError *error);
@interface LocationSearchManager : SearchManagerBase
- (void)searchLocationWithKeyword:(NSString *)keyword completionHandler:(LocationSearchCompletionHandler)completionHandler;
@end

使用@class會告訴編譯器有這么一個類存在,但是現(xiàn)在并不關(guān)心這個類的具體實現(xiàn),等到調(diào)用者在.m里使用的時候再import這個類即可。使用@class和@protocol分別聲明一個類和一個protocol。
使用前向引用的原因有兩個:

  • 提升編譯效率。
    如果import了LocationModel.h,那么當(dāng)LocationModel.h的內(nèi)容發(fā)生變化時,所有import了LocationModel.h的地方都需要重新編譯。如果.m引用了SearchManager.h,但是并沒有使用LocationModel,就會增加不必要的編譯,降低開發(fā)效率。
  • 解決交叉引用的問題。
    如果類A的頭文件import了B,類B的頭文件import了A,這樣在編譯時會報錯:“can not find interface declaration”,這是因為Objective-C不允許交叉引用。

3.只暴露必要的接口和實現(xiàn)

3.1不要暴露任何只在類內(nèi)部使用的私有方法

頭文件里只聲明那些給外部使用的公開方法,并且在設(shè)計時需要考慮到可測試性,遵循單一職責(zé)。
私有方法只定義在類內(nèi)部,并且為了進(jìn)行區(qū)別,建議在私有方法前加上前綴,例如- (void)p_myPrivateMethod。
由于Apple在它的編碼規(guī)范里聲明了,Apple公司擁有下劃線的方法前綴,就像它擁有NS,UI這些類名前綴一樣,因此不建議我們的私有方法直接使用下劃線作為前綴。否則,當(dāng)你在繼承Cocoa Touch的類時,有可能會覆蓋父類的私有方法,造成難以調(diào)試的錯誤。

3.2不要在頭文件里聲明類內(nèi)部遵循的protocol

錯誤的示例代碼:

//SearchManager.h
@interface SearchManager : NSObject<NSCoding, UITableViewDelegate>
@property (nonatomic, readonly) NSInteger * state;
@end

UITableViewDelegate是類內(nèi)部使用時遵循的protocol,沒有必要暴露給外部,因此應(yīng)該放到.m文件里。
NSCoding則描述了類的特性,用于告訴外部本類可以使用歸檔,因此應(yīng)該放在頭文件里。

4.nullability說明

在聲明時,可以使用下列關(guān)鍵字描述對象是否可以為nil。

關(guān)鍵字 說明
nullable 可空,用于描述objc對象
nonnull 不可空,用于描述objc對象
null_unspecified 不確定,用于描述objc對象
null_resettable set可空,get不為空。僅用于property
_Nullable 可空,用于描述C指針和block
_Nonnull 不可空,用于描述C指針和block
_Null_unspecified 不確定,用于描述C指針和block

示例代碼:

//SearchManager.h
#import "SearchManagerBase.h"
@class LocationModel;

typedef void(^LocationSearchCompletionHandler)(LocationModel *_Nullable location, NSError *_Nullable error);
@interface LocationSearchManager : SearchManagerBase
- (void)searchLocationWithKeyword:(nonnull NSString *)keyword completionHandler:(LocationSearchCompletionHandler _Nonnull)completionHandler;
@end

如果向一個使用nonnull修飾的值賦空,編譯器會給出警告。
在開發(fā)時,大部分時候使用的都是nonnull,因此Apple提供了一對宏NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END來進(jìn)行快速修飾,寫在兩個宏之間的屬性、方法,均會使用nonnull修飾。
示例代碼:

//LocationSearchManager.h

#import "SearchManagerBase.h"
@class LocationModel;

NS_ASSUME_NONNULL_BEGIN
typedef void(^LocationSearchCompletionHandler)(LocationModel *_Nullable location, NSError *_Nullable error);
@interface LocationSearchManager : SearchManagerBase
- (void)searchLocationWithKeyword:(NSString *)keyword completionHandler:(LocationSearchCompletionHandler)completionHandler;
@end
NS_ASSUME_NONNULL_END

5.定義枚舉

關(guān)于NS_ENUM和NS_OPTIONS的區(qū)別,參考這里。
簡單來說,NS_OPTIONS提供了按位掩碼的功能。

5.1 NS_ENUM

示例代碼:

typedef NS_ENUM(NSInteger,SearchState) {
    SearchStateNotSearch,
    SearchStateSearching,
    SearchStateSearchFinished,
    SearchStateSearchFailed
};

5.2 NS_OPTIONS

示例代碼,參考NSKeyValueObserving.h

typedef NS_OPTIONS(NSUInteger, NSKeyValueObservingOptions) {
    NSKeyValueObservingOptionNew,
    NSKeyValueObservingOptionOld,
    NSKeyValueObservingOptionInitial,
    NSKeyValueObservingOptionPrior
};

在使用時就可以用|組合多個option:

[_webView addObserver:self forKeyPath:@"title" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:NULL];

5.3 字符串枚舉

當(dāng)使用字典作為參數(shù)傳遞,或者作為返回值時,往往難以直接提供字典的key,現(xiàn)在使用字符串枚舉即可解決這個問題。
示例代碼,參考NSKeyValueObserving.h

//使用NS_STRING_ENUM宏,定義了一個枚舉類型
typedef NSString * NSKeyValueChangeKey NS_STRING_ENUM;

FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;
FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey;

//使用泛型,聲明了change參數(shù)用到的key,是在NSKeyValueChangeKey的枚舉范圍中
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;

6.使用extern向外部提供只讀常量

這不關(guān)@interface的事,但是和頭文件有關(guān),就放在一起說明了。

//SearchManager.h
extern NSString *const SearchErrorDomain;
extern NSInteger SearchDefaultTimeout;

@interface SearchManager : NSObject
@end
//SearchManager.m
NSString *const SearchErrorDomain = @"SearchErrorDomain";
const NSInteger SearchDefaultTimeout = 20;

@interface SearchManager()
@end

7.向子類和category提供父類的私有屬性

由于類的頭文件只存放那些暴露給外部的屬性和方法,在遇到這些情況時,會遇到障礙:

  • 在子類里或者category里,想要使用父類定義在.m里的私有屬性。
  • 在類的頭文件里屬性是readonly,但是在子類或者category里,需要readwrite權(quán)限。
    由于這些屬性并沒有暴露在頭文件里,因此需要另外建立一個私有頭文件,用來存放這些需要暴露給子類和category的屬性。
    可以參考Apple官方的UIGestureRecognizerSubclass.h。
    示例代碼:
//SearchManager.h
@interface SearchManager : NSObject
///外部訪問,只有讀權(quán)限
@property (nonatomic, readonly) SearchState state;
@end
//SearchManager.m
@interface SearchManager()
///內(nèi)部使用,有讀寫權(quán)限
@property (nonatomic, assign) SearchState state;
///只在內(nèi)部使用的私有屬性
@property (nonatomic, strong) id searchAPI;
@end
///暴露給子類和category的私有屬性和私有方法
//SearchManagerInternal.h
///限制使用此頭文件,防止被別的類誤用
#ifdef SEARCHMANAGER_PROTECTED_ACCESS

#import "SearchManager.h"
@interface SearchManager()
///在internal.h里,重新聲明為readwrite權(quán)限
@property (nonatomic, readwrite, assign) SearchState state;
///暴露私有屬性
@property (nonatomic, strong) id searchAPI;
///暴露私有方法
- (void)p_privateMethod;
@end

#else
#error Only be included by SearchManager's subclass or category!
#endif
///category的實現(xiàn)文件
//SearchManager+Category.m
///聲明私有頭文件的使用權(quán)限
#define SEARCHMANAGER_PROTECTED_ACCESS
///導(dǎo)入私有頭文件
#import "SearchManagerInternal.h"

@implementation SearchManager(Category)
- (void)categoryMethod {
    //擁有了讀寫權(quán)限
    self.state = SearchStateSearching;
    //可以訪問私有屬性
    [self.searchAPI startSearch];
    //可以使用私有方法
    [self p_privateMethod];
}
@end

SearchManagerInternal.h其實也是公開的,其他類也能夠?qū)氩⑹褂?,只能在開發(fā)時進(jìn)行約定。如果想要限制其他類導(dǎo)入,并且提示錯誤,Internal.h可以使用如下方式:

#ifdef MYCLASS_PROTECTED_ACCESS
//聲明部分
#else
#error Only be included by MYCLASS's subclass or category!
#endif

這樣在別的類內(nèi)意外地導(dǎo)入了Internal.h時就會產(chǎn)生編譯警告,并且無法直接使用。缺點是需要在所有使用到Internal.h的地方都#define MYCLASS_PROTECTED_ACCESS。

8.標(biāo)明designated initializer

指定初始化方法,即接收參數(shù)最多的那個初始化方法,其他初始化方法調(diào)用它即可,這樣設(shè)計的目的是為了保證所有初始化方法都正確地初始化實例變量。
在方法后面加上NS_DESIGNATED_INITIALIZER宏即可。這樣,當(dāng)你子類化這個類時,在子類的初始化方法里如果沒有正確地調(diào)用父類的designated initializer,編譯器就會給出警告。
實例代碼:

@interface WKWebView : UIView
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
@end

關(guān)于designated initializer更詳細(xì)的說明,參考:

  • Objective-C 拾遺:designated initializer
  • 正確編寫Designated Initializer的幾個原則

9.API版本控制

在更新接口,或者開發(fā)framework時,需要標(biāo)明版本信息,告訴使用者此接口的平臺限制、操作系統(tǒng)版本、是否可用、是否已棄用等。
蘋果給出了幾個自帶的宏用于標(biāo)明版本,Xcode在檢測到錯誤使用時會給出警告。只需要在方法名后面加上對應(yīng)的宏即可。

9.1 available

聲明本接口最低支持的操作系統(tǒng)版本。
當(dāng)你的接口使用了新系統(tǒng)的API,例如iOS8以上才有的UIAlertController,但是項目的deployment target卻是iOS7時,需要標(biāo)明此接口的版本信息,讓使用者進(jìn)行兼容。
示例:

//SearchManager.h

typedef NS_ENUM(NSInteger,SearchState) {
    SearchStateNotSearch,
    SearchStateSearching,
    SearchStateSearchFinished,
    SearchStateSearchFailed
} NS_ENUM_AVAILABLE_IOS(2_0);//此枚舉在iOS2.0以上才能使用

NS_CLASS_AVAILABLE_IOS(2_0) //此類在iOS2.0以上才能使用
@interface SearchManager : NSObject
- (void)reSearch NS_AVAILABLE_IOS(5_0);//此方法在iOS5.0以上才能使用
@end

這幾個宏有對應(yīng)平臺的版本,例如NS_AVAILABLE_MAC, NS_AVAILABLE_IOS, NS_AVAILABLE_IPHONE。
iOS10開始提供了新的available宏API_AVAILABLE,用來統(tǒng)一macOS、iOS、watchOS、tvOS幾個平臺。

API_AVAILABLE(macos(10.10))
API_AVAILABLE(macos(10.9), ios(10.0))
API_AVAILABLE(macos(10.4), ios(8.0), watchos(2.0), tvos(10.0))

9.2 unavailable

聲明此接口不可用,大多數(shù)時候是用于聲明所在平臺限制。
示例:

@interface SearchManager : NSObject
- (void)searchInWatch NS_UNAVAILABLE;//不能用此接口
- (void)searchInHostApp NS_EXTENSION_UNAVAILABLE_IOS;//extension里不能用此接口
- (void)search __TVOS_PROHIBITED;//tvOS里不能用此接口,可修飾枚舉,類,方法,參數(shù)
@end

iOS10開始提供了新的unavailable宏API_UNAVAILABLE:

API_UNAVAILABLE(macos)
API_UNAVAILABLE(watchos, tvos)

9.3 deprecated

聲明此接口已經(jīng)被棄用,可以同時加注釋注明替代接口。
當(dāng)deployment target版本號設(shè)置成大于或等于方法被棄用的版本號時,Xcode會給出警告。
示例:

//注明廢棄類
NS_CLASS_DEPRECATED_IOS(2_0, 9_0, "UIAlertView is deprecated. Use UIAlertController with a preferredStyle of UIAlertControllerStyleAlert instead")
@interface UIAlertView : UIView
@end
//注明廢棄API
@interface UIViewController : UIResponder
- (void)viewDidUnload NS_DEPRECATED_IOS(3_0,6_0);
@end
//注明廢棄枚舉
typedef NS_ENUM(NSInteger, UIStatusBarStyle) {
    UIStatusBarStyleDefault                                     = 0, // Dark content, for use on light backgrounds
    UIStatusBarStyleLightContent     NS_ENUM_AVAILABLE_IOS(7_0) = 1, // Light content, for use on dark backgrounds
    
    UIStatusBarStyleBlackTranslucent NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 1,
    UIStatusBarStyleBlackOpaque      NS_ENUM_DEPRECATED_IOS(2_0, 7_0, "Use UIStatusBarStyleLightContent") = 2,
}

iOS10開始提供了新的deprecated宏API_DEPRECATEDAPI_DEPRECATED_WITH_REPLACEMENT。前者可以注明棄用原因,后者可以注明替代接口。

API_DEPRECATED("No longer supported", macos(10.4, 10.8))
API_DEPRECATED("No longer supported", macos(10.4, 10.8), ios(2.0, 3.0), watchos(2.0, 3.0), tvos(9.0, 10.0))

API_DEPRECATED_WITH_REPLACEMENT("-setName:", tvos(10.0, 10.4), ios(9.0, 10.0))
API_DEPRECATED_WITH_REPLACEMENT("SomeClassName", macos(10.4, 10.6), watchos(2.0, 3.0))

10.額外的修飾符

10.1 泛型

在聲明時,對集合類型的對象增加泛型的修飾,就可以聲明集合內(nèi)存儲的數(shù)據(jù)類型。
例如:

@property (nonatomic, strong) NSMutableArray<NSString *> *myArray;

當(dāng)你向myArray里放入一個非NSString *類型的對象時,編譯器會給出警告。

@property(nonatomic, strong) NSMutableArray<__kindof UIView *> * viewArray;

_kindof只限定了存儲類型為UIView,因此也可以存儲UIView的子類,例如UIButton
更詳細(xì)的介紹,參考:[Objective—C語言的新魅力——Nullability、泛型集合與類型延拓

10.2 NS_REQUIRES_SUPER

NS_REQUIRES_SUPER宏用于聲明子類在重載父類的這個方法時,需要調(diào)用父類的方法。例如:

- (void)viewWillAppear:(BOOL)animated NS_REQUIRES_SUPER;

10.3 NS_NOESCAPE

NS_NOESCAPE用于修飾方法中的block類型參數(shù),例如:

@interface NSArray: NSObject
- (NSArray *)sortedArrayUsingComparator:(NSComparator NS_NOESCAPE)cmptr
@end

作用是告訴編譯器,cmptr這個block在sortedArrayUsingComparator:方法返回之前就會執(zhí)行完畢,而不是被保存起來在之后的某個時候再執(zhí)行。
類似于這樣的實現(xiàn):

- (void)performWithLock:(NS_NOESCAPE void (^)())block {  // exposed as @noescape to Swift
    [myLock lock];
    block();
    [myLock unlock];
}

編譯器知道之后,就會相應(yīng)地做一些優(yōu)化,例如去掉一些多余的對self的捕獲、retain、release操作。因為block的存活范圍僅限于本方法內(nèi),沒有必要再在block內(nèi)保留self了。
更詳細(xì)的介紹,參考這里。

11.寫注釋

頭文件就是文檔,需要讓使用者快速知道這個類的作用。一個好的方法名可以讓使用者快速理解,但大部分時候還是需要相應(yīng)的注釋。
寫好格式化注釋后,當(dāng)光標(biāo)停留在方法名和屬性上時,在Xcode右側(cè)的Quick Help欄里會出現(xiàn)注釋內(nèi)容,按住option并單擊,也會彈出注釋框。

11.1單行注釋

直接在方法或者屬性聲明的上一行使用///,后面加注釋,同時兼容Xcode和appleDoc。Xcode也支持//!,但是appleDoc不支持。

//SearchManagerBase.h

///搜索manager的基類
@interface SearchManagerBase : NSObject
///搜索狀態(tài)
@property (nonatomic, readonly) NSInteger * state;
@end

11.2多行注釋

多行注釋使用:

/**
 注釋內(nèi)容
*/

Xcode8提供了快速生成格式化注釋的快捷鍵:option+command+/。如果方法有參數(shù),會自動添加@param關(guān)鍵字,用于描述對應(yīng)的參數(shù)。
Apple提供了官方的headDoc語法,但是很多都已經(jīng)在Xcode中失效了,而且有些關(guān)鍵字也和appleDoc不兼容。下面幾種列舉出了在Xcode中仍然有效的一些關(guān)鍵字:

/**
 演示蘋果headDoc的語法。這里可以寫方法簡介
 
 @brief 方法的簡介(appleDoc不支持此關(guān)鍵字)
 @discussion 方法的詳細(xì)說明
 
 @code //示例代碼(這個在Xcode里常用,但是appleDoc不支持此關(guān)鍵字)
 UIView *view;
 @endcode
 
 @bug       存在的bug的說明
 @note      需要注意的提示
 @warning   警告
 @since     iOS7.0
 @exception 方法會拋出的異常的說明
 
 @attention 注意,從這里開始往下的關(guān)鍵字,appleDoc都不支持
 @author    編寫者
 @copyright 版權(quán)
 @date      日期
 @invariant 不變量
 @post      后置條件
 @pre       前置條件
 @remarks   備注
 @todo      todo text
 @version   版本
 */
- (void)sampleMethod;

在Xcode中,就會顯示為這樣:


comment.png

11.3 枚舉注釋

如果要給枚舉注釋,需要在每個枚舉值前注釋,按照如下格式:

///搜索狀態(tài)
typedef NS_ENUM(NSInteger,SearchState) {
    ///沒有開始搜索
    SearchStateNotSearch,
    ///搜索中
    SearchStateSearching,
    ///搜索結(jié)束
    SearchStateSearchFinished,
    ///搜索失敗
    SearchStateSearchFailed
};

11.4 幾個注釋約定

需要注釋的內(nèi)容:

  • 盡量為類添加描述,即便只有一句話。
  • 標(biāo)明某些參數(shù)和屬性的默認(rèn)值,比如超時time。
  • 如果屬性是KVO兼容的,即外部可以使用KVO監(jiān)聽此屬性,則在屬性注釋里聲明。
  • 回調(diào)block參數(shù)需要說明回調(diào)所在的線程,避免讓使用者在block里進(jìn)行多余的線程判斷。
  • 如果需要的話,說明使用此API需要的前置條件,防止被錯誤地調(diào)用。
  • 對使用了method swizzling的API進(jìn)行統(tǒng)一形式的標(biāo)注,方便遇到runtime的bug時進(jìn)行排查。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容