我试图延迟处理从 WinRT 中的键盘事件调用的方法(示例中的 SubmitQuery()),直到一段时间内(本例中为 500 毫秒)没有进一步的事件。
我只希望 SubmitQuery() 在我认为用户已完成输入时运行。
使用下面的代码,当 Task.Delay(500,cancelToken.Token); 时,我不断收到 System.Threading.Tasks.TaskCanceledException;叫做。请问我在这里做错了什么?
CancellationTokenSource cancellationToken = new CancellationTokenSource();
private async void SearchBox_QueryChanged(SearchBox sender, SearchBoxQueryChangedEventArgs args)
{
cancellationToken.Cancel();
cancellationToken = new CancellationTokenSource();
await Task.Delay(500, cancellationToken.Token);
if (!cancellationToken.IsCancellationRequested)
{
await ViewModel.SubmitQuery();
}
}
如果添加带有空操作的
ContinueWith()
,则 tsk.Exception
中的 ContinueWith()
属性。但它可以让您免于编写丑化代码的 try/catch。
await Task.Delay(500, cancellationToken.Token).ContinueWith(tsk => { });
更新:
不用编写代码来处理异常,布尔值会更干净。 仅当预期延迟取消时才首选此方式!。一种方法是创建一个辅助类(虽然我不太喜欢辅助类)
namespace System.Threading.Tasks
{
public static class TaskDelay
{
public static Task<bool> Wait(TimeSpan timeout, CancellationToken token) =>
Task.Delay(timeout, token).ContinueWith(tsk => tsk.Exception == default);
public static Task<bool> Wait(int timeoutMs, CancellationToken token) =>
Task.Delay(timeoutMs, token).ContinueWith(tsk => tsk.Exception == default);
}
}
例如:
var source = new CancellationTokenSource();
if(!await TaskDelay.Wait(2000, source.Token))
{
// The Delay task was canceled.
}
(别忘了处理源)
这是可以预料的。当你取消旧的
Delay
时,它会引发异常;这就是取消的原理。您可以在 try
周围放置一个简单的 catch
/Delay
来捕获预期的异常。
请注意,如果您想执行这样的基于时间的逻辑,Rx 比
async
更自然。
我认为值得添加一个评论来说明为什么它会这样工作。
该文档实际上是错误的,或者对于
Task.Delay
方法的 TaskCancelledException 写得不清楚。 Delay
方法本身永远不会抛出该异常。它将任务转换为取消状态,而到底引发异常的是await
。这里使用 Task.Delay
方法并不重要。它与任何其他已取消的任务的工作方式相同,这就是取消的预期工作方式。这实际上解释了为什么添加延续会神秘地隐藏异常。因为这是await
造成的。
Task.WhenAny
:
创建一个任务,该任务将在任何提供的任务完成后完成。
await Task.WhenAny(Task.Delay(500, token)); // Ignores the exception
这种方法的一个问题是它没有清楚地传达其意图,因此建议添加注释。另一个是它会导致分配(因为
params
签名中的 Task.WhenAny
)。
注意: 当
Task
发生故障且未直接或间接观察到其 Exception
时,会在未来某个时刻(非确定性)为此任务触发 TaskScheduler.UnobservedTaskException
事件。上面显示的技巧,使用 Task.WhenAny
方法,not 观察任务的 Exception
,因此事件 will 被触发。在 Task.Delay(500, token)
的特定情况下,任务以取消状态完成,而不是出现故障,因此无论如何它都不会触发事件。
奇怪的是,取消异常似乎只在取消标记位于 Task.Delay 上时才会抛出。将token放在ContinueWith上,不会抛出取消异常:
Task.Delay(500).ContinueWith(tsk => {
//code to run after the delay goes here
}, cancellationToken.Token);
如果你真的想捕获任何取消异常,你可以链接另一个 .ContinueWith() - 它将被传递到那里。
如果您不关心等待的任务可能引发的任何异常,您可以创建如下扩展方法:
public static class TaskExtensions
{
public static async Task<bool> TryAsync(this Task task)
{
try
{
await task;
return true;
}
catch
{
return false;
}
}
}
然后像这样使用它:
var wasCompleted = await Task.Delay(500, cancellationToken).TryAsync();
if (wasCompleted)
{
// do something
}
作为补充,您可以为
Task<T>
添加类似的扩展方法到 TaskExtensions
类。
public static async Task<(bool wasCompleted, T result)> TryAsync<T>(this Task<T> task)
{
try
{
var result = await task;
return (true, result);
}
catch
{
return (false, default(T));
}
}
用途:
var (wasCompleted, result) = await GetSomethingAsync().TryAsync();
if (wasCompleted)
{
var x = result.X;
}