问题 重构库是异步的,我怎么能避免重复自己?


我有一个像这样的方法:

    public void Encrypt(IFile file)
    {
        if (file == null)
            throw new ArgumentNullException(nameof(file));

        string tempFilename = GetFilename(file);
        using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
        {
            this.EncryptToStream(file, stream);
            file.Write(stream);
        }

        File.Delete(tempFilename);
    }

但是,我想要编写另一个方法,非常相似,但它调用WriteAsync,例如:

    public async Task EncryptAsync(IFile file)
    {
        if (file == null)
            throw new ArgumentNullException(nameof(file));

        string tempFilename = GetFilename(file);
        using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
        {
            this.EncryptToStream(file, stream);
            await file.WriteAsync(stream);
        }

        File.Delete(tempFilename);
    }

但是,我不喜欢有两种方法,实际上重复代码。我怎么能避免这个?正确的方法感觉我应该使用Action / Delegate,但签名是不同的....

思考?


11530
2017-09-10 21:00


起源

ADO.NET总是内部异步,只是简单地阻止任务...这是大锤解决方案,我有点喜欢缺乏替代方案。 - usr
这当然是我可以采取的一种方法。我已经阅读了相反的方法(即Task.Run调用同步代码,虽然看起来不赞成)。 - Adrian Luca Thomas
这更糟糕,因为这样你就不会获得任何线程解锁权益。完全没有意义。 - usr
我知道你的例子可能是简化的,但它看起来很好看。您可能会将一些内容移动到一个公共位置(参数检查,文件模式),但您主要是调用其他方法。它看起来可读性和可维护性。 - Tim Medora
它在可维护性/可读性方面对我有利,但是我绝对同意在Task.Run方法中对消费者没有任何好处。 @TimMedora - 它实际上是复制和粘贴!这不是太糟糕,但我在这里是一个完美主义者......如果可能的话,我想防止任何重复的代码.. - Adrian Luca Thomas


答案:


但是,我不喜欢有两种方法,实际上重复代码。我怎么能避免这个?

有一些方法,如我所述 关于棕色字段异步开发的MSDN文章

1)使异步API成为自然异步API。

这是最激烈的解决方案(在向后兼容性方面),但从技术角度来看,你可以认为它是最正确的。有了这种方法,你会的 更换  Encrypt 同 EncryptAsync

虽然这是IMO的最佳方法,但您的最终用户可能不同意。 :)

2)申请  阻止异步版本。

public void Encrypt(IFile file)
{
  EncryptAsync(file).GetAwaiter().GetResult();
}

请注意(正如我在我的博客中描述的那样), 为了避免你需要使用的死锁 ConfigureAwait(false) 异步版本中的任何地方

这个黑客的缺点:

  • 你真的必须使用 ConfigureAwait(false) 为每一个人 await 在你的 async 版本和它调用的每个方法。忘掉一个,你就有了死锁的可能性。请注意,某些库不使用 ConfigureAwait(false) 适用于所有平台的所有地方(特别是 HttpClient 在移动平台上)。

3)申请  在线程池线程上运行异步版本并阻塞

public void Encrypt(IFile file)
{
  Task.Run(() => EncryptAsync(file)).GetAwaiter().GetResult();
}

这种方法完全避免了死锁情况,但也有其自身的缺点:

  • 异步代码必须能够在线程池线程上运行。
  • 可以运行异步代码  线程池线程贯穿整个存在。如果异步代码隐式地依赖于单线程上下文的同步,或者它是否使用线程本地状态,那么如果没有一些重写,这种方法将无法工作。

4)传递旗帜参数。

如果你不愿采取方法(1),这是我最喜欢的方法。

private async Task EncryptAsync(IFile file, bool sync)
{
  if (file == null)
    throw new ArgumentNullException(nameof(file));

  string tempFilename = GetFilename(file);
  using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
  {
    this.EncryptToStream(file, stream);
    if (sync)
      file.Write(stream);
    else
      await file.WriteAsync(stream);
  }

  File.Delete(tempFilename);
}

public void Encrypt(IFile file)
{
  EncryptAsync(file, sync: true).GetAwaiter().GetResult();
}

public Task EncryptAsync(IFile file)
{
  return EncryptAsync(file, sync: false);
}

布尔标志肯定是丑陋的 - 并且是正确的OOP设计的红色标志 - 但它隐藏在一个 private 方法,并没有其他黑客那么危险。

我的文章中介绍了其他几个hacks(单线程线程池上下文和嵌套消息循环),但我通常不推荐它们。


另外,如果您的代码确实在使用 FileStream,您需要显式打开它以进行异步访问以获得真正的异步操作。也就是说,你必须调用构造函数传递 true 为了 isAsync 参数,或设置 FileOptions.Asynchronous 标志的值为 options 参数。


16
2017-09-10 21:41



哇,非常感谢,谢谢!我想我现在会选择解决方案1,但是如果我将来绝对需要两者,我会倾向于沿着2号或3号路线前行...但不是4,因为它是一个纯粹的纯粹主义者。谢谢! - Adrian Luca Thomas


像这样的东西:

public async Task EncryptAsync(IFile file, bool AAsync)
{
    if (file == null)
        throw new ArgumentNullException(nameof(file));

    string tempFilename = GetFilename(file);
    using (var stream = new FileStream(tempFilename, FileMode.OpenOrCreate))
    {
        this.EncryptToStream(file, stream);
        if (AAsync)
            await file.WriteAsync(stream);
        else
            file.Write(stream);
    }

    File.Delete(tempFilename);
}

-1
2017-09-10 21:06



我看到你来自哪里,但那将违反SOLID原则(即单一责任)。这里我们通过传入一个布尔值来改变方法的内部逻辑,以确定某些东西是否异步。还是)感谢你的建议! :-) - Adrian Luca Thomas
混合 async 代码与非async 代码在C#中是不好的做法。 - Setsu
“请更正任何语法错误” - 完成。参数用逗号分隔,而不是分号。该类型位于参数名称之前。和 file.Write 不返回任何可能的东西 await编辑。
尽管在每个其他可能异步方法调用的方法中都有条件,但是在某些情况下,这可能仍然比复制整个方法体的方法更糟糕。我不知道有任何第三种选择。 (注意:对于非async 版, EncryptAsync(file, false) 保证同步完成,并访问它 Result 财产保证不会被阻止。)
@RohitGupta这里的主要问题是你有一个必须调用的方法 async 语法,但实际上是在做同步工作。鉴于所涉及的重复数量,我同意之前的评论,即实际上只有两个版本,因为违反规则的好处并不重要。 - Setsu