談?wù)?Objective-C 鏈?zhǔn)秸Z法的實現(xiàn)

本文由我們團(tuán)隊的 康祖彬 童鞋撰寫赞厕,這是他的個人主頁:https://kangzubin.cn艳狐。

引言

對于 Objective-C 的語法,喜歡的人會覺得它是如此的優(yōu)雅皿桑,代碼可讀性強(qiáng)毫目,接近自然語言,開發(fā)者在調(diào)用大多數(shù)方法時不需要去查看注釋或文檔诲侮,通常只憑借方法名就可以大致知道這個方法的作用镀虐,可以理解為 代碼即注釋;而對于不喜歡的人來說沟绪,會覺得這種語法規(guī)則太啰嗦了刮便!

直到第三方自動布局框架 Masonry 的出現(xiàn),如下面代碼绽慈,大家才發(fā)現(xiàn)恨旱,原來 Objective-C 還可以這么玩!

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(superview.mas_top).with.offset(padding.top);
    make.left.equalTo(superview.mas_left).with.offset(padding.left);
    make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom);
    make.right.equalTo(superview.mas_right).with.offset(-padding.right);
}];

今天我們就來談一談在 Objective-C 中如何實現(xiàn)這種鏈?zhǔn)秸{(diào)用語法坝疼。

:這里要講的是 點鏈?zhǔn)秸Z法搜贤,不同于常見的 [[[[someObject a] and] b] someMethod:5] 中括號鏈?zhǔn)秸Z法

如何實現(xiàn)

我們先舉個例子钝凶,假如對于一個已有類實例 classInstance仪芒,現(xiàn)在要用句點.和小括號()的方式連續(xù)調(diào)用它的“方法” method1method2method3桌硫,...夭咬,如下圖所示:

鏈?zhǔn)秸Z法實現(xiàn)示意圖

從圖中我們可知,要實現(xiàn)鏈?zhǔn)秸Z法铆隘,主要包含 點語法卓舵、小括號調(diào)用連續(xù)訪問 三部分膀钠,下面我們一一來看:

  • 點語法:在 Objective-C 中掏湾,對于點語法的使用,最常見于屬性的訪問肿嘲,比如對在方法內(nèi)部用 self.xxx融击,在類的實例中用 classInstance.xxx
  • 小括號調(diào)用:Objective-C 中一般用中括號 [] 來實現(xiàn)方法的調(diào)用雳窟,而對于 Block 的調(diào)用則還是保留使用小括號 () 的方式尊浪,因此我們可以考慮用 Block 來實現(xiàn)在鏈?zhǔn)秸Z法中的 ()
  • 如何實現(xiàn)連續(xù)訪問封救?Block 可理解為帶有自動變量的匿名函數(shù)或函數(shù)指針拇涤,它也是有返回值的。我們可以把上述類實例每次方法的調(diào)用(實質(zhì)為 Block 的調(diào)用)的返回值都設(shè)為當(dāng)前類實例本身誉结,即 classInstance.method1() 返回了當(dāng)前 classInstance鹅士,此時可在其后面繼續(xù)進(jìn)行 .method2() 的調(diào)用,以此類推惩坑。

總結(jié)一句話就是:

“我們可以定義類的一些只讀的 Block 類型的屬性掉盅,并把這些 Block 的返回值類型設(shè)為當(dāng)前類本身,然后實現(xiàn)這些 Block 屬性的 getter 方法以舒≈憾唬”

聽起來很抽象番宁,不是很好理解曲饱,我們用一個 Demo 來說明氛雪。下面是一個鏈?zhǔn)接嬎闫鞯睦映迥啵梢赃B續(xù)地調(diào)用計算器的加、減砌溺、乘、除方法進(jìn)行計算。

Calculator.h

@interface Calculator : NSObject

@property (nonatomic, assign) NSInteger result; // 保存計算結(jié)果

// 下面分別定義加佣谐、減、乘方妖、除 四個只讀的 Block 類型的屬性狭魂,
// 設(shè)為只讀是為了限制只實現(xiàn) getter 方法,防止我們定義好的 Block 內(nèi)容被外部修改,
// 這里每個 Block 類型的屬性攜帶一個 NSInteger 類型的參數(shù)雌澄,而其返回值類型為當(dāng)前 Calculator 類型斋泄。
@property (readonly, nonatomic, copy) Calculator * (^add)(NSInteger num); 
@property (readonly, nonatomic, copy) Calculator * (^minus)(NSInteger num);
@property (readonly, nonatomic, copy) Calculator * (^multiply)(NSInteger num);
@property (readonly, nonatomic, copy) Calculator * (^divide)(NSInteger num);

@end

Calculator.m

#import "Calculator.h"

@implementation Calculator

- (instancetype)init {
    self = [super init];
    if (!self) {
        return nil;
    }
    self.result = 0;
    return self;
}

// 此處為 add 屬性的 getter 方法實現(xiàn),
// 前面聲明 add 屬性的類型為 Block 類型镐牺,所以此處 getter 方法應(yīng)返回一個 Block炫掐;
// 而對于返回的 Block,其返回值類型為 Calculator睬涧,所以在該 Block 里返回了 self募胃。

- (Calculator * (^)(NSInteger num)) add {
    return ^id(NSInteger num) {
        self.result += num;
        return self;
    };
}

- (Calculator * (^)(NSInteger num)) minus {
    return ^id(NSInteger num) {
        self.result -= num;
        return self;
    };
}

- (Calculator * (^)(NSInteger num)) multiply {
    return ^id(NSInteger num) {
        self.result *= num;
        return self;
    };
}

- (Calculator * (^)(NSInteger num)) divide {
    return ^id(NSInteger num) {
        NSAssert(num != 0, @"除數(shù)不能為零!");
        self.result /= num;
        return self;
    };
}

@end

測試代碼:

Calculator *calc = [[Calculator alloc] init]; // 初始化一個計算器類實例

calc.add(8).minus(4).multiply(6).divide(3); // 鏈?zhǔn)秸{(diào)用

NSLog(@"%d", (int)calc.result); // 輸出 8

分析:

上面通過 calc.add 訪問 calc 的 add 屬性會調(diào)用 [calc add] 方法畦浓,此方法會返回一個 Block 如下:

    ^id(NSInteger num) {
        self.result += num;
        return self;
    };

在這個 Block 中痹束,前面已聲明其返回值類型為:Calculator,所以在其里面返回了 self讶请,
這樣當(dāng)調(diào)用該 Block 時祷嘶,會返回 self(即類實例本身),流程如下:

(1) calc.add -> 獲得一個 Block夺溢;
(2) calc.add(8) -> Block 的執(zhí)行论巍,并返回了 self(即實例 calc)
(3) 于是在 calc.add(8) 后面可繼續(xù)訪問 calc 的其他屬性,實現(xiàn)一路點下去...

更簡潔的實現(xiàn)

上面通過先聲明類的一系列 Block 屬性企垦,再去實現(xiàn) Block 屬性的 getter 方法來實現(xiàn)鏈?zhǔn)秸{(diào)用环壤,感覺還是有點啰嗦,有沒有更簡潔的實現(xiàn)方式呢钞诡?我們來看看 Objective-C 中點語法的本質(zhì)郑现。

點語法的本質(zhì)

點語法的本質(zhì)1: 在 Objective-C 中,點語法實際上只是一種替換手段荧降,對于屬性的 getter 方法接箫,class.xxx 的寫法最終會被編譯器替換成 [class xxx];對于 setter 方法朵诫,即把 class.xxx 寫在等號左邊辛友,class.xxx = value 會被轉(zhuǎn)換成 [class setXxx:value],本質(zhì)上都是方法調(diào)用剪返。

點語法的本質(zhì)2: 也就是說废累,即使在 class 中并沒有顯式聲明 xxx 屬性,在編譯時脱盲,代碼中如果有 class.xxx 的寫法也會被替換成 [class xxx]邑滨,所以只要在 class 中有聲明一個名為 xxx 的方法,即可在代碼中其它地方放心地寫 class.xxx(這里暫時先不考慮把 class.xxx 寫在等號左邊被轉(zhuǎn)換成調(diào)用 setter 方法的情況)钱反。

所以掖看,最終的解決方案是:

“在定義類的頭文件的 @interface 中匣距,直接聲明某一方法名為: xxx,該方法的返回值類型為一個 Block哎壳,而此 Block 的返回值設(shè)為該類本身毅待。”

因此归榕,上述 Calculator.h 可修改為如下形式尸红,編譯同樣順利通過并正確運行,沒有報錯刹泄。

@interface Calculator : NSObject

@property (nonatomic, assign) NSInteger result; // 保存計算結(jié)果

// 上面的屬性聲明其實是可以省略的驶乾,只要聲明下面方法即可;
// 在 Objective-C 中循签,點語法只是一種替換手段级乐,class.xxx 的寫法(寫在等號左邊除外)最終會被編譯器替換成 [class xxx],本質(zhì)上是方法調(diào)用;

// add县匠、minus风科、multiply、divide 四個方法都會返回一個 Block乞旦,
// 這個 Block 有一個 NSInteger 類型的參數(shù)贼穆,并且其返回值類型為當(dāng)前 Calculator 類型;
// 下面四個方法的實現(xiàn)與上面 Calculator.m 中的一致兰粉。
- (Calculator * (^)(NSInteger num)) add;
- (Calculator * (^)(NSInteger num)) minus;
- (Calculator * (^)(NSInteger num)) multiply;
- (Calculator * (^)(NSInteger num)) divide;

@end

通過閱讀 Masonry 源碼故痊,我們可以發(fā)現(xiàn)它也是這么做的,只聲明了方法玖姑,并沒有聲明相應(yīng)的屬性愕秫。另外,對于 Masonry 鏈?zhǔn)秸Z法中的 .and焰络、.with 等寫法只是為了讓代碼讀起來更通順戴甩,實現(xiàn)方式為:聲明定義一個名為 "and" 或 “with” 的方法,在方法里直接返回 self闪彼,如下:

- (MASConstraint *)with {
    return self;
}

- (MASConstraint *)and {
    return self;
}

以上甜孤,關(guān)于 Objective-C 鏈?zhǔn)秸Z法的實現(xiàn)介紹完了。

存在小問題

雖然鏈?zhǔn)秸Z法使用起來很優(yōu)雅畏腕,看起來很簡潔缴川,但在 Xcode 里寫代碼時,有一個小小的不便捷:當(dāng)用點語法去訪問類某一個 Block 屬性時描馅,該 Block 后面的參數(shù) Xcode 并不會提示自動補(bǔ)全把夸,舉個例子:

XXXHTTPManager *http = [XXXHTTPManager manager];

// 下面 .get(...) 里面的參數(shù),Xcode 并不會提示自動補(bǔ)全流昏,需要手動去填寫扎即,.success(...) .failure(...) 等也一樣,
// 這里不能像傳統(tǒng)中括號 [] 方法調(diào)用那樣况凉,輸入方法名就可以自動提示該方法所有的參數(shù)并按回車自動補(bǔ)全谚鄙。
http.get(@"https://kangzubin.cn", nil).success(^(NSURLSessionDataTask *task, id responseObject) {
    // Success TODO
}).failure(^(NSURLSessionDataTask *task, NSError *error) {
    // Failure TODO
}).resume();

解決方案:Xcode 中有個強(qiáng)大但未被充分利用的功能:Code Snippets(代碼塊),我們可以把一些常用的代碼片段提取出來進(jìn)行復(fù)用刁绒,具體詳見這里闷营。

為什么要使用鏈?zhǔn)秸Z法?

在 iOS 6 AutoLayout 剛推出時知市,許多的開發(fā)者都覺得它必將快速取代原來 iOS 開發(fā)中使用的 Frame 布局傻盟,進(jìn)而都轉(zhuǎn)到使用 Constraint 進(jìn)行頁面布局。

然而等到真正使用的時候才發(fā)現(xiàn)原來 AutoLayout 的使用方法是如此的繁瑣I┍D锔啊!

如下跟啤,對于僅僅只向 superview 添加一個子 view1 并設(shè)置相應(yīng)的邊距诽表,用原生的方法進(jìn)行約束,就需要寫下面如此長的代碼隅肥!

[superview addConstraints:@[

    //view1 constraints
    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeTop
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeTop
                                multiplier:1.0
                                  constant:padding.top],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeLeft
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeLeft
                                multiplier:1.0
                                  constant:padding.left],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeBottom
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeBottom
                                multiplier:1.0
                                  constant:-padding.bottom],

    [NSLayoutConstraint constraintWithItem:view1
                                 attribute:NSLayoutAttributeRight
                                 relatedBy:NSLayoutRelationEqual
                                    toItem:superview
                                 attribute:NSLayoutAttributeRight
                                multiplier:1
                                  constant:-padding.right],

 ]];

使用這種方式來構(gòu)建布局簡直就是一種折磨竿奏,這也是為什么在 AutoLayout 剛剛出現(xiàn)的時候,并沒有什么人去使用它腥放。

真正使 AutoLayout 被開發(fā)者所使用接受的是大名鼎鼎的 Masonry泛啸,其中最關(guān)鍵的一點就是使用了 鏈?zhǔn)秸Z法,一行簡單易讀秃症,符合直覺的代碼就能夠創(chuàng)建一個約束實現(xiàn)上面功能候址,如下:

[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
    make.edges.equalTo(superview).with.insets(padding);
}];

關(guān)于效率,鏈?zhǔn)秸Z法肯定會比傳統(tǒng) Objective-C 方法調(diào)用低那么一丁點兒种柑。但對于一次鏈?zhǔn)椒椒ㄕ{(diào)用宗雇,不過只是包括一次屬性訪問,一次一臨時 Block 的創(chuàng)建莹规,一次 Block 的執(zhí)行赔蒲,而這些帶來的性能影響,幾乎可以忽略的良漱。

有人會說舞虱,鏈?zhǔn)秸Z法是對屬性(點語法)的誤用,本質(zhì)上沒有任何改變母市,反而使方法的調(diào)用層次更加深矾兜,不過在我看來,與它帶來的便捷患久、優(yōu)雅椅寺、簡單易讀而又不降低性能相比浑槽,即使是誤用又算什么 ?返帕!

References

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市荆萤,隨后出現(xiàn)的幾起案子镊靴,更是在濱河造成了極大的恐慌,老刑警劉巖链韭,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件偏竟,死亡現(xiàn)場離奇詭異,居然都是意外死亡敞峭,警方通過查閱死者的電腦和手機(jī)踊谋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來旋讹,“玉大人褪子,你說我怎么就攤上這事∑澹” “怎么了嫌褪?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長胚股。 經(jīng)常有香客問我笼痛,道長,這世上最難降的妖魔是什么琅拌? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任缨伊,我火速辦了婚禮,結(jié)果婚禮上进宝,老公的妹妹穿的比我還像新娘刻坊。我一直安慰自己,他們只是感情好党晋,可當(dāng)我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布谭胚。 她就那樣靜靜地躺著,像睡著了一般未玻。 火紅的嫁衣襯著肌膚如雪灾而。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天扳剿,我揣著相機(jī)與錄音旁趟,去河邊找鬼。 笑死庇绽,一個胖子當(dāng)著我的面吹牛锡搜,可吹牛的內(nèi)容都是我干的橙困。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼耕餐,長吁一口氣:“原來是場噩夢啊……” “哼凡傅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蛾方,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎上陕,沒想到半個月后桩砰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡释簿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年亚隅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片庶溶。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡煮纵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出偏螺,到底是詐尸還是另有隱情行疏,我是刑警寧澤,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布套像,位于F島的核電站酿联,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏夺巩。R本人自食惡果不足惜贞让,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柳譬。 院中可真熱鬧喳张,春花似錦、人聲如沸美澳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽制跟。三九已至柴墩,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凫岖,已是汗流浹背江咳。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留哥放,地道東北人歼指。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓爹土,卻偏偏與公主長得像,于是被迫代替她去往敵國和親踩身。 傳聞我的和親對象是個殘疾皇子胀茵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容