谈谈 Objective-C 链式语法的实现

本文由我们团队的 康祖彬 童鞋撰写,这是他的个人主页:https://kangzubin.cn。


引言


对于 Objective-C 的语法,喜欢的人会觉得它是如此的优雅,代码可读性强,接近自然语言,开发者在调用大多数方法时不需要去查看注释或文档,通常只凭借方法名就可以大致知道这个方法的作用,可以理解为 代码即注释;而对于不喜欢的人来说,会觉得这种语法规则太啰嗦了!


直到第三方自动布局框架 Masonry 的出现,如下面代码,大家才发现,原来 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 中如何实现这种链式调用语法。


注:这里要讲的是 点链式语法,不同于常见的 [[[[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


@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


#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


测试代码:


Calculator *calc = [[Calculator alloc] init]; // 初始化一个计算器类实例

 

calc.add(8).minus(4).multiply(6).divide(3); // 链式调用

 

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


分析:


上面通过 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 可修改为如下形式,编译同样顺利通过并正确运行,没有报错。


@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,如下:


- (MASConstraint *)with {

    return self;

}

 

- (MASConstraint *)and {

    return self;

}


以上,关于 Objective-C 链式语法的实现介绍完了。


存在小问题


虽然链式语法使用起来很优雅,看起来很简洁,但在 Xcode 里写代码时,有一个小小的不便捷:当用点语法去访问类某一个 Block 属性时,该 Block 后面的参数 Xcode 并不会提示自动补全,举个例子:


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 并设置相应的边距,用原生的方法进行约束,就需要写下面如此长的代码!


[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,其中最关键的一点就是使用了 链式语法,一行简单易读,符合直觉的代码就能够创建一个约束实现上面功能,如下:


[view1 mas_makeConstraints:^(MASConstraintMaker *make) {

    make.edges.equalTo(superview).with.insets(padding);

}];


关于效率,链式语法肯定会比传统 Objective-C 方法调用低那么一丁点儿。但对于一次链式方法调用,不过只是包括一次属性访问,一次一临时 Block 的创建,一次 Block 的执行,而这些带来的性能影响,几乎可以忽略的。


有人会说,链式语法是对属性(点语法)的误用,本质上没有任何改变,反而使方法的调用层次更加深,不过在我看来,与它带来的便捷、优雅、简单易读而又不降低性能相比,即使是误用又算什么 ?!


References


  • 链式语法与 Objective-C

    http://t.cn/R2cFiKR

  • 如何在 Objective-C 中实现链式语法?

    http://t.cn/RZ5niHa

  • 用 Block 实现链式编程

    http://t.cn/Rc2SlPd

  • Objective-C 一个链式加法计算器实现

    http://t.cn/Rc2Sjqo


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值