objective-c高级编程(ios与osx多线程和内存管理)(Blocks-1)

前言:

以下文字都是摘自ios与osx多线程和内存管理一书,方便以后自己过来查阅的.
本文梗概:什么是Blocks以及Blocks的语法和类型变量。

1. 什么是Blocks:

Blocks是c语言的扩充功能。用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数(不带有名称的函数,c语言的标准不允许存在这样的函数,而通过blocks,源代码中就能够使用匿名函数。)

int func(int count); 声明一个名为func的函数

int result = func(10); 通过使用func名称来调用该函数

int result = (*funcptr)(10);

像上面这样子,使用函数指针来代替直接调用函数,似乎不用知道函数名。但其实使用函数指针也需要知道函数名:像下面代码这样子,在赋值给函数指针时,若不使用想赋值的函数的名称,就无法取得该函数的地址。  

int (*funcptr) (int) = &func;
int result = (*funcptr) (10);

#####知道了什么是匿名函数,那么“带有自动变量值”是什么?
首先回顾c的函数中可能使用的变量.

  • 自动变量(局部变量)
  • 函数的参数
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量
    其中,在函数的多次调用之间能够传递值的变量有:
  • 静态变量(静态局部变量)
  • 静态全局变量
  • 全局变量

虽然这些变量的作用域不同,但在整个程序当中,一个变量总保持在一个内存区域。因此,虽然多次调用函数,但该变量值总能保持不变。

下面是案例分析

int buttonId = 0;  
void buttonCallback(int event)  
{
    printf("buttonId:%d enent=%d\n",buttonId,event);  
}  

如果只有一个按钮,该源码无问题。但是多个按钮呢?

int buttonId;  
void buttonCallback(int event)  
{
    printf("buttonId:%d event=%d\n",buttonId,event);  
}

void setButtonCallbacks()  
{
    for (int i = 0;i < BUTTON_MAX; ++i)
        {
            buttonId = i;
            setButtonCallback(BUTTON_IDOFFSET + i,&buttonCallback);
        }
}

该源码的问题很明显。全局变量buttonId只有一个,所有回调都使用for循环最后的值。当然如果不使用全局变量,回调方会将按钮的ID作为函数参数传递,就能解决该问题。

void buttonCallback(int buttonId, int event)
{
    printf("buttonId:%d enent=%d\n",buttonId,event);
}

但是,回调方在保持回调函数的指针以外,还必须保持回调方的按钮ID。
c++和objective-c使用类可保持变量值且能够多次持有该变量自身。它会声明持有成员变量的类,由类生成的实例或对象保持该成员变量的值。
思考下刚才例子中用来回调按钮的类。
但是,使用类来做刚才例子中的回调按钮,可以想象到,声明并实现c++,Objective-c的类增加了代码的长度。
Blocks提供了类似由c++和objective-c类生成实例或对象来保持变量值的方法,其代码量与编写c语言函数差不多。如“带有自动变量值”,Blocks保持自动变量的值。
下面我们使用Blocks实现上面的按钮回调:

void setButtonCallbacks()
{
    for (int i = 0; i < BUTTON_MAX; ++i)
    {
        setButtonCallbackUsingBlock(BUTTON_IDOFFSET + i,^(int event){
            printf("buttonId:%d event=%d\n",i,event);
        });
    }
}

该源码将“带有自动变量i值的匿名函数”设定为按钮的回调。Blocks中将该匿名函数部分称为“Block literal”,或简称为“Block”。

像这样,使用Blocks可以不声明c++和objective-c类,也没有使用静态变量,静态全局变量或全局变量时的问题,仅用编写c语言函数的源代码量即可使用带有自动变量值的匿名函数。
另外“带有自动变量值的匿名函数”这一概念并不是指Blocks,它还存在于其他许多程序语言中。在计算机科学中,此概念也称为闭包(Closure),lambda计算()等。

objective-c的block在其他程序语言中的名称:

程序语言 Block的名称
C + Blocks Block
Smalltalk Block
Ruby Block
LISP Lambda
Python Lambda
C++11 Lambda
Javascript Anonymous function

2. Blocks模式:

  • Block语法:

下面讲解带有自动变量值的匿名函数Block的语法,即Block表达式语法(Block Literal Syntax).
完整形式的Block语法与一般的c语言函数定义相比,仅有2点不同。

1. 没有函数名.
因为它是匿名函数。
2. 带有"^".
返回值类型前带有”^”(插入记号,caret)记号,因为OS X,IOS应用程序的源代码中将大量使用Block,所以插入该记号便于查找。

以下为Block语法的BN范式

Block_literal_expression ::= ^ block_decl compound_statement_body
block_decl ::=
block_decl ::= parameter_list
block_decl ::= type_expression

即使不了解BN范式,通过说明也能有个概念,如下所示:
^ 返回值类型 参数列表 表达式

  • 返回值类型:同c语言函数的返回值类型.
  • 参数列表:同c语言函数的参数列表.
  • 表达式:同c语言函数中允许的表达式. 当然与c函数一样,表达式中含有return语句时,其类型必须与返回值类型相同.

例如可以写出如下形式的Block语法:

^int (int count){return count + 1;}

Block语法可省略好几个项目。

  1. 省略返回值类型
    ^ 参数列表 表达式
    省略返回值类型时,如果表达式中有return语句就使用该返回值的类型,如果表达式中没有return语句久使用void类型。表达式中含有多个return语句时,所有return的返回值类型必须相同。
    前面源码省略其返回值类型时如下:

    ^(int count){return count + 1;}

  2. 如果不使用参数,参数列表也可省略
    ^ 表达式

以下为不使用参数的Block语法:

^void (void){ printf("Blocks\n");}

以下为省略参数的Block:

^{printf("Blocks\n");}
  • Block类型变量:

在定义c语言函数时,就可以将所定义函数的地址赋值给函数指针类型变量中

int func(int count)
{
    return count + 1;
}
int (*funcptr)(int) = &func;

这样一来,函数func的地址就能赋值给函数指针类型变量funcptr中了。
同样,也可将Block语法赋值给声明为Block类型的变量中。在有关Blocks的文档中,“Block”既指代码中的Block语法,也指由Block语法所生成的值。
声明Block类型变量的示例如下:

int (^blk)(int);

与前面使用函数指针的代码对比可知,声明Block类型变量仅仅是将声明函数指针类型变量的“*”变为“^”。
该Block类型变量与一般的c语言变量完全相同,可作为以下用途使用。

*自动变量
*函数参数
*静态变量
*静态全局变量
*全局变量

使用Block语法将Block赋值为Block类型变量。

int (^blk)(int) = ^(int count){return count + 1;};

由“^”开始的Block语法生成的Block被赋值给变量blk中。因为与通常的变量相同,所以当然也可以由Block类型变量向Block类型变量赋值。

int (^blk1)(int) = blk;

int (^blk2)(int);

blk2 = blk1;

在函数参数中使用Block类型变量可以向函数传递Block。

void func(int (^blk)(int))
{

在函数返回值中指定Block类型,可以将Block作为函数的返回值返回。

int (^func()(int))
{
    return ^(int count){return count + 1;};
}

由此可知,在函数参数和返回值中使用Block类型变量时,记述方式很复杂。
我们可以像使用函数指针类型时那样,使用typedef来解决该问题。

typedef int (^blk_t)(int);

如上,通过使用typedef可声明“blk_t”类型变量。我们试着在以上例子中的函数参数和函数返回值部分里使用一下。

/*原来的记述方式
void func(int (^blk)(int))
{
*/
void func(blk_t blk)
{
/*原来的记述方式
int (^func()(int))
{
*/
blk_t func()
{

另外,将赋值给Block类型变量中的Block方法像c语言通常的函数调用那样使用,这中方法与使用函数指针类型变量调用函数的方法几乎完全相同。变量funcptr为函数指针类型时,这样调用函数指针类型变量:

int result = (*funcptr)(10);

变量blk为Block类型的情况下,这样调用Block类型变量:

int result = blk(10);

通过Block类型变量调用Block与c语言通常的函数调用没有区别。在函数参数中使用Block类型变量并在函数中执行Block的例子如下:

int func(blk_t blk, int rate)
{
    return blk(rate);
}

在objective-c中也可使用:

- (int) methodUsingBlock:(blk_t)blk rate:(int)rate
{
    return blk(rate);
}

Block类型变量可完全像通常的c语言变量一样使用,因此也可以使用指向Block类型变量的指针,即Block的指针类型变量。

typedef int (^blk_t)(int);

blk_t blk = ^(int count){return count + 1;};
blk_t *blkptr = &blk;
(*blkptr)(10);

由此可知Block类型变量可像c语言中其他类型变量一样使用。

  • 截获自动变量值:

通过Block语法和Block类型变量的说明,我们已经理解了“带有自动变量值的匿名函数”中匿名函数。“带有自动变量值”在Blocks中表现为“截获自动变量值”。截获自动变量值的实例如下:

int main()
{
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d\n";
    void (^blk)(void) = ^{printf(fmt, val);};

    val = 2;
    fmt = "These values were changed. val = %d\n";

    blk();

    return 0;
}

代码中,Block语法的表达式使用的是它之前声明的自动变量fmt和val。Blocks中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行Block语法后,即使改写了Block中使用的自动变量的值也不会影响Block执行时自动变量的值。代码中就在Block语法后改写了Block中的自动变量val和fmt。
执行结果为:val = 10;
执行结果并不是我们期望的那样。而是执行Block语法时的自动变量的瞬间值。 该Block语法在执行时,字符串指针“val = %d\n”被赋值到自动变量fmt中,int值10被赋值到自动变量val中,因此这些值被保存(即被截获),从而在执行块时使用。
这就是自动变量值的截获

  • __block说明符_:

    实际上,自动变量值只能保存执行Block语法瞬间的值。保存后就不能改写该值。
    若想在Block语法的表达式中将值赋给在Block语法外声明的自动变量,需要在该自动变量上附加__block说明符,就能在Block内赋值。

__block int val = 0;

val称为__block变量。

  • 截获的自动变量:

    如果将值赋值给Block中截获的自动变量,就会产生编译错误。

    int val = 0;
    void (^blk)(void) = ^{val = 1;};
    

会产生如下的编译错误:

error: variable is not assignable (missing __block type specifier)
    void (^blk)(void) = ^(val = 1;);
                          ~~~  ^

那么截获Objective-C对象,调用变更该对象的方法也会产生编译错误吗?

id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    id obj = [[NSObject alloc] init];
    [array addObject:obj];
};

这是没有问题的,但向截获的变量array赋值则会产生编译错误。代码中截获的变量值为NSMutableArray类的对象。如果用C语言来描述,即是截获NSMutableArray类对象用的结构体实例指针。虽然赋值给截获的自动变量array的操作会产生编译错误,但使用截获的值却不会存在任何问题。下面代码向截获的自动变量赋值,会产生编译错误。

id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    array = [[NSMutableArray alloc] init];
};

这时候加上__block说明符就好了。

另外,在使用c语言数组时必须小心使用其指针,代码如下:

const char text[] = "hello";
void (^blk)(void) = ^{
    printf("%c\n",text[2]);
};

只是使用C语言的字符串字面量数组,并没有向截获的自动变量赋值,因此应该没有问题?但实际上会产生以下编译错误:

error: cannot refer to declaration with an array type inside block
        printf("%c\n",text[2]);

note: declared here
    const char text[] = "hello";
               ^

这是因为在现在的Blocks中,截获自动变量的方法并没有实现对c语言数组的截获。这时使用指针就可以解决问题。

const char *text[] = "hello";
void (^blk)(void) = ^{
    printf("%c\n",text[2]);
};
Zerlz wechat
扫码关注一个很懒的程序员!
Winter is coming, give me a penny!