Итак, первое, на что нужно обратить внимание, - это то, что метод, являющийся асинхронным, не означает «у него есть ключевое слово async
». Метод является асинхронным, когда он очень быстро возвращается к вызывающей стороне, а затем завершает любую операцию, которая была ему запрошена , после возврата к вызывающей стороне и позволяет вызывающей стороне продолжать делать все, что он хочет. Обычно это также позволяет вызывающему абоненту узнать, когда операция завершилась, была ли она успешной или нет, и иногда включить какой-либо результат операции.
Ключевое слово async
просто позволяет этому методу иметь ключевое слово await
. Если бы вы не пометили метод как async
, он бы не знал, были ли какие-либо случаи использования await
на самом деле обычными именами переменных, а не специальным ключевым словом. await
говорит, что вы хотите, чтобы метод планировал выполнение кода, следующего за await
, после завершения ожидаемой задачи.
Имея это в виду, мы можем просмотреть ваши методы и посмотреть, что они делают, и если они асинхронные.
public async Task<byte[]> GetAsync(string key)
{
async Task<byte[]> Action() => await this.Cache.GetAsync(key);
return await this.Execute(Action);
}
Итак, давайте сначала рассмотрим только внутренний метод:
async Task<byte[]> Action() => await this.Cache.GetAsync(key);
Выполняет асинхронную операцию, GetAsync
, планирует продолжение продолжения после его завершения, которое ничего не делает ... и затем возвращает точный результат, который вернул GetAsync
. Таким образом, кроме некоторых накладных расходов, связанных с добавлением продолжений, этот метод идентичен простому выписыванию:
Task<byte[]> Action() => this.Cache.GetAsync(key);
Теперь, когда мы посмотрим на метод, он находится в:
public async Task<byte[]> GetAsync(string key)
{
return await this.Execute(Action);
}
Мы можем видеть, что этот метод также вызывает асинхронный метод, добавляет к нему продолжение, которое ничего не делает ... и затем возвращает результат выполнения этого метода в точности как есть.
Теперь перейдем к следующему методу, опять же, сначала посмотрим на внутренний метод:
void Action() => this.Cache.DeleteAsync(key);
Здесь мы вызываем асинхронный метод, но мы не возвращаем Task
, который он нам дает. Это означает, что у нас нет возможности узнать, когда операция закончится или она прошла успешно. Поскольку DeleteAsync
является асинхронным (или мы можем предположить, учитывая имя), мы знаем, что этот метод вернётся, как только запустил асинхронную операцию, а не после выполнения основной операции.
public async Task DeleteAsync(string key)
{
await this.Execute(Action);
}
Это не компилируется. Action
вот метод, который возвращает void
, поэтому вы вызываете перегрузку Execute
, которая возвращает void
, и вы не можете await
a void
выражение. Если вы изменили код на:
public async Task DeleteAsync(string key)
{
Task Action() => this.Cache.DeleteAsync(key);
await this.Execute(Action);
}
Затем он будет скомпилирован, потому что вы будете вызывать версию Execute
, которая принимает Func<T>
и возвращает результат, так что вы сможете дождаться этой задачи, но, как мы видели из более ранние методы, ожидающие его, не делают ничего полезного, кроме как добавить некоторые накладные расходы, мы могли бы просто вернуть задачу и покончить с ней.
private void Execute(Action action)
{
this.Execute(() =>
{
action();
return 0;
});
}
Если мы внесем указанное выше изменение, оно никогда не будет вызвано, поскольку мы никогда не передадим делегат, который возвращает void
.
private T Execute<T>(Func<T> action)
{
if (someCondition)
{
try
{
return action();
}
catch (RedisConnectionException)
{
//do something
}
}
else
{
// do something else
}
return default(T);
}
Здесь все усложняется. В приведенных выше примерах оба метода возвращают что-то , поэтому эта перегрузка вызывается. Обе эти вещи являются задачами своего рода. Таким образом, этот код будет просто проверять условие и переходить к «// делать что-то еще», если оно ложно, как в синхронной версии. К сожалению, затем он будет возвращать значение по умолчанию, которое для Task
равно null
. Это, наверное, плохо. Когда эта задача будет возвращена, кто-то, вероятно, когда-нибудь await
выполнит ее, и тогда они просто получат исключение нулевой ссылки. Вызывающие, вероятно, хотят, чтобы происходило здесь, чтобы получить Task<T>
, результатом которого является значение по умолчанию.
Если условие истинно, хотя оно вызовет асинхронный метод, вычислите задачу, представляющую результаты этой операции, всякий раз, когда она завершается, и возвращайте ее. Соответственно, если в какой-то момент операция завершается с ошибкой и возвращает ошибочную задачу , ваш блок catch не будет запускаться . Блок catch
будет работать только в том случае, если action
выдает исключение вместо возврата сбойной задачи. (Чего не делает большинство асинхронных методов. В частности, любой async
метод никогда не сделает это.)
Переписать Execute
для получения ExecuteAsync
verion довольно просто. Вместо того, чтобы принимать функции, которые возвращают некоторый результат, или void, вам нужно принимать функции
которые возвращают Task<T>
или Task
, и вместо этого возвращают Task<T>
или Task
. Кроме этого, единственное, что нужно сделать, это await
любая задача в любое время, когда вы не хотите, чтобы остальная часть кода не выполнялась, пока эта задача не будет завершена:
private Task<T> ExecuteAsync<T>(Func<Task<T>> action)
{
if (someCondition)
{
try
{
return await action();
}
catch (RedisConnectionException)
{
//do something
}
}
else
{
// do something else
}
return default(T);
}
А затем перегрузка для метода без результата в его задаче:
private Task ExecuteAsync(Func<Task> action)
{
return this.ExecuteAsync(async () =>
{
await action();
return 0;
});
}