Nullability、__kindof、Generics

索引


  1. 关键字
    1.1 id
    1.2 instancetype
    1.3 __kindof
    1.4 nullable
    1.5 nonnull
    1.6 null_resettable
    1.7 _Null_unspecified
  2. 泛型
    2.1 系统类中的泛型
    2.2 泛型的使用
    2.3 自定义泛型
  3. 逆变 && 协变
    3.1 __covariant(协变)
    3.2 __contravariant(逆变)

引言


Xcode7 和 iOS9 已经出来很久了, 关于新特性这些东西, 我认为大家肯定也已经了解的很透彻了. 很明显一方面是为了迎合Swift, 另一方面则是提高我们开发人员的开发规范, 减少程序员之间的交流. 在这里算是做一个总结吧. 好脑子不如烂笔头, 毕竟有些东西不是天天在用. Note: 相关文章很多, 纯手打, 不喜欢可以路过. OK, Let's Go.

1、关键字


1.0、前奏: 了解 Demo

开始之前先来介绍一下 Demo. 这个 Demo 很简单, 就不放到 Github 上了.

1. MLPerson 类: 继承自 NSObject. 其中提供了4种构造方法, 一个属性, 和一个实例方法, 来看看 MLPerson.h:

@interface MLPerson : NSObject

/** 返回 id 指针的 构造方法 */
+ (id) person_id;

/** 返回 MLPerson *指针的 构造方法 */
+ (MLPerson *) person_MLPerson;

/** 返回 instancetype 的 构造方法 */
+ (instancetype) person_instancetype;

/** 返回 __kindof MLPerson *指针的 构造方法 */
+ (__kindof MLPerson *) person_kindof;

/** 姓名 */
@property (nonatomic, copy) NSString * name;

/** 奔跑 */
- (void) run;

/** 获取最好的朋友 */
- (instancetype) obtainBestFriend;

@end
2. MLMan 类: 继承自 MLPerson. 其中一个属性, 和一个实例方法, 来看看 MLMan.h:
@interface MLMan : MLPerson

/** 擅长运动 */
@property (nonatomic, copy) NSString * sport;

/** 踢足球 */
- (void) playFootball;

@end
3. MLWoman 类: 继承自 MLPerson. 其中一个属性, 和一个实例方法, 来看看 MLWoman.h:
@interface MLWoman : MLPerson

/** 喜欢的电影 */
@property (nonatomic, copy) NSString *movie;

/** 跳舞 */
- (void) dance;

@end

1.1、id

一个类的实例对象的指针, 万能指针. 在 objc.h 文件中的定义如下:

typedef struct objc_object *id;

可以看到, id 实际上是一个指向objc_object 结构体的指针, 那objc_object 这个结构体又是什么鬼呢? 查看objc.h 文件中的声明, 看到如下定义:

// 描述一个类的实例对象
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

头文件中已经写的很清楚了, 这个结构体用来描述一个类的实例对象, 那id 是一个指向这个结构体的指针, 所以我们可以理解为 id 实际上就是一个泛型指针, 可以指向所有Objective-C 对象. 所以我们可以有以下代码:

- (void) testMethod {  
    id array = [NSArray new];
    id string = [NSString new];
    id person = [MLPerson new];
}

以上只是对 id 指针做一个介绍, 本文中主要讨论当id 指针作为构造方法返回值时的情况.

说说 id 指针作为返回值时的弊端, 我会用代码实例对以下问题作出解释:

1、不能使用 "点语法": 不做过多解释, 看代码
- (void) testMethod {
 /**
  *  这样写是不可以的, 因为不可以使用 '点语法'.
  *  报错: Property 'name' not found on object of type 'id'
  *  很明显, 编译器告诉我们, 在 'id' 类型的对象中, 没有找到 'name' 这个属性.
  */
  [MLPerson person_id].name;
}
2、不能在编译的时候检查真实类型:
3、id 可以使用任何对象的方法
4、返回值得时候没有提示, 也就是说可以用任何指针指向该对象.

这三个弊端放在一起说, 是因为他们导致的问题很像.
其实对于构造方法中返回 id 类型还不够明显, 因为你的同事使用你的类初始化的时候, 他肯定知道该用什么指针去接收. 试想: 如果你有一个工具类, 其中提供了一个方法(非构造方法), 返回了一个id 类型的指针, 当他调用这个方法, 看到返回值是一个 id 类型时, 他当时的反应一定是懵逼的, 因为他不知道用什么指针去接收你返回的这个对象. 这会带来一个非常严重的问题, 就是他可以用任意的一个指针去接收你返回的对象。Objective-C的运行时特性, 导致在编译阶段并不会抛出异常, 但是在运行时阶段则会导致 Crash 这种严重的问题。举个例子来说明:

- (void) testMethod {

  // 弊端3: id 指针可以使用任何对象的方法
  id object = [MLPerson person_id];
  [object reloadData]; // Objective-C 的运行时特性, 导致在编译阶段并不会抛出异常, 但是在运行时阶段, 则会导致 Crash。
  
  // 弊端4: 任何指针指向该对象
  NSArray *aArray = [MLPerson person_id]; 
  [aArray objectAtIndex: 0]; // Objective-C 的运行时特性, 导致在编译阶段并不会抛出异常, 但是在运行时阶段, 则会导致 Crash。
}

1.2、instancetype

instancetype 这个关键字的用法和 id 其实区别不是很大, 但是要注意一点: instancetype 只能作为返回值, 不能用来定义一个变量. 代码如下:

- (void) testMethod {
  // 这里会抛出一个异常: Use of undeclared identifier 'instancetype'.
  // 使用了一个未定义的标识符 'instancetype'.
  instancetype object = [MLPerson person_instencetype];
}

来看看 instancetype 的好处:

1. 会自动识别当前类的对象.
- (void) testMethod {

  // 识别当前类对象, 直接调用对象方法
  [[MLPerson person_instancetype] run];

  // 调用其他对象方法, 将会抛出异常:
  // No visible @interface for 'MLPerson' declares the selector 'reloadData'
  // 'MLPerson' 类未定义 'reloadData' 方法
  [[MLPerson person_instancetype] reloadData];
}
2. 可以使用 '点语法'
- (void) testMethod {
  
  // 直接使用 '点语法'
  [MLPerson person_instancetype].name;
}
3. 如果用任意的指针指向该对象, 系统会在编译阶段就抛出警告
- (void) testMethod {

  // 任意指针指向 `instancetype` 返回的对象, 将会抛出警告:
  // ⚠️ Incompatible pointer types initializing 'NSArray *' with an expression of type 'MLPerson *'
  NSArray *array = [MLPerson person_instancetype];
}

其实 instancetype 这个关键字的弊端不是很明显, 如果非要吹毛求疵的话, 我想应该就是:

  1. 虽然 instancetypeid 相比有很多便捷之处, 但是instancetype 依然不能明确返回值的类型, 需要读取警告信息, 才能明确知道应该用什么指针来接收该对象, 在构造方法中, 返回值为 instancetype 类型, 如果你用子类指针去接收, 依然会抛出警告.
- (void) testMethod {
  
  // 父类中统一定义了构造方法, 用子类指针接受, 依然抛出异常:
  // ⚠️ Incompatible pointer types initializing 'MLMan *' with an expression of type 'MLPerson *'
   MLMan *man = [MLPerson person_instancetype];
}

不可否认, instancetype 相比id 而言强了不少, 也减少了很多隐在的风险(例如: Crash), 但是个人认为, 还是 __kindof 用起来感觉更友好一些. 接下来说说 __kindof 关键字.

1.3、__kindof

在说 __kindof 关键字之前, 先说另外一种返回值类型, 就是明确给出返回的类. 代码如下:

+ (MLPerson *) person_MLPerson;

这种写法弊端在于, 你只能用 MLPerson *指针去接收返回的对象, 使用子类去接收, 依然会抛出警告. 代码如下:

- (void) testMethod {
  
  // 父类中统一定义了构造方法, 用子类指针接受, 依然抛出异常:
  // ⚠️ Incompatible pointer types initializing 'MLMan *' with an expression of type 'MLPerson *'
  MLMan *man = [MLPseron person_MLPerson];
}

重点来了, __kindof 关键字完美的解决了问题. 从字面上来看kindof 的意思就是有点儿, 相当, 差不多 的意思, 你也可以理解为看起来像的意思, 那在我们这里, 当返回值是一个 __kindof MLPerson *的时候, 我们可以理解为看起来像 MLPerson 的对象.
__kindof 的好处, 包含了instancetype 的所有便捷之处, 并且也解决的instancetype 无法解决的问题. 代码如下:

- (void) testMethod {
  
  // 问题完美解决, 并不会抛出异常, 也不会在运行时阶段导致 Crash.
  MLPerson *person = [MLPerson person_kindof];
  MLMan *man = [MLPerson person_kindof];
  MLWoman *woman = [MLPerson person_kindof];
}

来看看Apple 系统类中使用__kindof关键字的情况. 例如:

- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier;

- (nullable __kindof UITableViewCell *)cellForRowAtIndexPath:(NSIndexPath *)indexPath;

@property (nonatomic, readonly) NSArray<__kindof UITableViewCell *> *visibleCells;

1.4、nullable

nullable: the value can be nil; bridges to an optional.
nullable 关键字用来修饰一个变量, 用nullable 修饰的变量预示着该变量可以(有可能)为空.

书写格式:

@interface MLTestModel : NSObject

@property (nonatomic, strong, nullable) NSString *aName;
@property (nonatomic, strong) NSString *_Nullable bName;
@property (nonatomic, strong) NSString *__nullable cName;

- (nullable NSString *) obtainDName;
- (NSString *__nullable) obtainEName;
- (NSString *_Nullable) obtainFName;

@end

使用效果:

@property (nonatomic, strong, nullable) NSString *aName

@property (nonatomic, strong) NSString *_Nullable bName
@property (nonatomic, strong) NSString *__nullable cName
- (nullable NSString *) obtainDName
- (NSString *__nullable) obtainEName
- (NSString *_Nullable) obtainFName

1.5、nonnull

nonnull: the value won’t be nil; bridges to a regular reference.
nonnull 关键字用来修饰一个变量, 用nonnull 修饰的变量预示着该变量不能为空.

书写格式:

@interface MLTestModel : NSObject

@property (nonatomic, strong, nonnull) NSString *aName;
@property (nonatomic, strong) NSString *_Nonnull bName;
@property (nonatomic, strong) NSString *__nonnull cName;

- (nonnull NSString *) obtainDName;
- (NSString *__nonnull) obtainEName;
- (NSString *_Nonnull) obtainFName;

@end

使用效果:

@property (nonatomic, strong, nonnull) NSString *aName
@property (nonatomic, strong) NSString *_Nonnull bName
@property (nonatomic, strong) NSString *__nonnull cName
- (nonnull NSString *) obtainDName
- (NSString *__nonnull) obtainEName
- (NSString *_Nonnull) obtainFName
向 nonnull 关键字修饰的变量传入空值时, 编译器会直接抛出警告

大部分情况下, 我们自定义了一个类, 这个类里面的很多属性都是不能为空的, 但是如果每一个变量都加上 nonnull 去修饰, 未免有些太过于繁琐, 所以这边有两个宏定义NS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END, 这两个宏定义之间的所有变量, 默认都以nonnull 关键字修饰. 如果某些属性可以为空, 那么直接用nullable 关键字去修饰该属性就可以了. 代码如下:

NS_ASSUME_NONNULL_BEGIN
@interface MLTestModel : NSObject

@property (nonatomic, copy) NSString *name;

@property (nonatomic, copy, nullable) NSString *nickName;

+ (NSString *) obtainNameFromPerson:(MLPerson *)person;

// NickName 有可能为空
+ (nullable NSString *) obtainNickNameFromPerson:(MLPerson *)person;

@end
NS_ASSUME_NONNULL_END

PS: 注意一点, nonnullproperty, 在.m 文件中, 最好在初始化的时候赋初值, 或override getter 方法, 否则明明你用nonnull 去修饰的变量, 到头来返回了一个空, 那你这即坑队友,也坑自己.

1.6、null_resettable

null_resettable: the value can never be nil when read, but you can set it to nil to reset it. Applies to properties only.
null_resettable 关键字用来修饰一个变量, 用null_resettable 修饰的变量预示着该变量的 getter 方法不可以为空, 但是 setter 方法可以为空.

书写格式:

@interface MLTestModel : NSObject

/**
  * Setter: 可以为空
  * Getter: 不能为空
  * Note: 使用这个关键字修饰的变量, 需要 override getter 方法, 保证 getter 不返回空值.
  */
@property (nonatomic, copy, null_resettable) IDCard *idCard;

@end


@implementation MLTestModel

#pragma mark - Override Set/Get Methods
#pragma mark -
#pragma mark Get IDCard
- (IDCard *) idCard {
  return _idCard ? _idCard : [IDCard new];
}

@end

1.7、_Null_unspecified

__ Null_unspecified: bridges to a Swift implicitly-unwrapped optional. This is the default.
_Null_unspecified__ 关键字用来修饰一个变量, 用_Null_unspecified__ 修饰的变量预示着该变量
不确定是否_为空. 变量的默认修饰符. 为了迎合 Swift 的可选类型隐式拆包.
这默认的就没什么可说的了, 例子:

/*
    _Null_unspecified: 不确定是否为空
 */
@property (nonatomic, copy) NSString *_Null_unspecified name_unspecified01;
@property (nonatomic, copy) NSString *__null_unspecified name_unspecified02;

2、泛型


2.0、泛型的好处

  1. 泛型很显然可以提高开发人员的开发规范, 减少开发人员之间的一些不必要的交流.
  2. __从集合中取出来的对象, 会有类型检测, 并且直接当做泛型的对象使用, 调用方法, '点语法'等. __

2.1、泛型的使用

先来看看泛型的书写规范: 在类型的后面定义泛型: NSArray *datas
再来看看泛型的用法, 我们以NSArray 为例:

普通 NSArray:

- (void) testMethod {
  
  // 先定义一个普通数组
  NSArray *arr = @[@"1"];
  NSString *string = [arr objectAtIndex: 0];
  NSInteger length = string.length;
}

上面这个代码块, 看上去没问题, 但是他存在隐患. 原因在于: 在未声明泛型的情况下, objectAtIndex 方法的返回值是 id类型, 看下图:

objectAtIndex 方法返回的 id 类型

id 类型意味着可以调用任何对象的方法并在编译阶段不会产生任何的警告, 这会导致运行时阶段产生 Crash. 看下面这段代码:

- (void) testMethod {
  
    // 这段代码, 编译器不会抛出任何警告, 但是在运行时阶段, 就 Crash 掉了, 因为 NSString 没有 reloadData 这个方法.
    NSArray *arr = @[@"1"];
    UITableView *tableView = [arr objectAtIndex: 0];
    [tableView reloadData];
}

泛型 NSArray:

- (void) testMethod {
  
  // 声明一个 NSString 泛型的 NSArray
  NSArray *arr = @[@"1"]; 

  // 可以直接当做泛型的类型来调用方法和点语法
  NSInteger length = [arr objectAtIndex: 0].length; 

  // 类型检测: 此时编译器会抛出警告
  // ⚠️ Incompatible pointer types initializing 'UITableView *' with an expression of type 'NSString *'
  UITableView *tableView = [arr objectAtIndex: 0]; 
}

再来看看我们定义了泛型之后, objectAtIndex 方法的返回值情况, 看下图:

objectAtIndex 返回的 泛型 类型

2.2、自定义泛型

当我们自己声明一个类的时候, 我们也想自己自定义泛型, 应该怎么办呢?
其实遇到了不会的问题, 我们最快速的解决办法, 就是看看苹果是怎么写的, 我们仿照他的写法, 基本上就能解决问题. 先来看看 NSArray 的头文件:


@interface NSArray<__covariant ObjectType> : NSObject 

@property (readonly) NSUInteger count;

- (ObjectType)objectAtIndex:(NSUInteger)index;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithObjects:(const ObjectType [])objects count:(NSUInteger)cnt NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder NS_DESIGNATED_INITIALIZER;

@end

我们看这句话 @interface NSArray<__covariant ObjectType>, 这句话实际上就是定义了一个泛型. __covariant先不用去管, 文章的后半部分会做说明,
再来看 - (ObjectType)objectAtIndex:(NSUInteger)index; 这个方法, 返回值的类型, 就是泛型的类型.
光说不练假把式, 我们自己尝试着去写一个泛型试一下:

@interface MLRepository : NSObject

/**
 *  向仓库中放入对象
 */
- (void) addObject:(ObjectType)object;

/**
 *  从仓库中获取对象
 */
- (ObjectType) obtainObject;

@end

我们来尝试着去使用一下

- (void) testMethod {

  MLRepository *rep = [[MLRepository alloc] init];
  [rep addObject: @"1"];
  NSString *string = [rep obtainObject];
}

来看看 addObject 方法 和 obtainObject 使用时的样子, 看截图:

addObject, 这个方法现在的参数类型就是我们定义的泛型
obtainObject, 这个方法现在的返回值类型也是我们定义的泛型

简单的模仿了一下NSArray 的写法, 我们也自定义出来了我们自己的泛型. 还是比较简单的. 再举两个例子, 也是真实项目中会遇到的:

情景1: 如果这个仓库中, 只允许放入 MLCar 和 MLCar 子类的情况.
@interface MLRepository : NSObject
@end
情景2: 如果这个仓库中, 只要是遵守某协议的对象都可以放入的情况
@interface MLRepository> : NSObject
@end

2.3、系统类中的泛型

来看看系统人家是怎么使用泛型的.


- (NSArray *)arrayByAddingObject:(ObjectType)anObject;
- (NSArray *)arrayByAddingObjectsFromArray:(NSArray *)otherArray;

- (void)removeObjectsForKeys:(NSArray *)keyArray;
- (void)setObject:(ObjectType)anObject forKey:(KeyType )aKey;

- (nullable ObjectType)anyObject;
- (BOOL)containsObject:(ObjectType)anObject;

细心的同学肯定发现了, 这几个方法都是出自集合数据类型的, 例如:NSArrayNSDictionaryNSSet. 没错, 泛型的使用场景就是:

  1. 在集合(数组, 字典, NSSet) 中使用泛型比较常见
  2. 当声明一个类的时候, 类里面的某些属性的类型不确定, 这时候我们才是会用泛型

3、逆变 && 协变


3.1、__covariant(协变)

__covariant: 协变, 用于泛型的数据强转类型, 可以向上强转, 子类 可以 转成 父类. 文字往往看起来很抽象, 很枯燥, 所以还是直接上代码吧. 依然用我们刚才定义的那个泛型:

@interface MLRepository<__covariant ObjectType> : NSObject

/**
 *  向仓库中放入对象
 */
- (void) addObject:(ObjectType)object;

/**
 *  从仓库中获取对象
 */
- (ObjectType) obtainObject;

@end

然后定义三个类

第一个是 MLCar, 继承自 NSObject

@interface MLCar : NSObject
@end

第二个是 MLBus, 继承自 MLCar

@interface MLBus : MLCar
@end

第三个是 MLTaxi, 继承自MLCar

@interface MLTaxi : MLCar
@end

然后我们写一个测试方法, 来看看什么是协变

- (void) testMethod {
  
  // 声明三个泛型的 MLRepository 仓库
  MLRepository *carRep = [[MLRepository alloc] init];
  MLRepository *busRep = [[MLRepository alloc] init];
  MLRepository *taxiRep = [[MLRepository alloc] init];

  // 刚才说过, 协变用于泛型的数据强转类型, 可以向上强转, 子类 可以 转成 父类.
  // 然后来看看相互之间赋值的情况
  carRep = busRep; // MLBus --> MLCar, 符合协变规则.
  carRep = taxiRep; // MLTaxi --> MLCar, 符合协变规则.

  // MLTaxi --> MLBus, 不符合协变规则. 编译器将会抛出警告: 
  // ⚠️ Incompatible pointer types assigning to 'MLRepository *` from `MLRepository *`
  busRep = taxiRep; 
  // 同理,  MLCar --> MLBus, 也是不符合协变规则的, 编译器依然会抛出警告
  busRep = carRep;
}

3.2、__contravariant(逆变)

__contravariant: 逆变, 用于泛型的数据强转类型, 可以向下强转, 父类 可以 转成 子类. 我还依然使用 MLRepository 这个例子:

@interface MLRepository<__contravariant ObjectType> : NSObject

/**
 *  向仓库中放入对象
 */
- (void) addObject:(ObjectType)object;

/**
 *  从仓库中获取对象
 */
- (ObjectType) obtainObject;

@end

再来一段测试代码, 看看效果:

- (void) testMethod {
  
  // 声明三个泛型的 MLRepository 仓库
  MLRepository *carRep = [[MLRepository alloc] init];
  MLRepository *busRep = [[MLRepository alloc] init];
  MLRepository *taxiRep = [[MLRepository alloc] init];

  // 刚才说过, 逆变用于泛型的数据强转类型, 可以向下强转, 父类 可以 转成 子类
  // 然后来看看相互之间赋值的情况
  busRep = carRep; //MLCar -->  MLBus, 符合逆变规则.
  taxiRep = carRep; // MLCar --> MLTaxi, 符合逆变规则.

  // MLTaxi --> MLBus, 不符合逆变规则. 编译器将会抛出警告: 
  // ⚠️ Incompatible pointer types assigning to 'MLRepository *` from `MLRepository *`
  busRep = taxiRep; 
  // 同理,  MLCar --> MLBus, 也是不符合逆变规则的, 编译器依然会抛出警告
  busRep = carRep;
}

协变逆变这东西, 有兴趣的小伙伴可以自己写写测试代码. 很快就理解了.


Lemon龙说:

如果您在文章中看到了错误 或 误导大家的地方, 请您帮我指出, 我会尽快更改

如果您有什么疑问或者不懂的地方, 请留言给我, 我会尽快回复您

如果您觉得本文对您有所帮助, 您的喜欢是对我最大的鼓励

如果您有好的文章, 可以投稿给我, 让更多的 iOS Developer 在这个平台能够更快速的成长


上一篇:

你可能感兴趣的:(Nullability、__kindof、Generics)