"using"语句与"try finally"的区别

90

我有一些属性需要使用读/写锁。我可以使用 try finallyusing 语句来实现。

使用 try finally:在 try 前获取锁,在 finally 中释放锁。

使用 using 语句:创建一个类,构造函数中获取锁,Dispose 方法中释放锁。

我在很多地方都使用读/写锁,所以一直在寻找比 try finally 更简洁的方法。我希望听听大家对于其中一种方法不被推荐的原因,或者为什么一种方法可能比另一种更好的想法。

方法1(try finally):

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        rwlMyLock_m .AcquireReaderLock(0);
        try
        {
            return dtMyDateTime_m
        }
        finally
        {
            rwlMyLock_m .ReleaseReaderLock();
        }
    }
    set
    {
        rwlMyLock_m .AcquireWriterLock(0);
        try
        {
            dtMyDateTime_m = value;
        }
        finally
        {
            rwlMyLock_m .ReleaseWriterLock();
        }
    }
}

方法二:

static ReaderWriterLock rwlMyLock_m  = new ReaderWriterLock();
private DateTime dtMyDateTime_m
public DateTime MyDateTime
{
    get
    {
        using (new ReadLock(rwlMyLock_m))
        {
            return dtMyDateTime_m;
        }
    }
    set
    {
        using (new WriteLock(rwlMyLock_m))
        {
            dtMyDateTime_m = value;
        }
    }
}

public class ReadLock : IDisposable
{
    private ReaderWriterLock rwl;
    public ReadLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireReaderLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseReaderLock();
    }
}

public class WriteLock : IDisposable
{
    private ReaderWriterLock rwl;
    public WriteLock(ReaderWriterLock rwl)
    {
        this.rwl = rwl;
        rwl.AcquireWriterLock(0);
    }

    public void Dispose()
    {
        rwl.ReleaseWriterLock();
    }
}

1
就像许多答案中已经说明的那样,方法2非常好,但为了避免每次使用锁时在堆上产生垃圾,您应该将ReadLock和WriteLock更改为结构体。即使using语句使用结构体的IDisposable接口,C#也足够聪明,可以避免装箱! - Michael Gehling
15个回答

104

从MSDN上看,using语句(C#参考)

使用 using 语句可以确保在调用对象方法时发生异常时仍会调用 Dispose 方法。您可以通过将对象放入 try 块中并在 finally 块中调用 Dispose 来实现相同的结果;实际上,这就是编译器如何将 using 语句转换的方式。编译时代码示例会扩展为以下代码(请注意额外的大括号以创建对象的有限范围):

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

基本上,它是相同的代码,但带有良好的自动 null 检查和变量的额外作用域。文档还指出它“确保正确使用 IDisposable 对象”,因此您可能会在将来获得更好的框架支持,处理任何晦涩的情况。

所以选择选项 2。

将变量放在一个立即在不再需要时结束的作用域中也是一个优点。


如何释放一个不能在using语句中实例化、不能被重复使用或作为out参数传递的资源?使用try/catch! - bjan
2
@bjan,嗯,你为什么要在那种情况下考虑使用using呢?这不是using的用途。 - chakrit
1
这就是为什么我也提到了 try/catch,因为它似乎是唯一可以通过 try/catch/finally 块处理的方式。希望 using 也能处理这个问题。 - bjan
嗯,是的,在那种情况下,我想try/finally是你唯一的选择。但在我看来,我认为在这种情况下应该始终有一些对象/代码负责维护对象的生命周期(在这种情况下应该始终调用Dispose())。如果一个类只处理实例化,而其他人必须记得释放它,我认为这有点不太好。不确定如何在语言层面上添加它。 - chakrit
我认为避免使用“using”的唯一原因是要完全避免可丢弃对象,因为这又是一个对象实例化,我猜是这个原因吧? - Demetris Leptos
我刚刚被我们的首席架构师告知使用块并不高效。我们应该使用finally和dispose connection。这是真的吗? - SamuraiJack

12

我肯定更喜欢第二种方法。在使用时更加简洁,而且容错性更强。

在第一种情况下,编辑代码的人必须小心,不要在Acquire(Read|Write)Lock调用和try之间插入任何内容。

虽然像这样对单个属性使用读/写锁通常是过度的。它们最好应用于更高级别。在此处,一个简单的锁通常就足够了,因为考虑到锁持有的时间,争用的可能性显然非常小,并且获取读/写锁比获取简单锁的操作成本更高。


如何保证程序的安全性?我知道使用try finally语句块可以确保finally块中的代码总是会被执行,但有没有什么方法可以避免dispose方法不被调用? - Jeremy
1
不,using语句本质上是try/finally模式的语法糖。 - Blair Conrad
使用模型确保对象始终被处理。 - Nathen Silver
如果您使用反射器,您会发现“using”块被编译器转换为try...finally结构,在IL级别上它们是等效的。“using”只是语法糖。 - Rob Walker
记住,在某些情况下,finally子句可能不会被调用。例如停电。 ;) - Quibblesome

11

考虑到两种解决方案都会掩盖异常的可能性。

显然,没有catchtry应该是一个不好的想法;请参见MSDN了解为什么using语句同样存在危险。

此外,请注意微软现在推荐使用ReaderWriterLockSlim而不是ReaderWriterLock。

最后,请注意微软示例使用两个try-catch块来避免这些问题,例如:

try
{
    try
    {
         //Reader-writer lock stuff
    }
    finally
    {
         //Release lock
    }
 }
 catch(Exception ex)
 {
    //Do something with exception
 }

一个简单、一致、干净的解决方案是一个不错的目标,但如果您无法只使用lock(this){return mydateetc;},那么您可能需要重新考虑这种方法;有了更多的信息,我相信Stack Overflow可以帮助您。


3
尝试语句并不一定掩盖异常。以我的例子为例,锁将被获取,然后如果在范围内抛出任何异常,锁将被释放,但异常仍将继续向上冒泡。 - Jeremy
1
@Jeremy:如果你的 finally 块抛出异常,它将掩盖 try 块中抛出的异常——这就是 MSDN 文章中所说的使用语法的(相同)问题。 - Steven A. Lowe
4
它不会掩盖异常,它会以与你的方式完全相同的方式替换异常。 - Jon Hanna

5

我个人尽可能经常使用C#的 "using" 语句,但是有一些特定的事情我会结合使用以避免上面提到的潜在问题。举个例子:

void doSomething()
{
    using (CustomResource aResource = new CustomResource())
    {
        using (CustomThingy aThingy = new CustomThingy(aResource))
        {
            doSomething(aThingy);
        }
    }
}

void doSomething(CustomThingy theThingy)
{
    try
    {
        // play with theThingy, which might result in exceptions
    }
    catch (SomeException aException)
    {
        // resolve aException somehow
    }
}

请注意,我将“using”语句分为一个方法和使用对象的另一个方法,并使用“try”/“catch”块。对于相关对象,我有时会嵌套几个“using”语句(在生产代码中有时会达到三到四层)。
在我的自定义IDisposable类的Dispose()方法中,我捕获异常(但不是错误)并记录它们(使用Log4net)。我从未遇到过任何这些异常可能会影响我的处理的情况。通常,潜在的错误会向上传播到调用堆栈,并以适当的消息(错误和堆栈跟踪)记录。
如果我某种方式遇到了Dispose()期间可能发生重大异常的情况,我会重新设计该情况。老实说,我怀疑那会发生。
与此同时,“using”的作用域和清理优势使其成为我最喜欢的C#特性之一。顺便说一句,我主要使用Java、C#和Python等编程语言,还有其他很多语言,因为“using”是我最喜欢的语言特性之一,因为它是一个实用的、每天都能用的工具。

提示,为了提高代码的可读性:通过不使用花括号来对using语句进行对齐和压缩,除了内部的"using"。 - Bent Tranberg

4

DRY说:第二个解决方案。第一个解决方案重复使用锁的逻辑,而第二个解决方案则没有。


4
"一堆属性"和在属性getter和setter级别上锁定看起来是错误的。您的锁定粒度过于细化。在大多数典型的对象使用中,您需要确保获取锁以同时访问不止一个属性。您的特定情况可能不同,但我有点怀疑。 无论如何,在访问对象而不是属性时获取锁定将显著减少您需要编写的锁定代码量。

是的,我完全理解你的观点。我的大多数属性实际上都是布尔值、整数,它们不需要锁定,因为它们应该是原子的。但有一些日期时间和字符串类型的属性需要加锁。由于只有少数需要锁定,所以我认为最好将锁定放在属性上。 - Jeremy

4

我喜欢第三个选项

private object _myDateTimeLock = new object();
private DateTime _myDateTime;

public DateTime MyDateTime{
  get{
    lock(_myDateTimeLock){return _myDateTime;}
  }
  set{
    lock(_myDateTimeLock){_myDateTime = value;}
  }
}

你有两个选项,第二个选项更干净,更容易理解正在发生的事情。

锁定语句与ReaderWriterLock的工作方式不同。 - chakrit
@chakrit:不过,除非你知道确实存在锁争用问题,否则它们很可能会更高效。 - Michael Burr
真的,但你应该始终首先选择简单锁。Subby的问题没有说明任何性能问题或要求不允许锁定。因此,没有理由尝试变得聪明。只需锁定并运行即可。 - user1228

2

Try/Catch块通常用于异常处理,而使用块则用于确保对象被释放。

对于读/写锁,try/catch可能是最有用的,但也可以同时使用两者,如下:

using (obj)
{
  try { }
  catch { }
}

这样你就可以隐式地调用你的IDisposable接口,同时使异常处理更加简洁。


1
以下内容创建了ReaderWriterLockSlim类的扩展方法,使您能够执行以下操作:
var rwlock = new ReaderWriterLockSlim();
using (var l = rwlock.ReadLock())
{
     // read data
}
using (var l = rwlock.WriteLock())
{
    // write data
}

这里是代码:

static class ReaderWriterLockExtensions() {
    /// <summary>
    /// Allows you to enter and exit a read lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitReadLock on dispose</returns>
    public static OnDispose ReadLock(this ReaderWriterLockSlim readerWriterLockSlim)
    {
        // Enter the read lock
        readerWriterLockSlim.EnterReadLock();
        // Setup the ExitReadLock to be called at the end of the using block
        return new OnDispose(() => readerWriterLockSlim.ExitReadLock());
    }
    /// <summary>
    /// Allows you to enter and exit a write lock with a using statement
    /// </summary>
    /// <param name="readerWriterLockSlim">The lock</param>
    /// <returns>A new object that will ExitWriteLock on dispose</returns>
    public static OnDispose WriteLock(this ReaderWriterLockSlim rwlock)
    {
        // Enter the write lock
        rwlock.EnterWriteLock();
        // Setup the ExitWriteLock to be called at the end of the using block
        return new OnDispose(() => rwlock.ExitWriteLock());
    }
}

/// <summary>
/// Calls the finished action on dispose.  For use with a using statement.
/// </summary>
public class OnDispose : IDisposable
{
    Action _finished;

    public OnDispose(Action finished) 
    {
        _finished = finished;
    }

    public void Dispose()
    {
        _finished();
    }
}

0

我认为方法2会更好。

  • 您的属性代码更简单易读。
  • 由于锁定代码不必多次重写,因此出错的可能性更小。

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