Кодовые контракты и асинхронность
Каков рекомендуемый способ добавления постусловий к методам async, которые возвращают Task<T>
?
Я прочитал следующее предложение:
http://social.msdn.microsoft.com/Forums/hu-HU/async/thread/52fc521c-473e-4bb2-a666-6c97a4dd3a39
Сообщение предлагает реализовать каждый метод как синхронный, сжимающий его, а затем реализовать асинхронный экземпляр в качестве простой оболочки. К сожалению, я не рассматриваю это как приемлемое решение (возможно, из-за моего собственного недоразумения):
- Асинхронный метод, хотя предполагается, что он является оболочкой для метода синхронизации, остается без какого-либо реального кодового контракта и поэтому может делать то, что он хочет.
- Кодовые базы, которые привязаны к асинхронности, вряд ли реализуют синхронизирующие копии для всего. В результате внедрение новых методов, содержащих
await
для других методов async, следовательно, должно быть асинхронным. Эти методы по сути являются асинхронными и не могут быть легко преобразованы в синхронные. Они не просто обертки.
Даже если мы отменили последнее утверждение, сказав, что мы могли бы использовать .Result
или .Wait()
вместо await
(что фактически вызвало бы тупик в некотором SyncContext
, и его нужно было бы переписать в async метод в любом случае), я все еще уверен в первом вопросе.
Есть ли альтернативные идеи или есть что-то, что я пропустил о кодовых контрактах и TPL?
Ответы
Ответ 1
Я указал это на команду Async, как это сделали другие. В настоящее время контракты и Async (почти) взаимоисключающие. Таким образом, по крайней мере некоторые люди в Microsoft знают о проблеме, но я не знаю, что они планируют делать с ней.
Я не рекомендую писать асинхронные методы как обертки для методов синхронизации. Фактически, я хотел бы сделать обратное.
Предпосылки могут работать. Я не пробовал это недавно; вам может понадобиться небольшая обертка вокруг вашего асинхронного метода, который включает предварительные условия.
Постусловия в значительной степени нарушены.
Утверждения и допущения работают нормально, но статическая проверка действительно ограничена, поскольку постословия нарушены.
Инварианты не имеют особого смысла в мире Async, где изменяемое состояние имеет тенденцию просто мешать. (Async аккуратно отталкивает вас от ООП и к функциональному стилю).
Будем надеяться, что в VS vNext контракты будут обновлены с помощью типа postcondition, поддерживающего асинхронную настройку, что также позволит статической проверке работать лучше с утверждениями в методах async.
В то же время вы можете иметь притворство-постусловие, написав предположение:
// Synchronous version for comparison.
public static string Reverse(string s)
{
Contract.Requires(s != null);
Contract.Ensures(Contract.Result<string>() != null);
return ...;
}
// First wrapper takes care of preconditions (synchronously).
public static Task<string> ReverseAsync(string s)
{
Contract.Requires(s != null);
return ReverseWithPostconditionAsync(s);
}
// Second wrapper takes care of postconditions (asynchronously).
private static async Task<string> ReverseWithPostconditionAsync(string s)
{
var result = await ReverseImplAsync(s);
// Check our "postcondition"
Contract.Assume(result != null);
return result;
}
private static async Task<string> ReverseImplAsync(string s)
{
return ...;
}
Некоторое использование кодовых контрактов просто невозможно, например, указывая постусловия на асинхронных элементах интерфейсов или базовых классов.
Лично я просто избегал Контрактов полностью в моем коде Async, надеясь, что Microsoft исправит его через несколько месяцев.
Ответ 2
Набрал это, но забыл нажать "Опубликовать"...:)
На данный момент не существует специализированной поддержки для этого. Лучшее, что вы можете сделать, это что-то вроде этого (не используя ключевое слово async
, но та же идея - возможно, переписывающий будет работать по-разному под асинхронным CTP, я еще не пробовал):
public static Task<int> Do()
{
Contract.Ensures(Contract.Result<Task<int>>() != null);
Contract.Ensures(Contract.Result<Task<int>>().Result > 0);
return Task.Factory.StartNew(() => { Thread.Sleep(3000); return 2; });
}
public static void Main(string[] args)
{
var x = Do();
Console.WriteLine("processing");
Console.WriteLine(x.Result);
}
Однако это означает, что метод async фактически не вернется, пока задача не завершит оценку, поэтому "обработка" не будет напечатана до истечения 3 секунд. Это похоже на проблему с методами, которые лениво возвращают IEnumerable
— Контракт должен перечислять все элементы в IEnumerable
, чтобы убедиться, что условие выполнено, даже если вызывающий объект фактически не использует все элементы.
Вы можете обойти это, изменив режим контрактов на Preconditions
, но это означает, что пост-условия не будут проверены.
Статический контролер также не может подключить Result
к лямбда, поэтому вы получите сообщение "Обеспечивает недоказанное". (В общем, статический контролер не все равно доказывает информацию о лямбда/делегатах.)
Я думаю, что для правильной поддержки задач/ожиданий команда Code Contracts будет иметь специальные задачи, чтобы добавить проверку предварительного условия только при доступе в поле Result
.
Ответ 3
Отправка нового ответа на этот старый поток, поскольку он возвращается Google как первый ответ на вопрос о CodeContract и Async
В то время как Контракт на методы async, возвращающие Task, работает правильно, и нет необходимости их избегать.
Стандартный договор для асинхронного метода:
[ContractClass(typeof(ContractClassForIFoo))]
public interface IFoo
{
Task<object> MethodAsync();
}
[ContractClassFor(typeof(IFoo))]
internal abstract class ContractClassForIFoo : IFoo
{
#region Implementation of IFoo
public Task<object> MethodAsync()
{
Contract.Ensures(Contract.Result<Task<object>>() != null);
Contract.Ensures(Contract.Result<Task<object>>().Status != TaskStatus.Created);
Contract.Ensures(Contract.Result<object>() != null);
throw new NotImplementedException();
}
#endregion
}
public class Foo : IFoo
{
public async Task<object> MethodAsync()
{
var result = await Task.FromResult(new object());
return result;
}
}
Если вы считаете, что контракт не выглядит корректным, я согласен, что он выглядит как неверный, но он действительно работает. И это не похоже на то, что контрактный автор переписывает задачу преждевременно.
Как Стивен поднял некоторые сомнения, еще несколько тестов и контрактов в моем случае сделали свое дело правильно.
Код, используемый для тестирования:
public static class ContractsAbbreviators
{
[ContractAbbreviator]
public static void EnsureTaskIsStarted()
{
Contract.Ensures(Contract.Result<Task>() != null);
Contract.Ensures(Contract.Result<Task>().Status != TaskStatus.Created);
}
}
[ContractClass(typeof(ContractClassForIFoo))]
public interface IFoo
{
Task<int> MethodAsync(int val);
}
[ContractClassFor(typeof(IFoo))]
internal abstract class ContractClassForIFoo : IFoo
{
public Task<int> MethodAsync(int val)
{
Contract.Requires(val >= 0);
ContractsAbbreviators.EnsureTaskIsStarted();
Contract.Ensures(Contract.Result<int>() == val);
Contract.Ensures(Contract.Result<int>() >= 5);
Contract.Ensures(Contract.Result<int>() < 10);
throw new NotImplementedException();
}
}
public class FooContractFailTask : IFoo
{
public Task<int> MethodAsync(int val)
{
return new Task<int>(() => val);
// esnure raises exception // Contract.Ensures(Contract.Result<Task>().Status != TaskStatus.Created);
}
}
public class FooContractFailTaskResult : IFoo
{
public async Task<int> MethodAsync(int val)
{
await Task.Delay(val).ConfigureAwait(false);
return val + 1;
// esnure raises exception // Contract.Ensures(Contract.Result<int>() == val);
}
}
public class Foo : IFoo
{
public async Task<int> MethodAsync(int val)
{
const int maxDeapth = 9;
await Task.Delay(val).ConfigureAwait(false);
if (val < maxDeapth)
{
await MethodAsync(val + 1).ConfigureAwait(false);
}
return val;
}
}