【发布时间】:2009-11-13 08:10:11
【问题描述】:
第一季度。为什么要用回调函数?
第二季度。回调是邪恶的吗?对那些人来说很有趣 谁知道呢,对别人来说是一场噩梦。
第三季度。回调的任何替代方案?
【问题讨论】:
第一季度。为什么要用回调函数?
第二季度。回调是邪恶的吗?对那些人来说很有趣 谁知道呢,对别人来说是一场噩梦。
第三季度。回调的任何替代方案?
【问题讨论】:
回调减少了耦合——被调用方被传递了一些指针,它不知道它背后是什么。回调是如此幸运的解决方案,以至于它们非常普遍。
例如查看sqlite3_exec()。你给它一个查询和一个可选的回调。它执行查询并在检索到每一行时调用回调。现在,快速执行查询和低资源消耗是 SQLite 的业务,您只需按照自己的意愿处理检索到的结果。您可以将它们添加到容器中并稍后处理,或者您可以立即一个接一个地处理它们,或者您可以将它们传真到某个地方并期望对方将它们传真回来 - SQLite 不在乎,它是完全抽象的,可以做好自己的工作。
【讨论】:
无论是否“在 C++ 中使用回调增加耦合”,我都建议使用事件处理程序样式,尤其是处理类似事件的事情。例如,具体一个设计模式而不是回调概念如下:
class MyClass
{
public:
virtual bool OnClick(...) = 0;
virtual bool OnKey(...) = 0;
virtual bool OnTimer(...) = 0;
virtual bool OnSorting(...) = 0
...
};
您可能仍然认为上述函数是回调,但是当您将它们视为已知的设计模式时,您不会感到困惑,因为您正在做 OO 并编写 C++。
Effo UPD@2009nov13 - 典型案例:框架、事件系统或并发编程模型等。以下示例应该很有用。
框架控制着整个流程,因为好莱坞原则规定“不要打电话给我们,我们会打电话给你”。 (这就是“回调”的确切含义)在每个普通函数或库中,调用者控制流程。
著名的 C 框架是 Linux 内核,Linux 驱动程序编写者知道他/她将在其中实现“struct file_operations”“read()”表示 OnRead(),“write()”表示 OnWrite( ) 等。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
...
};
但最简单的框架示例应该是:
The Framework | A developer to do
----------------------------+--------------------------------------
| class MyClass : public Actor
| {
| public:
pApplication->Init(...); | virtual bool OnInit(...) {}
pApplication->Run(...); | virtual int OnRun(...) {}
pApplication->Quit(...); | virtual void OnQuit(...) {}
| ...
| };
和pApplication->Init()会调用pActor->OnInit,pApplication->Run()会调用pActor->OnRun(),以此类推。大多数 Windows GUI 开发人员都有实现 OnMouseClick() 或 OnButtonPress() 等的经验。
我同意该线程的其他答案,他们根据相应的观点给出了正确的解释,例如分层方法中的处理程序,通用回调或异步操作等。适合您的想法由您决定。
【讨论】:
第三季度。回调的任何替代方案?
我更喜欢回调的函子形式。例如这种事:
class WidgetContainerOrProcessorOfSomeSort
{
public:
struct IWidgetFunctor
{
virtual bool operator()( const Widget& thisWidget )=0;
};
....
bool ProcessWidgets( IWidgetFunctor& callback )
{
.....
bool continueIteration = callback( widget[ idxWidget ] );
if ( !continueIteration ) return false;
.....
}
};
struct ShowWidgets : public WidgetContainerOrProcessorOfSomeSort::IWidgetFunctor
{
virtual bool operator(){ const Widget& thisWidget }
{
thisWidget.print();
return true;
}
};
WidgetContainterOrProcessorOfSomeSort wc;
wc.ProcessWidgets( ShowWidgets() );
对于简单的示例来说似乎总是有点冗长,但在现实世界中,我发现它比试图准确地记住如何构造复杂的函数指针声明要容易得多:-)
【讨论】:
之前为 Boost.Function 发布了一篇指向 Boost 的帖子。如果您正在为回调寻找更通用的解决方案,例如附加到同一个回调的多个函数或类似的东西,请考虑使用Boost.Signals。这个名字来源于信号和槽,这是现在一些人提到回调的方式,尤其是对于 GUI。
【讨论】:
回调用于调用异步操作,即在调用者按照自己的方式运行时在不同线程中运行的代码。您需要一些机制来知道异步操作何时完成。
为什么他们会是邪恶的?与任何其他编程资源一样,它们在明智地使用时很有用。事实上,例如,Windows API 和 .NET 框架广泛使用回调。
不了解 C++,但在 .NET 世界中,同步对象和事件是备选方案。
【讨论】:
如果我们在 C++ 上下文中,请考虑使用(例如generalized callbacks)。
背后的基本思想是回调(类方法)可以是任何名称,并且回调不需要派生自回调执行器已知的某个类。
回调的唯一限制是输入参数和返回值。所以这将夫妇减少到零...... :)
UDP:
@EffoStaff Effo 的答案是一个示例,其中回调应属于特定类(派生)并具有固定名称。在泛化回调的上下文中,所有这些“限制”都不存在。
【讨论】:
第一季度。如果您使用分层方法,其中较高级别调用较低级别并通过回调从较低级别获取反馈,则需要回调。
Q2。当采取一些预防措施时,它们并不比例如更糟。例外。在某些方面,它们是相似的。
Q3。更多的耦合:低爱知道高。
备注:
- 简单的方法(每个回调 1 个回调处理程序):通过接口注册 CallbackHandler 对象
- 使用信号(QT、boost、...),并确保每个回调使用唯一的信号以增强可追溯性
编辑:示例:
用户调用ProtocolHandler发送消息,ProtocolHandler调用用户发送回复:相互依赖。
分层:用户级别较高,ProtocolHandler 级别较低。在启动时,它为回复注册一个回调并调用 ProtocolHandler 发送消息。 ProtocolHandler 使用回调发送回复:只有用户依赖于 ProtocolHandler。
【讨论】:
在使用 Win32 风格回调的罕见情况下,请注意“模糊的可重入”问题(如果回调只是意味着将函数作为 arg 传递给无法想象并发问题的另一个函数,则以下内容无关紧要)。
Joe Duffy 的“Windows 上的并发编程”的出色索引列出了“可重入”下的 5 个主题,这些主题通常是“在同一线程上围绕 Robin-Hood 的谷仓递归”概念的专业化——换句话说,来自最受好评的-回答“被调用方传递了一些指针,它不知道它背后是什么。”,“不知道”有时可以引导-Robin-Hood's-barn。
我刚才所说的没有什么是特定于回调的,但是如果被调用方偶然发现了 Duffy 的场景之一,那么“晦涩的重入”可能会发生。换句话说,隐含在“回调”概念中的是,您似乎将在同一个线程中被回调,这是如何发生的,它是如何同步的。
如果您在 Duffy 的书名上使用 Google 搜索并将 Callback 这个词添加到您的搜索中,那么您将获得超过 10 页的点击量。
【讨论】: