使用 Task<T>.IsFaulted 表示 IO 故障而不传播异常

Using Task<T>.IsFaulted to denote IO failures without propagating exceptions

Before/without Task<>s,我正在使用如下所示的包装器对象来封装可能失败的 I/O 操作的结果,而不会将异常传播到调用堆栈:

public class FetchResult<T>
{
    public readonly bool Success;
    public readonly T Item;

    public FetchResult(bool success, T item)
    {
        this.Success = success;
        this.Item = item;
    }
}

我会这样使用它:

var userResult = Get("robert.paulson@fightclub.com");
if(!userResult.Success)
    // abort, or something

...

public FetchResult<User> Get(string email)
{
    try
    {
        // go to database here, and get the User
        return new FetchResult(true, new User());
    }
    catch
    {
        // log exception
        return new FetchResult(false, null);
    }
}

作为一个模型,这对我来说非常有用,因为它允许我在不使用 try/catch 作为程序控制流的情况下有效地管理异常,并为我提供了简单而细粒度的优雅服务降级。

然而,随着 Task<> 的出现,我很容易得到:

    public Task<FetchResult<User>> GetAsync(string email)

这似乎失控了。 看到我到处都迁移到 async,我正在考虑这样做:

    public Task<User> GetAsync(string email)

哪个 id 允许我做类似的事情:

var userTask = GetAsync("robert.paulson@fightclub.com");
await userTask;
if(userTask.IsFaulted) // (*) - see below
    // abort, or something

但是如果我的 GetAsync 方法 returns:

return Task<User>.FromException(new Exception());

在等待((*) 注释所在的位置)之后实际返回的内容似乎是已完成的 Task,其结果是 Task<User>,这是错误的。

为什么我在这种情况下得到了一个嵌套任务,我是否缺少一些语法糖来使整个事件更整洁?

Task.IsFaulted表示that the Task completed due to an unhandled exception。所以我不认为你可以用不同的方式设置它而不是让异常未被捕获。

public async Task<User> GetAsync(string email)
{
    // go to database here, and get the User
    return new User();
}

或者捕获、记录并重新抛出:

public async Task<User> GetAsync(string email)
{
    try
    {
        // go to database here, and get the User
        return new User();
    }
    catch
    {
        // log exception
        throw;
    }
}

我相信你是说你的 GetAsync 是 return 嵌套任务,因为你有这样的事情:

public async Task GetAsync(string email)
{
    // .... somewhere in the code
    return Task.FromException(new Exception());
}

但是当您使用 async 关键字声明一个方法时,C# 魔法会负责将其内容包装在 Task 中。因此,在这种情况下,您实际上最终得到的任务 return 是另一项任务 - Task<Task<T>>。如果删除 async 关键字,它将 return Task<T> 但显然你最终会使用标准同步方法。

有点不寻常,但您可以探索 Task.WhenAny method does not throw exception (see ) 这样的事实

var userTask = GetAsync("robert.paulson@fightclub.com");
await Task.WaitAny(userTask);
if(userTask.IsFaulted) // (*) - see below
    // abort, or something

任务库中有两种方法 FromException()FromException<TResult>() 都可以通过 TaskTask<TResult> 使用。

public static Task FromException(Exception exception)
{
   return FromException<VoidTaskResult>(exception);
}

public static Task<TResult> FromException<TResult>(Exception exception)
{
 ...
 ...
}

如果您调用 Task<TResult>.FromException()Task.FromException(),这两个调用没有区别。

您的方法签名是:public Task<User> GetAsync(string email) 现在,如果您尝试使用 Task<User>.FromException(new Exception()),这将是 return Task<VoidTaskResult>,这当然不是 Task<User> 类型。这意味着您可能遇到编译器错误。

如果您使用 Task<User>.FromException<User>(new Exception());Task.FromException<User>(new Exception());

,此编译时错误将消失

您得到评论中指定的 Task<Task<VoidTaskResult>> 意味着您的方法代码中还有一些示例代码中未提及的内容。

有关 .Net 源代码中 Task 方法的更多内部详细信息,请参阅 here

更新:

查看您的代码后发现了几个问题。

i) Return type as Task 避免了我之前在回答中提到的编译错误。

ii) 您 return 在 Async 方法中使用 Task 而无需等待意味着完整的任务对象将包装在 Another Task 类型中。

请参阅下面我调整以显示问题的示例。看到即使 return 类型从对象类型的任务更改为程序,仍然没有错误。这是因为 Object 是 C# 中任何自定义类型的基础。所以允许执行以下操作:

static async Task<object> GetAsync()
{
    try
    {
        throw new Exception();
    }
    catch (Exception e)
    {
        return Task.FromException<Program>(e);
    }
}

现在将 return 类型的方法更改为 Task<Program> 您将收到错误或更多警告。

现在解决问题的正确版本是等待任务,这样只有程序类型保留为结果,它将自动 returned 为 Task<Program>

正确版本:

 static async Task<Program> GetAsync()
        {
            try
            {
                throw new Exception();
            }
            catch (Exception e)
            {
                return await Task.FromException<Program>(e);
            }
        }

现在您将不会再看到任何嵌套任务。这是 await 工作原理的内部结构。如果你真的想知道为什么会这样,那么尝试使用 IL spy 分析所有上述 3 个版本的程序生成的 IL,你就会明白了。干杯!!!