我想编写一个带out
参数的异步方法,如下所示:
public async void Method1()
{
int op;
int result = await GetDataTaskAsync(out op);
}
我该怎么做GetDataTaskAsync
?
我想编写一个带out
参数的异步方法,如下所示:
public async void Method1()
{
int op;
int result = await GetDataTaskAsync(out op);
}
我该怎么做GetDataTaskAsync
?
您不能使用带有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
至于为什么 async 方法不支持 out-by-reference 参数?(或 ref 参数?)这是 CLR 的限制。我们选择以与迭代器方法类似的方式实现异步方法——即通过编译器将方法转换为状态机对象。CLR 没有安全的方法将“输出参数”或“引用参数”的地址存储为对象的字段。支持 out-by-reference 参数的唯一方法是异步功能由低级 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);
}
The C#7+ Solution is to use implicit tuple syntax.
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."
}));
}
return result utilizes the method signature defined property names. e.g:
var foo = await TryLogin(request);
if (foo.IsSuccess)
return foo.Result;
方法中不能有ref
orout
参数async
(如前所述)。
这需要在移动的数据中进行一些建模:
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-method-pattern,它基本上似乎与 async-await-paradigm 不兼容......
对我来说重要的是,我可以在单个 if 子句中调用 Try 方法,而不必预先定义外变量,但可以像以下示例中那样内联:
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);
}
像这样定义异步 Try 方法:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
像这样调用异步 Try 方法:
if ((await TryReceiveAsync()).Out(out string msg))
{
// use msg
}
对于多个输出参数,您可以定义其他结构(例如 AsyncOut<T,OUT1, OUT2>)或者您可以返回一个元组。
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
如果方法结果是true
. 否则,你不关心myOp
。
我喜欢这个Try
图案。这是一个整齐的图案。
if (double.TryParse(name, out var result))
{
// handle success
}
else
{
// handle error
}
但是,它具有挑战性async
。这并不意味着我们没有真正的选择。async
对于模式的准版本中的方法,您可以考虑以下三种核心方法Try
。
这看起来很像一个Try
只返回 atuple
而不是bool
带out
参数的同步方法,我们都知道在 C# 中是不允许的。
var result = await DoAsync(name);
if (result.Success)
{
// handle success
}
else
{
// handle error
}
使用返回true
且false
从不抛出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);
}
}
我们可以使用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# 编译器非常聪明,我认为你可以安全地选择这个选项,几乎可以肯定。
如果你只是TPL
按照设计使用呢?没有元组。这里的想法是我们使用异常重定向ContinueWith
到两个不同的路径。
await DoAsync(name).ContinueWith(task =>
{
if (task.Exception != null)
{
// handle fail
}
if (task.Result is StorageFile sf)
{
// handle success
}
});
exception
使用一种在发生任何故障时抛出的方法。这与返回 a 不同boolean
。这是一种与TPL
.
async Task<StorageFile> DoAsync(string fileName)
{
var folder = ApplicationData.Current.LocalCacheFolder;
return await folder.GetFileAsync(fileName);
}
在上面的代码中,如果找不到文件,就会抛出异常。这将调用将在其逻辑块中ContinueWith
处理的故障。Task.Exception
整齐吧?
听着,我们喜欢这种
Try
模式是有原因的。从根本上说,它非常简洁易读,因此是可维护的。当你选择你的方法时,看门狗的可读性。记住下一个开发人员,他在 6 个月内没有让你回答澄清问题。您的代码可能是开发人员将拥有的唯一文档。
祝你好运。
参数的一个很好的特性out
是,即使函数抛出异常,它们也可以用于返回数据。我认为与使用方法最接近的等效async
方法是使用新对象来保存async
方法和调用者都可以引用的数据。另一种方法是按照另一个答案中的建议传递一个代表。
请注意,这些技术都不会具有编译器所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.");
}
}
async
不接受参数的方法的限制out
仅适用于编译器生成的异步方法,这些方法使用async
关键字声明。它不适用于手工制作的异步方法。换句话说,可以创建Task
接受out
参数的返回方法。例如,假设我们已经有一个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
andContinueWith
方法有点尴尬,但没有其他选择,因为我们不能await
在该方法中使用便捷关键字。
使用示例:
if (await TryParseIntAsync("-13", out var result))
{
Console.WriteLine($"Result: {await result}");
}
else
{
Console.WriteLine($"Parse failed");
}
更新:如果异步逻辑太复杂而无法在没有 的情况下表达await
,则可以将其封装在嵌套的异步匿名委托中。参数TaskCompletionSource
仍然需要A。out
参数可能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}");
在这个简化的示例中,等待data
之前的等待rawDataLength
很重要,因为在出现异常的情况下,out
参数将永远不会完成。
这是为 C# 7.0 修改的 @dcastro 答案的代码,带有命名元组和元组解构,它简化了符号:
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/
我认为像这样使用 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):
}
对于真正希望将其保留在参数中的开发人员,这里可能是另一种解决方法。
将参数更改为数组或列表以包装实际值。请记住在发送到方法之前初始化列表。返回后,请务必在使用前检查值是否存在。谨慎编码。
您可以通过使用 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