现在来学习 iOS 9 的新特性,未免有点太晚了,毕竟 iOS 10.3 都准备出来了,而且这新特性是 Objective-C 的。但是这些『新』特性对我们开发来说,比较有价值,记录一下。

属性 nil 值

如果希望控制某个属性的 setter 方法和 getter 方法是否可以为 nil,可以使用以下关键字修饰:

nonnull / __nonnull / _Nonnull

使用了 nonnull / __nonnull / _Nonnull 修饰的属性的 setter 和 getter 都不能为 nil:

1
2
3
4
5
6
@property (strong, nonatomic, nonnull) NSArray *names; // 或者
@property (strong, nonatomic) NSArray * __nonnull names; // 修饰在 names 前面,再或者
@property (strong, nonatomic) NSArray * _Nonnull names;
// Setter
self.names = nil; // 警告提示:Null passed to a callee that requires a non-null argument

Xcode 智能提示:

nonnull / __nonnull / _Nonnull

nullable / __nullable / _Nullable

使用了 nullable / __nullable / _Nullable 修饰的属性的 setter 和 getter 都可以为 nil:

1
2
3
@property (strong, nonatomic, nullable) NSArray *names; // 或者
@property (strong, nonatomic) NSArray * __nullable names; // 再或者
@property (strong, nonatomic) NSArray * _Nullable names;

Xcode 智能提示:

nullable / __nullable / _Nullable

小 Tips:既然默认就是 nullable,为何有时候我们需要显式的去修饰呢?其实 nullable 更多的作用就是程序员之间的沟通交流,提醒某个属性的值可能为 nil,以谨慎后续的操作。

null_resettable

使用了 null_resettable 修饰的属性的 setter 可以为 nil,getter 不能为 nil:

1
@property (strong, nonatomic, null_resettable) NSArray *names;

Xcode 智能提示:

null_resettable

在 UIViewController.m 中声明的 view 属性:

1
@property(null_resettable, nonatomic,strong) UIView *view; // The getter first invokes [self loadView] if the view hasn't been set yet. Subclasses must call super if they override the setter or getter.

我们可以知道,view 属性是懒加载的,我们可以将 view 属性置为空(self.view = nil),但是我们在使用 view 属性的 getter 时,得到的 view 一定不是 nil 的(因为当判断到 view 为 ni 的时候,会重新创建),所以 null_resettable 修饰的很好,就是说你可以将 view 置为 nil,而且,你获取 view 的时候一定不会为 nil。

注意:如果使用了 null_resettable 的话,就必须重写 getter 或者 setter 方法,以处理传递的值为 nil 的情况(Xcode 会报警告 Nullability Issue: Synthesized setter ‘setNames:’ for null_resettable property ‘names’ does not handle nil),即:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 重写 setter
- (void)setNames:(NSArray *)names {
if (names == nil) {
names = @[@"Chileung"];
}
_names = names;
}
// 或者,重写 getter
- (NSArray *)names {
if (_names == nil) {
_names = @[@"Chileung"];
}
return _names;
}

null_unspecified / _Null_unspecified / __null_unspecified

使用了 null_unspecified / _Null_unspecified / __null_unspecified 修饰的属性的 setter 和 getter 不确定是否为 nil:

1
2
3
@property (strong, nonatomic, null_unspecified) NSArray *names;
@property (strong, nonatomic) NSArray * _Null_unspecified names;
@property (strong, nonatomic) NSArray * __null_unspecified names;

Xcode 智能提示:

null_unspecified / _Null_unspecified / __null_unspecified

😎注意:以上 4 种修饰关键字,只能用在指针类型的属性,不能用在 assign 属性,比如:

1
2
// 这样做是错误的:Nullability specifier 'nonnull' cannot be applied to non-pointer type 'NSUInteger' (aka 'unsigned long')
@property (assign, nonatomic, nonnull) NSUInteger age;

如果要修饰函数或者方法:

1
2
3
4
5
6
7
8
9
10
// 修饰参数
void testMethod(NSArray * __nonnull names) {
}
// 修饰参数和返回值
- (NSString * __nonnull)fuckName:(NSString * __nonnull)name {
// 各种 fuck
return @"Good Name";
}

如果希望一个类的大部分属性都是 nonnull 的,则可以使用两个宏:NS_ASSUME_NONNULL_BEGIN 和 NS_ASSUME_NONNULL_END,它俩之间的所有数组默认都是 nonnull 的。比如:

1
2
3
4
5
6
7
8
9
10
NS_ASSUME_NONNULL_BEGIN
@interface ViewController ()
@property (assign, nonatomic) NSUInteger age;
@property (strong, nonatomic) NSString *biu;
@property (strong, nonatomic) NSMutableArray *names;
@property (strong, nonatomic) NSMutableDictionary *books;
@end
NS_ASSUME_NONNULL_END

泛型

泛型这个概念在其他语言比如 Java 都玩烂了,在 Objective-C 中一般用在数组和字典中,有几个比较明显的好处:限制数组和字典存放的数据类型,还有就是取出数组和字典的元素之后可以使用点语法(比如 array.lastObject.length):

1
2
3
4
5
// 数组存放的是 NSString 类型的数据
@property (strong, nonatomic) NSMutableArray<NSString *> *names;
// key 为 NSString 类型,value 为 NSNumber 类型
@property (strong, nonatomic) NSMutableDictionary<NSString *, NSNumber *> *ages;

我们可以看到系统自带的 NSMutableArray 和 NSMutableDictionary 等是支持泛型的:

1
2
@interface NSMutableArray<ObjectType> : NSArray<ObjectType>
@interface NSMutableDictionary<KeyType, ObjectType> : NSDictionary<KeyType, ObjectType>

自定义泛型

我们也可以给我们自定义的类增加泛型,即自定义泛型。比如现在我们有个书包对象(Bag),这个书包对象只能装一种类型的东西,比如书本(Book)或者铅笔(Pencil),那么我们可以这样定义(忽略 .m 实现,ObjectType 名字可以随意写,不过要保持一致):

1
2
3
4
5
// Bag.h
@interface Bag<ObjectType> : NSObject
- (void)addObject:(ObjectType)anObject;
- (ObjectType)getAnObject;
@end

在使用的时候就很方便了:

1
2
3
4
5
6
7
8
9
10
11
- (void)test {
Bag<Book *> *bag4Book = [[Bag alloc] init];
[bag4Book addObject:[[Book alloc] init]]; // Xcode 会提示是 Book 类型
Book *book = [bag4Book getAnObject]; // Xcode 会提示是 Book 类型
Bag<Pencil *> *bag4Pencil = [[Bag alloc] init];
[bag4Pencil addObject:[[Pencil alloc] init]]; // Xcode 会提示是 Pencil 类型
Pencil *pencil = [bag4Pencil getAnObject]; // Xcode 会提示是 Pencil 类型
Bag *bag = [[Bag alloc] init]; // 如果不写类型,默认就是 id 类型
}

协变性 & 逆变性

平时我们使用 UIKit 的控件的时候,如果我们知道某一个 view 是 UIButton 类型的,我们可以强制转换,即:

1
2
UIButton btn = (UIButton *)view; // 父类转子类,需要强转
UIView *v = btn; // 子类转父类,不需要强制转换

泛型也是如此,不过子类转父类和父类转子类都是需要强制转换:

1
2
NSArray<NSMutableString *> *array = (NSArray<NSMutableString *> *)arr; // arr 为 NSArray<NSString *> 类型
NSArray<NSString *> arr = (NSArray<NSString *> *)array; // array 为 NSArray<NSMutableString *> 类型

如果不希望我们自定义的类定义的泛型也要那么麻烦的强转,我们可以使用 __covariant 或者 __contravariant 修饰 ObjectType,继续沿用上面 Bag 的示例:

1
2
3
// Bag.h
@interface Bag<__covariant ObjectType> : NSObject
@interface Bag<__contravariant ObjectType> : NSObject

__covariant 表示小类型(泛型类的子类类型) —> 大类型(泛型类的父类类型)不需要强转,即协变性。

__contravariant 表示大类型(泛型类的父类类型) —> 小类型(()泛型类的子类类型)不需要强转,即逆变性。

__kindof

在 Xcode 6 之前,我们使用 tableView 的 dequeueXXX 方法时,它返回的类型是 id 类型的,我们如果不看它的 Api 的话,还得通过 NSLog 或者打断点之类的方法才可以知道它返回的类型,比较麻烦。不过现在就不会了,如下:

__kindof

如果我们希望返回的类型是某个类型或者其子类(如 NSString、NSMutableString),我们就可以使用 __kindof 修饰:

1
2
// __kindof:告诉编译器返回值可能是 NSString 也可能是 NSMutableString 也可能是子类
- (__kindof NSString *)helloWorld {