美文网首页
Objective-C 高级编程-block

Objective-C 高级编程-block

作者: ccSundayChina | 来源:发表于2017-04-26 14:16 被阅读135次

    在前两篇中,我们介绍了一些关于C语言的重要概念,指针struct,这些基础知识是我们深入学习block的前提,在这里将尽可能的将关于block的相关知识讲解的详细一些,分享给那些想要提升自己的小伙伴们,文章会比较长,一下子写完实在是有些力不从心,所以会采取不断更新的方式。

    首先看一下什么是Blocks

    Blocks是C语言的扩充功能。可以用一句话来表示Blocks的扩充功能:带有自动变量(局部变量)的匿名函数。

    顾名思义,所谓匿名函数就是不带有名称的函数。C语言的标准不允许存在这样的函数。例如如下的源代码:

    int func(int count);
    

    它声明了名为func的函数。下面的源代码中为了调用该函数,必须使用该函数的名称func。

    int result = func(10);
    

    如果像下面这样,使用函数指针来代替直接调用函数,那么似乎不用知道函数名也能够使用该函数。

    int result = (*funcptr)(10);
    

    但其实使用函数指针也仍然要知道函数名称。像以下的源代码一样,在赋值给函数指针时,若不使用想赋值的函数的名称,就无法获得该函数的地址。

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

    而通过Blocks,源代码中就能够使用匿名函数了,即不带名称的函数。对于程序员而言,命名就是工作的本质,函数名、变量名、方法名、属性名、类名和框架等都必须具备。而能够编写不带名称的函数对程序员来说相当具有吸引力。

    到这里,我们知道了“带有自动变量值的匿名函数”中“匿名函数”的概念。那么“带有自动变量值”究竟是什么呢?

    首先回顾一下在C语言的函数中可能使用的变量。(以下的变量在内存中的分配是不一样的,可以参考这篇文章了解 C语言中的内存分配。)

    • 自动变量(局部变量)
    • 函数的参数
    • 静态变量(静态局部变量,方法中用static声明的变量)
    • 静态全局变量(方法外用static声明的变量)
    • 全局变量

    其中,在函数的多次调用之间能够传递的变量有:

    • 静态变量(静态局部变量)
    • 静态全局变量(只能在本文件中使用的全局变量)
    • 全局变量(外接也可以使用的全局变量)
      虽然这些变量的作用域不同,但在整个程序中,一个变量总保持在一个内存区域。因此,虽然多次调用函数,但该变量值总能保持不变,在任何时候以任何状态调用,使用的都是同样的变量值。

    另外,“带有自动变量值的匿名函数”这一概念并不仅指Blocks,它还存在于其他许多程序语言中。

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

    Blocks 模式

    一、Block语法

    下面我们详细讲解一下带有自动变量值得匿名函数Block的函数,即Block表达式语法。先看一个简单点的形式:

    ^(int event){
            printf("event:%d",event);
    }
    

    实际上,上述Block语法使用了省略方式,其完整形式如下:

    ^void(int event){
            printf("event:%d",event);
    }
    

    如上所示,完整形式的Block语法与一般的C语言函数定义相比,仅仅有两点不同。

    • 没有函数名
    • 带有“^”
      第一点不同是没有函数名,因为它是匿名函数。第二点不同是返回值类型前带有“^”(插入记号,caret)记号。因为OS X、iOS应用程序的源代码中将大量使用Block,所以插入该记号便于查找。

    Block的一般范式如下

    ^ 返回值类型   参数列表   表达式
    

    返回值类型同C语言函数的返回值类型,“参数列表”同C语言的参数列表,“表达式”同C语言函数中允许使用的表达式。当然与C语言函数一样,表达式中含有return语句时,其类型必须与返回值类型相同。

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

    ^ int (int count){return count+1};
    
    返回值的类型可以省略

    虽然前面出现过省略方式,但Block的语法可省略好几个项目。首先是返回值类型,如下所示:

    ^ 参数列表 表达式(返回值类型被省略)
    

    当你省略返回值类型时,如果表达式中有return语句就使用该返回值的类型,如果表达式中没有return语句就使用void类型。表达式中含有多个return语句时,所有return的返回值类型必须相同。

    前面的^ int (int count){return count+1};源代码省略其返回值类型时如下所示:

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

    该block语法将按照return语句的类型,返回int形的返回值。

    如果不使用参数,参数列表也可以省略。以下为不使用参数的Block语法:
    ^ void(void){printf("blocks\n")};
    

    可以进一步的省略:

    ^ {printf("blocks\n")};
    
    二、Block类型变量

    上面提到的Block语法单单从其技术方式上来看,除了没有名称以及带有“^”以外,其他都与C语言函数定义相同。在定义C语言函数时,就可以将所定义函数的地址赋值给函数指针类型变量中。

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

    这样一来,函数func的地址就能赋值给函数指针类型变量funcptr中了。
    同样地,在Block语法下,可将Block语法赋值给声明为Block类型的变量中。即源代码中一旦使用Block语法就相当于生成了可赋值给Block类型变量的“值”。Blocks中由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;//声明一个block类型变量blk1,并将blk赋值给blk1。
    int (^blk2)(int);//声明一个block类型变量blk2。
    blk2 = blk1;
    

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

    void func(int(^blk)(int)){}//int(^blk)(int)可以看做是一个block类型的变量blk。
    

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

    int (^func())(int){
    return ^(int count){
          return count+1
    };//block类型为int (^func()),参数列表为空,不是没有参数,而是可以任意参数。返回的^(int count){return count+1}可以作为是上述类型的block变量。
    }
    

    从上面可以看到,在函数参数和返回值中使用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();
    

    通过使用typedef,函数定义就变得更容易理解了。
    另外,将赋值给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语法在执行时,字符串指针被赋值到自动变量fmt中,int值10被赋值到自动变量val中,因此这些值被保存(即被截获),从而在执行块时使用。
    这就是自动变量值的截获。


    四、__block 说明符

    实际上,自动变量值截获只能保存执行Block语法瞬间的值。保存后就不能改写该值。下面我们尝试改写截获的自动变量值,看看会出现什么结果。下面的源代码中,Block语法之前声明的自动变量val值被赋予1。

    int val = 0;
    void (^blk)(void) = ^{val = 1;};
    blk();
    printf ("val = %d\n",val);
    

    以上为在Block语法外声明的给自动变量赋值的源代码。该源代码会产生编译错误。

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

    若想在Block语法的表达式中将值赋给在Block语法外声明的自动变量,需要在该自动变量上附加__block说明符。该源代码中,如果给自动变量声明int val附加__block说明符,就能实现在Block内赋值。

    __block int val = 0;
    void(^blk)(void) = ^{val = 1;};
    blk();
    printf("val = %d\n",val);
    

    该源代码的执行结果为

    val = 1;
    

    使用附有__block说明符的自动变量可在Block中赋值,该变量称为__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];
      };
    
    
    error: variable is not assignable(missing __block type specifier)
            array = [[NSMutableArray alloc]init];
             ~~~  ^
    

    这种情况下,需要给截获的自动变量附加__block说明符。

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

    接下来会介绍block是如何实现的,会从它的c++源码层面进行分析。(未完待续。。)

    参考书籍:Objective-C高级编程

    相关文章

      网友评论

          本文标题:Objective-C 高级编程-block

          本文链接:https://www.haomeiwen.com/subject/clsvzttx.html