objective-c集合的函数式扩展

本片文章主要是展示函数式方法的实现,所以代码篇幅占比较多,希望可以帮助到仍然在使用objective-c语言开发的同学

背景说明

在编写程序中适当的使用高阶函数,链式调用,柯里化函数可以使我们的代码更具可读性,更加优雅,所以现代化的高级语言swift kotlin python dart go都无一例外的原生支持函数式的特性,其中一个支持就是集合或者容器类型中定义了很多函数操作。 而相对较老的objective-c语言对函数式编程的支持则比较弱,并且缺少真正的泛型,但是还好有闭包这个特性至少可以让我们做一些函数式的事情。接下来我将展示通过给集合类NSArray增加分类,并给他添加函数式的方法扩展。

定义支持泛型的分类

为了更方便使用,我们给定义的分类加上泛型的支持,oc中引入的泛型被称之为轻量级泛型, 它的作用是使我们操作容器类型时更加便捷,但实际上并不能真正的对类型进行约束。oc中定义泛型类的方式只是在头文件(.h)声明而已,实现文件(.m)中并不能引用头文件中的泛型标记

@interface NSArray<ObjectType> (EEFunction)

- (NSArray<ObjectType> *)ee_select:(BOOL(^)(ObjectType item))function;
- (NSArray<ObjectType> *)ee_filter:(BOOL(^)(ObjectType item))function;
- (NSArray<ObjectType> *)ee_take:(NSUInteger)count;
- (BOOL)ee_any:(BOOL(^)(ObjectType item))function;
- (BOOL)ee_all:(BOOL(^)(ObjectType item))function;
- (NSUInteger)ee_count:(BOOL(^)(ObjectType item))function;
- (NSDictionary<id, NSArray<ObjectType> *> *)ee_groupBy:(id(^)(ObjectType item))function;
- (NSArray<ObjectType> *)ee_sort:(id(^)(ObjectType item))function;
- (NSArray<ObjectType> *)ee_sortDescending:(id(^)(ObjectType item))function;
- (NSArray *)ee_flatMap;
- (NSArray *)ee_flatMap:(id(^)(ObjectType item, NSUInteger idx))function;
- (id)ee_reduce:(id(^)(id result, ObjectType item))function;
- (id)ee_reduce:(id(^)(id result, ObjectType item))function initial:(id)initial;

@end

.h文件中的ObjectType在.m文件中都将成为id类型

实现并测试

@interface EEPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSInteger age;

@end

@implementation EEPerson

@end

- (NSArray<EEPerson *> *)generateObjects {
    NSMutableArray<EEPerson *> *arr = [NSMutableArray array];
    for (int i = 0; i < 10; i++) {
        EEPerson *person = EEPerson.new;
        person.name = [NSString stringWithFormat:@"%@_%d", i < 5 ? @"man" : @"woman", i + 1];
        person.age = 20 + i;
        [arr addObject:person];
    }
    return arr.copy;
}

select 查找

select是根据提供的选择条件函数,遍历序列,将符合条件元素组成新的序列

- (NSArray *)ee_select:(BOOL(^)(id item))function {
    NSMutableArray *result = [NSMutableArray array];
    [self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (function(obj)) {
            [result addObject:obj];
        }
    }];
    return result.copy;
}
// 查找满了18周岁的人
NSArray<EEPerson *> *original = [self generateObjects];
NSArray<EEPerson *> *result = [original ee_select:^BOOL(EEPerson * _Nonnull item) {
    return item.age >= 18;
}];
filter 过滤

filter是根据提供的过滤条件函数,遍历序列,将不符合条件元素组成新的序列

- (NSArray *)ee_filter:(BOOL(^)(id item))function {
    NSMutableArray *result = [NSMutableArray array];
    [self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (!function(obj)) {
            [result addObject:obj];
        }
    }];
    return result.copy;
}
NSArray<EEPerson *> *original = [self generateObjects];
NSArray<EEPerson *> *result = [original ee_filter:^BOOL(EEPerson * _Nonnull item) {
    return item.age % 2 == 0;
}];
// XCTAssertNotNil(result);
// XCTAssertTrue(result.count == 5);
// XCTAssertTrue(result[0].age == 21);
// XCTAssertTrue(result[1].age == 23);
// XCTAssertTrue(result[2].age == 25);
// XCTAssertTrue(result[3].age == 27);
// XCTAssertTrue(result[4].age == 29);
groupBy 分组

groupBy是根据提供的key值函数,遍历序列,对元素进行分组,组成一个分组字典,例如根据年龄区间进行分组

- (NSDictionary *)ee_groupBy:(id(^)(id item))function {
    NSMutableDictionary *groupedItems = [NSMutableDictionary dictionary];
    [self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        id key = function(obj);
        if (!key) {
            key = [NSNull null];
        }
        NSMutableArray *arrayForKey = [groupedItems objectForKey:key];
        if (!arrayForKey) {
            arrayForKey = [NSMutableArray array];
            [groupedItems setObject:arrayForKey forKey:key];
        }
        [arrayForKey addObject:obj];
    }];
    return groupedItems.copy;
}
NSArray<EEPerson *> *original = [self generateObjects];
NSDictionary<NSString *, NSArray<EEPerson *> *> *result = [original ee_groupBy:^id _Nonnull(EEPerson * _Nonnull item) {
    return [item.name componentsSeparatedByString:@"_"].firstObject;
}];
// XCTAssertNotNil(result);
// XCTAssertTrue(result.count == 2);
// XCTAssertTrue([result.allKeys containsObject:@"man"]);
// XCTAssertTrue([result.allKeys containsObject:@"woman"]);
// XCTAssertTrue(result[@"man"].count == 5);
// XCTAssertTrue(result[@"woman"].count == 5);
map 映射

map是根据提供的元素映射闭包,遍历序列,对元素进行逐一映射,映射后的元素放到一个新的数组中,并返回。
MVVM架构中,map操作常用在遍历model层返回的模型数组将其映射成viewModel数组

- (NSArray *)ee_map:(id(^)(id item, NSUInteger idx))function {
    NSMutableArray *result = [NSMutableArray array];
    [self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        id mapItem = function(obj, idx);
        if (mapItem) {
            [result addObject:mapItem];
        }
    }];
    return result.copy;
}
NSArray<EEPerson *> *original = [self generateObjects];
NSArray<EEPerson *> *result = [original ee_map:^id _Nonnull(EEPerson * _Nonnull item, NSUInteger idx) {
    item.age = 18;
    return item;
}];
// XCTAssertNotNil(result);
// XCTAssertTrue(result.count == 10);
// XCTAssertTrue([result ee_all:^BOOL(EEPerson * _Nonnull item) {
//     return item.age == 18;
// }]);
flatMap 扁平映射

flatMap严格来说是映射扁平, 它是根据提供的映射函数先将数组进行map操作,再对map后的数组进行扁平降维操作,对于二维数组来说扁平操作会将数组直接变成一维数组,而对一维数组进行扁平操作将不会有任何效果;另外swift中的扁平会优先对可选类型进行解包,如果不是可选类型则正常扁平

- (NSArray *)ee_flatMap {
    return [self ee_flatMap:^id _Nonnull(id  _Nonnull item, NSUInteger idx) {
        return item;
    }];
}

- (NSArray *)ee_flatMap:(id  _Nonnull (^)(id _Nonnull, NSUInteger))function {
    NSArray *tempArray = [self ee_map:function];
    BOOL isAllItemArray = [tempArray ee_all:^BOOL(id  _Nonnull item) {
        return [item isKindOfClass:NSArray.class];
    }];
    if (isAllItemArray) {
        NSMutableArray *flatArray = [NSMutableArray array];
        [tempArray enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            [flatArray addObjectsFromArray:obj];
        }];
        return flatArray.copy;
    } else {
        return tempArray;
    }
}
NSMutableArray<EEPerson *> *arr1 = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
    EEPerson *person = EEPerson.new;
    person.name = [NSString stringWithFormat:@"man_%d", i + 1];
    [arr1 addObject:person];
}
NSMutableArray<EEPerson *> *arr2 = [NSMutableArray array];
for (int i = 0; i < 5; i++) {
    EEPerson *person = EEPerson.new;
    person.name = [NSString stringWithFormat:@"woman_%d", i + 1];
    [arr2 addObject:person];
}
NSArray *original = @[arr1, arr2];
NSArray *result = [original ee_flatMap];
// XCTAssertNotNil(result);
// XCTAssertTrue(result.count == 5 + 5);
reduce 归纳累计

reduce通过接收一个累积函数并作用于序列的所有元素,最终累积为一个值
但需要注意的是,reduce累积函数并不能对所有元素作转换后再累积,例如求整型数组的平方和使用reduce函数并不能直接得到结果,应该使用map对数组元素进行转换之后再reduce

- (id)ee_reduce:(id(^)(id result, id item))function {
    return [self ee_reduce:function initial:nil];
}

- (id)ee_reduce:(id(^)(id result, id item))function initial:(id)initial {
    if (!function) {
        return nil;
    }
    __block id result = initial;
    [self enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        if (idx == 0 && !result) {
            result = obj;
        } else {
            result = function(result, obj);
        }
    }];
    return result;
}
// 原始数组
NSArray *arr = @[@1, @4, @6, @2];

// 求数组的平方和
NSNumber *num1 = [[arr ee_map:^id(NSNumber *item, NSUInteger idx) {
    return @(item.integerValue * item.integerValue);
}] ee_reduce:^id(NSNumber *result, NSNumber *item) {
    return @(result.integerValue + item.integerValue);
}];
// XCTAssertTrue(num1.integerValue == 57);

// 将数组连接成整型数
NSNumber *num2 = [arr ee_reduce:^id(NSNumber *result, NSNumber *item) {
    return @(result.integerValue * 10 + item.integerValue);
}];
// XCTAssertTrue(num2.integerValue == 1462);
其他函数

集合类的函数式的便捷方法可以有很多,可以根据自己的实际需要添加扩展,以下是其他常见的函数以及描述:

  • first 第一个符合条件的元素
  • any 是否有任意一个符合条件
  • all 是否所有元素都符合条件
  • count 符合条件的个数
  • sum 统计求和
    ... ...

集合的函数式方法不仅可以简化我们的代码,也可以培养我们编码的函数式思维。实际上除了集合类,字典类外,字符串可序列的类型都可以添加类似集合的函数式的通用操作支持。

欢迎拍砖留言

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容