Block

Objective-C编程(第2版)第28章 Block对象

Blocks

Block对象是一段代码。先给出一个Block对象的示例:

^{

    NSLog(@"This is an instruction within a block.");

}

看上去和C函数类似,都是在一个花括号内的一套指令。但是它没有函数名,相应的位置只有一个^符号。^表示这段代码是一个Block对象。

和函数一样,Block对象也可


Objective-C编程(第2版)以有实参和返回值。再给出一个Block对象的示例:

^(double dividend, double divisor) {

    double quotient = dividend / divisor;

    return quotient;

}

这段代码中的Block对象有两个实参,类型都是double,还返回一个double类型的值。

Block对象可以被当成一个实参来传递给可以接收block的方法。很多苹果的类都有可以接收block为实参的方法。

如果你有过其他编程语言的开发经验,则可能会将Block对象和匿名函数(anonymous function)、closurelambda放在一起进行比较。如果你熟悉函数指针(function pointer),那么Block对象也会看上去很熟悉。与函数指针相比,如果能正确地使用Block对象,就可以写出更简洁的代码。

创建一个新项目,类型为基于FoundationCommand Line Tool,名称为VowelMovementVowelMovement将使用Block对象枚举数组中的字符串并移除所有的元音字母,并将去除了元音字母的字符串保存到一个新的数组中。

main.m中,创建三个数组对象:一个用于保存最初的字符串;一个用于保存去除了元音字母的字符串;最后一个用于保存需要从字符串中移除的字符。

int main (int argc, const char * argv[])

{

    @autoreleasepool {


        // 创建两个数组对象,分别用于保存最初的字符串对象和去除元音字母后的版本

        NSArray *originalStrings = @[@"Sauerkraut", @"Raygun",

                                           @"Big Nerd Ranch", @"Mississippi"];


        NSLog(@"original strings: %@", originalStrings);



        NSMutableArray *devowelizedStrings = [NSMutableArray array];


        //  创建数组对象,保存需要从字符串中移除的字符

        NSArray *vowels = @[@"a", @"e", @"i", @"o", @"u"];


    }

    return 0;

}

这段代码没有新的知识点,仅仅是创建并设置数组对象。构建并运行程序,编译器会发出警告,提醒有未使用的变量,暂时忽略之。

28.1 使用Block对象

Using blocks

马上你就要编写自己的第一个Block对象。这个Block对象会复制一个给定的字符串,并移除给定字符串的所有元音字母,然后将去除了元音字母的字符串保存到devowelizedStrings数组中。

你将使用该Block对象作为实参,给originalStrings数组发送消息。首先我们要学习一些block语法。声明Block变量

Block对象可以用变量保存。将以下代码加入main.m,声明Block变量:

int main (int argc, const char * argv[])

{

    @autoreleasepool {

        // 创建两个数组对象,分别用于保存最初的字符串对象和去除元音字母后的版本

        NSArray *oldStrings = [NSArray arrayWithObjects:

            @"Sauerkraut", @"Raygun", @"Big Nerd Ranch", @"Mississippi", nil];


        NSLog(@"old strings: %@", oldStrings);


        NSMutableArray *newStrings = [NSMutableArray array];


        // 创建数组对象,保存需要从字符串中移除的字符

Objective-C编程(第2版)NSArray *vowels = [NSArray arrayWithObjects:

            @"a", @"e", @"i", @"o", @"u", nil];


        // 声明Block变量

        void (^devowelizer)(id, NSUInteger, BOOL *);


    }

    return 0;

}

下面对这段代码中的Block变量声明做一个详细的介绍。Block变量的名字(如devowelizer)是写在括号中,跟在^字符后面的。Block的声明需要包括Block的返回类型(void)以及它的实参的类型(idNSUINtegerBool*),这点类似函数的声明(见图28.1)。图28.1 Block变量声明

那么Block变量是什么类型的呢?它不是一个简单的“block(块)。它的类型是一个有着三个参数(一个对象指针、一个整数和一个BOOL指针),并且没有返回值的Block对象。这是enumerateObjectsUsingBlock:方法期望的Block类型。下面将介绍这三个实参的用法。编写Block对象

现在你要写一个Block对象,并将它赋给新的变量。在main.m中,编写一个方法复制原始字符串,并移除原始字符串的所有的元音字母,然后将去除了元音字母的字符串保存到devowelizedStrings数组中,最后将它赋给devowelizer,代码如下:

int main (int argc, const char * argv[])

{

    @autoreleasepool {

        ...


        // 声明Block变量

        void (^devowelizer)(id, NSUInteger, BOOL *);


        //  Block对象赋给变量

        devowelizer = ^(id string, NSUInteger i, BOOL *stop) {


            NSMutableString *newString = [NSMutableString stringWithString:string];


            // 枚举数组中的字符串,将所有出现的元音字母替换成空字符串

            for (NSString *s in vowels) {

                NSRange fullRange = NSMakeRange(0, [newString length]);

                [newString replaceOccurrencesOfString:s

                                               withString:@""

                                                  options:NSCaseInsensitiveSearch

                                                     range:fullRange];

            }


            [devowelizedStrings addObject:newString];


        }; // Block变量赋值结束


    }

    return 0;

}

注意,与其他的变量赋值一样,Block变量的赋值也需要以分号结束。再次构建程序,之前的警告信息(有未使用的变量)应该会消失。

此外,和其他变量一样,也可以将devowelizer的声明和赋值写在一起,代码如下:

void (^devowelizer)(id, NSUInteger, BOOL *) = ^(id string, NSUInteger i,

    BOOL *stop) {


    NSMutableString *newString = [NSMutableString stringWithString:string];


    // 枚举数组中的字符串,将所有的元音字母替换成空字符串

    for (NSString *s in vowels) {

        NSRange fullRange = NSMakeRange(0, [newString length]);

        [newString replaceOccurrencesOfString:s

                                   withString:@""

                                      options:NSCaseInsensitiveSearch

                                        range:fullRange];

    }


    [newStrings addObject:newString];

};

传递Block对象

main.m中,调用enumerateObjectsUsingBlock:并传入devowelizer,然后输出去除了元音


Objective-C编程(第2版)字母的字符串。

int main (int argc, const char * argv[])

{

    @autoreleasepool {

        …

        // 创建两个数组对象,分别用于保存最初的字符串对象和去除元音字母后的版本

        NSArray *oldStrings = [NSArray arrayWithObjects:

               @"Sauerkraut", @"Raygun", @"Big Nerd Ranch", @"Mississippi", nil];

        NSLog(@"old strings: %@", oldStrings);

        NSMutableArray *newStrings = [NSMutableArray array];


        // 创建数组对象,保存需要从字符串中移除的字符

        NSArray *vowels = [NSArray arrayWithObjects:

                              @"a", @"e", @"i", @"o", @"u", nil];


        // 声明Block变量

        void (^devowelizer)(id, NSUInteger, BOOL *);


        // Block对象赋给变量

        devowelizer = ^(id string, NSUInteger i, BOOL *stop) {


            NSMutableString *newString = [NSMutableString stringWithString:string];


            // 枚举数组中的字符串,将所有出现的元音字母替换成空字符串

            for (NSString *s in vowels) {

                NSRange fullRange = NSMakeRange(0, [newString length]);

                [newString replaceOccurrencesOfString:s

                                           withString:@""

                                              options:NSCaseInsensitiveSearch

                                                range:fullRange];

            }


            [newStrings addObject:newString];


        }; // Block变量赋值结束


        // 枚举数组对象,针对每个数组中的对象,执行Block对象devowelizer

        [oldStrings enumerateObjectsUsingBlock:devowelizer];

        NSLog(@"new strings: %@", newStrings);


    }

    return 0;

}

构建并运行程序,程序应该会输出以下两个数组。

2011-09-03 10:27:02.617 VowelMovement[787:707] old strings: (

    Sauerkraut,

    Raygun,

    "Big Nerd Ranch",

    Mississippi

)

2011-09-03 10:27:02.618 VowelMovement[787:707] new strings: (

    Srkrt,

    Rygn,

    "Bg Nrd Rnch",

    Msssspp

)

enumerateObjectsUsingBlock:方法要求传入的Block对象的三个实参类型是固定的。第一个实参是对象指针,指向当前(枚举)的对象。该指针的类型是id,所以无论数组包含的是什么类型的对象,都可以将地址赋给该指针。第二个实参的类型是NSUInteger,其值是当前对象在数组中的索引。第三个实参是指向BOOL变量的指针,该变量的默认值是NO。如果将该值设为YES,那么数组对象会在执行完当前的Block对象后终止枚举过程。修改block的代码,检查字符串是否包含字符y(包含大小写),如果有,则设置指针指向YES(会阻止Block对象进行枚举),终止枚举。

  NSRange yRange = [string rangeOfString:@"y"

                                   options:NSCaseInsensitiveSearch];


    // 是否包含字符'y'

    if (yRange.location != NSNotFound) {

        *stop = YES; // 执行完当前的Block对象后终止枚举过程

        return;       // 结束当前正在执行的Block对象

    }


    NSMutableString *newString = [NSMutableString stringWithString:string];


    // 枚举数组中的字符串,将所有出现的元音字母替换成空字符串

    for (NSString *s in vowels) {

         NSRange fullRange = NSMakeRange(0, [newString length]);

         [newString replaceOccurrencesOfString:s

                                   withString:@""

                                      options:NSCaseInsensitiveSearch

                                        range:fullRange];

    }


    [newStrings addObject:newString];


}; // Block变量赋值结束

构建并运行程序,程序应该还是会输出两组对象。但是,对于第二个数组,因为程序会在找到y字符后终止枚举过程,所以输出结果只有Srkrt

Objective-C编程(第2版)typedef

Block对象的语法可能会比较复杂。通过使用第11章介绍过的typedef关键字,可以将某个Block对象类型定义为一个新类型,以方便使用。需要注意的是,不能在方法的实现代码中使用typedef。也就是说,应该在实现文件的顶部,或者头文件内使用typedef。在main.m中,添加以下代码:

#import <Foundation/Foundation.h>


typedef void (^ArrayEnumerationBlock)(id, NSUInteger, BOOL *);


int main (int argc, const char * argv[])

{

这段代码中的typedef语句看上去与Block变量声明很像,但是,这里定义的是一个新的类型,而不是变量。跟在^字符后面的是类型名称。创建这个新类型后,就能简化相应Block对象的声明。

现在使用新的类型声明devowelizer

int main(int argc, const char * argv[])

{


    @autoreleasepool {


        ...


        // 声明Block变量

        void (^devowelizer)(id, NSUInteger, BOOL *);

        ArrayEnumerationBlock devowelizer;


        // Block对象赋给变量

        devowelizer = ^(id string, NSUInteger i, BOOL *stop) {

            ...

注意,这里的Block类型只是声明了Block对象的实参和返回类型,并没有实现真正的Block对象。

28.2 Block对象vs.其他回调

Blocks vs. other callbacks

27章曾介绍过两种回调机制:委托机制(delegation)和通告机制(notifications)。通过回调机制,程序能够在特定事件发生时调用指定的方法。虽然以上两种回调机制能够很好地完成任务,但是也有一个缺点,即回调的设置代码和回调方法的具体实现无法写在同一段代码中。而且这两段代码经常会间隔很远,甚至会出现在不同的文件中。以第27章所介绍的Callbacks为例,以下代码会回调zoneChange:方法:

[[NSNotificationCenter defaultCenter]

                         addObserver:logger

                            s    elector:@selector(zoneChange:)

                                    name:NSSystemTimeZoneDidChangeNotification

                               object:nil];

阅读这段代码的人很自然地会问,“zoneChange:方法是做什么的?为了回答这个问题,程序员必须要找到zoneChange:方法的实现,而这段代码可能相隔很多行代码。

然而,通过Block对象,将与回调相关的代码写在同一代码段中。例如,NSNotificationCenter有一个addObserverForName:object:queue:usingBlock:方法。这个方法和addObserver:selector:name:object:类似,但是它可以使用Block对象作为实参,而不使用选择器。调用addObserverForName:object:queue: usingBlock:之后,下一行代码就可以定义Block对象。这样其他的程序员阅读这段代码就非常方便。

本章最后的练习2就是让你使用Block对象改写Callbacks程序的。


Objective-C编程(第2版)28.3 深入学习Block对象

More on blocks

下面将介绍Block对象的其他功能。

返回值

VowelMovement创建的Block对象没有返回值,但是很多其他的Block对象有。对于有返回值的Block对象,可以像调用函数那样调用Block对象,然后使用其返回值。

让我们再一起看看本章开头介绍的Block对象示例:

^(double dividend, double divisor) {

    double quotient = dividend / divisor;

    return quotient;

}

这个Block对象有两个类型为double的实参,返回一个double类型的值。要在变量中保存这个Block,需要声明一个double类型的变量,然后再将Block赋值给这个变量:

// 声明divBlock变量

double (^divBlock)(double,double);


// Block对象赋给变量

divBlock = ^(double dividend, double divisor) {

    double quotient = dividend / divisor;

    return quotient;

}

你可以像调用函数一样调用divBlock,得到它的返回值:

double myQuotient = divBlock(42.0, 12.5);

匿名Block对象

匿名的Block对象是可以传递给方法的Block对象的,而不需要先赋值给变量。

让我们先看看匿名的整数。有三种方法可以将整数传递给方法:

//方法1: 声明、赋值和使用完全分开

int i;

i = 5;

NSNumber *num = [NSNumber numberWithInt:i];


// 方法2: 在一行中声明赋值使用

int i = 5;

NSNumber *num = [NSNumber numberWithInt:i];


//方法3:跳过变量声明步骤

NSNumber *num = [NSNumber numberWithInt:5];

如果采用第三种方法,就是匿名地传递一个整数。因为它没有名字,所以说它是匿名的。

而将Block对象传递给方法的办法和传递整数相同。分别用三行代码来声明Block对象,然后赋值,最后使用。但是匿名传递Block对象更加常用。本章最后的练习1就是使用一个匿名的Block对象来修改VowelMovement程序。

外部变量

Block对象通常会(在其代码中)使用外部创建的其他变量(基本类型的变量,或者是指向其他对象的指针)。这些外部创建的变量叫做外部变量(external variables)。当执行Block对象时,为了确保其下的外部变量能够始终存在,相应的Block对象会捕获(captured)这些变量。

对基本类型的变量,捕获意味着程序会拷贝变量的值,并用Block对象内的局部变量保存。对指针类型的变量,Block对象会使用强引用。这意味着凡是Block对象用到的对象,都会被保留。所以在相应的Block对象被释放前,这些对象一定不会被释放(这也是Block对象和函数之间的差别,函数无法做到这点)。

Block对象中使用self

如果需要写一个使用selfBlock对象,就必须要多做几步工作来避免造成强引用循环。考虑一下这个例子,BNREmployee实例创建了一个Block对象,每次执行的时候就会打印出这个BNREmployee


Objective-C编程(第2版)实例:

myBlock = ^{

    NSLog(@"Employee: %@", self);

};

BNREmployee有一个指向Block对象(myBlock)的指针。这个Block对象会捕获self,所以它有一个指回BNREmployee实例的指针。现在就陷入强引用循环了。

为了打破这个强引用循环,可以先在Block对象外声明一个__weak指针;然后将这个指针指向Block对象使用的self;最后在Block对象中使用这个新的指针:

__weak BNREmployee *weakSelf = self; // 一个弱引用指针

myBlock = ^{

    NSLog(@"Employee: %@", weakSelf);

};

现在这个Block对象对BNREMployee实例是弱引用,强引用循环打破了。

然而,由于是弱引用,所以self指向的对象在Block执行的时候可能会被释放。

为了避免这种情况的发生,可以在Block对象中创建一个对self的局部强引用:

__weak BNREmployee *weakSelf = self; // 弱引用

myBlock = ^{

    BNREmployee *innerSelf = weakSelf; // 局部强引用

    NSLog(@"Employee: %@", innerSelf);

};

通过创建innerSelf强引用,就可以在BlockBNREmployee实例中再次创建一个强引用循环。但是,由于innerSelf引用是针对Block内部的,所以只有在Block执行的时候它才会执行,而Block结束之后就会自动消失。

每次写Block对象的时候都引用self会是一个很好的练习。

Block对象中无意使用self

如果直接在Block对象中使用实例变量,那么block会捕获self,而不会捕获实例变量。这是实例变量的一个鲜为人知的特点。例如,以下这段代码直接存取一个实例变量:

__weak BNREmployee *weakSelf = self;

myBlock = ^{

    BNREmployee *innerSelf = weakSelf; // 局部强引用

    NSLog(@"Employee: %@", innerSelf);

    NSLog(@"Employee ID: %d", _employeeID);

};

编译器是这么解读这段代码的:

__weak BNREmployee *weakSelf = self;

myBlock = ^{

    BNREmployee *innerSelf = weakSelf; // 局部强引用

    NSLog(@"Employee: %@", innerSelf);

    NSLog(@"Employee ID: %d", self->_employeeID);

};

->语法看上去是不是很熟悉?这个语法实际是用来后去堆上的成员结构的。从最底层来说,对象实际就是结构。

由于编译器将_employeeID看成是self->_employeeIDself就被Block对象无意地捕获了。这样又会造成之前使用weakSelfinnerSelf避免的强引用循环。

怎样解决呢?不要直接存取实例变量。使用存取方法!

__weak BNREmployee *weakSelf = self;

myBlock = ^{

    BNREmployee *innerSelf = weakSelf; // 局部强引用

    NSLog(@"Employee: %@", innerSelf);

    NSLog(@"Employee ID: %d", innerSelf.employeeID);

};

现在没有直接地使用self了,就不会造成无意识地强引用循环。

在这种情况下,重要的是要理解编译器是如何思考的,这样才能避免隐藏的强引用循环。然而,绝不要使用->语法来存取对象的实例变量。这么做非常危险,可能会超越这个Block对象的范围。存取方法是你的小伙伴,应该坚持使用它们。

修改外部变量

Block对象中,被捕获的变量是常数,程序无法修改变量所保


Objective-C编程(第2版)存的值。

如果需要在Block对象内修改某个外部变量,则可以在声明相应的外部变量时,在前面加上__block关键字。

例如,以下代码可以在Block对象内将外部变量counter的值增加1

__block int counter = 0;

void (^counterBlock)() = ^{ counter++; };

...

counterBlock(); // counter增加1,数值为1

counterBlock(); // counter增加1,数值为2

如果这段代码没有使用__block关键字,那么编译器会在Block对象的定义处报错。28.4 练习

Challenge

练习1:匿名Block对象

试修改本章中的代码,要求在调用enumerateObjectsUsingBlock:方法时使用匿名Block对象。也就是说,保留Block对象,但是不声明Block变量。

练习2NSNotificationCenter

27章的Callbacks程序使用NSNotificationCenteraddObserver:selector: name:object:方法注册了一个观察者要求当指定的通知发生时调用zoneChange:方法。Callbacks程序以此实现了回调机制。试修改Callbacks程序,改用addObserverForName:object:queue:usingBlock:方法。

addObserverForName:object:queue:usingBlock:方法要求传入一个Block对象。当指定的通知发生时,通知中心就会执行这个Block对象,而不是与观察者打交道。这意味着修改后的程序永远不会调用zoneChange:方法,所以要将相应的代码移入Block对象中。

对传入addObserverForName:object:queue:usingBlock:方法的Block对象,需要有一个参数(类型为NSNotification *),并且没有返回值。这一点与zoneChange:方法相同。

queue:参数可以传入nil,该参数与并发(concurrency)有关,本书不做讨论。

关于addObserverForName:object: queue:usingBlock:方法,可以在开发文档中查找关于该方法的详细说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值