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

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

我如何做到这一点在GetDataTaskAsync?


当前回答

我有同样的问题,因为我喜欢使用Try-method-pattern,基本上似乎与async- wait-paradigm不兼容…

对我来说重要的是,我可以在一个if-子句中调用try -方法,而不必预先定义out-变量,但可以像下面的例子那样内联执行:

if (TryReceive(out string msg))
{
    // use msg
}

所以我提出了以下解决方案:

注意:新的解决方案更优越,因为它可以与在这里的许多其他答案中描述的仅返回元组的方法一起使用,这可能经常在现有代码中找到!

新的解决方案:

Create extension methods for ValueTuples: public static class TupleExtensions { public static bool TryOut<P2>(this ValueTuple<bool, P2> tuple, out P2 p2) { bool p1; (p1, p2) = tuple; return p1; } public static bool TryOut<P2, P3>(this ValueTuple<bool, P2, P3> tuple, out P2 p2, out P3 p3) { bool p1; (p1, p2, p3) = tuple; return p1; } // continue to support larger tuples... } Define async Try-method like this: public async Task<(bool, string)> TryReceiveAsync() { string message; bool success; // ... return (success, message); } Call the async Try-method like this: if ((await TryReceiveAsync()).TryOut(out string msg)) { // use msg }


旧的解决方案:

Define a helper struct: public struct AsyncOut<T, OUT> { private readonly T returnValue; private readonly OUT result; public AsyncOut(T returnValue, OUT result) { this.returnValue = returnValue; this.result = result; } public T Out(out OUT result) { result = this.result; return returnValue; } public T ReturnValue => returnValue; public static implicit operator AsyncOut<T, OUT>((T returnValue ,OUT result) tuple) => new AsyncOut<T, OUT>(tuple.returnValue, tuple.result); } Define async Try-method like this: public async Task<AsyncOut<bool, string>> TryReceiveAsync() { string message; bool success; // ... return (success, message); } Call the async Try-method like this: if ((await TryReceiveAsync()).Out(out string msg)) { // use msg }

对于多个out参数,您可以定义额外的结构(例如AsyncOut<T,OUT1, OUT2>),或者您可以返回一个元组。

其他回答

out形参的一个很好的特性是,即使函数抛出异常,也可以使用它们返回数据。我认为与异步方法最接近的等效方法是使用一个新对象来保存异步方法和调用者都可以引用的数据。另一种方法是在另一个答案中传递一个委托。

请注意,这两种技术都不具有编译器所具有的任何类型的强制。也就是说,编译器不会要求你在共享对象上设置值或调用传入委托。

下面是一个使用共享对象模拟ref和out的示例实现,用于async方法和其他各种ref和out不可用的场景:

class Ref<T>
{
    // Field rather than a property to support passing to functions
    // accepting `ref T` or `out T`.
    public T Value;
}

async Task OperationExampleAsync(Ref<int> successfulLoopsRef)
{
    var things = new[] { 0, 1, 2, };
    var i = 0;
    while (true)
    {
        // Fourth iteration will throw an exception, but we will still have
        // communicated data back to the caller via successfulLoopsRef.
        things[i] += i;
        successfulLoopsRef.Value++;
        i++;
    }
}

async Task UsageExample()
{
    var successCounterRef = new Ref<int>();
    // Note that it does not make sense to access successCounterRef
    // until OperationExampleAsync completes (either fails or succeeds)
    // because there’s no synchronization. Here, I think of passing
    // the variable as “temporarily giving ownership” of the referenced
    // object to OperationExampleAsync. Deciding on conventions is up to
    // you and belongs in documentation ^^.
    try
    {
        await OperationExampleAsync(successCounterRef);
    }
    finally
    {
        Console.WriteLine($"Had {successCounterRef.Value} successful loops.");
    }
}

Alex在可读性上有很好的观点。同样,函数也具有足够的接口来定义返回的类型,并且还可以获得有意义的变量名。

delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
    bool canGetData = true;
    if (canGetData) callback(5);
    return Task.FromResult(canGetData);
}

调用者提供一个lambda(或一个命名函数),智能感知通过从委托复制变量名来提供帮助。

int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);

这种特殊的方法类似于“Try”方法,如果方法结果为真,则设置myOp。否则,你不会关心myOp。

在异步方法中不能有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个月后不需要你回答澄清问题。您的代码可能是开发人员拥有的唯一文档。

祝你好运。

c# 7+的解决方案是使用隐式元组语法。

    private async Task<(bool IsSuccess, IActionResult Result)> TryLogin(OpenIdConnectRequest request)
    { 
        return (true, BadRequest(new OpenIdErrorResponse
        {
            Error = OpenIdConnectConstants.Errors.AccessDenied,
            ErrorDescription = "Access token provided is not valid."
        }));
    }

返回结果使用方法签名定义的属性名。例句:

var foo = await TryLogin(request);
if (foo.IsSuccess)
     return foo.Result;