如您所知,Objective-C 并不支持原生的 生成器,但它们可能非常有用,可以表达一些想法。例如 Python 有这样的支持,并且有时它们被 非常频繁地使用。Python 中最简单的具有意义的生成器看起来像这样
def countfrom(n):
while True:
yield n
n += 1
yield 语句与常规函数中的 return 类似。区别在于生成器状态被保存,在下一次调用时将被恢复,并且执行将继续(而不是像常规函数那样从头开始)。
Mike Ash 在他很久以前的一个精彩的博客中 讨论了这个问题。他 提出了创建 Objective-C 生成器的一个解决方案。类似的技术也被用于 libextobjc 中的 EXTCoroutine,这是来自 libextobjc 的一种技术。
SMGenerator 建议在 Objective-C 中创建生成器的另一种方法。让我们更深入地探讨并讨论这种方法的优势和劣势。
生成器背后的主要思想是它们在 "return" 时保存其状态。如果我们在另一个线程中启动我们的函数并在代码执行 yield 结果时停止呢?在停止之前,我们将该结果值传递给原始线程,该线程将通过生成器返回。在下一次执行时,我们只需重新启动线程并继续函数的评估。这个想法相当简单,让我们看看生成器将是什么样的。
SMGenerator 的主要目标是创建和使用上的简单性。生成器通常用于循环中,所以如果我们可以编写类似这样的代码,那将非常酷
for (NSObject *object in generator) {
//...
}
使用 SMGenerator,您可以这样做,因为它采用了 NSFastEnumeration 协议。使用 SMGenerator,Objective-C 中有意义的简单生成器将是
SM_GENERATOR(^(NSNumber *n) {
while (TRUE) {
SM_YIELD(n);
n = @([n intValue] + 1);
}
}, (@1));
SM_GENERATOR 和 SM_YIELD 是宏,允许我们编写更少的代码。SM_GENERATOR 至少有一个参数 - block,它使用 SM_YIELD 产生值。如果 block 有参数,您可以将它们传递给 SM_GENERATOR 作为第二个、第三个等参数。
您有两种使用生成器的方式
像这样
SMGenerator *generator = SM_GENERATOR(^(NSNumber *n) {
while (TRUE) {
SM_YIELD(n);
n = @([n intValue] + 1);
}
}, (@1));
for (NSNumber *num in generator) {
NSLog(@"Number %@", num);
}
或者在某些情况下,也可以避免使用局部变量。
for (NSNumber *num in SM_GENERATOR(^(NSNumber *n) {
while (TRUE) {
SM_YIELD(n);
n = @([n intValue] + 1);
}
}, (@1))) {
NSLog(@"Number %@", num);
}
SMGenerator有一个next方法
/*!
* @method next
*
* @abstract
* Produces next value
*
* @discussion
* This method waits while next value will be processed by external block
* If external block is ended, this method returns nil
*
* @result
* Next generated value or nil
*/
- (id)next;
因此,只需向生成器发送“next”消息就可以简单地获取值
NSLog(@"Number 1 %@", [generator next]);
NSLog(@"Number 2 %@", [generator next]);
NSLog(@"Number 3 %@", [generator next]);
顺便说一下,在用户块中同时使用多个SM_YIELD语句不是问题。
SMGenerator *generator = SM_GENERATOR(^{
while (TRUE) {
SM_YIELD(@"one");
SM_YIELD(@"two");
SM_YIELD(@"three");
}
});
SMGenerator使用GCD在自身的队列上运行用户块。因此,该块在另一个线程中运行,并使用信号量实现与原始线程的同步(不仅限于主线程,也可能是创建您的生成器的任何线程)。实际上,只有当原始线程阻塞时,用户代码才会工作。
通过这种方法,可以安全地从外部作用域修改对象和变量。例如,我们可以将“有意义的简单生成器”重写如下
__block NSNumber *n = @(1);
SMGenerator *generator = SM_GENERATOR(^{
while (TRUE) {
SM_YIELD(n);
n = @([n intValue] + 1);
}
});
可能在阅读了上一节后,你说:“停一下,如果我们异步计算下一个值,那么当请求生成器关于下一个值的询问时,它只是返回已经产生的值”。这是一个合理的评论。实际上,你可以用SMGenerator做这件事!只需使用SM_ASYNC_GENERATOR而不是SM_GENERATOR。这对于性能要求较高的生成器来说可能是一个真正的重大进步。我们的先前列出示例以异步方式重写如下
SM_ASYNC_GENERATOR(^(NSNumber *n) {
while (TRUE) {
SM_YIELD(n);
n = @([n intValue] + 1);
}
}, (@1));
但是,你应该小心使用SM_ASYNC_GENERATOR,因为它具有异步逻辑。在SM_ASYNC_GENERATOR中,使用__block变量或修改外部对象是潜在的!
任何想法的实现都有自己的警告。所以让我们强调一些SMGenerator的警告。
让我们比较SMGenerator、MAGenerator和EXTCoroutine提供的语法。
我们已经看到了使用SMGenerator的样子,但让我们再次重复这段代码
SMGenerator *generator = SM_GENERATOR(^(NSNumber *n) {
while (TRUE) {
SM_YIELD(n);
n = @([n intValue] + 1);
}
}, (@42));
for (NSNumber *num in generator) {
NSLog(@"Number %@", num);
}
使用MAGenerator得到相同的结果
GENERATOR(int, CountFrom(int start), (void)) {
__block int n;
GENERATOR_BEGIN(void) {
n = start;
while(TRUE) {
GENERATOR_YIELD(n);
n++;
}
}
GENERATOR_END
}
int (^counter)(void) = CountFrom(42);
for(int i = 0; i < 10; i++) {
NSLog(@"Number %d", counter());
}
使用EXTCoroutine
__block int n;
int (^generator)(int) = coroutine(int from)({
n = from;
while(TRUE) {
yield n;
n++;
}
});
for(int i = 0; i < 10; i++) {
NSLog(@"%d", generator(42));
}
MAGenerator稍微有些冗长,同时EXTCoroutine则更加简单,但您在使用它时必须非常小心,因为如果写错了东西,它可能不会按预期工作。
int (^generator)(int) = coroutine(int n)({
while(TRUE) {
yield n;
n++;
}
});
它将产生42、43、43、43、43、43、43、43、43、43、43。这很明显吗?猜不是...
SMGenerator
SMGenerator *fileFinder = SM_GENERATOR(^(NSString *path, NSString *ext) {
NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath: path];
for (NSString *subpath in enumerator) {
if([[subpath pathExtension] isEqualToString: ext]) {
SM_YIELD((id)[path stringByAppendingPathComponent: subpath]);
}
}
}, @"/Applications", @"app");
for(NSString *path in fileFinder) {
NSLog(@"%@", path);
}
MAGenerator
GENERATOR(id, FileFinder(NSString *path, NSString *extension), (void))
{
__block NSString *subpath;
__block NSDirectoryEnumerator *enumerator;
GENERATOR_BEGIN(void) {
enumerator = [[NSFileManager defaultManager] enumeratorAtPath: path];
for (subpath in enumerator) {
if([[subpath pathExtension] isEqualToString: extension]) {
GENERATOR_YIELD((id)[path stringByAppendingPathComponent: subpath]);
}
}
}
GENERATOR_END
}
for(NSString *path in MAGeneratorEnumerator(FileFinder(@"/Applications", @"app"))) {
NSLog(@"%@", path);
}
使用EXTCoroutine实现的有限生成器变为无限(它们只是一直重复)。因此,我们需要编写更多代码来处理这个细微差别。
__block NSString *subpath;
__block NSDirectoryEnumerator *enumerator;
NSString * (^generator)(NSString *, NSString *) = coroutine(NSString *path, NSString *ext)({
enumerator = [[NSFileManager defaultManager] enumeratorAtPath: path];
for (subpath in enumerator) {
if([[subpath pathExtension] isEqualToString: ext]) {
yield [path stringByAppendingPathComponent: subpath];
}
}
yield (NSString *)nil;
});
NSString *path;
do {
path = generator(@"/Applications", @"app");
if (path != nil) {
NSLog(@"%@", path);
}
} while (path != nil);
正如您从这两个示例中看到的那样,由于采用新的方法,SMGenerator提出了更稳健且更简单的解决方案。
使用生成器,特别是SMGenerator的成本是什么?
让我们进行一些测试,比较同步和异步的SMGenerator、MAGenerator、EXTCoroutine,以及没有生成器实现相同任务的性能。
对于每个测试,我们测量每个实现执行10次,然后排除2个最大值和2个最小值,然后计算中位数。所有与测试相关的代码(iOS,OSX)您都可以在此GitHub项目(https://github.com/shkutkov/ObjectiveCGeneratorsPerformance)中找到。您可以抓取项目并自行运行。我已在iOS模拟器和iPhone 5上运行iOS测试,在MacBookPro 13"(2,26Gh Intel Core 2 Duo)上运行OSX测试。
实现 | iOS模拟器 | iPhone 5 | MacBookPro |
---|---|---|---|
同步SMGenerator | 1.350800 | 3.800238 | 0.883701 |
异步SMGenerator | 1.049956 | 3.843883 | 0.889207 |
MAGenerator | 0.059778 | 0.214627 | 0.012960 |
EXTCoroutine | 0.039936 | 0.116216 | 0.003566 |
没有生成器 | 0.002214 | 0.010248 | 0.001619 |
如您所见,在这次合成测试中,SMGenerator同步和异步版本都是输家。
让我们用相同的实现只打印1000个值到控制台。
生成器名称 | iOS模拟器 | iPhone 5 | MacBookPro |
---|---|---|---|
同步SMGenerator | 0.705184 | 0.658221 | 0.293223 |
异步SMGenerator | 0.695445 | 0.637643 | 0.275483 |
MAGenerator | 0.652017 | 0.580639 | 0.246619 |
EXTCoroutine | 0.646062 | 0.581865 | 0.244638 |
没有生成器 | 0.639307 | 0.562586 | 0.214950 |
如您在此示例中看到的那样,在此示例中没有SLGenerator与其他实现之间几乎没有什么区别。
让我们看一下最后一个具有重计算的示例。
计算素数的代码将不会很高效,但对我们来说这不是大问题 - 我们只需要生成器中的“重”任务。(如果您对代码感兴趣,请参阅PrimeNumbersGeneratorManager)
生成器名称 | iOS模拟器 | iPhone 5 | MacBookPro |
---|---|---|---|
同步SMGenerator | 1.319347 | 4.643134 | 3.077379 |
异步SMGenerator | 1.192727 | 4.269168 | 2.797900 |
MAGenerator | 1.972622 | 4.876356 | 3.539097 |
EXTCoroutine | 1.973660 | 4.878249 | 3.540615 |
没有生成器 | 1.097907 | 3.298502 | 2.108130 |
在这个例子中,我们模拟了值生成需要一些时间,并且对值进行处理的场景。没有生成器的实现从代码角度来看并不优雅,但真的很快。您还可以看到,SMGenerator的异步版本在所有其他生成器实现中是最快的。这不是出乎意料的,因为在处理结果的同时,我们的生成器正在处理新的值。这可以提高您的代码在多核处理器上的性能。
SMGenerator提供了两种方式将值传递到用户块:通过参数或者直接使用外部作用域的变量。第一种方式更加健壮(尤其是在异步版本中)且更具解释性。但除此之外,它还略微提高了效率(根据未在此展示的性能测试结果)。
如果您有任何问题、建议或补丁,请联系我:[点击此处[email protected]。
SMGenerator是以MIT许可证发布的。有关完整和合法的许可证,请参阅LICENSE文件。鼓励在任何和所有类型的项目中使用,只要遵守许可证条款(它们很简单)!