iOS >> Blocks >> 修改block外部变量的值

8

我熟悉使用__block语句可以在Block内使变量“可分配”。

但是当使用一些将Blocks作为方法参数的Objective-C特性时,即使没有使用__block语句声明,某些变量也是可分配的。

以下是两个示例代码:

[UIView animateWithDuration:2 animations:^
     {
         self.animatedView.backgroundColor = [UIColor blueColor];
         self.animatedView.center = CGPointMake(100, 100);
     }];

(animatedView是一个简单的UIView,连接了IBOutlet。)
    int myInt = 10;
    NSMutableString* mString = [NSMutableString stringWithString:@"Mutable Hello"];
    NSString* imString = @"Imutable Hello";

    void (^myBlock)(void) = ^
    {
        [mString appendString:@" Block"]; //working
        imString = @"Imutable Hello Block"; //error
        myInt = 11; //error
    };

我的问题是:为什么我可以给UIView实例属性赋值? 我不是在处理一个对象并改变它,比如我的mString。
我期望 'center' 属性的行为像我的myInt,因为它是直接访问的C结构体,而不是对象的指针。
我期望 'backgroundColor' 的行为像我的imString,因为它是一个指向对象的指针,并被分配了一个新对象,不是吗?
我在文档中找不到满意的解释...如果有人能提供一个解释或指导我去找一个,我会很感激。
3个回答

8
这是赋值和使用的区别。使用指的是方法调用。你完全可以在实例上调用方法([mString appendString:@" Block"]; //working),但是如果没有标记变量告诉编译器它应该启用,你就不能赋值(imString = @"Imutable Hello Block"; //error)。
下面是代码:
self.animatedView.backgroundColor = [UIColor blueColor];

仍然不是真正的赋值操作,它是一个“隐藏”的方法调用。点符号表示法从来不是赋值操作,它只是方法调用的语法糖。实际上,它被翻译为:

[[self animatedView] setBackgroundColor:[UIColor blueColor]];

将值分配给局部变量和分配给对象内部变量的区别在于它们所驻留的内存位置。基本上,这取决于它们是否存在足够长的时间以便发挥作用。这就是堆栈和堆上的数据之间的区别。


1
该方法具有副作用(欠缺更好的术语)。区别在于内存位置。局部变量创建在堆栈上,所以除非你告诉编译器将它们复制到堆上,否则它们是不可访问的。编译器在使数据在块内可用时也使用了许多“技巧”(指针和基元之间采取的路线不同)。这是一个深入的主题,请阅读此线程:https://dev59.com/Im7Xa4cB1Zd3GeqPvOzM - Wain
@wain,你不应该混淆编译器可能进行的优化效果,这只会导致混乱。实际上重要的是在块作用域之外捕获了哪个变量,并且“捕获”意味着在块的作用域内创建了一个带有_const_限定符的原始变量副本。启用ARC并且对于可保留对象的捕获变量,“复制”也意味着保留/释放。 - CouchDeveloper
@CouchDeveloper:“启用ARC并为可保留对象捕获的变量,‘copy’也意味着保留/释放。”即使没有启用ARC,在对象指针类型的捕获变量中,这也意味着保留/释放。但是保留/释放是在块被复制时发生的,而不是在创建时发生的。 - newacct
@OhadRegev:“Property”意味着“getter/setter方法对”。它不表示其他任何东西。 getter/setter方法在内部如何实现是您无法关心的实现细节。 - newacct
@newacct 实际上,如果启用了ARC并且变量被捕获,它们将被“const copied”,对于可保留指针来说,这意味着它们被保留(并在块执行后释放)。如果复制块,无论是否启用ARC,Objective-C指针都将被保留(并在块被释放时释放)。然而,在捕获可保留指针时执行的保留/释放通常会因优化效果而省略。;) - CouchDeveloper
显示剩余2条评论

3
为了允许在一个代码块中修改变量,要使用 _block 存储类型修改符——请参见“block 存储类型”._
__block NSString* imString = @"Imutable Hello";

参考自苹果文档 在块内使用的变量遵循以下规则:
  1. 可以访问全局变量,包括存在于封闭词法范围内的静态变量。
  2. 与函数的参数一样,可以访问传递到块中的参数。局部作用域内的堆(非静态)变量被捕获为const变量。
  3. 它们的值是在程序的块表达式点上获取的。在嵌套块中,该值从最近的封闭范围中获取。

  4. 使用 __block 存储限定符声明的封闭词法范围内的局部变量以引用方式提供,因此是可变的。

  5. 任何更改都会反映在封闭词法范围中,包括在同一封闭词法范围内定义的任何其他块。这些将在“__block 存储类型”中更详细地讨论。

  6. 在块的词法范围内声明的局部变量,与函数中的局部变量行为完全相同。

  7. 每次调用块时都会提供该变量的新副本。这些变量反过来又可以作为const或引用变量在封闭于该块内的块中使用。

你忘了提到这是从https://developer.apple.com/library/ios/documentation/cocoa/Conceptual/Blocks/Articles/bxVariables.html#//apple_ref/doc/uid/TP40007502-CH6-SW2中直接复制的。解释一下它如何适用于特定的问题会更有帮助。 - Martin R
我读了这份文档,但它仍然没有回答我的问题。是哪些规则使得那些未被定义为 __block 的 UIView 属性在动画块内可以被赋值?为什么我的基本变量不可赋值,而 UIView 的基本属性可以;为什么我不能给指针赋新对象,除非在声明时使用 __block,而 UIView 属性可以。 - Ohad Regev

3
在您的第一个示例中,块“捕获”了变量self,它是指向可保留对象的指针。当您编写以下内容时,不会修改self本身:
self.someProperty = someValue;

self的值仍然保持不变 - 也就是说,它仍然指向同一个对象。

如果您写下以下代码:

self = nil;

好的...这个答案实际上有所指向...那么我将一个对象传递给Block,这意味着它的属性是可以分配的?我测试了一下,在我的类中添加了一个int属性和一个int ivar - 没有__block -,我发现我可以在块内为它们赋新值。哪个原则/规则使得属性/ivars可以在块内被分配?而在定义Block的方法内部的本地变量则不能? - Ohad Regev
1
@OhadRegev 能够调用一个方法的事实是,接收器指针(这里是_self_)在调用方法时不会因此而改变(这实际上就是分配属性的全部内容)。请注意,_self_是一个指针,它的值可能是类似于self equals 0xabcd1230的东西,在调用方法之前,调用方法之后它仍然是相同的:self equals 0xabcd1230 - CouchDeveloper
1
@OhadRegev 捕获变量(这里是指针_self_,其值为0xabcd1230)是_const_只是在处理块或lambda时的一种常见约定。它们也可以是可修改的。 - CouchDeveloper

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接