为什么使用“using”语句可以提高C#性能?

13

在大多数情况下,C#编译器可以自动调用Dispose()。就像using模式的大多数情况一样:

public void SomeMethod()
{
    ...

    using (var foo = new Foo())
    {
        ...
    }

    // Foo isn't use after here (obviously).
    ...
}

由于foo没有被使用(这是一种非常简单的检测),并且因为它没有作为另一个方法的参数提供(这适用于许多用例并可以扩展),编译器可以自动并立即调用Dispose()而不需要开发者进行操作。

这意味着在大多数情况下,如果编译器做了一些聪明的工作,using就变得非常无用了。IDisposable对我来说似乎足够低级,可以被编译器考虑。

现在为什么不这样做呢?如果开发者很,那么这是否会提高性能呢?


9
并不是那么简单。 - Amy B
我刚开始考虑一个简单的情况,实际上这是非常普遍的。 - Wernight
1
你不能只为简单情况设计。很少有情况是如此明确的。 - Adam Robinson
如果无论如何它仍然这样做,它如何提高性能?此外,它会改变事情的执行顺序(在MVC BeginForm可能会出现问题)。 - Rangoric
11个回答

21

有几点需要注意:

调用Dispose方法并不会提高性能。 IDisposable是为使用受限制或无法由运行时管理的资源的场景而设计的。

没有明确和显然的机制可以告诉编译器如何处理代码中的IDisposable对象。什么情况下会自动销毁它,什么情况下不会?如果该实例在方法之外被暴露出来,是否应自动销毁?仅仅因为我将一个对象传递给另一个函数或类,并不意味着我希望它在方法作用域之外仍可用。

例如,考虑一个工厂模式,它需要一个Stream用于反序列化一个类的实例。

public class Foo
{
    public static Foo FromStream(System.IO.Stream stream) { ... }
}

而我将其称为:

Stream stream = new FileStream(path);

Foo foo = Foo.FromStream(stream);
现在,当函数退出时,我可能希望或者不希望Stream被处理掉。如果Foo的工厂从Stream中读取了所有必要的数据,并且不再需要它,那么我希望它被处理掉。如果Foo对象必须持有流并在其生命周期内使用它,则不希望将其处理掉。
同样,对于从构造函数之外获取的实例(如Control.CreateGraphics()),这些实例可以存在于代码之外,因此编译器不会自动处理它们。
给用户控制(并提供像using块这样的习惯用语)可以使用户的意图清晰明确,并且更容易发现未正确处理IDisposable实例的地方。如果编译器自动释放某些实例,则调试会更加困难,因为开发人员必须解密自动释放规则如何适用于每个使用IDisposable对象的代码块。
最终,按照约定,有两个原因要在类型上实现IDisposable
  1. 您正在使用非托管资源(意味着您正在进行返回类似句柄的 P/Invoke 调用,该句柄必须通过另一个 P/Invoke 调用释放)
  2. 该类型具有应在此对象的生命周期结束时处理的IDisposable实例
在第一种情况下,所有这些类型都应该实现一个终结器来调用Dispose并释放所有非托管资源(这是为了防止内存和句柄泄漏)。

1
我认为这个答案比当前得分最高的更清晰,因为它解释了IDisposable的用途。 - Tamás Szelei

16

垃圾回收(虽然与IDisposable无直接关系,但它可以清理未使用的对象)并不简单。

让我稍微改一下措辞。自动调用Dispose()并不简单。它也不会直接提高性能。稍后会详细介绍。

如果你有以下代码:

public void DoSomeWork(SqlCommand command)
{
    SqlConnection conn = new SqlConnection(connString);

    conn.Open();

    command.Connection = conn;

    // Rest of the work here
 }

编译器怎么知道您何时使用完conn对象?或者如果您将引用传递给某个持有它的其他方法呢?

显式调用Dispose()或者使用using块明确表示您的意图,并强制执行正确的清理工作。

现在回到性能问题。仅仅在一个对象上调用Dispose()并不能保证任何性能提升。当您使用不受管理的资源时,Dispose()方法用于“清理”对象。

性能提升可以出现在使用不受管理的资源时。如果托管对象没有正确处理其不受管理的资源,则会出现内存泄漏。非常丑陋。

让编译器决定何时调用Dispose()会带走这种清晰度水平,并使由不受管理的资源引起的调试内存泄漏变得更加困难。


1
你的例子并没有真正解释清楚事情 - 没有在方法中的上下文,编译器很明显知道一旦方法完成,conn对象就不再需要了。 - David_001
@David_001 - 我刚才也在考虑这个问题。我将回到之前的例子。 - Justin Niessner
12
"垃圾收集并不那么简单。" -- 值得注意的是,IDisposable和垃圾收集器没有任何关系。有一个常见误解认为调用Dispose()会触发GC,但实际上,Dispose()只是另一个普通的方法,没有任何特别之处。 - Matt Greer
1
Dispose 方法存在的原因是为了可靠地处理一些非托管资源。垃圾回收器不是确定性的 - 它可以在任何时候运行,或者根本不运行。如果非托管资源一直保留到它们被垃圾回收器处理,它们可能会泄漏并引起问题。这就是为什么 Dispose 存在的原因,所以您需要能够显式地调用它。using 可以帮助解决这个问题。 - thecoop
@Justin:好的,这很公平。看起来 OP 更感兴趣的是编译器注入调用 Dispose(),而不是 GC 在运行时找出它。当然,GC 绝对无法找出这个(当然编译器也不能)。 - Matt Greer
显示剩余3条评论

3

请查看C# Using语句的MSDN文章using语句只是一个快捷方式,可以避免在许多地方进行try和finally操作。调用dispose不像垃圾回收那样是低级功能。

正如您所看到的,using被翻译为。

{
  Font font1 = new Font("Arial", 10.0f);
  try
  {
    byte charset = font1.GdiCharSet;
  }
  finally
  {
    if (font1 != null)
      ((IDisposable)font1).Dispose();
  }
}

编译器如何知道应该将finally块放在哪里? 它在垃圾收集上调用吗?

垃圾收集不会在你离开方法后立即发生。阅读 文章 以更好地理解垃圾收集。只有当没有对对象的引用时才会发生。资源可能会被占用比需要的时间长得多。

我的想法是,编译器不应保护不清理资源的开发人员。仅因为一种语言是受管理的并不意味着它将保护你不受自己的伤害。


3
你正在要求编译器对你的代码进行语义分析。事实上,源代码中某个时间点之后没有被显式引用的内容并不意味着它没有被使用。如果我创建了一系列引用并将其中一个传递给一个方法,这个方法可能会将该引用存储在属性或其他持久容器中,那么我真的应该期望编译器跟踪所有这些内容并弄清楚我的真正意图吗?
易变实体也可能是一个问题。
此外,“using() {....}”更具可读性和直观性,在可维护性方面非常有价值。
作为工程师或程序员,我们追求高效,但这很少与懒惰相同。

2

C++支持这种语法,称之为“引用类型的堆栈语义”。我支持将其添加到C#中,但需要使用不同的语法(基于是否将局部变量传递给另一个方法来改变语义并不是一个好主意)。


在我看来,如果C#和VB都有一种标记字段的方法来指示它应该被视为可处理的,并且会生成一个IDisposable实现来处理它们(将代码添加到IDisposable中的用户代码之前,除非属性指定将其分隔成自己的方法),那么它们都将受益匪浅。编译器支持还将简化VB.net事件解绑和在字段初始化程序中创建的IDisposable的清理(在vb.net下很困难,在C#下则不可能)。 - supercat

2
我认为你在考虑终结器。在C#中,终结器使用析构函数语法,并由垃圾回收自动调用。只有在清理非托管资源时才适合使用终结器。
Dispose旨在允许尽早清理非托管资源(也可用于清理托管资源)。
检测实际上比看起来更棘手。如果你有像这样的代码:
var mydisposable = new... AMethod(mydisposable); // (not used again)
可能有一些代码在AMethod中持有对myDisposable的引用。
- 可能在该方法内部将其分配给实例变量 - 也可能myDisposable订阅了AMethod中的事件(那么事件发布者持有对myDisposable的引用) - 也可能另一个线程被AMethod生成 - 也可能mydisposable被AMethod内的匿名方法或lambda表达式“封闭”。
所有这些都使得很难确切地知道您的对象是否不再使用,因此Dispose存在是为了让开发人员说“好的,我知道现在运行我的清理代码是安全的”。请记住,dispose不会释放您的对象 - 只有GC可以做到这一点。(GC确实有魔力来理解我描述的所有情况,并且它知道何时清理您的对象。如果您真的需要在GC检测到没有引用时运行代码,则可以使用finalizer)。但是要小心使用finalizer - 它们仅适用于您的类拥有的非托管分配。如果您需要非托管句柄清理,请阅读有关SafeHandles的内容。您可以在这里阅读更多相关信息:http://msdn.microsoft.com/en-us/magazine/bb985010.aspxhttp://www.bluebytesoftware.com/blog/2005/04/08/DGUpdateDisposeFinalizationAndResourceManagement.aspx

我删除了之前的评论,因为我误读了你的话。 - Adam Robinson

1

编译器不负责解释您的应用程序中的作用域以及执行类似于确定何时不再需要内存之类的操作。实际上,我非常确定这是一个无法解决的问题,因为无论编译器多聪明,它都无法知道在运行时您的程序会是什么样子。

这就是为什么我们有垃圾回收机制。垃圾回收机制的问题在于它在不确定的时间间隔内运行,并且通常如果一个对象实现了IDisposable,则原因是因为您希望立即处理它。比如说,就在现在立刻处理。数据库连接等结构不仅可被处理是因为它们在被处理时有某些特殊工作要做,而且还因为它们很稀缺。


0

我认为OP的意思是:“为什么要使用‘using’,当编译器应该能够很容易地自动解决它。”

我认为OP的意思是:

public void SomeMethod() 
{ 
    ... 

    var foo = new Foo();

    ... do stuff with Foo ... 


    // Foo isn't use after here (obviously). 
    ... 
} 

应该等同于

public void SomeMethod() 
{ 
    ... 

    using (var foo = new Foo())
    {
    ... do stuff with Foo ... 
    }

    // Foo isn't use after here (obviously). 
    ... 
} 

因为Foo没有再次使用。

当然,答案是编译器不能很容易地解决它。垃圾回收(在.NET中神奇地调用“Dispose()”)是一个非常复杂的领域。仅仅因为符号在下面没有被使用并不意味着变量没有被使用。

以这个例子为例:

public void SomeMethod() 
{ 
    ... 

    var foo = new Foo();
    foo.DoStuffWith(someRandomObject);
    someOtherClass.Method(foo);

    // Foo isn't use after here (obviously).
    // Or is it?? 
    ... 
} 

在这个例子中,someRandomObject和someOtherClass可能都引用了Foo指向的内容,所以如果我们调用Foo.Dispose(),它们就会出问题。你说你只是想象简单的情况,但唯一能让你提出的“简单情况”是当你从Foo中不进行任何方法调用并且不将Foo或其成员传递给其他任何东西时 - 实际上,你根本不使用Foo,在这种情况下,你可能没有必要声明它。即使如此,你也永远无法确定某种反射或事件黑客是否通过其创建来引用了Foo,或者Foo在其构造函数期间是否与其他东西连接起来。

2
GC不会调用Dispose。GC不会调用Dispose。GC不会调用Dispose。现在大家都明白了,**GC不会调用Dispose**。垃圾回收器仅调用终结器;如果您需要在对象被回收时调用Dispose,而该对象没有被正确处理(这仅在您自己使用非托管资源时才为真,而不是仅消耗其他IDisposable对象),则必须自己使终结器调用Dispose(false),这也意味着您需要一个Dispose(bool disposing),其中Dispose调用Dispose(true) - Adam Robinson
你每天都在学习新东西。我发誓微软认证考试让我回答“Dipose是由GC调用的”。我猜你的意思是,如果我使用其他IDisposable对象来管理资源,它们应该在极少数情况下自行处理资源关闭,以防某人没有正确地释放资源? - Matt Mitchell
C# 不会调用 Dispose 方法,请参见 https://dev59.com/TnI-5IYBdhLWcg3wwLW3#1691850。 - Wernight
是的,就像@Adam Robinson已经说过的那样。我已经更新了我的释放知识。似乎每个使用dispose都应该包括一个finalizer,这是微软文档所不推荐的,因为最终如果应用程序崩溃等,您不希望资源句柄保持打开状态,这也是您首先编写dispose的原因。 - Matt Mitchell
@Graphain:不是每个实现都应该。实现“IDisposable”的两个(传统)原因:要么您直接使用非托管资源(即调用P/Invoke并获得某种句柄,必须通过另一个P/Invoke调用显式释放),要么您正在使用其他“IDisposable”对象。仅当您直接使用资源时,才应使用终结器。事实上,在终结器中处理(甚至访问)托管资源是明确禁止的。 - Adam Robinson

0

对于垃圾回收器来说,很难知道您在同一方法中不会再使用此变量。显然,如果您离开该方法并且不保留对变量的进一步引用,则垃圾回收器将处理它。但是,在您的示例中使用using告诉垃圾回收器,您确定之后不再使用此变量。


0

using语句与性能无关(除非您认为避免资源/内存泄漏是性能问题)。

它为您提供的保证是,当对象超出范围时,即使在using块内发生异常,也会调用IDisposable.Dispose方法。

然后,Dispose()方法负责释放对象使用的任何资源。这些通常是未经管理的资源,例如文件、字体、图像等,但也可能是对托管对象进行的简单“清理”活动(不包括垃圾回收)。

当然,如果Dispose()方法实现不好,则using语句提供零效益。


是的,我确实考虑过。内存性能,有时候还会出现锁定问题(当文件或数据库被打开时)。 - Wernight

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