Làm thế nào để viết một phương thức async với tham số out?


176

Tôi muốn viết một phương thức async với một outtham số, như thế này:

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

Làm thế nào để tôi làm điều này trong GetDataTaskAsync?

Câu trả lời:


279

Bạn không thể có các phương thức async với refhoặc outtham 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);
}

10
Không quá phức tạp, điều này có thể tạo ra quá nhiều vấn đề. Jon Skeet đã giải thích điều này rất tốt ở đây stackoverflow.com/questions/20868103/
triệt

3
Cảm ơn sự Tuplethay thế. Rất hữu ích.
Luke Võ

19
nó là xấu có Tuple. : P
tofutim

36
Tôi nghĩ Named Tuples trong C # 7 sẽ là giải pháp hoàn hảo cho việc này.
orad

3
@orad Tôi đặc biệt thích điều này: Nhiệm vụ async riêng tư <(bool thành công, công việc, thông báo chuỗi)> TryGetJobAsync (...)
J. Andrew

51

Bạn không thể có refhoặc outtham số trong asynccá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.


2
Tôi thích giải pháp này thay vì sử dụng Tuple. Sạch hơn!
MiBol

30

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;

12

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.


9

Một tính năng hay của các outtham 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 asyncphương thức sẽ là sử dụng một đối tượng mới để giữ dữ liệu mà cả asyncphươ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 outcó. 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 refoutsử dụng với asynccác phương thức và các tình huống khác nhau có sẵn refoutkhô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.");
    }
}

6

Tôi yêu các Trymẫ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 asynccác phương thức trong một phiên bản gần đúng của Trymẫu.

Cách tiếp cận 1 - xuất cấu trúc

Điều này trông giống như một Tryphương thức đồng bộ chỉ trả về một tuplethay vì boolvới một outtham 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 truecủa falsevà 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 Tryphươ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);
    }
}

Cách tiếp cận 2 - vượt qua trong các phương thức gọi lại

Chúng ta có thể sử dụng anonymouscá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 Trymẫu nhưng đặt outtham 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.

Cách tiếp cận 3 - sử dụng Tiếp tục với

Điều gì nếu bạn chỉ sử dụng TPLnhư 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 exceptionkhi 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 ContinueWithsẽ xử lý Task.Exceptiontrong khối logic của nó. Gọn gàng

Nghe này, có một lý do chúng tôi yêu Trymẫ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.


1
Về cách tiếp cận thứ ba, bạn có chắc rằng ContinueWithcá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 ContinueWithsẽ 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.
Theodor Zoulias

1
Chúc mừng @TheodorZoulias, đó là một con mắt sắc nét. Đã sửa.
Jerry Nixon

1
Ném ra các ngoại lệ cho kiểm soát dòng chảy là một mùi mã lớn đối với tôi - nó sẽ làm tăng hiệu suất của bạn.
Ian Kemp

Không, @IanKemp, đó là một khái niệm khá cũ. Trình biên dịch đã phát triển.
Jerry Nixon

4

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:

  1. 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);
     }
  2. 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);
     }
  3. 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.


Đây là một giải pháp rất thông minh!
Theodor Zoulias

2

Giới hạn của các asyncphương thức không chấp nhận outtham 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 asynctừ 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 Taskcác phương thức trả về chấp nhận outtham số. Ví dụ: giả sử rằng chúng ta đã có một ParseIntAsyncphương thức ném và chúng ta muốn tạo một phương thức TryParseIntAsynckhô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 TaskCompletionSourcevà các ContinueWithphươ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 awaittừ 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 TaskCompletionSourcevẫn sẽ cần cho outtham số. Có thể outtham 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, GetRawDataAsyncFilterDataAsyncđược gọi là liên tiếp. Các outtham số được hoàn thành về việc hoàn thành các phương pháp thứ hai. Các GetDataAsyncphươ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 datatrước khi chờ đợi rawDataLengthlà rất quan trọng trong ví dụ đơn giản này, bởi vì trong trường hợp ngoại lệ, outtham số sẽ không bao giờ được hoàn thành.


1
Đây là một giải pháp rất tốt đẹp cho một số trường hợp.
Jerry Nixon

1

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

Bạn không cần NuGet nếu sử dụng .net-4.7 hoặc netst Chuẩn-2.0.
b Liệu

Này, bạn nói đúng! Tôi vừa gỡ cài đặt gói NuGet và nó vẫn hoạt động. Cảm ơn!
Paul Marangoni

1

Đâ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/


-2

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

Không bao giờ sử dụng .Result. Đó là một mô hình chống. Cảm ơn!
Ben
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.