【问题标题】:C# 7 - Why can't I return this awaitable type from an async method?C# 7 - 为什么我不能从异步方法返回这个等待类型?
【发布时间】:2017-08-30 11:07:58
【问题描述】:

背景

Try<T>

我正在使用的应用程序使用Try<T> 类型来处理函数样式的错误。 Try<T> 实例表示值或错误,类似于Nullable<T> 表示值或null 的方式。在函数的范围内可能会抛出异常,但是将异常“冒泡”到更高级别的组件会被通过返回值“管道”它们所取代。

这是Try<T> 实现的要点。 Error 类基本等价于Exception

public class Try<T> {

    private readonly T value;
    private readonly Error error;

    public bool HasValue { get; }

    public T Value {
        get {
            if (!HasValue) throw new InvalidOperationException();
            return value;
        }
    }

    public Error Error {
        get {
            if (HasValue) throw new InvalidOperationException();
            return error;
        }
    }

    internal Try(Error error) {
        this.error = error;
    }

    internal Try(T value) {
        this.value = value;
        HasValue = true;
    }
}

public static class Try {
    public static Try<T> Success<T>(T value) => new Try<T>(value);
    public static Try<T> Failure<T>(Error error) => new Try<T>(error);
}

async

我正在处理的应用程序也是高度异步的,并且使用标准的async/await 习惯用法。代码库专门使用Task&lt;T&gt;,不使用普通的旧Taskasync void 方法。在您通常会看到Task 的地方,使用Task&lt;FSharp.Core.Unit&gt;

正如你想象的那样,许多异步操作可能会出错,因此Task&lt;Try&lt;T&gt;&gt; 类型被大量使用。这工作正常,但会导致很多视觉混乱。由于 C# 7 现在允许 async 方法返回自定义等待类型,我想使用此功能创建一个有效的类 Task&lt;Try&lt;T&gt;&gt; 可以从 async 方法返回。

TryTask&lt;T&gt;

所以我创建了一个自定义的可等待任务类(它实际上将大部分功能委托给 Task&lt;Try&lt;T&gt;&gt; 字段)和一个随附的 AsyncMethodBuilder 类。

[AsyncMethodBuilder(typeof(TryTaskBuilder<>))]
public class TryTask<T>
{
    private readonly Task<Try<T>> _InnerTask;

    public TryTask(Func<Try<T>> function)
    {
        if (function == null) throw new ArgumentNullException(nameof(function));
        _InnerTask = new Task<Try<T>>(function);
    }

    internal TryTask(Task<Try<T>> task)
    {
        _InnerTask = task;
    }

    public void Start() => _InnerTask.Start();

    public TaskStatus Status => _InnerTask.Status;

    public Try<T> Result => _InnerTask.Result;

    public TaskAwaiter<Try<T>> GetAwaiter() => _InnerTask.GetAwaiter();

    public void Wait() => _InnerTask.Wait();
}

public static class TryTask
{
    public static TryTask<T> Run<T>(Func<Try<T>> function)
    {
        var t = new TryTask<T>(function);
        t.Start();
        return t;
    }

    public static TryTask<T> FromValue<T>(T value) => new TryTask<T>(Task.FromResult(Try.Success(value)));
    public static TryTask<T> FromError<T>(Error error) => new TryTask<T>(Task.FromResult(Try.Failure<T>(error)));
    public static TryTask<T> FromResult<T>(Try<T> result) => new TryTask<T>(Task.FromResult(result));
    public static TryTask<T> FromTask<T>(Task<Try<T>> task) => new TryTask<T>(task);
}

public class TryTaskBuilder<T>
{
    private AsyncTaskMethodBuilder<Try<T>> _InnerBuilder;

    public TryTaskBuilder()
    {
        _InnerBuilder = new AsyncTaskMethodBuilder<Try<T>>();
    }

    public static TryTaskBuilder<T> Create() =>
        new TryTaskBuilder<T>();

    public TryTask<T> Task =>
        default(TryTask<T>);

    public void Start<TStateMachine>(ref TStateMachine stateMachine)
        where TStateMachine : IAsyncStateMachine =>
        _InnerBuilder.Start(ref stateMachine);

    public void SetStateMachine(IAsyncStateMachine stateMachine) =>
        _InnerBuilder.SetStateMachine(stateMachine);

    public void SetResult(Try<T> result) =>
        _InnerBuilder.SetResult(result);

    public void SetException(Exception exception) =>
        _InnerBuilder.SetResult(exception.AsError<T>());

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine =>

        _InnerBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);

    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine =>
        _InnerBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
}

问题

为了使TryTask&lt;T&gt; 真正有用,我要做的第一件事是定义函数式letbind,map 高阶函数,它们将“解包”值并对其执行操作。这是一个例子:

public async static TryTask<T2> Bind<T1, T2>(
    this TryTask<T1> source, 
    Func<T1, Try<T2>> binding)
    {
        Try<T1> result1 = await source;

        Try<T2> result2 = result1.HasValue
            ? binding(result1.Value)
            : Try.Failure<T2>(result1.Error);

        return result2;
    }

此方法将无法编译,并出现错误CS0029:无法将类型Try&lt;T2&gt; 隐式转换为T2,在最后一行的符号result2 上。

如果我将最后一行更改为return result2.Value;,它将编译,但如果result2 有错误,那将无效。

问题

我怎样才能绕过这个错误并让这个类型作为async 方法的返回类型工作?在返回Task&lt;T&gt; 的典型async 方法中,您可以使用语句return default(T);,编译器会为您将T 包装在Task&lt;T&gt; 中。就我而言,我希望它在TryTask&lt;T&gt; 中包装Try&lt;T&gt;,但编译器希望它应该在某些东西中包装T。编译器使用什么方法来决定如何进行这种“包装”?

【问题讨论】:

  • 您是否尝试将T2 的值从Try 中提取出来并返回?
  • 这将编译,但它会忽略result2.HasValue == false 的潜力。必须从函数返回 Try&lt;T&gt;,以便可以将错误通过管道传递给函数调用链中的后续函数。
  • Try&lt;T2&gt;TryTask&lt;T2&gt; 一样吗?
  • 为了与正常的async 语法保持一致,您的方法声明需要返回TryTask&lt;Try&lt;T2&gt;&gt;。当然,这与您想要的语法冲突,但根据错误消息,这似乎是编译器想要的。尽管如此,我对你重新实现Task&lt;T&gt; 的愿望感到困惑。毕竟,Task&lt;T&gt; 已经可以包含结果或错误。 TryTask&lt;T&gt; 添加了什么?也许这只是我的头,但我不明白这一点。
  • @PeterDuniho Try&lt;T&gt; 类型背后的想法是在操作的返回类型中包含错误的可能性,而不是“抛出”异常,其中“捕获”对于调用代码是可选的。在某种意义上Task&lt;T&gt; 是类似的,但它有点不方便,因为它的错误处理被设计为适合标准异常抛出。我想要一个始终返回有效的Try&lt;T&gt; 结果(可能包含错误)并且从不抛出异常的任务。这统一了任务内外的错误处理,默认系统保持独立。

标签: c# asynchronous functional-programming async-await task


【解决方案1】:

如果我正确理解这一点(如果没有规范,这有点困难),根本问题是任务式提案的原作者 Lucian Wischik 对 async lambdas、as described here 的类型推断。

在你的情况下,这意味着:

void F<T>(Func<TryTask<T>> func) { }

F(async () => Try.Success(42));

lambda 返回 Try&lt;int&gt;,并且您希望编译器以某种方式从中找出 lambda 的类型应该是 Func&lt;TryTask&lt;int&gt;&gt;。但是根据上面链接的文档,没有什么好的方法。

这不是您的 Bind 的问题,但语言设计者选择让方法和 lambda 行为一致,而不是让方法更强大。

所以,据我所知,你想做的事情是不可能的。您可以考虑通过在the csharplang repo 创建问题来与 C# 的设计者分享您的用例,也许有人会想出如何解决这些问题并使其在未来版本的 C# 中发挥作用。

【讨论】:

  • 在这种情况下,我没有直接使用 lambdas。我看到了泛化的async 返回类型会使async lambdas 的类型推断更加困难。您是说在异步方法中将return T 标准“包装”到Task&lt;T&gt; 是由编译器将整个方法视为async lambda 并推断类型来完成的?
  • @JamesFaix 不。我的意思是,他们不能让 lambdas 工作,所以他们决定也不让它工作于方法,以保持一致性。
  • 我可以看到那里的推理。感谢您提供设计讨论的链接。
【解决方案2】:

接受的答案是正确的,但您可能需要考虑language-ext library,它的所有扩展方法都有delegate based Try implementation,以及Async 变体,以及转换@ 的ToAsync() 扩展987654323@ 到TryAsync&lt;A&gt;。它还有TryOption&lt;A&gt;TryOptionAsync&lt;A&gt; 用于返回SomeNoneFail

// Example of an action that could throw an exception
public Try<int> Foo() => () => 10;

// Synchronous Try
var result = Foo().IfFail(0);

// Synchronous Try
var result = Foo().Match(
    Succ: x => x,
    Fail: e => 0
);

// Asynchronous Try
var result = await Foo().IfFailAsync(0);

// Asynchronous Try
var result = await Foo().MatchAsync(
    Succ: x => x,
    Fail: e => 0
);

// Manually convert a Try to a TryAsync.  All operations are
// then async by default
TryAsync<int> bar = Foo().ToAsync();        

// Asynchronous Try
var result = await bar.IfFail(0);

// Asynchronous Try
var result = await bar.Match(
    Succ: x => x,
    Fail: e => 0
);

它有一个 massive selection of functional types,如果你正在实现像 Try 这样的类型,你可能会发现它很有用。

【讨论】:

  • 我实际上是昨天开始研究的。谢谢,非常相关!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-10-14
  • 2018-09-03
  • 1970-01-01
  • 2013-06-04
  • 2017-03-21
  • 2017-06-04
相关资源
最近更新 更多