本文由我們團(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)用它的“方法” method1
,method2
,method3
桌硫,...夭咬,如下圖所示:
從圖中我們可知,要實現(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ì)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)雅椅寺、簡單易讀而又不降低性能相比浑槽,即使是誤用又算什么 ?返帕!