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");
Pattern Matching
Другой вариант - создать второй интерфейс только для асинхронных служб:
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;
}
}
}
Выражение сопоставления с образцом вызывает другую ветвь в зависимости от типа текущей службы. Привязка к типу создает строго типизированный экземпляр (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!");
}
}
Переключатель операторы не являются исчерпывающими, поэтому нам нужно добавить раздел default
, чтобы перехватывать ошибки во время выполнения.