Objective-C 链式语法的实现
对于 Objective-C 的语法,喜欢的人会觉得它是如此的优雅,代码可读性强,接近自然语言,开发者在调用大多数方法时不需要去查看注释或文档,通常只凭借方法名就可以大致知道这个方法的作用,可以理解为 代码即注释
;而对于不喜欢的人来说,会觉得这种语法规则太啰嗦了!
直到第三方自动布局框架 Masonry 的出现,如下面代码,大家才发现,原来 Objective-C 还可以这么玩!
Objective-C
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);
}];
1
2
3
4
5
6
|
[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 中如何实现这种链式调用语法。
注:这里要讲的是 点链式语法
,不同于常见的 [[[[someObject a] and] b] someMethod:5] 中括号链式语法
。
如何实现
我们先举个例子,假如对于一个已有类实例 classInstance
,现在要用句点.
和小括号()
的方式连续调用它的“方法” method1
,method2
,method3
,…,如下图所示:
从图中我们可知,要实现链式语法,主要包含 点语法
、小括号调用
、连续访问
三部分,下面我们一一来看:
- 点语法:在 Objective-C 中,对于点语法的使用,最常见于属性的访问,比如对在方法内部用
self.xxx
,在类的实例中用classInstance.xxx
; - 小括号调用:Objective-C 中一般用中括号
[]
来实现方法的调用,而对于Block
的调用则还是保留使用小括号()
的方式,因此我们可以考虑用Block
来实现在链式语法中的()
; - 如何实现连续访问?:
Block
可理解为带有自动变量的匿名函数或函数指针,它也是有返回值的。我们可以把上述类实例每次方法的调用(实质为Block
的调用)的返回值都设为当前类实例本身,即classInstance.method1()
返回了当前classInstance
,此时可在其后面继续进行.method2()
的调用,以此类推。
总结一句话就是:
“我们可以定义类的一些只读的 Block 类型的属性,并把这些 Block 的返回值类型设为当前类本身,然后实现这些 Block 属性的 getter 方法。”
听起来很抽象,不是很好理解,我们用一个 Demo 来说明。下面是一个链式计算器的例子,可以连续地调用计算器的加、减、乘、除方法进行计算。
Calculator.h
Objective-C
@property (nonatomic, assign) NSInteger result; // 保存计算结果
// 下面分别定义加、减、乘、除 四个只读的 Block 类型的属性,
// 设为只读是为了限制只实现 getter 方法,防止我们定义好的 Block 内容被外部修改,
// 这里每个 Block 类型的属性携带一个 NSInteger 类型的参数,而其返回值类型为当前 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
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@interface Calculator : NSObject
@property (nonatomic, assign) NSInteger result; // 保存计算结果
// 下面分别定义加、减、乘、除 四个只读的 Block 类型的属性,
// 设为只读是为了限制只实现 getter 方法,防止我们定义好的 Block 内容被外部修改,
// 这里每个 Block 类型的属性携带一个 NSInteger 类型的参数,而其返回值类型为当前 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
Objective-C
@implementation Calculator
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.result = 0;
return self;
}
// 此处为 add 属性的 getter 方法实现,
// 前面声明 add 属性的类型为 Block 类型,所以此处 getter 方法应返回一个 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, @"除数不能为零!");
self.result /= num;
return self;
};
}
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
#import "Calculator.h"
@implementation Calculator
- (instancetype)init {
self = [super init];
if (!self) {
return nil;
}
self.result = 0;
return self;
}
// 此处为 add 属性的 getter 方法实现,
// 前面声明 add 属性的类型为 Block 类型,所以此处 getter 方法应返回一个 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, @"除数不能为零!");
self.result /= num;
return self;
};
}
@end
|
测试代码:
Objective-C
calc.add(8).minus(4).multiply(6).divide(3); // 链式调用
NSLog(@"%d", (int)calc.result); // 输出 8
1
2
3
4
5
|
Calculator *calc = [[Calculator alloc] init]; // 初始化一个计算器类实例
calc.add(8).minus(4).multiply(6).divide(3); // 链式调用
NSLog(@"%d", (int)calc.result); // 输出 8
|
分析:
Objective-C
^id(NSInteger num) {
self.result += num;
return self;
};
在这个 Block 中,前面已声明其返回值类型为:Calculator,所以在其里面返回了 self,
这样当调用该 Block 时,会返回 self(即类实例本身),流程如下:
(1) calc.add -> 获得一个 Block;
(2) calc.add(8) -> Block 的执行,并返回了 self(即实例 calc)
(3) 于是在 calc.add(8) 后面可继续访问 calc 的其他属性,实现一路点下去...
1
2
3
4
5
6
7
8
9
10
11
12
13
|
上面通过 calc.add 访问 calc 的 add 属性会调用 [calc add] 方法,此方法会返回一个 Block 如下:
^id(NSInteger num) {
self.result += num;
return self;
};
在这个 Block 中,前面已声明其返回值类型为:Calculator,所以在其里面返回了 self,
这样当调用该 Block 时,会返回 self(即类实例本身),流程如下:
(1) calc.add -> 获得一个 Block;
(2) calc.add(8) -> Block 的执行,并返回了 self(即实例 calc)
(3) 于是在 calc.add(8) 后面可继续访问 calc 的其他属性,实现一路点下去...
|
更简洁的实现
上面通过先声明类的一系列 Block 属性,再去实现 Block 属性的 getter 方法来实现链式调用,感觉还是有点啰嗦,有没有更简洁的实现方式呢?我们来看看 Objective-C 中点语法的本质。
点语法的本质1: 在 Objective-C 中,点语法实际上只是一种替换手段,对于属性的 getter 方法,
class.xxx
的写法最终会被编译器替换成[class xxx]
;对于 setter 方法,即把class.xxx
写在等号左边,class.xxx = value
会被转换成[class setXxx:value]
,本质上都是方法调用。点语法的本质2: 也就是说,即使在 class 中并没有显式声明
xxx
属性,在编译时,代码中如果有class.xxx
的写法也会被替换成[class xxx]
,所以只要在 class 中有声明一个名为xxx
的方法,即可在代码中其它地方放心地写class.xxx
(这里暂时先不考虑把class.xxx
写在等号左边被转换成调用 setter 方法的情况)。
所以,最终的解决方案是:
“在定义类的头文件的 @interface 中,直接声明某一方法名为: xxx,该方法的返回值类型为一个 Block,而此 Block 的返回值设为该类本身。”
因此,上述 Calculator.h
可修改为如下形式,编译同样顺利通过并正确运行,没有报错。
Objective-C
@property (nonatomic, assign) NSInteger result; // 保存计算结果
// 上面的属性声明其实是可以省略的,只要声明下面方法即可;
// 在 Objective-C 中,点语法只是一种替换手段,class.xxx 的写法(写在等号左边除外)最终会被编译器替换成 [class xxx],本质上是方法调用;
// add、minus、multiply、divide 四个方法都会返回一个 Block,
// 这个 Block 有一个 NSInteger 类型的参数,并且其返回值类型为当前 Calculator 类型;
// 下面四个方法的实现与上面 Calculator.m 中的一致。
- (Calculator * (^)(NSInteger num)) add;
- (Calculator * (^)(NSInteger num)) minus;
- (Calculator * (^)(NSInteger num)) multiply;
- (Calculator * (^)(NSInteger num)) divide;
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@interface Calculator : NSObject
@property (nonatomic, assign) NSInteger result; // 保存计算结果
// 上面的属性声明其实是可以省略的,只要声明下面方法即可;
// 在 Objective-C 中,点语法只是一种替换手段,class.xxx 的写法(写在等号左边除外)最终会被编译器替换成 [class xxx],本质上是方法调用;
// add、minus、multiply、divide 四个方法都会返回一个 Block,
// 这个 Block 有一个 NSInteger 类型的参数,并且其返回值类型为当前 Calculator 类型;
// 下面四个方法的实现与上面 Calculator.m 中的一致。
- (Calculator * (^)(NSInteger num)) add;
- (Calculator * (^)(NSInteger num)) minus;
- (Calculator * (^)(NSInteger num)) multiply;
- (Calculator * (^)(NSInteger num)) divide;
@end
|
通过阅读 Masonry
源码,我们可以发现它也是这么做的,只声明了方法,并没有声明相应的属性。另外,对于 Masonry
链式语法中的 .and
、.with
等写法只是为了让代码读起来更通顺,实现方式为:声明定义一个名为 “and” 或 “with” 的方法,在方法里直接返回 self
,如下:
Objective-C
return self;
}
- (MASConstraint *)and {
return self;
}
1
2
3
4
5
6
7
|
- (MASConstraint *)with {
return self;
}
- (MASConstraint *)and {
return self;
}
|
以上,关于 Objective-C 链式语法的实现介绍完了。
存在小问题
虽然链式语法使用起来很优雅,看起来很简洁,但在 Xcode 里写代码时,有一个小小的不便捷:当用点语法去访问类某一个 Block 属性时,该 Block 后面的参数 Xcode 并不会提示自动补全,举个例子:
Objective-C
// 下面 .get(...) 里面的参数,Xcode 并不会提示自动补全,需要手动去填写,.success(...) .failure(...) 等也一样,
// 这里不能像传统中括号 [] 方法调用那样,输入方法名就可以自动提示该方法所有的参数并按回车自动补全。
http.get(@"https://kangzubin.cn", nil).success(^(NSURLSessionDataTask *task, id responseObject) {
// Success TODO
}).failure(^(NSURLSessionDataTask *task, NSError *error) {
// Failure TODO
}).resume();
1
2
3
4
5
6
7
8
9
|
XXXHTTPManager *http = [XXXHTTPManager manager];
// 下面 .get(...) 里面的参数,Xcode 并不会提示自动补全,需要手动去填写,.success(...) .failure(...) 等也一样,
// 这里不能像传统中括号 [] 方法调用那样,输入方法名就可以自动提示该方法所有的参数并按回车自动补全。
http.get(@"https://kangzubin.cn", nil).success(^(NSURLSessionDataTask *task, id responseObject) {
// Success TODO
}).failure(^(NSURLSessionDataTask *task, NSError *error) {
// Failure TODO
}).resume();
|
解决方案:Xcode 中有个强大但未被充分利用的功能:Code Snippets(代码块),我们可以把一些常用的代码片段提取出来进行复用,具体详见这里。
为什么要使用链式语法?
在 iOS 6 AutoLayout 刚推出时,许多的开发者都觉得它必将快速取代原来 iOS 开发中使用的 Frame
布局,进而都转到使用 Constraint
进行页面布局。
然而等到真正使用的时候才发现原来 AutoLayout 的使用方法是如此的繁琐!!!
如下,对于仅仅只向 superview 添加一个子 view1 并设置相应的边距,用原生的方法进行约束,就需要写下面如此长的代码!
Objective-C
//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],
]];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
[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],
]];
|
使用这种方式来构建布局简直就是一种折磨,这也是为什么在 AutoLayout 刚刚出现的时候,并没有什么人去使用它。
真正使 AutoLayout 被开发者所使用接受的是大名鼎鼎的 Masonry,其中最关键的一点就是使用了 链式语法
,一行简单易读,符合直觉的代码就能够创建一个约束实现上面功能,如下:
Objective-C
make.edges.equalTo(superview).with.insets(padding);
}];
1
2
3
|
[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];
|
关于效率,链式语法肯定会比传统 Objective-C 方法调用低那么一丁点儿。但对于一次链式方法调用,不过只是包括一次属性访问,一次一临时 Block 的创建,一次 Block 的执行,而这些带来的性能影响,几乎可以忽略的。
有人会说,链式语法是对属性(点语法)的误用,本质上没有任何改变,反而使方法的调用层次更加深,不过在我看来,与它带来的
便捷、优雅、简单易读而又不降低性能相比,即使是误用又算什么 ?!
Objective-C 链式语法的实现的更多相关文章
- 谈谈 Objective-C 链式语法的实现
引言 对于 Objective-C 的语法,喜欢的人会觉得它是如此的优雅,代码可读性强,接近自然语言,开发者在调用大多数方法时不需要去查看注释或文档,通常只凭借方法名就可以大致知道这个方法的作用,可以 ...
- Objective C 链式调用
起因 某日使用DKChainableAnimationKit的时候发现可以如下写代码: view.animation.rotate(180).anchorTopLeft.thenAfter(1.0). ...
- [Objective-C]用Block实现链式编程
看这篇博客时最快让你上手ReactiveCocoa之基础篇看到作者介绍链式编程那一块,发现自己的钻研精神不足.想想自己使用链式编程也有段时间了,对,就是 Masonry 库.自己一直享受点语法带来的效 ...
- PHP弱类型语法的实现
PHP弱类型语法的实现 前言 借鉴了 TIPI, 对 php 源码进行学习 欢迎大家给予意见, 互相沟通学习 弱类型语法实现方式 (弱变量容器 zval) 所有变量用同一结构表示, 既表示变量值, 也 ...
- 算法与数据结构(二) 栈与队列的线性和链式表示(Swift版)
数据结构中的栈与队列还是经常使用的,栈与队列其实就是线性表的一种应用.因为线性队列分为顺序存储和链式存储,所以栈可以分为链栈和顺序栈,队列也可分为顺序队列和链队列.本篇博客其实就是<数据结构之线 ...
- Bytom的链式交易和花费未确认的交易
当我们基于比原做应用的时候,在构建交易过程中会遇到以下两种情况.多个地址向一个地址转账,还有一种就是从一个地址分批次向多个地址转账.那我们今天就来介绍一下这两种交易构建的具体流程,以及贴出具体实现的代 ...
- 内部排序->基数排序->链式基数排序
文字描述 基数排序是和前面各类排序方法完全不相同,前面几篇文章介绍的排序算法的实现主要是通过关键字间的比较和移动记录这两种操作,而实现基数排序不需要进行记录关键字间的比较.基数排序是一种借助多关键字排 ...
- poj-1459-最大流dinic+链式前向星-isap+bfs+stack
title: poj-1459-最大流dinic+链式前向星-isap+bfs+stack date: 2018-11-22 20:57:54 tags: acm 刷题 categories: ACM ...
- C++实现链队类——合肥工业大学数据结构实验5:链式队列
实验5 5.1 实验目的 熟练掌握队列的顺序链式存储结构. 熟练掌握队列的有关算法设计,并在链队列上实现. 根据具体给定的需求,合理设计并实现相关结构和算法. 5.2 实验要求 5.2.1链队列实验要 ...
随机推荐
- Hive 按某列的部分排序 以及 删列操作
脑袋果然还是智商不足. 涉及到的小需求: 某个表test 有一列 tc: a字符串+b字符串+c字符串 拼接组成 把test表,按b字符串排序 输出 遇到的问题: select 里面必须包含 orde ...
- 一次性下载CVPR2016的所有文章
wget --no-clobber --convert-links --random-wait -r -p -E -e robots=off -U mozilla http://www.cv-foun ...
- bzoj2959: 长跑
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #i ...
- Bzoj1497 [NOI2006]最大获利
Time Limit: 5 Sec Memory Limit: 64 MBSubmit: 4449 Solved: 2181 Description 新的技术正冲击着手机通讯市场,对于各大运营商来 ...
- dedecms /member/reg_new.php SQL Injection Vul
catalog . 漏洞描述 . 漏洞触发条件 . 漏洞影响范围 . 漏洞代码分析 . 防御方法 . 攻防思考 1. 漏洞描述 Dedecms会员中心注入漏洞 2. 漏洞触发条件 http://127 ...
- Google Guava vs Apache Commons for Argument Validation
It is an established good practice to validate method arguments at the beginning of the method body. ...
- MOOCULUS微积分-2: 数列与级数学习笔记 5. Another comparison test
此课程(MOOCULUS-2 "Sequences and Series")由Ohio State University于2014年在Coursera平台讲授. PDF格式教材下载 ...
- FZU xxx游戏(拓扑排序+暴力)
xxx游戏 Time Limit: 1000MS Memory Limit: 32768 KB Description 小M最近很喜欢玩XXX游戏.这个游戏很简单,仅由3个场景(分别为1.2. ...
- HD1269迷宫城堡(有向图 && 划分连通块)
迷宫城堡 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)Total Submis ...
- Base64复习
http://www.cnblogs.com/chengxiaohui/articles/3951129.html