Objective-C库中的回调使用选择器或块。

19

问题

我们正在使用Objective-C开发一个自定义的EventEmitter inspired消息系统。对于侦听器提供回调,我们应该要求使用blocks还是selectors?为什么?

作为使用第三方库的开发人员,您更喜欢使用哪种方式?哪种方式最符合苹果的发展轨迹、指南和实践?

背景

我们正在使用Objective-C开发全新的iOS SDK,其他第三方将使用该SDK将功能嵌入其应用程序中。我们SDK的一个重要部分将需要将事件通信给侦听器。

我知道有五种在Objective-C中进行回调的模式,其中三种不适用:

  • NSNotificationCenter - 不能使用,因为它不能保证通知观察者的顺序,并且没有办法让观察者阻止其他观察者接收事件(就像 JavaScript 中的 stopPropagation() 一样)。
  • Key-Value Observing - 看起来并不是很适合架构,因为我们真正拥有的是消息传递,而不总是与“状态”相关。
  • Delegates and Data Sources - 在我们的情况下,通常会有许多侦听器,而不是单个可以正确称为委托的侦听器。

以下两个是备选方案:

  • 选择器 - 在这种模式下,调用者提供一个选择器和一个目标,共同处理事件。
  • - 在iOS 4中引入的块允许功能在不绑定到对象(如观察者/选择器模式)的情况下传递。

这可能看起来像一个玄学的问题,但我觉得有一个客观的“正确”答案,只是我对Objective-C太没有经验了,无法确定。如果有更好的StackExchange网站来回答这个问题,请帮助我将其移动到那里。

更新#1-2013年4月

我们选择使用作为指定事件处理程序的回调方式。我们很大程度上满意这个选择,并且不打算删除基于块的监听器支持。它确实有两个显着的缺点:内存管理和设计阻抗。

内存管理

块最容易在堆栈上使用。通过将长期存在的块复制到堆上来创建块会引入有趣的内存管理问题。

这段代码中,对包含对象的方法进行调用的块会隐式地增加 self 的引用计数。假设您的类具有 name 属性的 setter 方法,如果在块内调用 name = @"foo",编译器会将其视为 [self setName:@"foo"] 并保留 self,以使其在块仍然存在时不被释放。

实现 EventEmitter 意味着拥有长期存在的块。为了防止隐式保留,Emitter 的使用者需要在块外创建一个对 self__block 引用,例如:

__block *YourClass this = self;
[emitter on:@"eventName" callBlock:...
   [this setName:@"foo"];...
}];

这种方法唯一的问题是在处理程序调用之前,this可能已被销毁。因此,当用户被销毁时,必须注销他们的侦听器。
设计阻抗
经验丰富的Objective-C开发人员希望使用熟悉的模式与库进行交互。委托是一个非常熟悉的模式,因此规范的开发人员希望使用它。
幸运的是,委托模式和基于块的侦听器并不是相互排斥的。虽然我们的emitter必须能够处理来自许多地方的侦听器(只有一个委托将无法工作),但我们仍然可以公开一个接口,允许开发人员与emitter交互,就好像他们的类是委托一样。
我们还没有实现这个功能,但根据用户的请求,我们可能会这样做。
更新 #2 — 2013年10月
我已经不再从事引发这个问题的项目,很高兴地回到了我的JavaScript本土。
接管这个项目的聪明开发人员正确地决定完全放弃我们的自定义基于块的EventEmitter。即将发布的版本已经转向ReactiveCocoa
这使得它们具有比我们先前的 EventEmitter 库更高级别的信号模式,并且允许它们更好地封装状态在信号处理程序中,而不是像我们的基于块的事件处理程序或类级别方法那样。

考虑到这是一个设计/“白板”类型的问题,[程序员交流社区]可能是更好的选择——只是提供另一个选项。 - jscs
谢谢Josh,我看了一下Programmers并考虑在那里发布这个问题,但它似乎更多是关于语言无关的类型问题。由于这是特定于语言/系统的问题,所以我认为应该在这里发布,但正如我在问题中提到的那样,我当然不确定。 - jimbo
4个回答

7
个人而言,我不太喜欢使用委托。由于Objective-C的结构,如果我必须创建单独的对象/添加协议才能被通知到您的事件之一,并且我必须实现五分之六,它会使代码变得混乱。出于这个原因,我更喜欢块。
尽管块也有它们的缺点(例如,内存管理可能会很棘手),但它们易于扩展,实现简单,在大多数情况下都是合理的。
虽然苹果的设计结构可能使用发送者-委托方法,但这仅用于向后兼容。最近的苹果API已经使用块(例如CoreData),因为它们是Objective-C的未来。尽管当过度使用时它们可能会使代码变得混乱,但它也允许更简单的“匿名代表”,这在Objective C中是不可能的。
最终,它真正归结为这一点: 为了使用块而放弃一些旧的、过时的平台是否值得?委托的一个主要优点是它保证可以在任何版本的OBJC运行时中工作,而块是语言的较新添加。
至于NSNotificationCenter/KVO,则它们都很有用,各有其用途,但作为委托,它们不应被使用。两者都无法将结果发送回发送方,在某些情况下,这是至关重要的(例如-webView: shouldLoadRequest:)。

我们的目标是支持iOS 4.x及以上版本,因此块应该在我们的目标上得到支持。 - jimbo

1

我认为正确的做法是两者都实现,将其用作客户端,并看看哪种方式最自然。这两种方法都有优点,它们的使用取决于上下文和您期望 SDK 的使用方式。

选择器的主要优点是简单的内存管理——只要客户端正确注册和注销,就不需要担心内存泄漏。对于块,内存管理可能会变得复杂,具体取决于客户端在块内执行的操作。回调方法也更容易进行单元测试。当然可以编写可测试的块, 但根据我的观察,这并不是常见的做法。

块的主要优点是灵活性——客户端可以轻松引用本地变量而不必使它们成为 ivars。

因此,我认为这取决于用例——对于这样一个通用的设计问题,没有“客观正确答案”。


感谢您提出内存管理的问题,考虑到这一点非常好。不幸的是,对于这个项目来说,实现两者并进行A/B测试是行不通的,因为一旦有用户使用库,就几乎不可能将功能从中删除(由于业务原因,我们几乎立即会有用户)。 - jimbo
我并不是说你应该做 A/B 测试,而是说你应该亲自编写客户端代码。这就像进行测试驱动开发一样——实际上使用接口作为客户端有助于你设计更好的接口。 - Christopher Pickslay

1

写得很好!

从编写大量JavaScript的角度来看,事件驱动编程在我个人看来比来回委托要干净得多。

关于侦听器的内存管理方面,我的解决方法(在很大程度上借鉴了Mike Ash的MAKVONotificationCenter),在调用者和发射器的dealloc实现中进行交换(如此看来),以便安全地双向删除侦听器。

我不确定这种方法有多安全,但想法是尝试它直到它出问题为止。


0
关于库的一件事是,你只能在某种程度上预测它将如何使用。因此,您需要提供尽可能简单、开放和熟悉的解决方案,以满足用户需求。
  • 对我来说,所有这些都最适合委托。虽然您是正确的,它只能有一个侦听器(委托),但这意味着没有限制,因为用户可以编写一个了解所有所需侦听器并通知它们的类作为委托。当然,您可以提供一个注册类。它将调用所有已注册对象上的委托方法。
  • 块也很好。
  • 您称之为选择器的内容被称为目标/操作,简单而强大。
  • 对我来说,KVO 似乎不是最佳解决方案,因为它可能会削弱封装性,或者导致错误的心理模型,即如何使用您的库的类。
  • NSNotifications 可以很好地通知某些事件,但用户不应被迫使用它们,因为它们相当非正式。而且您的类无法知道是否有人收听。

关于API设计的一些有用的思考: http://mattgemmell.com/2012/05/24/api-design/


感谢您的回复。在我们的情况下,委托并不完全匹配,因为许多不同的实体将通过它发出许多不同的处理程序来监听不同类型的事件。我读了马特的文章。可惜他根本没有提到块。 - jimbo
我仍然认为,委托/协议对你很有用。发送的消息必须符合某种协议。它也可以是非正式的。如果发送方必须将其消息打包在特定块中或符合某个协议,这并没有太大区别。我不是说你不应该使用块。例如,AFNetworking做得很好。但是使用块时,您必须预测用户将如何使用它,因为您必须向他提供块参数(是的,我知道__block,但那真的不太优雅)。 - vikingosegundo

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