美文网首页iOS面试闪充
程序员面试闪充--Block

程序员面试闪充--Block

作者: 谦谦君子修罗刀 | 来源:发表于2018-01-15 15:08 被阅读51次

    1、介绍

    Block是OC中非常重要的一种技术手段


    2、从c函数和oc函数的区别来定义block

    C函数写法:int add(int num1, int num2)
    OC函数写法:-(void)show:(int num1)
    由C到block的转变:void(^myBlock)()

    3、Block的基本使用

    创建一个命令行项目,选择OS X->Application->Command Line Tool

    写代码之前牢记三句话

    1、Block是C语言的
    2、Block是一个数据类型
    3、Block是一个提前准备好的代码,在需要的时候执行

    1)最简单的Block

    //block是一个提前好的代码,所以在赋值的时候要赋一段代码  (定义时,把block当成一个数据类型)
    void (^myBlock)() = ^{
        NSLog(@"hello");
    };
    
    //执行,执行时把block当成函数
    myBlock();
    

    至此,最简单的block就定义好了。它的两个特点是:1、类型比函数多了一个^符号。2、设置数值时,有一个^符号,内容是{}括起来的一段代码。

    2)定义带参数的block

    格式: void (^block名称)(参数列表) = ^(参数列表){//代码实现}

    在代码中,如果要使用到参数,就必须要带上变量名,所以定义x,y。在本代码中,把^号后面的小括号当成一个参数。

    void (^sumBlock)(int,int) = ^(int x,int y){
        NSLog(@"%d",x+y);
    }
    
    //执行
    sumBlock(10,20);
    

    3)定义带返回值的Block

    格式:返回值类型(^block名称)(参数列表)=^返回类型(参数列表){//代码实现}

    int (^sumBlock2)(int,int)=^int(int a,int b){
        return a+b;
    }
    
    //执行
    NSLog(@"%d",sumBlock2(4,3));
    

    此处恰好说明,block是一段提前准备好的代码,当调用它时,它将返回值返回。

    4)block的速记符号

    在代码中敲"inlineBlock",xCode会返回已经定义好的block格式。

    4、Block的应用场景

    如果有这样的代码,每个函数都有很多重复的部分,这个时候我们通常会想到要重构代码。也就是说把相同的代码抽取出来,放在一个地方维护,如果需求变化就只要调整一个地方。

    void day1(){
        NSLog(@"起床");
        NSLog(@"上班");
        
        NSLog(@"入职");
        NSLog(@"要账号");
        NSLog(@"看代码");
        
        NSLog(@"下班");
        NSLog(@"睡觉");
    }
    
    void day2(){
        NSLog(@"起床");
        NSLog(@"上班");
        
        NSLog(@"需求分析");
        NSLog(@"熟悉公司环境");
        
        NSLog(@"下班");
        NSLog(@"睡觉");
    }
    
    void day3(){
        NSLog(@"起床");
        NSLog(@"上班");
        
        NSLog(@"写代码");
        NSLog(@"开会");
        NSLog(@"测试");
        
        NSLog(@"下班");
        NSLog(@"睡觉");
    }
    
    void day4(){
        NSLog(@"写代码");
        NSLog(@"测试");
        }
    

    1)重构的步骤

    1、新建一个方法
    2、将重复的代码复制到新方法中
    3、根据需求,调整参数。

    2)重构的内容

    建立一个新的方法

    void myWork(){
        //这几句每个方法都一样的
        NSLog(@"起床");
        NSLog(@"出门");
        NSLog(@"坐车");
        
        //中间是每天的工作内容
        //…… 有可能有几条
        
        NSLog(@"下班");
        NSLog(@"回家");
        NSLog(@"睡觉");
    }
    

    在上面代码中,有很多重复的事情要做。但是每天的工作内容是不一样的,也就是说要传入不同的参数。于是问题来了,该怎样去确定重构的参数呢?

    其实我们只要将代码放入其中,让它执行即可。而Block正是一段预先准备好的代码,在需要的时候执行。

    在每天的工作内容中,调用一个定义好的block,每次调用都会执行block的内容,该问题就能迎刃而解。现在来写重构的代码。

    void myWork(void(^dayWork)()){
        NSLog(@"起床");
        NSLog(@"出门");
        NSLog(@"坐车")
        
        //这里调用参数
        dayWork();
        
        NSLog(@"下班");
        NSLog(@"回家");
        NSLog(@"睡觉");
    }
    

    此时,在其他普通的方法中,就可以省去那些多余的代码,只要把不一样的代码传入到block即可。

    void day1(){
        myWork(^{
            NSLog(@"入职");
            NSLog(@"要账号");
            NSLog(@"看代码");
        });
    }
    

    在主函数中调用函数day1()

    int main(int argc,const cahr * argv[]){
        @autoreleasepool {
            day1();
        }
        return 0;
        
    }
    

    自此已经可以将重复的代码省去了。
    其它方法也以此类推,写出每天工作中不同的操作。

    void day2(){
        myWork(^{
            NSLog(@"需求分析");
            NSLog(@"熟悉公司环境")
        });
    }
    
    void day3(){
        myWork(^{
            NSLog(@"写代码");
            NSLog(@"开会")
        });
    }
    void day4(){
        myWork(^{
            NSLog(@"写代码");
            NSLog(@"开会")
            NSLog(@"测试")
    
        });
    }
    

    调用

    int main(int argc,const cahr * argv[]){
        @autoreleasepool {
            day1();
            day2();
            day3();
            day4();
        }
        return 0;
        
    }
    

    于是,问题又来了。
    day1()-day5()的调用实在太过死板,是否能实现一次调用呢?

    思路:这个时候,我们应该创建一个新的方法,设置一个day的参数,根据day来决定具体执行哪个块的代码。

    void dailyWork(int day){
        //定义一个block变量,没有设置代码
        void(^work)();
        //根据day来决定调用哪些代码
        switch(day){
         case 1:
            work = ^{
                  NSLog(@"入职");
                  NSLog(@"要账号");
                  NSLog(@"看代码");
            };
            break;
        case 2:
            work = ^{
                 NSLog(@"需求分析");
                 NSLog(@"熟悉公司环境")
            };
            break;
         case 3:
            work = ^{
                  NSLog(@"写代码");
                  NSLog(@"开会")
            };
            break;
         case 4:
            work = ^{
                NSLog(@"写代码");
                NSLog(@"开会")
                NSLog(@"测试")
            };
            break;
        default:
        //不是day1-4时调用
        work = ^{
            NSLog(@"周末不加班");
        }
           break;
        }
        //***这里很重要**
        //上面已经做完赋值操作了,此时,调用block
        //此时正是体现了,block可以当做参数传递这一原理。
        myWork(work);
    }
    

    调用

    int main(int argc,const cahr * argv[]){
        @autoreleasepool {
           for(int i=0;i<7;i++){
            dailyWork(i)
           }
               }
        return 0;
        
    }
    

    结果如下图所示:


    3)另一种重构方式--Block当做返回值

    上面的重构方式,是新创建了一个方法dayWork()来封装了每一天的内容。如果说有一个函数专门返回一个block,那么只要将执行dayWork()函数这行代码替换为函数返回的block就行。
    接下来尝试一下用block当做返回值的写法。

    如此一来,myWork这个重构的函数就不需要block作为参数了。


    如果要将block当做返回值,需要先单独定义一下block的类型
    速记符:typedefBlock

    定义一个block类型

    typedef void(^workBlock())();
    

    接下来声明一个函数,此处的返回值就是该workBlock

    workBlock workFunc();
    

    当这个方法实现了之后,将返回值代替dayWork()函数即可完成操作。


    workBlock block = workFunc();
    block();
    

    于是,现在要解决的问题是怎样让函数返回block呢?
    在用block做参数的那份代码中,dailyWork方法的返回值是空,而现在我们可以将它的返回值类型修改为workBlock类型,然后在最后返回一个work。

    事实上,这个dailyWork方法,是应该要被workFunc()调用的。因此将它的方法名称修改为workFunc()。

    workBlock workFunc(int day){}
    

    将day作为参数加入到block中,如此一来就可以按照天数来决定调用哪一块代码

    //声明一个函数,该返回值是workBlock
    workBlock workFunc(int day);
    
      workBlock block = workFunc(day);
        block();
    

    修改天数的方法内容

    void day1(){
        myWork(1);
    }
    
    void day2(){
        myWork(2);
        
    }
    
    void day3(){
        myWork(3);
    }
    
    void day4(){
        myWork(4);
    }
    

    调用

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            for (int i = 0; i < 7; i++) {
                myWork(i);
                NSLog(@"______");
            }
           
            
        }
        return 0;
    }
    

    5、Block反向传值

    通常在项目中,代理做的就是反向传值的功能。
    而现在我们可以用Block来做这样的操作操作
    在第一个界面中,用一个label用来显示姓名,当点击按钮的时候跳转到另外一个页面。当用户输入了姓名之后点击保存按钮再将文本传给上一个页面。


    • 新建一个控制器,名为EditViewController继承自EditViewController。在mainStoryboard中绑定该类。
    • 对按钮和标签进行连线。此处省略。
    • 分析返回的代码。block是一组预先准备好的代码,在需要的时候执行,也就是说,它是被调用方。
    //在.h文件中声明一个Block属性。需要用copy
    #import <UIKit/UIKit.h>
    
    @interface EditViewController : UIViewController
    @property(nonatomic,copy)void (^completion)(NSString *text);
    @end
    

    在.m文件中执行块代码。在这段代码中,需要将用户输入的文本属性传到上个控制器中。

    //.m文件
    @interface EditViewController ()
    @property (weak, nonatomic) IBOutlet UITextField *nameText;
    
    @end
    
    @implementation EditViewController
    - (IBAction)save:(id)sender {
        
        //block预先准备好的代码
        self.completion(self.nameText.text);
        [self.navigationController popViewControllerAnimated:YES];
        
    }
    @end
    

    如此,被执行的这边已经完成,

    • 现在来做第一个页面的准备工作。
      传值工作一般在prepareForSegue方法里面操作。在这个方法中,先获取到下一个页面的控制器。在它的block的值传给第一个界面的标签保存。
    -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender{
        EditViewController *editVC = segue.destinationViewController;
        editVC.completion = ^(NSString *str){
            self.nameLabel.text = str;
        };
        
    }
    
    • 总结:

    调用方:准备块代码。类似于代理方式协议方法的实现,不同的是块代码都在一起并没有单独的实现一个方法。

    被调用方: 执行块代码。前提是有要执行的代码,也就是在.h文件中定义一个块代码的属性,接着在需要的时候执行即可。

    • 结果

    6、Block常见面试题

    1、下面这段代码输出为多少?

    void demoBlock() {
        int x = 10;   //外部变量x
        void(^myBlock)() = ^ {
        NSLog(@”%d”,x);
    };
          x = 20;   //在外部修改x的值
          myBlock();
    }
    

    答案:输出为10。在定义block的时候,如果引用了外部变量,会对外部变量进行拷贝。记录住在block时变量的数值。如果之后在外部修改变量的值也不会影响block内部的数值变化。

    2、如果在block里面修改外部变量的值,会报错吗?

    void demoBlock() {
        int x = 10;   //外部变量x
        void(^myBlock)() = ^ {
        x = 80
        NSLog(@”%d”,x);
    };
          x = 20;   //在外部修改x的值
          myBlock();
    }
    

    答案:在默认情况下,不允许block内部修改外部变量的数值。因为拷贝之后它与原数值指向了不同的地址。若是能改变则会破坏代码的可读性。

    3、需求千变万化。若我非要将x的值改成80又当如何?为什么?

    void demoBlock() {    
      __block int x = 10
        int x = 10;   //外部变量x
        void(^myBlock)() = ^ {
        };
            myBlock();
            NSLog(@”%d”,x);
         };
    

    答案:如果要在block中,修改外部变量的值,需要用__block修饰符号。使用了__block,说明函数不关心具体数值的具体变化。

    在block中,如果引用了外部使用的__block变量,block定义之后,外部变量的指针地址同样会变成堆区的地址。之所以可以修改是因为它们的指针指向的地址是相同的。

    4、下面是一段能正确运行的代码。为啥定义成mutableString能在block内部对外部变量进行修改了?

    void demoBlock() {
        NSMutableString *strM = [NSMutableString stringWithString:@”张三”];
    
      void(^myBlock)() = ^{
        [strM setString:@”lisa”];
    };
        myBlock();
        NSLog(@”%@”,strM);
        
    }
    

    答案:在block中引用外部变量要拷贝一份到堆中。
    这个时候拷贝到堆中的地址与在栈中的地址是一样的。因此,它指向zhangsan
    在setString的时候,将指针指向的地址的值进行修改。


    但是如果用stringWithString方法就会报错,因为这相当于又开辟了一个新的空间。在堆中将strM的地址指向了新的空间。


    关注微信公众号:“程序员面试闪充”。每周小视频,不见不散。
    小视频传送门:小视频传送门

    相关文章

      网友评论

        本文标题:程序员面试闪充--Block

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