如果我有一个应用程序,它使用一个包含许多阶段的管道,在所有阶段上使用foreach
执行,并调用:
CanExecute
Execute
接口是这样的:
public interface IService
{
bool CanExecute(IContext subject);
IContext Execute(IContext subject);
}
它基本上接受一个上下文,并返回一个它变得更丰富的上下文。
在其中一个阶段Execute
方法中,我需要调用一个服务,并希望执行异步操作。因此,现在Execute
方法需要更改为例如
Task<IContext> ExecuteAsync(IContext subject);
其中CCD_ 4用于对服务的呼叫。
所有其他阶段都没有异步代码,但现在需要更改,因为最佳实践是"一路异步"。
当引入异步代码时,必须进行这些更改是正常的吗?
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;
}
}
}
模式匹配表达式根据当前服务的类型调用不同的分支。对类型进行Nathing会生成一个强类型实例(a或b(,该实例可用于调用适当的方法。
开关表达式是详尽无遗的——如果编译器无法验证所有选项是否与模式匹配,则会生成警告。
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!");
}
}
Switch语句并不详尽,因此我们需要添加default
部分以在运行时捕获错误。
引入异步代码时必须进行这些更改是正常的吗?
当您更改任何方法的签名时,必须进行更改是正常的。如果您想重命名它并更改返回类型,那么是的,该方法调用的所有地方都必须更改。
改变它们的最好方法是让它们也异步,一直到链的上游。