Câu trả lời:
Bạn không thể có các phương thức async với ref
hoặc out
tham số.
Lucian Wischik giải thích lý do tại sao điều này không thể thực hiện được trên luồng MSDN này: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cann -ref-hoặc-out-tham số
Đối với lý do tại sao các phương thức async không hỗ trợ các tham số ngoài tham chiếu? (hoặc tham số ref?) Đó là giới hạn của CLR. Chúng tôi đã chọn thực hiện các phương thức async theo cách tương tự như các phương thức lặp - tức là thông qua trình biên dịch chuyển đổi phương thức thành một đối tượng máy-trạng thái. CLR không có cách nào an toàn để lưu địa chỉ của "tham số out" hoặc "tham số tham chiếu" dưới dạng trường của một đối tượng. Cách duy nhất để có các tham số ngoài tham chiếu được hỗ trợ là nếu tính năng async được thực hiện bằng cách viết lại CLR cấp thấp thay vì viết lại trình biên dịch. Chúng tôi đã kiểm tra cách tiếp cận đó, và nó đã có rất nhiều thứ cho nó, nhưng cuối cùng nó sẽ tốn kém đến mức nó không bao giờ xảy ra.
Một cách giải quyết điển hình cho tình huống này là phương thức async trả về Tuple thay thế. Bạn có thể viết lại phương thức của bạn như vậy:
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);
}
Tuple
thay thế. Rất hữu ích.
Tuple
. : P
Bạn không thể có ref
hoặc out
tham số trong async
các phương thức (như đã được ghi chú).
Điều này hét lên cho một số mô hình trong dữ liệu di chuyển xung quanh:
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;
}
Bạn có khả năng sử dụng lại mã của mình dễ dàng hơn, cộng với đó là cách dễ đọc hơn các biến hoặc bộ dữ liệu.
Giải pháp C # 7 + là sử dụng cú pháp tuple ẩn.
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."
}));
}
kết quả trả về sử dụng tên thuộc tính chữ ký phương thức xác định. ví dụ:
var foo = await TryLogin(request);
if (foo.IsSuccess)
return foo.Result;
Alex đã làm cho một điểm tuyệt vời về khả năng đọc. Tương tự, một hàm cũng đủ giao diện để xác định (các) loại được trả về và bạn cũng nhận được các tên biến có ý nghĩa.
delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
bool canGetData = true;
if (canGetData) callback(5);
return Task.FromResult(canGetData);
}
Người gọi cung cấp lambda (hoặc một chức năng được đặt tên) và trợ giúp bằng cách sao chép (các) tên biến từ đại biểu.
int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);
Cách tiếp cận cụ thể này giống như một phương pháp "Thử" trong đó myOp
được đặt nếu kết quả phương thức là true
. Nếu không, bạn không quan tâm myOp
.
Một tính năng hay của các out
tham số là chúng có thể được sử dụng để trả về dữ liệu ngay cả khi hàm ném ngoại lệ. Tôi nghĩ rằng tương đương gần nhất để làm điều này với một async
phương thức sẽ là sử dụng một đối tượng mới để giữ dữ liệu mà cả async
phương thức và người gọi có thể tham chiếu. Một cách khác là thông qua một đại biểu như được đề xuất trong câu trả lời khác .
Lưu ý rằng cả hai kỹ thuật này sẽ không có bất kỳ loại thực thi nào từ trình biên dịch out
có. Tức là, trình biên dịch sẽ không yêu cầu bạn đặt giá trị trên đối tượng được chia sẻ hoặc gọi một đại biểu được thông qua.
Dưới đây là một triển khai ví dụ sử dụng một đối tượng được chia sẻ để bắt chước ref
và out
sử dụng với async
các phương thức và các tình huống khác nhau có sẵn ref
và out
không có sẵn:
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.");
}
}
Tôi yêu các Try
mẫu. Đó là một mô hình gọn gàng.
if (double.TryParse(name, out var result))
{
// handle success
}
else
{
// handle error
}
Nhưng, đó là thử thách với async
. Điều đó không có nghĩa là chúng ta không có lựa chọn thực sự. Dưới đây là ba cách tiếp cận cốt lõi mà bạn có thể xem xét cho async
các phương thức trong một phiên bản gần đúng của Try
mẫu.
Điều này trông giống như một Try
phương thức đồng bộ chỉ trả về một tuple
thay vì bool
với một out
tham số, mà tất cả chúng ta đều biết là không được phép trong C #.
var result = await DoAsync(name);
if (result.Success)
{
// handle success
}
else
{
// handle error
}
Với một phương pháp mà lợi nhuận true
của false
và không bao giờ ném một exception
.
Hãy nhớ rằng, việc ném một ngoại lệ trong một
Try
phương thức sẽ phá vỡ toàn bộ mục đích của mẫu.
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);
}
}
Chúng ta có thể sử dụng anonymous
các phương thức để đặt các biến ngoài. Đó là cú pháp thông minh, mặc dù hơi phức tạp. Với liều lượng nhỏ, nó ổn.
var file = default(StorageFile);
var exception = default(Exception);
if (await DoAsync(name, x => file = x, x => exception = x))
{
// handle success
}
else
{
// handle failure
}
Phương thức tuân theo các điều cơ bản của Try
mẫu nhưng đặt out
tham số được truyền trong các phương thức gọi lại. Nó được thực hiện như thế này.
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ó một câu hỏi trong đầu tôi về hiệu suất ở đây. Nhưng, trình biên dịch C # rất thông minh, đến nỗi tôi nghĩ rằng bạn an toàn khi chọn tùy chọn này, gần như chắc chắn.
Điều gì nếu bạn chỉ sử dụng TPL
như thiết kế? Không có tuple. Ý tưởng ở đây là chúng tôi sử dụng các ngoại lệ để chuyển hướng ContinueWith
đến hai đường dẫn khác nhau.
await DoAsync(name).ContinueWith(task =>
{
if (task.Exception != null)
{
// handle fail
}
if (task.Result is StorageFile sf)
{
// handle success
}
});
Với một phương pháp ném một exception
khi có bất kỳ loại thất bại. Điều đó khác với việc trở về a boolean
. Đó là một cách để giao tiếp với TPL
.
async Task<StorageFile> DoAsync(string fileName)
{
var folder = ApplicationData.Current.LocalCacheFolder;
return await folder.GetFileAsync(fileName);
}
Trong đoạn mã trên, nếu không tìm thấy tệp, một ngoại lệ sẽ được đưa ra. Điều này sẽ gọi thất bại ContinueWith
sẽ xử lý Task.Exception
trong khối logic của nó. Gọn gàng
Nghe này, có một lý do chúng tôi yêu
Try
mẫu. Về cơ bản, nó rất gọn gàng và dễ đọc và kết quả là có thể duy trì được. Khi bạn chọn cách tiếp cận của bạn, cơ quan giám sát cho dễ đọc. Hãy nhớ nhà phát triển tiếp theo trong 6 tháng và bạn không phải trả lời các câu hỏi làm rõ. Mã của bạn có thể là tài liệu duy nhất mà nhà phát triển sẽ có.
May mắn nhất.
ContinueWith
các cuộc gọi xích có kết quả như mong đợi không? Theo sự hiểu biết của tôi, lần thứ hai ContinueWith
sẽ kiểm tra sự thành công của lần tiếp theo đầu tiên, chứ không phải sự thành công của nhiệm vụ ban đầu.
Tôi gặp vấn đề tương tự như tôi thích khi sử dụng mẫu Phương thức thử mà về cơ bản dường như không tương thích với mô hình không đồng bộ ...
Quan trọng đối với tôi là tôi có thể gọi phương thức Thử trong một mệnh đề if duy nhất và không phải xác định trước các biến ngoài trước, nhưng có thể thực hiện theo dòng như trong ví dụ sau:
if (TryReceive(out string msg))
{
// use msg
}
Vì vậy, tôi đã đưa ra giải pháp sau đây:
Xác định cấu trúc trợ giúp:
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);
}
Xác định async Phương thức thử như thế này:
public async Task<AsyncOut<bool, string>> TryReceiveAsync()
{
string message;
bool success;
// ...
return (success, message);
}
Gọi phương thức Thử async như thế này:
if ((await TryReceiveAsync()).Out(out string msg))
{
// use msg
}
Đối với nhiều tham số ngoài, bạn có thể xác định các cấu trúc bổ sung (ví dụ AsyncOut <T, OUT1, OUT2>) hoặc bạn có thể trả về một tuple.
Giới hạn của các async
phương thức không chấp nhận out
tham số chỉ áp dụng cho các phương thức không đồng bộ do trình biên dịch tạo, các phương thức này được khai báo bằng async
từ khóa. Nó không áp dụng cho các phương thức async thủ công. Nói cách khác, có thể tạo Task
các phương thức trả về chấp nhận out
tham số. Ví dụ: giả sử rằng chúng ta đã có một ParseIntAsync
phương thức ném và chúng ta muốn tạo một phương thức TryParseIntAsync
không ném. Chúng ta có thể thực hiện nó như thế này:
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);
}
Sử dụng TaskCompletionSource
và các ContinueWith
phương pháp là một chút khó khăn, nhưng không có lựa chọn nào khác vì chúng ta không thể sử dụng thuận tiện await
từ khóa bên trong phương pháp này.
Ví dụ sử dụng:
if (await TryParseIntAsync("-13", out var result))
{
Console.WriteLine($"Result: {await result}");
}
else
{
Console.WriteLine($"Parse failed");
}
Cập nhật: Nếu logic async quá phức tạp để được thể hiện mà không có await
, thì nó có thể được gói gọn bên trong một đại biểu ẩn danh không đồng bộ lồng nhau. A TaskCompletionSource
vẫn sẽ cần cho out
tham số. Có thể out
tham số có thể được hoàn thành trước khi hoàn thành nhiệm vụ chính, như trong ví dụ dưới đây:
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);
}))();
}
Ví dụ này giả định sự tồn tại của ba phương pháp không đồng bộ GetResponseAsync
, GetRawDataAsync
và FilterDataAsync
được gọi là liên tiếp. Các out
tham số được hoàn thành về việc hoàn thành các phương pháp thứ hai. Các GetDataAsync
phương pháp có thể được sử dụng như thế này:
var data = await GetDataAsync("http://example.com", out var rawDataLength);
Console.WriteLine($"Data: {data}");
Console.WriteLine($"RawDataLength: {await rawDataLength}");
Chờ đợi data
trước khi chờ đợi rawDataLength
là rất quan trọng trong ví dụ đơn giản này, bởi vì trong trường hợp ngoại lệ, out
tham số sẽ không bao giờ được hoàn thành.
Tôi nghĩ rằng sử dụng ValueTuples như thế này có thể hoạt động. Trước tiên, bạn phải thêm gói 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):
}
Đây là mã của câu trả lời của @ dcastro được sửa đổi cho C # 7.0 với bộ giải mã được đặt tên và bộ giải mã tuple, giúp hợp lý hóa ký hiệu:
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);
}
Để biết chi tiết về các tuple mới có tên, tuple nghĩa đen và giải mã tuple, hãy xem: https://bloss.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/
Bạn có thể làm điều này bằng cách sử dụng TPL (thư viện song song tác vụ) thay vì sử dụng trực tiếp từ khóa đang chờ.
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