我想写一个带out参数的async方法,像这样:

public async void Method1()
{
    int op;
    int result = await GetDataTaskAsync(out op);
}

我如何做到这一点在GetDataTaskAsync?


当前回答

这与Michael Gehling提供的答案非常相似,但我有自己的解决方案,直到我找到了他的解决方案,并注意到我不是第一个想到使用隐式转换的人。

无论如何,当nullable设置为启用时,我也想共享

public readonly struct TryResult<TOut>
{
    #region constructors

    public TryResult(bool success, TOut? value) => (Success, Value) = (success, value);

    #endregion

    #region properties

    public                                            bool  Success { get; init; }
    [MemberNotNullWhen(true, nameof(Success))] public TOut? Value   { get; init; }

    #endregion

    #region methods

    public static implicit operator bool(TryResult<TOut> result) => result.Success;
    public static implicit operator TryResult<TOut>(TOut value) => new (true, value);

    public void Deconstruct(out bool success, out TOut? value) => (success, value) = (Success, Value);

    public TryResult<TOut> Out([NotNullWhen(true)] out TOut? value)
    {
        value = Value;

        return this;
    }

    #endregion
}

然后你可以像这样写一个Try方法:

public static async Task<TryResult<byte[]>> TryGetBytesAsync(string file) =>
    File.Exists(file)
        ? await File.ReadAllBytesAsync(file)
        : default(TryResult<byte[]>);

像这样叫它:

if ((await TryGetBytesAsync(file)).Out(out var bytes))
    Console.WriteLine($"File has {bytes.Length} bytes.");

其他回答

你不能有带ref或out参数的异步方法。

Lucian Wischik解释了为什么这是不可能的MSDN线程:http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have-ref-or-out-parameters

As for why async methods don't support out-by-reference parameters? (or ref parameters?) That's a limitation of the CLR. We chose to implement async methods in a similar way to iterator methods -- i.e. through the compiler transforming the method into a state-machine-object. The CLR has no safe way to store the address of an "out parameter" or "reference parameter" as a field of an object. The only way to have supported out-by-reference parameters would be if the async feature were done by a low-level CLR rewrite instead of a compiler-rewrite. We examined that approach, and it had a lot going for it, but it would ultimately have been so costly that it'd never have happened.

对于这种情况,一个典型的解决方法是让async方法返回一个元组。 你可以像这样重写你的方法:

public async Task Method1()
{
    var tuple = await GetDataTaskAsync();
    int op = tuple.Item1;
    int result = tuple.Item2;
}

public async Task<Tuple<int, int>> GetDataTaskAsync()
{
    //...
    return new Tuple<int, int>(1, 2);
}

在异步方法中不能有ref或out参数(如前所述)。

这需要在数据移动中进行一些建模:

public class Data
{
    public int Op {get; set;}
    public int Result {get; set;}
}

public async void Method1()
{
    Data data = await GetDataTaskAsync();
    // use data.Op and data.Result from here on
}

public async Task<Data> GetDataTaskAsync()
{
    var returnValue = new Data();
    // Fill up returnValue
    return returnValue;
}

您获得了更容易重用代码的能力,而且它比变量或元组更具可读性。

我喜欢Try模式。这是一个整齐的图案。

if (double.TryParse(name, out var result))
{
    // handle success
}
else
{
    // handle error
}

但是,使用异步是有挑战性的。这并不意味着我们没有真正的选择。下面是Try模式的准版本中可以考虑的异步方法的三种核心方法。

方法1 -输出一个结构

这看起来很像一个sync Try方法,只返回一个元组,而不是一个带out形参的bool类型,我们都知道这在c#中是不允许的。

var result = await DoAsync(name);
if (result.Success)
{
    // handle success
}
else
{
    // handle error
}

使用一个返回true或false且从不抛出异常的方法。

请记住,在Try方法中抛出异常会破坏该模式的整个目的。

async Task<(bool Success, StorageFile File, Exception exception)> DoAsync(string fileName)
{
    try
    {
        var folder = ApplicationData.Current.LocalCacheFolder;
        return (true, await folder.GetFileAsync(fileName), null);
    }
    catch (Exception exception)
    {
        return (false, null, exception);
    }
}

方法2 -传入回调方法

我们可以使用匿名方法来设置外部变量。它的语法很聪明,尽管有点复杂。小剂量的话,没问题。

var file = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => file = x, x => exception = x))
{
    // handle success
}
else
{
    // handle failure
}

该方法遵循Try模式的基本原则,但设置了要传递给回调方法的参数。是这样做的。

async Task<bool> DoAsync(string fileName, Action<StorageFile> file, Action<Exception> error)
{
    try
    {
        var folder = ApplicationData.Current.LocalCacheFolder;
        file?.Invoke(await folder.GetFileAsync(fileName));
        return true;
    }
    catch (Exception exception)
    {
        error?.Invoke(exception);
        return false;
    }
}

我对这里的表现有一个疑问。但是,c#编译器非常聪明,我认为你选择这个选项是安全的,几乎可以肯定。

方法3 -使用ContinueWith

如果您只是按照设计使用TPL呢?没有元组。这里的思想是使用异常将ContinueWith重定向到两条不同的路径。

await DoAsync(name).ContinueWith(task =>
{
    if (task.Exception != null)
    {
        // handle fail
    }
    if (task.Result is StorageFile sf)
    {
        // handle success
    }
});

使用在出现任何类型的失败时都会抛出异常的方法。这与返回布尔值不同。这是与TPL沟通的一种方式。

async Task<StorageFile> DoAsync(string fileName)
{
    var folder = ApplicationData.Current.LocalCacheFolder;
    return await folder.GetFileAsync(fileName);
}

在上面的代码中,如果没有找到文件,则抛出异常。这将调用处理Task的失败ContinueWith。其逻辑块中的异常。整洁的,是吧?

听着,我们喜欢尝试模式是有原因的。从根本上说,它是如此的整洁和可读,因此,是可维护的。当你选择你的方法,看门狗的可读性。记住下一个开发者,他们在6个月后不需要你回答澄清问题。您的代码可能是开发人员拥有的唯一文档。

祝你好运。

我认为像这样使用ValueTuples是可行的。你必须先添加ValueTuple NuGet包:

public async void Method1()
{
    (int op, int result) tuple = await GetDataTaskAsync();
    int op = tuple.op;
    int result = tuple.result;
}

public async Task<(int op, int result)> GetDataTaskAsync()
{
    int x = 5;
    int y = 10;
    return (op: x, result: y):
}

异步方法不接受输出参数的限制仅适用于编译器生成的异步方法,这些方法使用async关键字声明。它不适用于手工制作的异步方法。换句话说,可以创建Task返回接受输出参数的方法。例如,假设我们已经有一个会抛出的ParseIntAsync方法,我们想创建一个不抛出的TryParseIntAsync方法。我们可以这样实现它:

public static Task<bool> TryParseIntAsync(string s, out Task<int> result)
{
    var tcs = new TaskCompletionSource<int>();
    result = tcs.Task;
    return ParseIntAsync(s).ContinueWith(t =>
    {
        if (t.IsFaulted)
        {
            tcs.SetException(t.Exception.InnerException);
            return false;
        }
        tcs.SetResult(t.Result);
        return true;
    }, default, TaskContinuationOptions.None, TaskScheduler.Default);
}

使用TaskCompletionSource和ContinueWith方法有点尴尬,但是没有其他选择,因为我们不能在这个方法中使用方便的await关键字。

使用的例子:

if (await TryParseIntAsync("-13", out var result))
{
    Console.WriteLine($"Result: {await result}");
}
else
{
    Console.WriteLine($"Parse failed");
}

更新:如果异步逻辑太复杂,没有await就无法表达,那么可以将它封装在嵌套的异步匿名委托中。out参数仍然需要TaskCompletionSource。out参数可以在之前完成 主任务的完成,如下例所示:

public static Task<string> GetDataAsync(string url, out Task<int> rawDataLength)
{
    var tcs = new TaskCompletionSource<int>();
    rawDataLength = tcs.Task;
    return ((Func<Task<string>>)(async () =>
    {
        var response = await GetResponseAsync(url);
        var rawData = await GetRawDataAsync(response);
        tcs.SetResult(rawData.Length);
        return await FilterDataAsync(rawData);
    }))();
}

这个例子假设存在三个异步方法GetResponseAsync, GetRawDataAsync和FilterDataAsync被调用 在继承。out参数在第二个方法完成时完成。GetDataAsync方法可以这样使用:

var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");

在这个简化的示例中,在等待rawDataLength之前等待数据是很重要的,因为在异常的情况下,out参数将永远不会完成。