我的问题:RAII 在多大程度上替代了其他设计模式
像垃圾收集?我假设手动内存管理
不用于表示系统中的共享所有权。
我不确定是否称其为设计模式,但在我同样强烈的意见中,仅谈论内存资源,RAII 几乎解决了 GC 可以解决的所有问题,同时引入的更少。
对象的共享所有权是糟糕设计的标志吗?
我认为共享所有权在大多数情况下远非理想,因为高级设计不一定需要它。唯一一次我发现它不可避免是在持久数据结构的实现过程中,它至少被内化为实现细节。
我发现 GC 或共享所有权的最大问题是,它不会让开发人员在应用程序资源方面免除任何责任,但会让人产生这样做的错觉。如果我们有这样的情况(Scene 是资源的唯一逻辑所有者,但其他事物持有指向它的引用/指针,例如存储用户定义的场景排除列表以从渲染中省略的相机):
假设应用程序资源就像一个图像,它的生命周期与用户输入相关联(例如:当用户请求关闭包含它的文档时应该释放图像),然后正确释放资源的工作无论有没有 GC,都是一样的。
如果没有 GC,我们可能会将其从场景列表中删除并允许调用其析构函数,同时触发事件以允许 Thing1、Thing2 和 Thing3 将指向它的指针设置为 null 或删除将它们从列表中提取出来,这样它们就没有悬空指针。
使用 GC,基本上是一样的。我们在触发事件时从场景列表中删除资源,以允许 Thing1、Thing2 和 Thing3 将它们的引用设置为 null 或从列表中删除它们,以便垃圾收集器可以收集它。
雷达下的无声程序员错误
这种情况的不同之处在于发生程序员错误时会发生什么,例如 Thing2 未能处理删除事件。如果Thing2 存储了一个指针,它现在有一个悬空指针,我们可能会崩溃。这是灾难性的,但我们可能很容易在单元和集成测试中捕捉到一些东西,或者至少 QA 或测试人员会很快捕捉到一些东西。我不在任务关键型或安全关键型环境中工作,所以如果崩溃的代码设法以某种方式发布,如果我们能够获得错误报告、重现它、检测它并相当快地修复它,那仍然不是那么糟糕.
如果Thing2 存储了一个强引用并共享所有权,我们就有一个非常静默的逻辑泄漏,并且直到Thing2 被销毁(它可能在关闭之前它可能不会被销毁)才会被释放。在我的领域中,错误的这种沉默性质是非常有问题的,因为即使在交付之后它也可能被悄悄地忽视,直到用户开始注意到在应用程序中工作一个小时会导致它占用千兆字节的内存,例如,并开始减速直到他们重新启动它。到那时,我们可能已经积累了大量这些问题,因为它们很容易像隐形战斗机一样在雷达下飞行,而我最不喜欢的就是隐形战斗机。
正是由于这种沉默的本性,我倾向于不喜欢热情地共享所有权,而 TBH 我一直不明白为什么 GC 如此受欢迎(可能是我的特殊领域——我承认我非常不了解那些有使命感的领域) -critical,例如)到我渴望新语言的地步没有 GC。我发现调查与共享所有权相关的所有此类泄漏非常耗时,有时调查数小时才发现泄漏是由我们无法控制的源代码(第三方插件)引起的。
弱引用
对于Thing1、Thing2 和Thing3,弱引用在概念上对我来说是理想的。这将允许他们事后检测资源何时被破坏,而无需延长其生命周期,也许我们可以保证在这些情况下发生崩溃,或者有些人事后甚至可以优雅地处理这个问题。对我来说,问题是弱引用可以转换为强引用,反之亦然,因此在内部和第三方开发人员中,即使弱引用本来可以,仍然可能有人不小心将强引用存储在Thing2更合适。
我过去曾尝试鼓励在内部团队中尽可能多地使用弱引用,并记录应该在 SDK 中使用它。不幸的是,很难在如此广泛和混杂的人群中推广这种做法,而且我们仍然存在逻辑漏洞。
在任何给定时间,任何人都可以轻松地通过在对象中存储对它的强引用来延长对象的生命周期,这在俯视庞大的代码库时开始变得非常可怕这会泄漏大量资源。我经常希望需要一种非常明确的语法来将任何类型的强引用存储为某种对象的成员,这至少会导致开发人员三思而后行。
显式破坏
所以我倾向于显式销毁持久性应用程序资源,如下所示:
on_removal_event:
// This is ideal to me, not trying to release a bunch of strong
// references and hoping things get implicitly destroyed.
destroy(app_resource);
...因为我们可以依靠它来释放资源。我们不能完全确定系统中的某些东西最终不会出现悬空指针或弱引用,但至少这些问题在测试中往往很容易检测和重现。它们不会被忽视并积累多年。
对我来说,一个棘手的案例一直是多线程。在这些情况下,我发现有用而不是全面的垃圾收集,或者说,shared_ptr,就是以某种方式简单地推迟销毁:
on_removal_event:
// *May* be deferred until threads are finished processing the resource.
destroy(app_resource);
在某些系统中,持久线程以某种方式统一,使得它们具有processing 事件,例如,我们可以在未处理线程的时间片中以延迟方式标记要销毁的资源(几乎开始感觉像是停止世界的 GC,但我们保持显式销毁)。在其他情况下,我们可能会使用引用计数,但要避免shared_ptr,其中资源的引用计数从零开始,并将使用上面的显式语法销毁,除非线程通过增加计数器在本地延长其生命周期临时(例如:在本地线程函数中使用作用域资源)。
虽然看起来很迂回,但它避免了将 GC 引用或 shared_ptr 暴露给外部世界,这很容易诱使一些开发人员(在您的团队内部或第三方开发人员内部)存储强引用(shared_ptr,例如)作为Thing2 之类的对象的成员,从而无意中延长了资源的生命周期,并且可能远远超过适当的时间(可能一直到应用程序关闭)。
RAII
同时,RAII 与 GC 一样自动消除物理泄漏,但此外,它还适用于内存以外的资源。我们可以将它用于一个作用域互斥体,一个在销毁时自动关闭的文件,我们甚至可以使用它来通过作用域保护等自动逆转外部副作用。
因此,如果有选择,我必须选择一个,这对我来说很容易 RAII。在我工作的领域中,由共享所有权引起的无声内存泄漏绝对是致命的,如果(并且很可能)在测试期间尽早发现,悬挂指针崩溃实际上是更可取的。即使在一些非常模糊的事件中,如果它在发生错误的站点附近出现崩溃,这仍然比使用内存分析工具并试图找出谁在涉水数百万时忘记释放引用更可取的代码行。坦率地说,GC 为我的特定领域(VFX,在场景组织和应用程序状态方面有点类似于游戏)引入的问题比它解决的问题要多,除了那些非常无声的共享所有权泄漏之外,原因之一是因为它会给开发人员一种错误的印象,即他们不必考虑资源管理和持久应用程序资源的所有权,而无意中导致左右逻辑泄漏。
“RAII 何时失败”
我在整个职业生涯中遇到的唯一一个我想不出任何可能的方法来避免某种共享所有权的情况是,当我实现了一个持久数据结构库时,如下所示:
我用它来实现一个不可变的网格数据结构,它可以修改部分而不是唯一的,就像这样(用 400 万个四边形进行测试):
每一帧,当用户拖动并雕刻它时,都会创建一个新的网格。不同之处在于,新网格是强引用部分,不是由画笔制作的唯一部分,因此我们不必复制所有顶点、所有多边形、所有边等。不可变版本使线程安全、异常安全、无损编辑、撤消系统、实例化等。
在这种情况下,不可变数据结构的整个概念都围绕共享所有权展开,以避免复制并非唯一的数据。这是一个真实的案例,无论如何我们都无法避免共享所有权(至少我想不出任何可能的方式)。
这是我遇到的唯一可能需要 GC 或引用计数的情况。其他人可能也遇到过一些他们自己的情况,但根据我的经验,很少有案例真正需要在设计级别共享所有权。