聊一下Objective-C中的泛型

6,411 阅读6分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

在iOS中聊泛型,一般情况下大家想到的肯定是Swift中的泛型,但是实际上Objective-C中也有泛型,只是一般情况下我们往往忽略了OC中泛型的特性。

今天要了解的就是我所理解的OC中的泛型。

NSArray中的泛型

因为工作的缘故,我还是必须跟进一些老项目的,我经常在老项目中看见这样一些代码:

@property (nonatomic, strong) NSMutableArray *dataSource;

NSMutableArray *mutableArray = [NSMutableArray array];

写的挺正常的,使用strong修饰可变数组,初始化数组也是使用的工厂方法。

其实上面的这种写法可以理解为:

@property (nonatomic, strong) NSMutableArray<id> *dataSource;

NSMutableArray<id> *mutableArray = [NSMutableArray array];

只要是一个对象类型,都可以被塞进数组中去。

这对于我们开发而言不是什么好事,一般情况下我们数组中只保存一种数据类型,最好是能够明确数组的类型:

/// 明确数据源是ResultItem类型
@property (nonatomic, strong) NSMutableArray<ResultItem *> *dataSource;

/// 明确数组里面是NSString类型
NSMutableArray<NSString *> *mutableArray = [NSMutableArray array];

这样我就能更好的理解数组中存储的数据类型了,虽然代码量增加了,但是对于维护代码的人,和对数组赋值的人,锁定了数组的数据类型,就告诉了编译器,这数组是不能随便被赋值的,需要匹配好类型。

我们看看NSArray的一个方法,感觉这个泛型是不是和Swift中定义的有那么点类似呢。

@interface NSArray<ObjectType> (NSExtendedArray)

- (NSArray<ObjectType> *)arrayByAddingObject:(ObjectType)anObject;

@end

NSDictionary中的泛型

NSDictionary中的泛型定义和NSArray十分相似,这里就做过多的展示,大家看看这个例子就可以了:

NSDictionary<NSString *, NSValue *>

记住,OC语言不要嫌弃多写点代码,虽然代码太长不是什么好事,但是代码写清楚很有必要。

定义代理时,使用的泛型

在OC中我们定义一个代理时,一般都会用到泛型,比如有下面这段代码:

#import <UIKit/UIKit.h>

@class CommentModel;

@protocol CommentCellDelegate <NSObject>

- (void)deleteButtonAction:(UIButton *)button commentModel:(CommentModel *)model;

@end

@interface CommentCell : UITableViewCell

@property (nonatomic, strong) CommentModel *model;

@property (nonatomic, weak) id<CommentCellDelegate> delegate;

@end

我们在声明delegate这个属性的时候,用的是id<CommentCellDelegate>,它说明什么:

首先delegate它是一个对象类型,其次这个对象类型必须遵守CommentDelegate协议。

于是我们看到了遵守CommentDelegate协议的类型一般都是这么写的:

@interface CommentListController ()<CommentCellDelegate, UITableViewDataSource, UITableViewDelegate>

尝试在OC中使用泛型做JSON转模型

我在OC中尝试通过泛型去编写网络请求响应的基类,给大家现个丑:

.h文件:

#import <Foundation/Foundation.h>

#import <YYModel.h>

NS_ASSUME_NONNULL_BEGIN

@interface ListItem: NSObject<YYModel>

@property (nonatomic , copy) NSString *topicTittle;
@property (nonatomic , copy) NSString *upTime;
@property (nonatomic , copy) NSString *topicDesc;
@property (nonatomic , assign) NSInteger numId;
@property (nonatomic , copy) NSString *topicImageUrl;
@property (nonatomic , assign) NSInteger topicStatus;
@property (nonatomic , assign) NSInteger topicOrder;

@end


@interface BaseResponse<T: NSObject<YYModel> *> : NSObject <YYModel>

@property (nonatomic , assign) NSInteger code;
@property (nonatomic, strong) NSArray<T>  *list;

@end

NS_ASSUME_NONNULL_END

.m文件:

@implementation ListItem

+ (NSDictionary<NSString *,id> *)modelCustomPropertyMapper {

    return @{
             @"numId": @"id"
             };

}

@end


@implementation BaseResponse

+ (NSDictionary<NSString *,id> *)modelContainerPropertyGenericClass {

    return @{
             @"list": [NSObject<YYModel> class]
             };
}

@end

明眼人都看得出我想干什么。

BaseResponse类型和其定义的list类型的数组元素,通过泛型约束遵守YYModel协议。

每次请求的时候都使用BaseResponse去接收,然后一步到位进行YYModel协议的转换。

是不是有点Codable协议的想法?

于是我接着写网络请求:

- (void)request {

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

    [manager POST:@"" parameters:nil headers:nil progress:^(NSProgress * _Nonnull uploadProgress) {

    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

        BaseResponse<ListItem *>* response = [BaseResponse<ListItem *> yy_modelWithJSON:responseObject];

        NSLog(@"response: %@", response);
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

    }];
}

那么激动人心的时候到了,可以转换成为BaseResponse<ListItem *>吗?

答案是遗憾的: image.png

list被转成了NSDictionary,而不是ListItem。

出问题的地方也非常显而易见,就是这里:

+ (NSDictionary<NSString *,id> *)modelContainerPropertyGenericClass {

    return @{
             @"list": [NSObject<YYModel> class]
             };
}

我在这里将list对应的是[NSObject<YYModel> class],而实际上我想对应的是在@interface BaseResponse<T: NSObject<YYModel> *> : NSObject <YYModel>声明的<T: NSObject<YYModel> *>,但是你是没办法在.m中这么写的:

image.png

image.png

使用了没有定义的T类型

走到这一步,通过泛型进行通用的模型转换就破灭了。

这也是为何,OC语言中对于网络请求一直都是一个请求,做一个返回模型的原因,而模型的外层总是相同的,因为无法通过泛型做统合,OC中的泛型,一般当作一个占位符看待。

OC的更多是使用其动态性

之前看过一个为NSArray扩展map函数的分类,其实现如下

@implementation NSArray (Map)

- (NSArray *)_map:(id(^)(id))handle {
    if (!handle || !self) return self;
    
    NSMutableArray *arr = NSMutableArray.array;
    for (id obj in self) {
        id new = handle(obj);
        [arr addObject:new];
    }
    return arr.copy;
}

@end

map后的block是id(^)(id obj)这么一个类型,数组的元素是个id类型,而返回类型也是id类型,不是它不想用泛型约束,而是没有办法用泛型约束。

在使用这种NSArra的map函数中,我们只能通过运行时对数组类型的具体定义进而定义其具体类型。

id类型

在我写这些例子的时候,经常使用的是OC的id类型

id是一个一个比较灵活的对象指针,并且是一个指向任何一个继承了Object(或者NSObject)类的对象。而在cocoa的开发环境里,NSObject是所有类的根类。所以id可以指向任何一个cocoa的合法对象。

它与NSObject有相似的地方,也有不同的地方:

  • id是一种通用的对象类型,她可以用类存储属于任何类的对象,可以理解为万能指针

  • 在id的定义中,已经包装好了*号,id指针只能指向os的对象

  • NSObject 和id都可以指向任何对象

  • NSObject对象会惊醒编译时检查(需要强制类型转换)

  • id不需要强制类型转换,id可以直接使用

  • 编译器看到id以后,认为是动态类型,不在检查类型

参考文档

iOS OC实现map、filter、reduce等高阶函数

iOS中id类型

总结

我很难得了写一篇关于OC的技术文章,主要是我在写Swift的时候,想通过Swift去倒推OC中的泛型用法和尝试。

整体而言OC的泛型非常羸弱,不过在对于NSArray、NSDictionary使用上,通过声明泛型,可以让我们更加清晰的理解数据结构和类型。

而Delegate中的泛型使用,可以认为是对类型的约束

在Swift中经常使用的泛型统和编写基类响应体,但是OC中对此支持并不友好,OC毕竟更多的特性是语言的动态性。

如果大家对OC中代码的泛型有兴趣,可以看看这个Demo,因为是我随意写的,记得切换到fastlane_auto分支去查看代码。

我们下期见。