@inerface的11條規範寫法

總結一些interface聲明時的規範,相關宏的介紹,定義方法時有用的修飾符,編寫註釋的規範,最終寫出一個合格的頭文件。ios

  • 1.讀寫權限
    • 1.1實例變量的@public,@protected,@private關鍵字
    • 1.2屬性的readonly,readwrite關鍵字
  • 2.前向聲明
  • 3.只暴露必要的接口和實現
    • 3.1不要暴露任何只在類內部使用的私有方法
    • 3.2不要在頭文件裏聲明類內部遵循的protocol
  • 4.nullability說明
  • 5.定義枚舉
    • 5.1 NS_ENUM
    • 5.2 NS_OPTIONS
    • 5.3 字符串枚舉
  • 6.使用extern向外部提供只讀常量
  • 7.向子類和category提供父類的私有屬性
  • 8.標明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.讀寫權限

.h文件裏的聲明是用於暴露給外部的接口,而類內部的私有方法、私有屬性和實例變量,應該放到.m文件的interface extension裏。git

1.1 實例變量的@public,@protected,@private關鍵字

這3個關鍵字用於修飾實例變量,不能用於修飾屬性。當錯誤地使用了實例變量時,Xcode會報錯提示。github

關鍵字 說明
@private 做用範圍只能在自身類
@protected 做用範圍在自身類和繼承本身的子類,什麼都不寫,默認是此屬性。
@public 做用範圍最大,在任何地方。

示例代碼:web

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

因爲會暴露私有變量,而且沒有@property的一些高級關鍵字,不多在頭文件裏聲明實例變量。優先使用@property。macos

1.2 屬性的readonly,readwrite關鍵字

頭文件中的屬性是用於描述這個對象的一系列特性集合。 聲明@property時,在.h裏使用readonly,讓外部只有讀的權限,在.m裏使用readwrite,使內部擁有讀寫權限。swift

示例代碼:api

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

2.前向聲明

當在@interface的接口裏用到了其餘類,不要在.h裏直接導入類的頭文件,這樣會讓使用此頭文件的地方也導入這些沒必要要的其餘頭文件。正確的作法是使用關鍵字@class進行前向聲明。固然,若是是繼承了父類,仍是須要import父類的頭文件。 示例代碼:bash

//SearchManager.h
#import "SearchManagerBase.h"//導入父類的頭文件

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

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

使用@class會告訴編譯器有這麼一個類存在,可是如今並不關心這個類的具體實現,等到調用者在.m裏使用的時候再import這個類便可。使用@class和@protocol分別聲明一個類和一個protocol。 使用前向引用的緣由有兩個:markdown

  • 提高編譯效率。 若是import了LocationModel.h,那麼當LocationModel.h的內容發生變化時,全部import了LocationModel.h的地方都須要從新編譯。若是.m引用了SearchManager.h,可是並無使用LocationModel,就會增長沒必要要的編譯,下降開發效率。
  • 解決交叉引用的問題。 若是類A的頭文件import了B,類B的頭文件import了A,這樣在編譯時會報錯:「can not find interface declaration」,這是由於Objective-C不容許交叉引用。

3.只暴露必要的接口和實現

3.1不要暴露任何只在類內部使用的私有方法

頭文件裏只聲明那些給外部使用的公開方法,而且在設計時須要考慮到可測試性,遵循單一職責。 私有方法只定義在類內部,而且爲了進行區別,建議在私有方法前加上前綴,例如- (void)p_myPrivateMethod。 因爲Apple在它的編碼規範裏聲明瞭,Apple公司擁有下劃線的方法前綴,就像它擁有NS,UI這些類名前綴同樣,所以不建議咱們的私有方法直接使用下劃線做爲前綴。不然,當你在繼承Cocoa Touch的類時,有可能會覆蓋父類的私有方法,形成難以調試的錯誤。app

3.2不要在頭文件裏聲明類內部遵循的protocol

錯誤的示例代碼:

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

UITableViewDelegate是類內部使用時遵循的protocol,沒有必要暴露給外部,所以應該放到.m文件裏。 而NSCoding則描述了類的特性,用於告訴外部本類可使用歸檔,所以應該放在頭文件裏。

4.nullability說明

在聲明時,可使用下列關鍵字描述對象是否能夠爲nil。

關鍵字 說明
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修飾的值賦空,編譯器會給出警告。 在開發時,大部分時候使用的都是nonnull,所以Apple提供了一對宏NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END來進行快速修飾,寫在兩個宏之間的屬性、方法,均會使用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.定義枚舉

關於NS_ENUM和NS_OPTIONS的區別,參考這裏。 簡單來講,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 字符串枚舉

當使用字典做爲參數傳遞,或者做爲返回值時,每每難以直接提供字典的key,如今使用字符串枚舉便可解決這個問題。 示例代碼,參考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參數用到的key,是在NSKeyValueChangeKey的枚舉範圍中
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context;
複製代碼

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

這不關@interface的事,可是和頭文件有關,就放在一塊兒說明了。

//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權限。 因爲這些屬性並無暴露在頭文件裏,所以須要另外創建一個私有頭文件,用來存放這些須要暴露給子類和category的屬性。 能夠參考Apple官方的UIGestureRecognizerSubclass.h。 示例代碼:
//SearchManager.h
@interface SearchManager : NSObject
///外部訪問,只有讀權限
@property (nonatomic, readonly) SearchState state;
@end
複製代碼
//SearchManager.m
@interface SearchManager()
///內部使用,有讀寫權限
@property (nonatomic, assign) SearchState state;
///只在內部使用的私有屬性
@property (nonatomic, strong) id searchAPI;
@end
複製代碼
///暴露給子類和category的私有屬性和私有方法
//SearchManagerInternal.h
///限制使用此頭文件,防止被別的類誤用
#ifdef SEARCHMANAGER_PROTECTED_ACCESS

#import "SearchManager.h"
@interface SearchManager()
///在internal.h裏,從新聲明爲readwrite權限
@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的實現文件
//SearchManager+Category.m
///聲明私有頭文件的使用權限
#define SEARCHMANAGER_PROTECTED_ACCESS
///導入私有頭文件
#import "SearchManagerInternal.h"

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

SearchManagerInternal.h其實也是公開的,其餘類也可以導入並使用,只能在開發時進行約定。若是想要限制其餘類導入,而且提示錯誤,Internal.h可使用以下方式:

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

這樣在別的類內意外地導入了Internal.h時就會產生編譯警告,而且沒法直接使用。缺點是須要在全部使用到Internal.h的地方都#define MYCLASS_PROTECTED_ACCESS

8.標明designated initializer

指定初始化方法,即接收參數最多的那個初始化方法,其餘初始化方法調用它便可,這樣設計的目的是爲了保證全部初始化方法都正確地初始化實例變量。 在方法後面加上NS_DESIGNATED_INITIALIZER宏便可。這樣,當你子類化這個類時,在子類的初始化方法裏若是沒有正確地調用父類的designated initializer,編譯器就會給出警告。 實例代碼:

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

關於designated initializer更詳細的說明,參考:

9.API版本控制

在更新接口,或者開發framework時,須要標明版本信息,告訴使用者此接口的平臺限制、操做系統版本、是否可用、是否已棄用等。 蘋果給出了幾個自帶的宏用於標明版本,Xcode在檢測到錯誤使用時會給出警告。只須要在方法名後面加上對應的宏便可。

9.1 available

聲明本接口最低支持的操做系統版本。 當你的接口使用了新系統的API,例如iOS8以上纔有的UIAlertController,可是項目的deployment target倒是iOS7時,須要標明此接口的版本信息,讓使用者進行兼容。 示例:

//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
複製代碼

這幾個宏有對應平臺的版本,例如NS_AVAILABLE_MAC, NS_AVAILABLE_IOS, NS_AVAILABLE_IPHONE。 iOS10開始提供了新的available宏API_AVAILABLE,用來統一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

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

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

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

API_UNAVAILABLE(macos)
API_UNAVAILABLE(watchos, tvos)
複製代碼

9.3 deprecated

聲明此接口已經被棄用,能夠同時加註釋註明替代接口。 當deployment target版本號設置成大於或等於方法被棄用的版本號時,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 泛型

在聲明時,對集合類型的對象增長泛型的修飾,就能夠聲明集合內存儲的數據類型。 例如:

@property (nonatomic, strong) NSMutableArray<NSString *> *myArray;
複製代碼

當你向myArray裏放入一個非NSString *類型的對象時,編譯器會給出警告。

@property(nonatomic, strong) NSMutableArray<__kindof UIView *> * viewArray;
複製代碼

_kindof只限定了存儲類型爲UIView,所以也能夠存儲UIView的子類,例如UIButton。 更詳細的介紹,參考:Objective—C語言的新魅力——Nullability、泛型集合與類型延拓

10.2 NS_REQUIRES_SUPER

NS_REQUIRES_SUPER宏用於聲明子類在重載父類的這個方法時,須要調用父類的方法。例如:

- (void)viewWillAppear:(BOOL)animated NS_REQUIRES_SUPER;
複製代碼

10.3 NS_NOESCAPE

NS_NOESCAPE用於修飾方法中的block類型參數,例如:

@interface NSArray: NSObject
- (NSArray *)sortedArrayUsingComparator:(NSComparator NS_NOESCAPE)cmptr
@end
複製代碼

做用是告訴編譯器,cmptr這個block在sortedArrayUsingComparator:方法返回以前就會執行完畢,而不是被保存起來在以後的某個時候再執行。 相似於這樣的實現:

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

編譯器知道以後,就會相應地作一些優化,例如去掉一些多餘的對self的捕獲、retain、release操做。由於block的存活範圍僅限於本方法內,沒有必要再在block內保留self了。 更詳細的介紹,參考這裏

11.寫註釋

頭文件就是文檔,須要讓使用者快速知道這個類的做用。一個好的方法名可讓使用者快速理解,但大部分時候仍是須要相應的註釋。 寫好格式化註釋後,當光標停留在方法名和屬性上時,在Xcode右側的Quick Help欄裏會出現註釋內容,按住option並單擊,也會彈出註釋框。

11.1單行註釋

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

//SearchManagerBase.h

///搜索manager的基類
@interface SearchManagerBase : NSObject
///搜索狀態
@property (nonatomic, readonly) NSInteger * state;
@end
複製代碼

11.2多行註釋

多行註釋使用:

/**
 註釋內容
*/
複製代碼

Xcode8提供了快速生成格式化註釋的快捷鍵:option+command+/。若是方法有參數,會自動添加@param關鍵字,用於描述對應的參數。 Apple提供了官方的headDoc語法,可是不少都已經在Xcode中失效了,並且有些關鍵字也和appleDoc不兼容。下面幾種列舉出了在Xcode中仍然有效的一些關鍵字:

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

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

comment.png

11.3 枚舉註釋

若是要給枚舉註釋,須要在每一個枚舉值前註釋,按照以下格式:

///搜索狀態
typedef NS_ENUM(NSInteger,SearchState) {
    ///沒有開始搜索
    SearchStateNotSearch,
    ///搜索中
    SearchStateSearching,
    ///搜索結束
    SearchStateSearchFinished,
    ///搜索失敗
    SearchStateSearchFailed
};
複製代碼

11.4 幾個註釋約定

須要註釋的內容:

  • 儘可能爲類添加描述,即使只有一句話。
  • 標明某些參數和屬性的默認值,好比超時time。
  • 若是屬性是KVO兼容的,即外部可使用KVO監聽此屬性,則在屬性註釋裏聲明。
  • 回調block參數須要說明回調所在的線程,避免讓使用者在block裏進行多餘的線程判斷。
  • 若是須要的話,說明使用此API須要的前置條件,防止被錯誤地調用。
  • 對使用了method swizzling的API進行統一形式的標註,方便遇到runtime的bug時進行排查。

參考

相關文章
相關標籤/搜索