【问题标题】:Is there a Using pattern that does not rely on IDisposable?是否存在不依赖 IDisposable 的 Using 模式?
【发布时间】:2009-05-01 18:06:21
【问题描述】:

我想创建一个内部消息传递系统,它可以告诉我某些代码被调用的持续时间。我在考虑易于使用,使 SystemMessage 类实现 IDisposable。

我会在 SystemMessage 的构造函数期间设置一个时间戳,如果调用了 Dispose,我可以计算出持续时间。

问题是我不想让对象 GC'ed。我希望它作为 MessageCollection 的一部分保留。

C# 中是否有另一种结构可以让我使用 Using 语句,而无需踩到 IDisposable 的预期功能。

Using (message = Collection.CreateNewMessage("FileDownlading"))
{
    // I wonder how long it is taking me to download this file in production?
    // Lets log it in a message and store for later pondering.
    WebClass.DownloadAFile("You Know This File Is Great.XML");
}
// we fell out of the using statement, message will figure out how long
// it actually took to run.
// This was clean and easy to implement, but so wrong?

【问题讨论】:

  • using 块的目的是在对象超出范围时将其处理掉。你应该用它做任何其他事情。
  • 迈克尔,我同意。是否有另一种可以以这种方式使用的 C# 构造?

标签: c# idisposable using misuse


【解决方案1】:

问题是我不想让对象 GC'ed。我希望它作为 MessageCollection 的一部分保留。

调用 Dispose 不会导致对象被 GC'ed - 当 GC 进行扫描并且没有任何对象引用它时会发生这种情况。如果您仍然通过 MessageCollection 引用该对象,它会一直存在。

Dispose 可以防止它被 Finalized,但是由于您没有使用 Dispose 来清理资源,因此您将没有 Finalizer,您也不会关心。

所以,真正唯一的问题是让你的 calss 实现 IDisposable 的语义令人困惑,即使没有可处置的资源。

就个人而言,我不认为这是一个问题。如果消费者调用 Dispose,那么很好 - 他们得到时间记录。如果他们不这样做,那么他们就不会获得 itme 印章,最糟糕的是他们会违反 FxCop。

然而,这有点不直观 - 所以如果这是供公众使用,我建议提供一个更容易发现的替代方案,例如:

// C# 3+ lambda syntax
Collection.CreateNewMessage("FileDownlading", () => {
    // I wonder how long it is taking me to download this file in production?    
    // Lets log it in a message and store for later pondering.    
    WebClass.DownloadAFile("You Know This File Is Great.XML");
});

// C# 2 anonymous delegate syntax
Collection.CreateNewMessage("FileDownlading", delegate() {
    // I wonder how long it is taking me to download this file in production?    
    // Lets log it in a message and store for later pondering.    
    WebClass.DownloadAFile("You Know This File Is Great.XML");
});

// Method
void CreateNewMessage(string name, Action action) {
   StopWatch sw = StopWatch.StartNew();
   try {
      action();
   } finally {
      Log("{0} took {1}ms", name, sw.ElapsedMilliseconds);
   }
}

这将运行一个 Action 委托并为其计时。

【讨论】:

  • 该特定语法不会(它是一个 lambda 表达式),但它基本上是一个委托,因此您可以轻松地在 2.0 中执行等效操作。
  • 使用 C#2 语法获取此错误:“无法将匿名方法转换为类型 'System.Delegate',因为它不是委托类型”。找到这个页面:staceyw.spaces.live.com/blog/cns!F4A38E96E598161E!1042.entry
【解决方案2】:

你在寻找类似于闭包的东西吗?

http://en.wikipedia.org/wiki/Closure_(computer_science)

你可以伪造一些东西......像这样......

private TimeSpan GetDuration(Action a)
        {
            var start = DateTime.Now;
            a.Invoke();
            var end = DateTime.Now;
            return end.Subtract(start);
        }

        public void something()
        {
            string message;
            var timeSpan = GetDuration(() => { message = "Hello"; } );
        }

【讨论】:

    【解决方案3】:

    我不认为 using 是你想要的。为什么不让构造函数记录时间,然后在调用 DownloadAFile 时记录时间增量?在您的示例中,如果有异常,它将记录异常的时间,就好像当时下载了文件一样。

    如果您真的希望行为像您的示例一样,只需使用 try / finally 块,然后在 finally 中进行日志记录。 using 只是 try / finally 块和调用 Dispose 的语法糖。

    与您的示例等效的代码如下所示:

    try 
    {
        var message = Collection.CreateNewMessage("FileDownlading"); 
        //...
        WebClass.DownloadAFile("You Know This File Is Great.XML");
    }
    finally 
    {
        //You can change this to do logging instead.
        message.Dispose(); 
    }
    

    【讨论】:

    • 我在这里寻找语法糖...试图使其在许多地方尽可能容易使用。如果我的选择使用不当或让用户调用 message.EndTime(),我将使用 EndTime() 方法。
    【解决方案4】:

    您可以在 DownloadAFile() 中使用 System.Diagnostics.Stopwatch 来在每次调用时进行计时。

    或者,只需在对 DownloadAFile() 的调用周围添加秒表代码(取决于您希望它如何工作)。

    在这种情况下使用 IDisposable 不是一个好主意。

    【讨论】:

    • 我很难找到一个可靠的答案,但不清楚 StopWatch 是否使用 DateTime 字段来存储经过的时间,或者它是否使用后台线程来运行计时器(类似于 System.Timers。定时器)。如果它确实使用了线程,那么由于保持每条消息的计时器计数所需的上下文切换,您将面临严重的可伸缩性问题。
    • @Yoooder:当它真正需要做的只是计算两个滴答计数之间的差异时,我无法想象它使用后台线程。但是,和你一样,我找不到可靠的文档。
    • 我做了一些快速测试,果然看起来 StopWatch 不像 Timer 那样使用线程。我认为这是有道理的,因为计时器需要以指定的时间间隔引发事件并且不能等待阻塞 - 而秒表只需要知道它何时开始以及何时停止。
    • System.Timers.Timer 不使用线程来计时;只是“完成”回调被发布到线程池工作线程而不是 UI 线程。像任何多线程使用一样,这在某些情况下会带来好处,但会为粗心的人带来一些警告。
    【解决方案5】:

    我最近一直在看这个,也许 PostSharp 可以帮助你。它允许您使用属性装饰方法,该属性将在您的方法启动和停止时调用。

    http://www.postsharp.org/

    我不确定它会做你喜欢的事,但它值得研究,它有你渴望的“语法糖”!

    克里斯

    【讨论】:

    • 这看起来可能适用于我正在尝试做的事情。谢谢。
    【解决方案6】:

    嗯,这是一个老歌,但似乎没有其他人发布我认为是最佳风格的答案(根据我对您的要求的理解):

    using (MessageTimer timer = Collection.CreateNewTimedMessage("blabla"))
    {
       // ...
    }
    

    重要的一点是CreateNewTimedMessage 可能会作为副作用创建并存储一个半永久性的Message 对象,但它返回一个临时计时对象(使用StopWatch,或一些类似的机制)在using 块的范围内不存在。 (MessageTimer 可以引用Message,但不能反过来。)

    因此,处理MessageTimer 对象可能会产生在某处记录最后时间的副作用,但该对象本身不会存活或复活;这不是滥用using 构造,因为您确实是在处理一个对象。

    MessageTimer 的实际实现可能与 Joe 的回答类似。)

    【讨论】:

      【解决方案7】:

      不是真的。你能找到的最接近的是这样的(无论如何,这就是 using() 语句的底层发生的事情:)

      var message = Collection.CreateNewMessage("FileDownloading")
      
      try
      {
          WebClass.DownloadAFile("You Know This File Is Great.XML");
      }
      finally
      {
          message.HowLongHaveIBeenAlive();
      }
      

      【讨论】:

      • 这就是我想要发生的事情,我只是不想在整个代码中添加 try finally 部分。
      【解决方案8】:

      using 语句旨在在您完成对象后处理它们。在一个对象被丢弃后抓住它的想法并不完全是有意的。你可以尝试做一些简单的事情:

      message = Collection.CreateNewMessage("FileDownlading");
      DateTime dtStart = DateTime.Now;
      WebClass.DownloadAFile("You Know This File Is Great.XML");
      DateTime dtEnd = DateTime.Now;
      
      // perform comparison here to see how long it took.
      
      // dispose of DateTimes
      dtStart = dtEnd = null;
      

      【讨论】:

        【解决方案9】:

        可以说这是对 using 构造的滥用,因此我可能不会在以这种方式使用 IDisposable 的共享类库中实现公共类。

        但是我已经看到了这种事情,并且认为如果它保留在您的应用程序内部是可以的。

        问题是我不想 有对象GC'ed。我想要它 留在周围 消息集合。

        我完全不明白。 IDisposable 与 GC 无关,如果将其作为 MessageCollection 的元素引用,您的消息将保持活动状态。

        您的消息类可能类似于下面的示例。调用 Dispose 后,它仍然存在并且运行良好。大多数实现 IDisposable 的类在调用 Dispose 后不可用,因此如果在调用 Dispose 后访问成员,它们的实现将引发 ObjectDisposedException。但这绝不是强制性的。

        class Message : IDisposable
        {
            private Stopwatch _stopwatch = Stopwatch.StartNew();
            private long _elapsedTicks;
            private string _message;
        
            public Message(string message)
            {
                _message = message;
            }
        
            public void Dispose()
            {
               _elapsedTicks = _stopwatch.ElapsedTicks;
               ... anything else including logging the message ...
            }
        
            ...
        }
        

        【讨论】:

        • 您是正确的,IDisposable 在技术上并不直接调用 GC,但是大多数开发人员并不知道 - 如果您询问开发人员何时调用 Dispose 以及他们期望它做什么,他们会说“清理一个对象并为 GC 做准备”。从技术上讲,您可以使用它来重置对象的状态,但是这是一种非常糟糕的做法,因为它是一种非常意外的行为。
        • 您说 Dispose 将“清理对象并为 GC 准备”是不正确的。 Dispose 的通常用途是确定性地释放非托管资源。唯一“为 GC 准备对象”(如果有的话)是对超出范围的对象的最后引用。
        • 我明白,但是约定(以及普遍理解的 Dispose 用法)是为 GC 准备一个对象。还有许多编码标准和指南规定,在调用 Dispose() 之后,对对象的任何方法调用都应该抛出 ObjectDisposedException。虽然您可以在技术上回收 Disposed 对象,但这是一种非常出乎意料且建议反对的做法。为常规目的保留 Dispose() 更安全、更容易接近。
        • 我同意你的观点,这将是对 IDisposable 的滥用,因此我的评论是我不会在公共 API 中使用它。我也不提倡回收处理过的物品。不管那个稻草人“大多数开发人员”怎么想,处理一个对象并不能为 GC 做好准备。
        【解决方案10】:

        “using”语句实际上编译为 Try/Finally,它期望对象实现 IDisposable——“using”只是语言提供的快捷方式。

        出于您的目的,特别是因为您想回收对象,我会考虑编写您自己的界面。当您调用方法开始计时时,只需获取 datetime.now 的值;当你停止计时器时再次。然后从 stoptime 中减去 starttime,就得到了持续时间。

        避免在每个类实例上使用真正的 Timer 对象。 Timer 使用 ThreadPool 线程,这意味着每条消息将消耗至少一个线程——如果系统中的消息过多,您的应用程序将因线程切换而变慢。此外,如果计时器没有正确处理,它们可能不会释放它们的 ThreadPool 线程,并且您基本上会有线程泄漏。

        【讨论】:

        • 秒表比使用日期时间更准确。如果您确实想使用 DateTime(例如 .NET 1.x),请更喜欢 UtcNow 而不是 Now,因为它不会在夏令时开始/结束时发生跳转。
        • 你知道秒表是否使用工作线程来计时?我现在很好奇,一直没能找到答案。
        • 稍后进行一些研究,看起来 StopWatches 不使用工作线程,所以您是正确的,它们应该是此类工作的理想选择。
        【解决方案11】:

        最初 IDisposable 旨在作为在 C# 中引入确定性清理的一种方式,但我已经看到 authorsimplementations 使用 using/Dispose 功能来实现与您的内容类似的东西正在谈论。

        就我个人而言,我对此不太满意,因为它破坏了IDisposable 背后的概念,但由于没有其他选择,它归结为你想成为多正统的问题。我当然可以理解您为什么要这样做,但是这样做会使您更难解释IDisposable 接口的目的和重要性。

        【讨论】:

        • 使用 Dispose() 来重置对象而不是为 GC 做准备的潜在问题是,没有其他众所周知的接口专门用于让对象为 GC 做好准备。此外,调用已处置对象的方法应通过“ObjectDisposedException”的约定意味着回收已处置对象是非常不标准的。
        • ...尽管您可以仔细实现 IDispose 模式并忽略异常的约定。问题在于,未来的开发人员需要知道 Dispose 需要同时执行这两个目的——而且消费者也需要知道,即使它已被处理,它也是可重用的。
        【解决方案12】:

        再次查看您的问题后,我看不出所讨论的对象实际上是消息,除非它更像是跟踪消息(用于调试帮助)。

        如果您正在寻找更多类似这些方面的东西,那么这里是一个非常使用委托的粗略方法。本质上,您为要调用的每个方法和时间创建一个委托,然后将委托和方法参数传递给负责实际调用方法并计时其持续时间的助手。

        我的示例中明显的缺点是我牺牲了类型安全的参数,因为我不确定它是否正是您正在寻找的。另一个问题是,每次您想要调用具有您还没有委托的方法签名的方法时,您都需要添加一个新委托:

        using System;
        
        namespace ConsoleApplication4
        {
            class Program
            {
                static void Main(string[] args)
                {
        
                    SomeCaller callerOne;
                    YetAnotherCaller callerTwo;
        
                    callerOne = new SomeCaller(SomeMethod);
                    LogCallDuration(callerOne, new object[] { 15 });
        
                    callerOne = new SomeCaller(SomeOtherMethod);
                    LogCallDuration(callerOne, new object[] { 22 });
        
                    callerTwo = new YetAnotherCaller(YetAnotherMethod);
                    LogCallDuration(callerTwo, null);
        
                    Console.ReadKey();
                }
        
                #region "Supporting Methods/Delegates"
        
                delegate void SomeCaller(int someArg);
                delegate void YetAnotherCaller();
        
                static void LogCallDuration(Delegate targetMethod, object[] args)
                {
                    DateTime start = DateTime.UtcNow;
                    targetMethod.DynamicInvoke(args);
                    DateTime stop = DateTime.UtcNow;
        
                    TimeSpan duration = stop - start;
        
                    Console.WriteLine(string.Format("Method '{0}' took {1}ms to complete", targetMethod.Method.Name, duration.Milliseconds));
        
                }
        
                #endregion "Supporting Methods/Delegates"
        
                #region "Target methods, these don't have to be in your code"
                static void SomeMethod(int someArg)
                {
                    // Do something that takes a little time
                    System.Threading.Thread.Sleep(1 + someArg);
                }
        
                static void SomeOtherMethod(int someArg)
                {
                    // Do something that takes a little time
                    System.Threading.Thread.Sleep(320 - someArg);
                }
        
                static void YetAnotherMethod()
                {
                    // Do something that takes a little time
                    System.Threading.Thread.Sleep(150);
                }
                #endregion "Target methods"
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-01-29
          • 2015-01-25
          • 2011-09-15
          • 2015-02-03
          • 1970-01-01
          • 1970-01-01
          • 2012-02-11
          相关资源
          最近更新 更多