使用管道时一直异步

Posted

技术标签:

【中文标题】使用管道时一直异步【英文标题】:Async all the way when using pipeline 【发布时间】:2020-03-02 01:33:40 【问题描述】:

如果我有一个应用程序使用具有多个阶段的管道,要在所有阶段上使用foreach 执行并调用:

CanExecute
Execute

界面是这样的:

public interface IService

    bool CanExecute(IContext subject);

    IContext Execute(IContext subject);

它基本上接受一个上下文并返回一个它变得更丰富的上下文。

Execute 方法的一个阶段中,我需要调用一个服务,并且想要执行异步操作。 所以现在Execute 方法需要更改为例如

Task<IContext> ExecuteAsync(IContext subject);

await 用于调用服务。

所有其他阶段都没有异步代码,但现在需要更改,因为最佳实践是“一直异步”。

在引入异步代码时必须进行这些更改是否正常?

【问题讨论】:

是的,我看到Task.FromResult 在所有实现异步方法的方法中使用了很多,但实际上是同步的。 使用ValueTask 而不是Task 以避免为这些同步操作分配新对象。如果您使用 C#8,您可以使用默认接口成员默认返回同步结果,并且仅覆盖 async 方法,例如 public ValueTask&lt;IContext&gt; ExecuteAsync(IContex context)=&gt;new ValueTask(Execute(context)); 【参考方案1】:

C# 8 提供了多种方法来避免修改同步服务。 C# 7 也可以使用模式匹配语句来处理这个问题。

默认实现成员

接口版本控制是默认接口成员的主要用例之一。它们可用于避免在接口更改时更改现有类。您可以为ExecuteAsync 添加一个默认实现,将Execute 的结果作为ValueTask 返回。

假设你有这些接口:

public interface IContext

public interface IService

    public bool CanExecute(IContext subject);

    public IContext Execute(IContext subject);       


public class ServiceA:IService

    public bool CanExecute(IContext subject)=>true;
    public IContext Execute(IContext subject)return subject;


要创建异步服务不修改同步服务,您可以向 IService 添加默认实现并在新服务中覆盖它:

public interface IService

    public bool CanExecute(IContext subject);

    public IContext Execute(IContext subject);

    public ValueTask<IContext> ExecuteAsync(IContext subject)=>new ValueTask<IContext>(Execute(subject));



public class ServiceB:IService

    public bool CanExecute(IContext subject)=>true;
    public IContext Execute(IContext subject)=>ExecuteAsync(subject).Result;

    public async ValueTask<IContext> ExecuteAsync(IContext subject)
    
        await Task.Yield();
        return subject;
    
    

ServiceB.Execute 仍然需要一个主体,而有意义的一件事是调用ExecuteAsync() 并阻止,就像看起来一样丑陋。如果调用Execute,另一种可能性是抛出:

public IContext Execute(IContext subject)=>throw new InvalidOperationException("This is an async service");

模式匹配

另一种选择是为异步服务创建第二个接口:

public interface IService

    public bool CanExecute(IContext subject);

    public IContext Execute(IContext subject);        



public interface IServiceAsync:IService
        
    public ValueTask<IContext> ExecuteAsync(IContext subject);    

两个服务实现将保持不变。管道代码将根据服务的类型进行更改以进行不同的调用:

async Task Main()

    IService[] pipeline=new[](IService)new ServiceA(),new ServiceB();
    IContext ctx=new Context();
    foreach(var svc in pipeline)
    
        if (svc.CanExecute(ctx))
        
            var result=svc switch  IServiceAsync a=>await a.ExecuteAsync(ctx),
                                    IService b => b.Execute(ctx);        
            ctx=result;
        
    

模式匹配表达式根据当前服务的类型调用不同的分支。对类型进行 Natch 会产生一个强类型实例(a 或 b),可用于调用适当的方法。

Switch 表达式是详尽的 - 如果编译器无法验证所有选项是否与模式匹配,编译器将生成警告。

C# 7

C# 7 没有 switch 表达式,因此需要更详细的模式匹配 switch 语句:

if (svc.CanExecute(ctx))

    switch (svc)
    
        case IServiceAsync a:
            ctx=await a.ExecuteAsync(ctx);                    
            break;                    
        case IService b :
            ctx=b.Execute(ctx);        
            break;
        default:
            throw new InvalidOperationException("Unknown service type!");
    

开关 语句 并不详尽,因此我们需要添加 default 部分以在运行时捕获错误。

【讨论】:

Panagiotis Kanavos 感谢您的回复,使用 c#7 作为 8 目前无法使用【参考方案2】:

在引入异步代码时必须进行这些更改是否正常?

当您更改任何方法的签名时,必须进行更改是正常的。如果您想重命名它并更改返回类型,那么是的,调用该方法的任何地方都必须更改。

更改它们的最佳方法是使它们也异步,一直到链上。

【讨论】:

以上是关于使用管道时一直异步的主要内容,如果未能解决你的问题,请参考以下文章

在模板中使用异步管道可观察到的不适用于单个值

尝试在我当前的 Angular 应用程序中使用异步管道而不是订阅

Angular 5:使用异步管道搜索 - 显示加载指示器

嵌套在 ngIf 中时,异步管道订阅无法正常工作

命名管道 - 异步窥视

离子生命周期取消/订阅 Firebase 数据库流(使用异步管道)