ContinueWith 和 TaskCancellation - 如果任务失败,如何 return 默认值?
ContinueWith and TaskCancellation - How to return default-values if task fails?
我阅读了一些关于 TaskCancellations 的帖子。但是,我找不到一个简单问题的解决方案:任务失败时如何获取默认值?
我不能 (!) 修改任务本身并在其周围放置一个 try catch 包装器。我当然可以在 await 周围放一个 try-catch,但我想用 ContinueWith
来处理这个问题——如果可能的话。
public Task<List<string>> LoadExample()
{
Task<List<string>> task = LoadMyExampleTask();
task.ContinueWith(t => default(List<string>), TaskContinuationOptions.OnlyOnFaulted);
return task;
}
我认为这是处理问题的正确方法。但是,我的应用程序抛出 JsonParseException
(在 LoadMyExampleTask
中调用)。我希望得到 null 或(甚至更好)一个空列表。
其实我想要的只是:
var emptyOrFilledList = await LoadExample(); // guaranteed no exception thrown
基于 Luaan 的出色回答,我写了一个带有默认值选项的扩展方法:
public static Task<T> DefaultIfFaulted<T>(this Task<T> @this, T defaultValue = default(T))
{
return @this.ContinueWith(t => t.IsCompleted ? t.Result : defaultValue);
}
编辑:await myTask.DefaultifFaulted()
刚刚抛出一个
[ERROR] FATAL UNHANDLED EXCEPTION: System.AggregateException
你确定每个异常都被捕获了吗?
如果你想要那个,你不能 return 原始 任务 - 你需要 return 延续。
public Task<List<string>> LoadExample()
{
Task<List<string>> task = LoadMyExampleTask();
return task.ContinueWith(t =>
t.IsFaulted || t.IsCanceled ? default(List<string>) : t.Result);
}
当原始任务出错时,您的原始代码确实允许继续 运行,但您没有读取 that 任务的状态 - 事实上任务有一个处理错误的延续,与原始任务上的 await
将执行的操作完全无关。
当然,把它变成一个通用的辅助方法是相当容易的:
public static Task<T> DefaultIfFaulted<T>(this Task<T> @this)
{
return @this.ContinueWith (t => t.IsCanceled || t.IsFaulted ? default(T) : t.Result);
}
正如所承诺的那样,这里有 DefaultIfFaulted<T>
个名副其实的变体(以及这个问题的标题)。他们保留先前任务的行为,除非它出现故障(具体来说,取消被传播而不是被忽略或被 AggregateException
掩盖):
Old-school (.NET 4.0) 方式:
public static Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
// The continuation simply returns the antecedent task unless it's faulted.
Task<Task<T>> continuation = task.ContinueWith(
t => (t.Status == TaskStatus.Faulted) ? Task.FromResult(default(T)) : t,
TaskContinuationOptions.ExecuteSynchronously
);
return continuation.Unwrap();
}
Async/await方式(简单但较慢):
public static async Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
try
{
return await task.ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
return default(T);
}
}
Async/await 方式(性能几乎与 Unwrap
相同):
public static async Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
// Await completion regardless of resulting Status (alternatively you can use try/catch).
await task
.ContinueWith(_ => { }, TaskContinuationOptions.ExecuteSynchronously)
.ConfigureAwait(false);
return task.Status != TaskStatus.Faulted
// This await preserves the task's behaviour
// in all cases other than faulted.
? await task.ConfigureAwait(continueOnCapturedContext: false)
: default(T);
}
测试(通过以上所有测试):
using Xunit;
[Fact]
public async Task DefaultIfFaultedTest()
{
var success = Task.Run(() => 42);
var faulted = Task.Run(new Func<int>(() => { throw new InvalidOperationException(); }));
Assert.Equal(42, await success.DefaultIfFaulted());
Assert.Equal(0, await faulted.DefaultIfFaulted());
await Assert.ThrowsAsync<TaskCanceledException>(() =>
{
var tcs = new TaskCompletionSource<int>();
tcs.SetCanceled();
return tcs.Task.DefaultIfFaulted();
});
}
我阅读了一些关于 TaskCancellations 的帖子。但是,我找不到一个简单问题的解决方案:任务失败时如何获取默认值?
我不能 (!) 修改任务本身并在其周围放置一个 try catch 包装器。我当然可以在 await 周围放一个 try-catch,但我想用 ContinueWith
来处理这个问题——如果可能的话。
public Task<List<string>> LoadExample()
{
Task<List<string>> task = LoadMyExampleTask();
task.ContinueWith(t => default(List<string>), TaskContinuationOptions.OnlyOnFaulted);
return task;
}
我认为这是处理问题的正确方法。但是,我的应用程序抛出 JsonParseException
(在 LoadMyExampleTask
中调用)。我希望得到 null 或(甚至更好)一个空列表。
其实我想要的只是:
var emptyOrFilledList = await LoadExample(); // guaranteed no exception thrown
基于 Luaan 的出色回答,我写了一个带有默认值选项的扩展方法:
public static Task<T> DefaultIfFaulted<T>(this Task<T> @this, T defaultValue = default(T))
{
return @this.ContinueWith(t => t.IsCompleted ? t.Result : defaultValue);
}
编辑:await myTask.DefaultifFaulted()
刚刚抛出一个
[ERROR] FATAL UNHANDLED EXCEPTION: System.AggregateException
你确定每个异常都被捕获了吗?
如果你想要那个,你不能 return 原始 任务 - 你需要 return 延续。
public Task<List<string>> LoadExample()
{
Task<List<string>> task = LoadMyExampleTask();
return task.ContinueWith(t =>
t.IsFaulted || t.IsCanceled ? default(List<string>) : t.Result);
}
当原始任务出错时,您的原始代码确实允许继续 运行,但您没有读取 that 任务的状态 - 事实上任务有一个处理错误的延续,与原始任务上的 await
将执行的操作完全无关。
当然,把它变成一个通用的辅助方法是相当容易的:
public static Task<T> DefaultIfFaulted<T>(this Task<T> @this)
{
return @this.ContinueWith (t => t.IsCanceled || t.IsFaulted ? default(T) : t.Result);
}
正如所承诺的那样,这里有 DefaultIfFaulted<T>
个名副其实的变体(以及这个问题的标题)。他们保留先前任务的行为,除非它出现故障(具体来说,取消被传播而不是被忽略或被 AggregateException
掩盖):
Old-school (.NET 4.0) 方式:
public static Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
// The continuation simply returns the antecedent task unless it's faulted.
Task<Task<T>> continuation = task.ContinueWith(
t => (t.Status == TaskStatus.Faulted) ? Task.FromResult(default(T)) : t,
TaskContinuationOptions.ExecuteSynchronously
);
return continuation.Unwrap();
}
Async/await方式(简单但较慢):
public static async Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
try
{
return await task.ConfigureAwait(false);
}
catch (Exception ex) when (!(ex is OperationCanceledException))
{
return default(T);
}
}
Async/await 方式(性能几乎与 Unwrap
相同):
public static async Task<T> DefaultIfFaulted<T>(this Task<T> task)
{
// Await completion regardless of resulting Status (alternatively you can use try/catch).
await task
.ContinueWith(_ => { }, TaskContinuationOptions.ExecuteSynchronously)
.ConfigureAwait(false);
return task.Status != TaskStatus.Faulted
// This await preserves the task's behaviour
// in all cases other than faulted.
? await task.ConfigureAwait(continueOnCapturedContext: false)
: default(T);
}
测试(通过以上所有测试):
using Xunit;
[Fact]
public async Task DefaultIfFaultedTest()
{
var success = Task.Run(() => 42);
var faulted = Task.Run(new Func<int>(() => { throw new InvalidOperationException(); }));
Assert.Equal(42, await success.DefaultIfFaulted());
Assert.Equal(0, await faulted.DefaultIfFaulted());
await Assert.ThrowsAsync<TaskCanceledException>(() =>
{
var tcs = new TaskCompletionSource<int>();
tcs.SetCanceled();
return tcs.Task.DefaultIfFaulted();
});
}