如何写一个异步方法与参数?

我想写一个带有out参数的异步方法,就像这样:

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

如何在GetDataTaskAsync中做到这一点?

171916 次浏览

你不能有带有refout参数的异步方法。

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上不可能

至于为什么异步方法不支持引用输出参数? (或引用参数?)这是CLR的局限性。我们选择了 以类似于迭代器方法的方式实现异步方法。 通过编译器将方法转换为 state-machine-object。的地址,CLR没有安全的方法存储 作为对象字段的“输出参数”或“引用参数”。 支持引用外参数的唯一方法是if 异步特性是通过低级的CLR重写来完成的,而不是 compiler-rewrite。我们检查了这种方法,它有很多进展 为了它,但它最终会非常昂贵,以至于它永远不会 已经发生了。< / p >
对于这种情况,一个典型的解决方法是让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);
}

async方法中不能有refout参数(如上所述)。

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

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;
}

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

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”方法,如果方法结果是true,则设置myOp。否则,你不会关心myOp

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

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

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

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

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;

我认为像这样使用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):
}

下面是@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);
}
有关新的命名元组、元组字面量和元组解构的详细信息,请参见: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/ < / p >

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

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

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

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

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

新的解决方案:

  1. 为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...
    }
    
  2. 像这样定义async Try-method:

     public async Task<(bool, string)> TryReceiveAsync()
    {
    string message;
    bool success;
    // ...
    return (success, message);
    }
    
  3. 像这样调用async try方法:

     if ((await TryReceiveAsync()).TryOut(out string msg))
    {
    // use msg
    }
    

旧的解决方案:

  1. 定义一个helper结构:

     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);
    }
    
  2. 像这样定义async Try-method:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
    {
    string message;
    bool success;
    // ...
    return (success, message);
    }
    
  3. 像这样调用async try方法:

     if ((await TryReceiveAsync()).Out(out string msg))
    {
    // use msg
    }
    

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

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

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

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

方法1 -输出一个结构

这看起来很像一个同步Try方法,只返回tuple,而不是带有out参数的bool,我们都知道这在c#中是不允许的。

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

使用一个返回falsetrue且从不抛出exception的方法。

记住,在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 -传入回调方法

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

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

该方法遵循Try模式的基本原理,但将out参数设置为传入回调方法。是这样做的。

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

使用一个在出现任何类型的失败时抛出exception的方法。这与返回boolean不同。它是一种与TPL通信的方式。

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

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

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

祝你好运。

async方法不接受out参数的限制只适用于编译器生成的异步方法,这些方法是用async关键字声明的。它不适用于手工制作的异步方法。换句话说,可以创建接受out参数的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);
}

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

使用的例子:

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

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

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);
}))();
}
这个例子假设存在三个异步方法GetResponseAsyncGetRawDataAsyncFilterDataAsync被调用 在继承。out参数在第二个方法完成时完成。GetDataAsync方法可以这样使用:

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

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

您可以通过使用TPL(任务并行库)而不是直接使用await关键字来做到这一点。

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

对于真正想要保持参数的开发人员,这里可能有另一种解决方法。

将参数更改为数组或List以封装实际值。记得在发送到方法之前初始化列表。返回后,在使用它之前一定要检查值是否存在。小心编码。

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

无论如何,我想分享,因为我的也支持当nullable设置为enable

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