我想写一个带out参数的async方法,像这样:
public async void Method1()
{
int op;
int result = await GetDataTaskAsync(out op);
}
我如何做到这一点在GetDataTaskAsync?
我想写一个带out参数的async方法,像这样:
public async void Method1()
{
int op;
int result = await GetDataTaskAsync(out op);
}
我如何做到这一点在GetDataTaskAsync?
当前回答
异步方法不接受输出参数的限制仅适用于编译器生成的异步方法,这些方法使用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参数将永远不会完成。
其他回答
我喜欢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):
}
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;
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。