三、是消息?还是属性?

经过三篇文章的铺垫,这里我们终于要讨论到核心部分,就是关于ObjC的消息机制,但不涉及任何关于runtime的内容。

点语法与中括号

我们知道在ObjC中给对象或者类发消息的标准语法是使用一对中括号:

1
2
3
4
NSArray* array = @[@(1),@(2),@(3)];
[array count];
[array objectAtIndex:1];
[array objectEnumerator];

这里我们看一下countobjectAtIndex:objectEnumerator的声明:

1
2
3
4
//Foundition NSArray.h 18行
@property (readonly) NSUInteger count;
- (ObjectType)objectAtIndex:(NSUInteger)index;
- (NSEnumerator<ObjectType> *)objectEnumerator;

count作为readonly属性修饰的@property是有getter方法的,所以我们可以通过发送getter消息获取其值,所以对于property来说,中括号和点语法是等效的:

1
2
[array count];
array.count;

关于这一点,在Cocoa很多头文件中可以看到这样的声明:

1
2
3
4
5
6
// UIKit UIView.h 541行
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly) CGSize intrinsicContentSize NS_AVAILABLE_IOS(6_0);
#else
- (CGSize)intrinsicContentSize NS_AVAILABLE_IOS(6_0);
#endif

这里UIView的intrinsicContentSize属性既有@property声明的形式,也有用实例方法声明的形式,通过UIKIT_DEFINE_AS_PROPERTIES宏控制。

这里为什么要用两种方式声明我还不是很明白,有知道的朋友可以请加群(100179587)留言,先谢过

然而对于实例方法,两种调用方法则不完全一样,对于objectEnumerator这种没有参数的方法,是支持点语法的,而objectAtIndex:则不能用点语法获取。

1
2
array.objectEnumerator; //可以使用
array.objectAtIndex; //不存在

仔细思考一下,会发现这个特点很好理解,我们前面说过用构造类似setter和getter的方法模拟@property效果,这里的objectEnumerator恰好就是一个结构和getter方法相同的方法。所以编译器会无视objectEnumerator是getter方法还是普通方法,于是我们就可以使用点语法调用它。

类似getter构造,是指没有传递任何参数

然而对于objectAtIndex:这个传递了一个参数的方法,则不能使用点语法调用。

block的引入

iOS 4.0开始支持block以后,为通过点语法,调用带参数方法带来可行性。代码块(block)既然也是实例,那就可以作为property,也可以作为方法参数传递,Cocoa库中就增加了很多block特性,常见的用法有:

1
2
3
4
5
6
7
8
9
// 数组遍历block
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
// UIView动画block
[UIView animateWithDuration:0.24 animations:^{
}];

于是我们可以声明block类型的property,这里还用到第一章里面说的例子:

1
2
3
@interface NSArray (Functional)
- (NSArray*)map:(id (^)(id x))map;
@end

上面是我们给NSArray增加的map方法,我们将其写成不带参数的property

1
2
3
// 下面两种写法等效
@property (readonly) NSArray* (^map)(id (^)(id x));
- (NSArray* (^)(id (^)(id x)))map;

有一丢丢区别,@porperty的写法,xcode可以进行代码补全,而实例方法的写法则不行。

因为block可以传递参数,所以我们将之前例子的中传递的block参数,作为参数写在另一个block里了,这里可能有点绕,理清思路就是

1
2
3
NSArray* (^)() //去了参数的样子
id (^)(id x) //这是这个block的唯一一个参数,也是个block
NSArray* (^)(id (^)(id x)) //这是组合起来的样子

然后我们实现这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 这个是之前已经实现的map方法
- (NSArray*)map:(id (^)(id))map
{
NSMutableArray* array = [NSMutableArray array];
[self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
id x = map(obj);
if(x) [array addObject:x];
}];
return [array copy];
}
// 这个是增加的代码
- (NSArray* (^)(id (^handle)(id)))map
{
return ^(id (^handle)(id)) {
return [self map:handle];
};
}

于是乎,我们可以这样写代码了:

1
2
3
4
5
array = array.map(^id(id x) {
return @([x integerValue] * 2);
})
// [@(1),@(2),@(3)] 变成 [@(2),@(4),@(6)]

通过点语法,调用带参数方法,是不是跟Ruby和Python的写法一样了。

然后我们改造一下第一篇的第一个例子(具体实现留给你们,比上面NSSArray的map要简单),下面是声明:

1
@property (nonatomic,readonly) NSString* (^localizedString)(void);

于是我们就可以这样来写本地化:

1
NSString* str = @"你好,世界".localizedString();

还没有完

上面说的所有内容,也支持类方法,和类属性。

也许你只用过类方法(+开头的),而没用过类属性,我们声明一个:

1
2
// 用类方法改造 NSStringFromCGPoint(CGPoint p)
@property (class,nonatomic,readonly) NSString* (^p)(CGPoint p);

这里用到的是class关键字进行修饰,于是这个属性的getter方法也变成了类方法,实现如下:

1
2
3
4
5
6
+ (NSString *(^)(CGPoint))p
{
return ^(CGPoint p){
return NSStringFromCGPoint(p);
};
}

于是我们就可以用点语法来调用这个修改后的本地化方法:

1
NSString.p(CGPointMake(100, 100));

其实并没有比NSStringFromCGPoint简单多少,但是有一点很重要,就是我们将NSStringFromCGPoint()这个孤零零的C语言函数,变成NSString的类方法,更利于大脑记忆。