首页 > 解决方案 > 在静态方法中使用语句是否有任何影响包装

问题描述

在不从美学角度卷入意见的情况下,让我们关注以下内容的技术含义。

我非常喜欢将表达式主体方法用于简单的方法和属性以及方法链接。IMO 它们看起来干净整洁,没有花括号的多余噪音(是的,我也使用 F#)。

但是,对于包含 using 语句的方法,您不能使用表达式主体方法。如果不关注下面的语法细节,如果有的话,技术含义将是执行以下操作:

class ...
{
  HttpClient client;

  public async Task SaveSomething(X value) =>
    Using
     .Disposable(await client.PostAsync("..", value))
     .Act(response => response.EnsureSuccessStatusCode());

  public async Task<X> GetSomething() =>
    await Using
     .Disposable(await client.GetAsync(".."))
     .Act(response => response.ReadAsAsync<X>());
}

静态包装器实现:

static class Using
{
  public static DisposableAct<TDisposable> Disposable<TDisposable>(TDisposable disposable)
    where TDisposable : IDisposable => new DisposableAct<TDisposable>(disposable);
}

class DisposableAct<TDisposable> where TDisposable : IDisposable
{
  private readonly TDisposable disposable;

  public DisposableAct(TDisposable disposable) => this.disposable = disposable;

  public TResult Act<TResult>(Func<TDisposable, TResult> act)
  {
    using (disposable)
    {
      return act(disposable);
    }
  }
}

相对于:

class ...
{
  HttpClient client;

  public async Task SaveSomething(X value)
  {
    using (var response = await client.PostAsync("..", value))
    {
      response.EnsureSuccessStatusCode();
    }
  }

  public Task<X> GetSomething()
  {
    using (var response = await client.PostAsync("..", value))
    {
      return await response.ReadAsAsync<X>();
    }
  }
}

标签: c#functional-programmingc#-6.0c#-7.0

解决方案


您的包装器与您尝试复制的代码之间存在许多差异。

  1. 一旦async方法返回,而不是在异步操作完成后,您就处置可支配资源。您尝试复制的代码在异步操作完成之前不会释放资源。如果你在等待任何东西后使用了可丢弃资源,它已经被丢弃了。

  2. 如果在调用之前发生异常Act,则一次性资源会泄漏。没有这样的机会泄漏其他代码中的可支配资源。

  3. 如果您Act多次调用,一次性资源将已被处置。

  4. 如果你从不调用Act包装器,那么一次性资源永远不会被释放。这基本上是#2,但如果代码的作者做错了。

  5. 您正在创建多个其他对象,从而增加了内存压力。既有一次性包装器,也有其他async方法,即额外的状态机。

#1 是你可以在你的实现中修复的东西,而不需要改变调用者使用它的方式,其他的是你设计它的方式所固有的,解决这些问题需要改变调用者使用操作的方式来修复.

因此,就修复所有这些事情而言,第一件事就是完全删除包装器对象。它为程序员创造了许多犯错的机会(#3、#4 和其他一些我没有费心提及的),并且是#5 的重要组成部分。相反,只需让一个静态方法接受两个参数,一个一次性资源和一个对其采取的操作。这为您提供了确保它始终(或至少更接近于)正确完成所需的控制。

public static TResult UseDisposable<TDisposable, TResult>(TDisposable disposable, Func<TDisposable, TResult> function)
    where TDisposable : IDisposable
{
    using (disposable)
    {
        return function(disposable);
    }
}

接下来,如果您想支持异步方法,您需要专门为此进行重载,在其中意识到它是一个异步方法并相应地处理它。幸运的是await,这很容易编写。(请注意,从技术上讲,async这里使用意味着我们正在创建一个状态机,如果我们手动完成,我们可以在技术上避免这种情况。如果你想避免与原来的差异,你要么需要手动完成整个事情(如果您想确保所有正确的错误处理和取消行为都正确完成,这非常困难)。如果您可以忍受添加的分配,那么代码不会比您尝试的代码复杂太多复制。

public static async Task<TResult> UseDisposable<TDisposable, TResult>(TDisposable disposable, Func<TDisposable, Task<TResult>> function)
    where TDisposable : IDisposable
{
    using (disposable)
    {
        return await function(disposable);
    }
}

那么当然你需要这两个重载的版本来进行非结果返回操作:

public static void UseDisposable<TDisposable>(TDisposable disposable, Action<TDisposable> action)
    where TDisposable : IDisposable
{
    using (disposable)
    {
        action(disposable);
    }
}
public static async Task UseDisposable<TDisposable, TResult>(TDisposable disposable, Func<TDisposable, Task> action)
    where TDisposable : IDisposable
{
    using (disposable)
    {
        await action(disposable);
    }
}

在这一点上值得注意的是,上述重载并没有完全解决在一次性资源进入using. 上面的版本缩小了代码的可能性窗口,特别是消除了很多可能的误用,但并没有完全消除它。如果你担心它,你可以更进一步,而不是接受一次性资源,你可以接受一个生成的方法可支配资源。这完全消除了这种可能性。幸运的是,所有这些重载都可以并排放置,因此您可以拥有所有这些重载,然后仅在您担心在其构造函数和使用开始之间的任何位置可能发生异常时才使用一次性生成器版本。

public static TResult UseDisposable<TDisposable, TResult>(Func<TDisposable> disposableGenerator, Func<TDisposable, TResult> function)
    where TDisposable : IDisposable
{
    using (var disposable = disposableGenerator())
    {
        return function(disposable);
    }
}
public static async Task<TResult> UseDisposable<TDisposable, TResult>(Func<TDisposable> disposableGenerator, Func<TDisposable, Task<TResult>> function)
    where TDisposable : IDisposable
{
    using (var disposable = disposableGenerator())
    {
        return await function(disposable);
    }
}
public static void UseDisposable<TDisposable>(Func<TDisposable> disposableGenerator, Action<TDisposable> action)
    where TDisposable : IDisposable
{
    using (var disposable = disposableGenerator())
    {
        action(disposable);
    }
}
public static async Task UseDisposable<TDisposable, TResult>(Func<TDisposable> disposableGenerator, Func<TDisposable, Task> action)
    where TDisposable : IDisposable
{
    using (var disposable = disposableGenerator())
    {
        await action(disposable);
    }
}

您可能还想制作最初的 4 个重载扩展方法,如果您使用后四个重载方法,并且您发现您使用它们足以使其有意义。


推荐阅读