【问题标题】:Disposing object from same object从同一对象处理对象
【发布时间】:2016-10-16 06:48:57
【问题描述】:

我在设计一个fluent API,用法有点像这样:

IUser user = work
                 .Timeout(TimeSpan.FromSeconds(5))
                 .WithRepository(c => c.Users)
                 .Do(r => r.LoadByUsername("matt"))
                 .Execute();

所以,假设work 的类型为IUnitOfWork,但方法WithRepository(c => c.Users) 返回一个名为IActionFlow<IUserRepository> 的接口,即IDisposable

当我调用Execute() 并获得最终结果时,我失去了对该IActionFlow<IUserRepository> 实例的引用,因此我无法处理它。

将实例置于Execute() 方法上的缺点是什么?

类似:

public TResult Execute()
{
    // ...
    Dispose();
    return result;
}

代码似乎编译得很好,但我正在寻找可能因此而出现的奇怪行为或错误。这是不好的做法吗?

【问题讨论】:

标签: c# dispose fluent


【解决方案1】:

你可以有Using这样的方法:

public static TResult Using<TDisposable, TResult>(Func<TDisposable> factory,
    Func<TDisposable, TResult> fn) where TDisposable : IDisposable {
    using (var disposable = factory()) {
        return fn(disposable);
    }
}

那么您的代码将如下所示:

var user = Using(() => work.
    Timeout(TimeSpan.FromSeconds(5)).
    WithRepository(c => c.Users),
    repository => repository.Do(r => r.LoadByUsername("matt")).
        Execute());

这将使您的 API 保持流畅,同时您将在 Execute 完成的同一时刻处置 WithRepository

【讨论】:

  • 我喜欢这个解决方案的巧妙之处,但在 IMO 看来,这太过分了。
【解决方案2】:

选项 1:

您能否将您的代码包装在 using 块中,以便自动调用 dispose,

using(var repository = work.Timeout(TimeSpan.FromSeconds(5)).WithRepository(c => c.Users))
{
   IUser user = repository
                 .Do(r => r.LoadByUsername("matt"))
                 .Execute();

}

因此,您的Execute 方法不需要调用Dispose()

public TResult Execute()
{
    // ...
    //Dispose();
    return result;
}

选项 2:

您可以在属性中分配结果并返回存储库对象,您可以使用该对象显式调用Dispose 方法。

类似这样的东西(可以进一步重构),

using(var repository = work
                 .Timeout(TimeSpan.FromSeconds(5))
                 .WithRepository(c => c.Users)
                 .Do(r => r.LoadByUsername("matt"))
                 .Execute())
{

   IUser user = repository.Result;
   //repository.Dispose();
}

//******

public TResult Result { get; set; }

public IActionFlow<IUserRepository> Execute()
{
    // ...
    //Dispose();
    this.Result = result;
    return this;
}

注意:调用Dispose() 方法后,垃圾收集可能会根据可用的服务器资源在任何阶段进行。所以在 Execute 方法中调用它可能会产生意想不到的奇怪问题。

【讨论】:

  • 我可以,但这会使我的 API 完全不流畅
  • @MatiasCicero 提供了一个替代想法,但必须进一步重构它。
  • 甚至可以将选项 2 包装在 using 块中
【解决方案3】:

Disposable 对象必须从客户端代码中进行处理,您将看到的副作用之一是,当您的消费者使用您的 API 时,他们会收到代码分析错误(如果他们使用它,很多人会这样做)比如:“CA2213: Disposable fields should be dispose”请参阅https://msdn.microsoft.com/en-us/library/ms182328.aspx,了解有关该错误的详细信息。

如上所述,您不应该在“执行”方法中处理绝对必要的东西,请采取以下情况:

场景#1

  1. 您有一些必须处理的开放连接, 连接是通过 Execute 以外的其他方法打开的。
  2. “Do”方法执行期间发生错误,dispose 永远不会被调用,因为 execute 永远不会被调用,并且打开的连接保持打开状态。请注意,即使 Do never 在您的机器上失败,也不能保证它不会因为其他线程的机器发生随机事件而在生产中失败。

附: using 语句和整个 IDisposable 功能都是为了解决这种情况而构建的,无论情况如何,都会迫使优秀的开发人员进行处理。

场景#2

  1. execute 方法做了一些一次性的事情,例如打开一个连接,然后它会做一些事情。

在这种情况下,您不需要 dispose 方法,您可以通过将连接包装在 using 语句中来简单地在 execute 方法中释放连接,如下所示:

public void Execute(object whatever){
    using (var conn = new Connection()){
        //method body
    }
}

如果你有场景#1,你必须让客户端使用using语句,并且必须承受它的不流畅性(析构函数不会削减它),如果你有场景#2你不需要完全处置。

【讨论】:

    【解决方案4】:

    在我看来,您正在尝试处置存储库。

    如果您希望保持 API 完好无损,我建议这样做:

    using(var repo = work.Timeout(TimeSpan.FromSeconds(5))
          .WithRepository(c => c.Users))
    {
        IUser user = repo.Do(r => r.LoadByUsername("matt")).Execute();
        //Do whatever with user here if lazy loading.
    }
    

    我知道这种方式会将您的流畅 API 分成两部分,但您可以以这种方式使用它,而无需进行任何更改。

    另一种选择是为结果创建一个包装类,该类维护对存储库的引用(例如 ActionFlowExecution)并实现 IDisposable:

    public class ActionFlowExecution<TResult, TRepository> : IDisposable
    {
        private TRepository _repository;
        internal ActionFlowExecution(TResult result, TRepository repository)
        {
            Result = result;
            _repository = repository;
        }
    
        public TResult Result { get; private set; }
    
        public void Dispose()
        {
            if(_repository != null)
            {
                _repository.Dispose();
                _repository = null;
            }
        }
    }
    

    然后您可以像这样调用您的 API:

    using(var execution = work
                 .Timeout(TimeSpan.FromSeconds(5))
                 .WithRepository(c => c.Users)
                 .Do(r => r.LoadByUsername("matt"))
                 .Execute())
    {
        IUser user = execution.Result;
        //Do whatever with user here 
    }
    

    我同意其他人的观点,即从内部处理对象是个坏主意。 我在五分钟内写了这篇文章,所以可能会有一些错别字,但我想你会明白的。

    【讨论】:

      【解决方案5】:

      只要您以后不使用该对象,您就可以安全地执行您提到的操作。这里唯一但非常重要的是命名。我会调用方法ExecuteAndDispose。其他选项要么破坏您的流畅 API,要么只是矫枉过正而且也不是最容易使用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-12-16
        • 1970-01-01
        • 2011-04-26
        • 2010-11-22
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多