Более простой способ игнорирования определенных типов исключений при ожидании Задачи - PullRequest
2 голосов
/ 19 октября 2019

В ожидании Task я хотел бы иметь простой способ игнорировать определенные типы исключений, такие как OperationCanceledException или TimeoutException или что-то еще. У меня появилась идея написать метод расширения, который мог бы обернуть мой Task и подавить тип Exception, который я привел бы в качестве аргумента. Итак, я написал это:

public static async Task Ignore<TException>(this Task task)
    where TException : Exception
{
    try
    {
        await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return;
        throw;
    }
}

Я могу использовать это так, и это работает ОК:

await myTask.Ignore<OperationCanceledException>();

Проблема в том, что он поддерживает только один тип исключения, и я долженнаписать другую версию для двух типов, другую для трех типов, другую для четырех и так далее. Это выходит из-под контроля, потому что я также хочу перегрузку этого расширения, которое будет игнорировать исключения задач, которые возвращают результат (Task<Result>). Поэтому мне понадобится еще одна серия методов расширения, чтобы охватить этот случай.

Вот моя реализация игнорирования одного типа исключения при ожидании Task<Result>:

public static async Task<TResult> Ignore<TResult, TException>(
    this Task<TResult> task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

Использование:

var result = await myInt32Task.Ignore<int, OperationCanceledException>(0);

Мой вопрос заключается в том, могу ли я написать эти методы так, чтобы они могли обрабатывать несколько типов игнорируемых исключений, без необходимости писать отдельный метод для каждого числа типов?

Ответы [ 4 ]

1 голос
/ 20 октября 2019

Как указано в ответе Vector Sigma , можно объединить мои оригинальные однотипные методы, чтобы добиться игнорирования нескольких типов исключений. Цепочка Ignore для Task<TResult> довольно неловкая из-за необходимого повторения типа TResult и defaultValue. Прочитав принятый ответ на вопрос о частичном выводе типа , я понял, как это исправить. Мне нужно ввести обобщенную задачу-оболочку struct, которая будет содержать это состояние и содержать цепной метод Ignore. Это предполагаемое использование:

var result = await myInt32Task.WithDefaultValue(0)
    .Ignore<OperationCanceledException>()
    .Ignore<TimeoutException>();

Вот оболочка задачи, которую я назвал TaskWithDefaultValue, и метод расширения WithDefaultValue.

public readonly struct TaskWithDefaultValue<TResult>
{
    private readonly Task<TResult> _task;
    private readonly TResult _defaultValue;

    public TaskWithDefaultValue(Task<TResult> task, TResult defaultValue)
    {
        _task = task;
        _defaultValue = defaultValue;
    }

    public Task<TResult> GetTask() => _task;
    public TaskAwaiter<TResult> GetAwaiter() => _task.GetAwaiter();

    public TaskWithDefaultValue<TResult> Ignore<TException>()
        where TException : Exception
    {
        var continuation = GetContinuation(_task, _defaultValue);
        return new TaskWithDefaultValue<TResult>(continuation, _defaultValue);

        async Task<TResult> GetContinuation(Task<TResult> t, TResult dv)
        {
            try
            {
                return await t.ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                if (ex is TException) return dv;
                throw;
            }
        }
    }
}

public static TaskWithDefaultValue<TResult> WithDefaultValue<TResult>(
    this Task<TResult> task, TResult defaultValue)
{
    return new TaskWithDefaultValue<TResult>(task, defaultValue);
}
1 голос
/ 19 октября 2019

Я бы использовал цепочку задач, чтобы избежать инициализации выполнения задачи в методе расширения.

public static Task<TResult> Ignore<TResult>(this Task<TResult> self, TResult defaultValue, params Type[] typesToIgnore)
{
    return self.ContinueWith(
        task =>
        {
            if (task.IsCanceled 
                && (typesToIgnore.Any(t => typeof(OperationCanceledException) == t || t.IsSubclassOf(typeof(OperationCanceledException))))) {
                return defaultValue;
            }

            if (!task.IsFaulted)
            {
                return task.Result;
            }

            if (typesToIgnore.Any(t => task.Exception.InnerException.GetType() == t ||
                                task.Exception.InnerException.GetType().IsSubclassOf(t)))
            {
                return defaultValue;
            }

            throw task.Exception.InnerException;
        }, TaskContinuationOptions.ExecuteSynchronously);
}
1 голос
/ 20 октября 2019

Насколько я понимаю, ваше желание состоит в том, чтобы иметь возможность игнорировать более 1 типа исключений в ожидании вашего Task. Ваше собственное решение кажется вашим лучшим вариантом для меня. Вы всегда можете просто «связать» вызовы, используя предложенное решение:

await myTask.Ignore<OperationCanceledException>().Ignore<IOException>().Ignore<TimeoutException>();

Это должно вернуть задачу, которая, по сути, представляет собой три вложенных блока try-catch. Если вы активно не хотите более элегантного определения , вы всегда можете уйти с более элегантным использованием ;)

Единственное, что не такэлегантная проблема заключается в том, что в случае TResult -возврата Task s вам придется «распространять» значение по умолчанию несколько раз. Если это не очень большая проблема, то вы можете получить то же самое:

await myTask.Ignore<int, OperationCanceledException>(0).Ignore<int, TimeoutException>(0);

В качестве "неясного" бонуса, обратите внимание, что таким образом вы можете очень легко предоставить различные возвращаемые значения по умолчанию для разные исключения . Так что необходимость повторения значения по умолчанию может в конечном итоге привести к вашей выгоде! Например, у вас может быть причина для возврата 0 в TimeOutException, но -1 в OperationCanceledException и т. Д. Если в итоге это становится вашей целью , помните, что использование is может оказаться не тем, что вам действительно нужно, скореечем точное Type равенство, потому что вы можете также хотеть возвратить разные значения по умолчанию для разных исключений, которые происходят из одного и того же Type (этот анализ начинает становиться довольно сложным, но вы, конечно, понимаете, в чем дело).

Обновление

Максимальный уровень элегантности цепочки вызовов для TResult версии, похоже, достигается за счет проверки типов во время компиляции:

public static async Task<TResult> Ignore<TResult, TException>(
this Task<TResult> task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

public static async Task<TResult> Ignore<TResult, TException>(
this Task task, TResult defaultValue)
    where TException : Exception
{
    try
    {
        return await (Task<TResult>)task;
    }
    catch (Exception ex)
    {
        if (ex is TException) return defaultValue;
        throw;
    }
}

public static Task Ignore<TException>(this Task task)
    where TException : Exception
{
    try
    {
        //await seems to create a new Task that is NOT the original task variable.
        //Therefore, trying to cast it later will fail because this is not a Task<TResult>
        //anymore (the Task<TResult> has been "swallowed").

        //For that reason, await the task in an independent function.
        Func<Task> awaitableCallback = async () => await task;

        awaitableCallback();

        //And return the original Task, so that it can be cast back correctly if necessary.
        return task;
    }
    catch (Exception ex)
    {
        //Same upon failure, return the original task.
        if (ex is TException) return task;
        throw;
    }
}

public static async Task<int> TestUse()
{
    Task<int> t = Task<int>.Run(() => 111);

    int result = await t.Ignore<TaskCanceledException>()
                        .Ignore<InvalidOperationException>()
                        .Ignore<int, TimeoutException>(0);

    return result;
}

Если вы готовы пожертвовать безопасностью во время компиляции, вы можете облегчить боль повторения, только указав исключения, которые вы хотите игнорировать, и добавив вызов «casting» в конце. Конечно, у этого есть свои проблемы, но вам нужно делать это только тогда, когда вам нужно игнорировать множественные исключения. В противном случае вам подходит один тип и соответствующий одиночный вызов Ignore<TResult, TException>().

Редактировать

На основе соответствующего комментария , поскольку шаблон асинхронного ожидания / ожиданиякажется, порождает новую задачу, которая оборачивает ожидаемый параметр задачи, переданный в методах Ignore выше, вызов примера действительно завершается с InvalidCastException, так как промежуточные вызовы игнорирования фактически изменили , задача и исходная задача получаетпотерял где-то в цепочке звонков. Поэтому метод «casting» Ignore был немного изменен, чтобы позволить возвращать исходное задание в конце, чтобы его можно было успешно откатить после всех вызовов Ignore по последним TResult на основе Ignore звонок. Код выше был изменен, чтобы исправить этот сценарий. Это не делает весь шаблон особенно элегантным, но, по крайней мере, сейчас он работает правильно.

1 голос
/ 19 октября 2019

Да, вы можете, но вы не можете сделать это, используя Generics.

Если вы хотите передать Type s как params, вы можете сделать это:

public static async Task<TResult> Ignore<TResult>
        (this Task<TResult> task, TResult defaultValue, params Type[] typesToIgnore)
        {
            try
            {
                return await task;
            }
            catch (Exception ex)
            {
                if (typesToIgnore.Any(type => type.IsAssignableFrom(ex.GetType())))
                {
                    return defaultValue;
                }

                throw;
            }
        }

Теперь, это гораздо менее привлекательно и , у вас нет generic constraint (where TException ...), но он должен выполнить работу.

...