天天看點

别搞混類别(Category)、拓展(Extension)

很多人都知道類别、分類的用法,但是對于一些細節就不是很清楚了,本文主要梳理下這3個概念的細節

類别(Category)

檔案特征

  • 類别檔案有2個,分别為 .h 和 .m
  • 命名為: “類名+類别名.h”和“類名+類别名.m”

檔案内容格式

.h 檔案格式

#import "類名.h"

@interface 類名 (類别名)
// 在此處聲明方法
@end
           

.m 檔案格式

#import "類名+類别名.h"

@implementation 類名 (類别名)
// 在此處實作聲明的方法
@end
           

類别的作用

拓展目前類,為類添加方法

類别的局限性

  • 無法向現有的類添加執行個體變量(編譯器報“instance variables may not be placed in categories”)。Category 一般隻為類提供方法的拓展,不提供屬性的拓展。但是利用 Runtime 可以在 Category 中添加屬性
  • 方法名稱沖突的情況下,如果 Category 中的方法與目前類的方法名稱重名,Category 具有更高的優先級,類别中的方法将完全取代現有類中的方法(調用方法的時候不會去調用現有類裡面的方法實作)。
  • 當現有類具有多個 Category 的時候,如果每個 Category 都有同名的方法,那麼在調用方法的時候肯定不會調用現有類的方法實作。系統根據編譯順序決定調用哪個 Category 下的方法實作。(可以在 Targets -> Build phases -> Compile Sources 下給多個 Category 更換順序看看到底在執行哪個方法)

Category 的使用和注意

  1. Category 中的方法如果和現有類方法一緻,工程中任何調用目前類的方法的時候都會去調用 Category 裡面的方法(比如:UIViewCtroller、UITableView這些)的方法時要慎重。因為用Category重寫類中的方法會對子類造成很大的影響。比如:用Category 重寫了 UIViewCtroller 的方法 A,那麼如果你在工程中用到的所有繼承自 UIViewCtroller 的子類,去調用方法 A 時,執行的都是 Category 中重寫的方法 A,如果不幸的是,你寫的方法 A 有 Bug,那麼會造成整個工程中調用該方法的所有 UIViewCtroller 子類的不正常。除非你在子類中重寫了父類的方法 A,這樣子類調用方法 A 時是調用的自己重寫的方法 A,消除了父類 Category 中重寫方法對自己的影響
  2. Category拓展方法按照有沒有重寫目前類中的方法,分為未重寫的拓展方法和重寫拓展方法。且類引用自己的 Category 時,隻能在 .m 檔案中引用(.h 檔案引用自己的類别會報錯)。子類引用父類的 Category 在 .h 或 .m 都可以。如果類調用 Category 中重寫的方法,不用引入 Category 頭檔案,系統會自動調用 Category 中的重寫方法
  3. Category 中如果重寫了 A 類從父類繼承來的某方法,不會影響與 A 同層級的 B 類
  4. 子類會不會繼承父類的 Category: Category 中重寫的方法會對子類造成影響,但是子類不會繼承非重寫的方法(現有類中沒有的方法)。但是在子類中引入父類 Category 的聲明檔案後,子類就會繼承 Category 的非重寫方法。繼承的表現是:當子類的方法和父類 Category 中的方法名完全相同,那麼子類裡的方法會覆寫掉父類 Category,相當于子類重寫了繼承自父類的方法
  5. Category 的作用是向下有效的。即隻會影響到該類的所有子類。比如 A 類和 B 類是繼承自 Super 類的2個子類,當給 A 類添加一個 Category sayHello 方法,僅有A 類的子類才可以使用 sayHello 方法

拓展(Extension)

檔案特征

  • 隻存在一個檔案
  • 命名方式:“類名_拓展名.h”
#import "類名.h"

@interface 類名 ()
// 在此添加私有成員變量、屬性、聲明方法
@end
           

拓展的作用

  1. 為類增加額外的屬性、成員變量、方法聲明
  2. 一般将類拓展直接寫到目前類的 .m 檔案中。不單獨建立
  3. 一般私有的屬性和方法寫到類拓展中
  4. 和 Category 類似,但是小括号裡面沒有拓展的名字

拓展的局限性

  1. Extension 中添加的屬性、成員變量、方法屬于私有(隻可以在本類的 .m 檔案中通路、調用。在其他類裡面是無法通路的,同時子類也是無法繼承的)。假如我們有這樣一個需求,一個屬性對外是隻讀的,對内是可以讀寫的,那麼我們可以通過 Extension 實作。
  2. 通常 Extension 都寫在 .m 檔案中,不會單獨建立一個 Extension 檔案。而且 Extension 必須寫到 @implementation 上方,否則編譯報錯
  3. 類拓展定義的方法和屬性必須在類的實作檔案中實作。如果單獨定義類擴充的檔案并且隻定義屬性的話,也需要将類實作檔案中包含進類擴充檔案,否則會找不到屬性的 setter 和 getter 方法。
//Web.h
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Web : Person
@end
NS_ASSUME_NONNULL_END

//Web.m
#import "Web.h"
#import "Web+H5.h"
@interface Web ()
@property (nonatomic, strong) NSString *skillStacks;
@end
@implementation Web
- (void)test {
    self.skills =  @"iOS && Web && Node && Hybrid";
    self.skillStacks = @"iOS && Web && Node && Hybrid";
}
- (void)show {
    NSLog(@"%@",self.skillStacks);
}
@end
           

總結

  1. Category 隻能拓充方法,不能拓展屬性和成員變量(包含成員變量會報錯。屬性雖然不可以直接拓展,利用 Runtime 可以實作)
  2. 如果 Category 中聲明了1個屬性,那麼 Category 隻會生成 setter 和 getter 的聲明,不會有實作
  3. Extension 也被成為匿名的 Category
  4. 分類的方法本質是追加在目前類方法清單後,是以分類的方法會覆寫目前類的方法。

「小插曲」:為 Category 實作屬性的 Setter 和 Getter

#import "Person.h"

NS_ASSUME_NONNULL_BEGIN

@interface Person (Student)


/**< 學号*/
@property (nonatomic, strong) NSString *studyNumber;

@end

NS_ASSUME_NONNULL_END


#import "Person+Student.h"
#import <objc/message.h>

@implementation Person (Student)

- (void)sayHi {
    NSLog(@"大家好,我叫%@,我今年%zd歲了",self.name,self.age);
}


/*
 * 傳統的做法是在 setter 裡面這樣寫
 _studyNumber = studyNumber;
 ARC 自動管理記憶體
 MRC
 [_studyNumber release];
 [studyNumber retain];
 _studyNumber = studyNumber;
 
 但是在 Category裡面不會生成對應的執行個體變量,是以我們可以利用 Runtime 為我們的 category 關聯屬性的值
 setter:objc_setAssociatedObject(self, @selector(firstView), firstView, OBJC_ASSOCIATION_RETAIN);
 getter:objc_getAssociatedObject(self, @selector(firstView));
 }
 */
- (void)setStudyNumber:(NSString *)studyNumber {
    objc_setAssociatedObject(self, @selector(studyNumber), studyNumber
                             , OBJC_ASSOCIATION_RETAIN);
}

//@selector(studyNumber)
- (NSString *)studyNumber {
    return  objc_getAssociatedObject(self, @selector(studyNumber));
}

@end
           

說明:

objc_setAssociatedObject

的第二個參數是

const void * _Nonnull key

是以可以用 “studyNumber” 或者利用

@selector()

的特性傳回的資料類型也滿足,是以示例代碼選用第二種方式

給分類添加屬性的時候,為了避免多人開發對于屬性添加造成的覆寫,我們需要為屬性起一個獨特的名字。比如我們的工程是元件化、子產品化開展的工程,那麼我們可以為屬性命名的時候在前面添加目前子產品的字首。

比如我們在 Login-Register-Module 子產品為 NSURL 的 Category 添加一個 title 的屬性的時候,可以這樣命名 LR_Title。請檢視下面的代碼

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSURL (Title)

@property (nonatomic, copy) NSString *LR_title;

@end

NS_ASSUME_NONNULL_END

#import "NSURL+Title.h"
#import <objc/runtime.h>

@implementation NSURL (Title)

- (void)setLR_title:(NSString *)LR_title
{
    objc_setAssociatedObject(self, @selector(LR_title), LR_title
                             , OBJC_ASSOCIATION_RETAIN);
}

- (NSString *)LR_title
{
    return objc_getAssociatedObject(self, @selector(LR_title));
}

@end