C# 如何编写带out参数的异步方法?

C# 如何编写带out参数的异步方法?,c#,async-await,C#,Async Await,我想用out参数编写一个异步方法,如下所示: public async void Method1() { int op; int result = await GetDataTaskAsync(out op); } public async Task<AsyncOut<bool, string>> TryReceiveAsync() { string message; bool success; // ...

我想用
out
参数编写一个异步方法,如下所示:

public async void Method1()
{
    int op;
    int result = await GetDataTaskAsync(out op);
}
 public async Task<AsyncOut<bool, string>> TryReceiveAsync()
 {
     string message;
     bool success;
     // ...
     return (success, message);
 }
 if ((await TryReceiveAsync()).Out(out string msg))
 {
     // use msg
 }
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);
}
var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");

如何在
GetDataTaskAsync
中执行此操作?

您不能使用带有
ref
out
参数的异步方法

Lucian Wischik解释了为什么在这个MSDN线程上不可能做到这一点:

至于为什么异步方法不支持out-by-reference参数? (或参考参数?)这是CLR的一个限制。我们选择了 以类似于迭代器方法的方式实现异步方法——即。 通过编译器将方法转换为 状态机对象。CLR没有安全的方法来存储 作为对象字段的“输出参数”或“参考参数”。 通过引用参数支持out的唯一方法是 异步功能是通过低级CLR重写而不是通过 编译器重写。我们研究了这种方法,它有很多进展 但它最终将是如此昂贵,以至于它永远不会 已经发生了

这种情况的一个典型解决方法是让异步方法返回一个元组。 您可以重新编写方法,如下所示:

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);
}
公共异步任务方法1()
{
var tuple=等待GetDataTaskAsync();
int op=tuple.Item1;
int result=tuple.Item2;
}
公共异步任务GetDataTaskAsync()
{
//...
返回新的元组(1,2);
}

async
方法中不能有
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;
}
公共类数据
{
公共int Op{get;set;}
公共int结果{get;set;}
}
公共异步void方法1()
{
数据数据=等待GetDataTaskAsync();
//从现在开始使用data.Op和data.Result
}
公共异步任务GetDataTaskAsync()
{
var returnValue=新数据();
//填写返回值
返回值;
}

您可以更轻松地重用代码,而且它比变量或元组更具可读性。

Alex在可读性方面提出了一个很好的观点。同样,函数也是一个接口,足以定义返回的类型,并且还可以获得有意义的变量名

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

此特定方法类似于“Try”方法,如果方法结果为
true
,则设置
myOp
。否则,您就不关心
myOp

参数的一个很好的特性是,即使在函数抛出异常时,它们也可以用于返回数据。我认为与使用
async
方法进行此操作最接近的等效方法是使用一个新对象来保存
async
方法和调用者都可以引用的数据。另一种方法是

请注意,这两种技术都不会像
out
那样从编译器中强制执行。也就是说,编译器不需要您在共享对象上设置值或调用传入的委托

int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);
下面是一个使用共享对象来模拟
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.");
    }
}
class-Ref
{
//字段而不是支持传递到函数的属性
//接受'ref T'或'out T'。
公共价值观;
}
异步任务操作示例异步(Ref successfulLoopsRef)
{
var things=new[]{0,1,2,};
var i=0;
while(true)
{
//第四次迭代将抛出一个异常,但我们仍然有
//通过successfulLoopsRef将数据传回调用方。
事物[i]+=i;
successfulLoopsRef.Value++;
i++;
}
}
异步任务UsageExample()
{
var successCounterRef=新Ref();
//请注意,访问successCounterRef没有意义
//直到OperationExampleSync完成(失败或成功)
//因为没有同步。在这里,我想传球
//变量为被引用对象的“暂时给予所有权”
//对象转换为OperationExampleSync。决定约定取决于
//您和您所属的文档^^。
尝试
{
等待操作示例异步(successCounterRef);
}
最后
{
WriteLine($“Had{successCounterRef.Value}successfully循环。”);
}
}
是使用隐式元组语法

    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."
        }));
    }

我认为像这样使用valuetuple是可行的。但是,您必须首先添加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):
}
public async void Method1()
{
(int op,int result)tuple=await GetDataTaskAsync();
int op=tuple.op;
int result=tuple.result;
}
公共异步任务GetDataTaskAsync()
{
int x=5;
int y=10;
返回(op:x,结果:y):
}

以下是@dcastro的答案代码,它针对C#7.0修改了命名元组和元组解构,简化了符号:

public async void Method1()
{
    // Version 1, named tuples:
    // just to show how it works
    /*
    var tuple = await GetDataTaskAsync();
    int op = tuple.paramOp;
    int result = tuple.paramResult;
    */

    // Version 2, tuple deconstruction:
    // much shorter, most elegant
    (int op, int result) = await GetDataTaskAsync();
}

public async Task<(int paramOp, int paramResult)> GetDataTaskAsync()
{
    //...
    return (1, 2);
}
public async void Method1()
{
//版本1,命名元组:
//只是为了展示它是如何工作的
/*
var tuple=等待GetDataTaskAsync();
int op=tuple.paramOp;
int result=tuple.paramResult;
*/
//第2版,元组解构:
//短得多,优雅得多
(int op,int result)=等待GetDataTaskAsync();
}
公共异步任务GetDataTaskAsync()
{
//...
返回(1,2);
}
有关新命名元组、元组文字和元组解构的详细信息,请参见:

我遇到了与我喜欢使用Try方法模式相同的问题,它基本上似乎与异步等待范式不兼容

对我来说很重要的一点是,我可以在单个if子句中调用Try方法,并且之前不必预先定义out变量,但可以像下面的示例那样在线执行:

if (TryReceive(out string msg))
{
    // use msg
}
因此,我提出了以下解决方案:

  • 定义辅助结构:

     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);
     }
    
  • 对于多个输出参数,可以定义其他输出参数
    var result = await DoAsync(name);
    if (result.Success)
    {
        // handle success
    }
    else
    {
        // handle error
    }
    
    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);
        }
    }
    
    var file = default(StorageFile);
    var exception = default(Exception);
    if (await DoAsync(name, x => file = x, x => exception = x))
    {
        // handle success
    }
    else
    {
        // handle failure
    }
    
    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;
        }
    }
    
    await DoAsync(name).ContinueWith(task =>
    {
        if (task.Exception != null)
        {
            // handle fail
        }
        if (task.Result is StorageFile sf)
        {
            // handle success
        }
    });
    
    async Task<StorageFile> DoAsync(string fileName)
    {
        var folder = ApplicationData.Current.LocalCacheFolder;
        return await folder.GetFileAsync(fileName);
    }
    
    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);
    }
    
    if (await TryParseIntAsync("-13", out var result))
    {
        Console.WriteLine($"Result: {await result}");
    }
    else
    {
        Console.WriteLine($"Parse failed");
    }
    
    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);
        }))();
    }
    
    var data = await GetDataAsync("http://example.com", out var rawDataLength);
    Console.WriteLine($"Data: {data}");
    Console.WriteLine($"RawDataLength: {await rawDataLength}");
    
    private bool CheckInCategory(int? id, out Category category)
        {
            if (id == null || id == 0)
                category = null;
            else
                category = Task.Run(async () => await _context.Categories.FindAsync(id ?? 0)).Result;
    
            return category != null;
        }
    
    if(!CheckInCategory(int? id, out var category)) return error