我经常在C ++中使用的东西是让类A
通过B
构造函数和析构函数处理另一个类A
的状态进入和退出条件,以确保如果该范围内的某些东西引发了异常,则当范围退出时B将具有已知状态。就首字母缩略词而言,这不是纯粹的RAII,但它仍然是一种既定的模式。
在C#中,我经常想做
class FrobbleManager
{
...
private void FiddleTheFrobble()
{
this.Frobble.Unlock();
Foo(); // Can throw
this.Frobble.Fiddle(); // Can throw
Bar(); // Can throw
this.Frobble.Lock();
}
}
需要像这样做
private void FiddleTheFrobble()
{
this.Frobble.Unlock();
try
{
Foo(); // Can throw
this.Frobble.Fiddle(); // Can throw
Bar(); // Can throw
}
finally
{
this.Frobble.Lock();
}
}
如果我想在Frobble
返回时保证FiddleTheFrobble
状态。
private void FiddleTheFrobble()
{
using (var janitor = new FrobbleJanitor(this.Frobble))
{
Foo(); // Can throw
this.Frobble.Fiddle(); // Can throw
Bar(); // Can throw
}
}
其中FrobbleJanitor
看起来大致,如
class FrobbleJanitor : IDisposable
{
private Frobble frobble;
public FrobbleJanitor(Frobble frobble)
{
this.frobble = frobble;
this.frobble.Unlock();
}
public void Dispose()
{
this.frobble.Lock();
}
}
这就是我想要的方式。现在赶上现实,因为我想要使用需要 FrobbleJanitor
与 using
一起使用。我可以认为这是一个代码审查问题,但有些事情在唠叨我。
问题:以上是否会被视为滥用using
和IDisposable
?
答案 0 :(得分:68)
我认为这是滥用使用声明。我知道我在这个职位上占少数。
我认为这是一种滥用行为有三个原因。
首先,因为我希望“使用”用于使用资源并且在您完成它时处理它。更改程序状态不是使用资源并且更改它不是处理任何东西。因此,“使用”变异和恢复状态是一种滥用;这段代码误导了随意的读者。
其次,因为我希望“使用”出于礼貌使用,而不是必需。你完成它后使用“使用”处理文件的原因不是因为必要这样做,而是因为它是礼貌的 - 其他人可能正在等待使用该文件,所以说“现在完成”是道德上正确的事情。我希望我能够重构“使用”,以便将使用过的资源保留更长时间,并在以后处理,并且这样做的唯一影响是稍微给其他进程带来不便 。对程序状态具有语义影响的“使用”块是滥用的,因为它隐藏了一个重要的,需要的程序状态突变,它看起来像是为了方便和礼貌,而不是必需。< / p>
第三,你的计划的行动是由其国家决定的;谨慎操纵国家的必要性正是我们首先进行这种对话的原因。让我们考虑一下如何分析原始程序。 如果你把它带到我办公室的代码审查中,我要问的第一个问题是“如果抛出异常,锁定欺骗是否真的正确?”从你的程序中可以明显地看出,无论发生什么事情,这件事都会积极地重新锁定。 是吗?抛出异常。该程序处于未知状态。我们不知道Foo,Fiddle或Bar是否扔了,他们为什么扔了,或者他们对其他没有清理的状态发生了什么突变。你可以说服我在那种可怕的情况下总是重新锁定正确的事情吗? 也许是,也许不是。我的观点是,使用最初编写的代码,代码审阅者知道提出问题。使用“使用”的代码,我不知道问这个问题;我假设“using”块分配一个资源,稍微使用它,并在完成后礼貌地处理它,而不是“using”块的右括号变异当违反任意程序状态一致性条件时,我的程序处于异常状态。 使用“using”块来产生语义效果会使该程序碎片化为: 非常有意义。当我看到那个单一的紧密支撑时,我不会立即认为“该支具具有副作用,这对我的程序的全局状态具有深远的影响”。但是,当你像这样滥用“使用”时,它会突然发生。 如果我看到你的原始代码,我会问的第二件事是“如果在解锁之后但在输入尝试之前抛出异常会发生什么?”如果您正在运行非优化程序集,则编译器可能在尝试之前插入了无操作指令,并且可能会在无操作上发生线程中止异常。这种情况很少见,但它确实发生在现实生活中,特别是在Web服务器上。在这种情况下,解锁发生但锁永远不会发生,因为在尝试之前抛出了异常。完全有可能这个代码容易受到这个问题的影响,而且应该实际编写 再一次,也许它确实如此,也许它没有,但我知道问这个问题。对于“使用”版本,它容易出现同样的问题:在Frobble被锁定之后但在输入与使用相关联的尝试保护区域之前,可能抛出线程中止异常。但是使用“使用”版本,我认为这是“那么什么?”情况。不幸的是,如果发生这种情况,但我认为“使用”只是礼貌,而不是改变极其重要的程序状态。我假设如果在错误的时间发生了一些可怕的线程中止异常,那么,好吧,垃圾收集器最终将通过运行终结器来清理该资源。 }
bool needsLock = false;
try
{
// must be carefully written so that needsLock is set
// if and only if the unlock happened:
this.Frobble.AtomicUnlock(ref needsLock);
blah blah blah
}
finally
{
if (needsLock) this.Frobble.Lock();
}
答案 1 :(得分:31)
我不这么认为,必然。 IDisisableable技术上 意味着用于具有非托管资源的东西,但是using指令只是实现try .. finally { dispose }
的公共模式的一种巧妙方式。
你可能应该在IDisposable上添加另一个接口以将其推得更远一些,向其他开发人员解释为什么该接口意味着IDisposable。
还有很多其他的选择,但最终,我想不出任何会像这样整洁,所以去吧!
答案 2 :(得分:26)
如果你只想要一些干净的范围代码,你也可以使用lambdas,ála
myFribble.SafeExecute(() =>
{
myFribble.DangerDanger();
myFribble.LiveOnTheEdge();
});
.SafeExecute(Action fribbleAction)
方法包含try
- catch
- finally
块。
答案 3 :(得分:24)
参与C#语言设计团队的Eric Gunnerson对this answer提出了几乎相同的问题:
道格问道:re:带超时的锁定语句......
在使用多种方法处理常见模式之前,我已经完成了这个技巧。通常锁定获取,但有其他一些。问题是它总是感觉像是一个黑客,因为对象实际上不是一次性的,而是“回调最后一个范围的”。
道格
当我们决定[sic] using语句时,我们决定将其命名为“using”,而不是更具体地处理对象,以便它可以用于这种情况。
答案 4 :(得分:11)
这是一个滑坡。 IDisposable有一份合同,一份由终结者支持的合同。在你的情况下,终结器是没用的。您不能强制客户端使用using语句,只能鼓励他这样做。您可以使用以下方法强制它:
void UseMeUnlocked(Action callback) {
Unlock();
try {
callback();
}
finally {
Lock();
}
}
但如果没有lamdas,这往往会有点尴尬。也就是说,我像你一样使用了IDisposable。
然而,你的帖子中有一个细节,使得它非常接近反模式。您提到这些方法可以抛出异常。这不是呼叫者可以忽略的。他可以做三件事:
后两者要求调用者显式写一个try块。现在,using语句会受到妨碍。它可能会诱使客户陷入昏迷状态,使他相信你的班级正在照顾国家,而且不需要做额外的工作。这几乎从来都不准确。
答案 5 :(得分:6)
真实世界的例子是ASP.net MVC的BeginForm。基本上你可以写:
Html.BeginForm(...);
Html.TextBox(...);
Html.EndForm();
或
using(Html.BeginForm(...)){
Html.TextBox(...);
}
Html.EndForm调用Dispose,Dispose只输出</form>
标记。关于这一点的好处是{}括号创建了一个可见的“范围”,这使得更容易看到表单中的内容和不包含的内容。
我不会过度使用它,但基本上IDisposable只是说“当你完成它时你必须调用这个函数”。 MvcForm使用它来确保窗体关闭,Stream使用它来确保流关闭,你可以使用它来确保对象被解锁。
就个人而言,如果以下两条规则属实,我只会使用它,但我是由我设定的:
最后,这完全取决于期望。如果一个对象实现了IDisposable,我认为它需要做一些清理,所以我称之为。我认为它通常胜过“关机”功能。
话虽如此,我不喜欢这句话:
this.Frobble.Fiddle();
由于FrobbleJanitor现在“拥有”Frobble,我想知道在看门人的Frobble上打电话给Fiddle是不是更好?
答案 6 :(得分:4)
我们在代码库中充分利用了这种模式,之前我已经看过它 - 我相信它一定也在这里讨论过。一般来说,我没有看到这样做有什么问题,它提供了一个有用的模式,并没有造成真正的伤害。
答案 7 :(得分:4)
明白这一点:我同意大多数人认为这是脆弱的,但很有用。我想指出System.Transaction.TransactionScope课程,它会做你想做的事情。
一般来说,我喜欢这种语法,它会从真正的肉中消除很多混乱。请考虑给助手类一个好名字 - 也许......范围,就像上面的例子。该名称应该表明它封装了一段代码。 *范围,*块或类似的东西应该这样做。
答案 8 :(得分:4)
注意:我的观点可能偏离了我的C ++背景,所以应该根据可能的偏见评估我的答案的价值......
8.13使用声明
[...]
资源是实现System.IDisposable的类或结构,它包含一个名为Dispose的无参数方法。使用资源的代码可以调用Dispose来指示不再需要该资源。如果未调用Dispose,则最终会因垃圾收集而自动处理。
使用资源的代码当然是以using
关键字开头的代码,直到范围附加到using
。
所以我猜这是好的,因为Lock是一种资源。
可能关键字using
选择错误。也许它应该被称为scoped
。
然后,我们几乎可以将任何事物视为资源。文件句柄。网络连接......一个线程?
线程???
using
关键字?是否闪亮到(ab)使用using
关键字来确保线程的工作在退出范围之前结束了吗?
Herb Sutter似乎认为它是有光泽的,因为他提供了一个有趣的IDispose模式用于等待线程的工作结束:
http://www.drdobbs.com/go-parallel/article/showArticle.jhtml?articleID=225700095
以下是从文章中复制粘贴的代码:
// C# example
using( Active a = new Active() ) { // creates private thread
…
a.SomeWork(); // enqueues work
…
a.MoreWork(); // enqueues work
…
} // waits for work to complete and joins with private thread
虽然未提供Active对象的C#代码,但编写的是C#使用IDispose模式作为析构函数中包含C ++版本的代码。通过查看C ++版本,我们看到一个析构函数在退出之前等待内部线程结束,如本文的其他摘录所示:
~Active() {
// etc.
thd->join();
}
因此,就Herb而言,它是闪亮的。
答案 9 :(得分:3)
我相信你的问题的答案是否定的,这不会是对IDisposable
的滥用。
我理解IDisposable
接口的方式是,一旦处理完对象,你不应该使用它(除了你可以像你一样经常调用它的Dispose
方法要)。
由于每次进入FrobbleJanitor
语句时都会显式创建 new using
,因此您永远不会使用相同的FrobbeJanitor
对象两次。由于它的目的是管理另一个对象,Dispose
似乎适合释放这个(“托管”)资源的任务。
(顺便说一下,演示Dispose
正确实现的标准示例代码几乎总是表明应该释放托管资源,而不仅仅是文件系统句柄等非托管资源。)
我个人唯一担心的是,using (var janitor = new FrobbleJanitor())
发生的事情与try..finally
和Lock
&amp; Unlock
操作直接可见。但采取哪种方法可能归结为个人偏好。
答案 10 :(得分:1)
它不是滥用。您正在使用它们创建的内容。但是你可能需要根据你的需要相互考虑。例如,如果您选择“艺术性”,那么您可以使用“使用”,但如果您的代码被执行了很多次,那么出于性能原因,您可以使用'try'...'finally'构造。因为“使用”通常涉及对象的创作。
答案 11 :(得分:1)
我认为你做得对。重载Dispose()将是一个问题,同一个类后来实际上必须进行清理,并且该清理的生命周期改变为与您希望持有锁的时间不同。但是既然你创建了一个单独的类(FrobbleJanitor),它只负责锁定和解锁Frobble,那么事情就会解耦,你就不会遇到这个问题。
我会重命名FrobbleJanitor,可能会像FrobbleLockSession那样。