จะเขียนวิธี async ด้วยพารามิเตอร์ out ได้อย่างไร


176

ฉันต้องการเขียนวิธีการซิงค์พร้อมoutพารามิเตอร์เช่นนี้

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

ฉันจะทำสิ่งนี้ได้GetDataTaskAsyncอย่างไร

คำตอบ:


279

คุณไม่สามารถมีวิธีการซิงค์กับพารามิเตอร์refหรือout

Lucian Wischik อธิบายว่าทำไมจึงเป็นไปไม่ได้ในเธรด MSDN นี้: http://social.msdn.microsoft.com/Forums/en-US/d2f48a52-e35a-4948-844d-828a1a6deb74/why-async-methods-cannot-have -ref หรือออกพารามิเตอร์

ทำไมเมธอด async จึงไม่รองรับพารามิเตอร์แบบอ้างอิงอ้างอิง? (หรือพารามิเตอร์อ้างอิง?) นั่นคือข้อ จำกัด ของ CLR เราเลือกที่จะใช้วิธีการแบบอะซิงโครนัสในวิธีที่คล้ายกับวิธีตัววนซ้ำ - เช่นผ่านคอมไพเลอร์เปลี่ยนวิธีการให้เป็นสถานะเครื่องวัตถุ CLR ไม่มีวิธีที่ปลอดภัยในการจัดเก็บที่อยู่ของ "พารามิเตอร์ออก" หรือ "พารามิเตอร์อ้างอิง" เป็นฟิลด์ของวัตถุ วิธีเดียวที่จะได้รับการสนับสนุนพารามิเตอร์ out-by-reference คือถ้าคุณลักษณะ async ถูกทำโดย CLR ระดับต่ำเขียนใหม่แทนคอมไพเลอร์เขียนใหม่ เราตรวจสอบวิธีการนั้นและมีหลายสิ่งที่เกิดขึ้น แต่ในที่สุดมันก็มีค่าใช้จ่ายสูงจนไม่เคยเกิดขึ้น

วิธีแก้ปัญหาทั่วไปสำหรับสถานการณ์นี้คือการให้เมธอด async ส่งคืน Tuple แทน คุณสามารถเขียนวิธีการของคุณใหม่เช่น:

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
ไกลจากความซับซ้อนเกินไปสิ่งนี้สามารถสร้างปัญหาได้มากเกินไป Jon Skeet อธิบายได้ดีมากที่นี่stackoverflow.com/questions/20868103/…
MuiBienCarlota

3
ขอบคุณสำหรับTupleทางเลือก มีประโยชน์มาก
ลุค Vo

19
Tupleมันเป็นเรื่องที่น่าเกลียดมี : P
tofutim

36
ฉันคิดว่าNamed Tuplesใน C # 7 จะเป็นโซลูชั่นที่สมบูรณ์แบบสำหรับสิ่งนี้
orad

3
@orad ฉันชอบสิ่งนี้โดยเฉพาะ: งาน async ส่วนตัว <(ความสำเร็จของบูล, งานงาน, ข้อความสตริง)> TryGetJobAsync (... )
J. Andrew Laughlin

51

คุณไม่สามารถrefหรือoutพารามิเตอร์ใน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;
}

คุณสามารถใช้รหัสของคุณได้ง่ายขึ้นอีกทั้งยังสามารถอ่านได้ง่ายกว่าตัวแปรหรือทูเปิล


2
ฉันชอบวิธีนี้แทนการใช้ Tuple สะอาดมากขึ้น!
MiBol

31

โซลูชัน C # 7 +คือการใช้ไวยากรณ์ของ tuple โดยปริยาย

    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;

12

อเล็กซ์เป็นจุดที่ดีในการอ่าน ฟังก์ชั่นนั้นมีอินเทอร์เฟซเพียงพอที่จะกำหนดประเภทที่จะถูกส่งคืนและคุณจะได้รับชื่อตัวแปรที่มีความหมาย

delegate void OpDelegate(int op);
Task<bool> GetDataTaskAsync(OpDelegate callback)
{
    bool canGetData = true;
    if (canGetData) callback(5);
    return Task.FromResult(canGetData);
}

ผู้โทรจัดเตรียมแลมบ์ดา (หรือฟังก์ชั่นที่มีชื่อ) และความช่วยเหลือในการจัดเก็บข้อมูลด้วยการคัดลอกชื่อตัวแปรจากผู้รับมอบสิทธิ์

int myOp;
bool result = await GetDataTaskAsync(op => myOp = op);

วิธีการนี้เป็นเหมือนวิธี "ลอง" ซึ่งmyOpตั้งไว้ถ้าผลลัพธ์เป็นtrueวิธี myOpมิฉะนั้นคุณไม่สนใจเกี่ยวกับ


9

คุณลักษณะที่ดีอย่างหนึ่งของ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.");
    }
}

6

ฉันรักTryรูปแบบ มันเป็นรูปแบบที่เป็นระเบียบ

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

asyncแต่ก็มีความท้าทาย ไม่ได้หมายความว่าเราไม่มีตัวเลือกจริง ที่นี่สามหลักวิธีคุณสามารถพิจารณาสำหรับasyncวิธีการในการเป็นเสมือนรุ่นของTryรูปแบบ

วิธีที่ 1 - ส่งออกโครงสร้าง

ลักษณะนี้มากที่สุดเช่นซิงค์Tryวิธีเดียวที่กลับtupleแทนboolกับoutพารามิเตอร์ซึ่งเราทุกคนรู้ไม่ได้รับอนุญาตใน C #

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

ด้วยวิธีการที่ผลตอบแทนtrueของและไม่เคยพ่นfalseexception

โปรดจำไว้ว่าการโยนข้อยกเว้นใน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 เดือนและคุณไม่ต้องตอบคำถามที่ชัดเจน รหัสของคุณสามารถเป็นเอกสารเดียวที่นักพัฒนาจะมี

ขอให้โชคดี


1
เกี่ยวกับวิธีที่สามคุณแน่ใจหรือไม่ว่าการContinueWithโทรสายโซ่มีผลที่คาดหวัง ตามความเข้าใจของฉันครั้งที่สองContinueWithจะตรวจสอบความสำเร็จของการต่อเนื่องครั้งแรกไม่ใช่ความสำเร็จของงานต้นฉบับ
Theodor Zoulias

1
ไชโย @TheodorZoulias นั่นคือดวงตาที่คมชัด แก้ไขแล้ว.
Jerry Nixon

1
การขว้างข้อยกเว้นสำหรับการควบคุมการไหลเป็นเรื่องใหญ่สำหรับฉัน - มันจะทำให้ประสิทธิภาพของคุณดีขึ้น
Ian Kemp

ไม่ @IanKemp เป็นแนวคิดที่ค่อนข้างเก่า คอมไพเลอร์มีการพัฒนา
Jerry Nixon

4

ฉันมีปัญหาเดียวกันกับที่ฉันชอบใช้วิธีลองแบบซึ่งโดยทั่วไปดูเหมือนจะไม่เข้ากันกับกระบวนทัศน์แบบ async-await-paradigm ...

สิ่งสำคัญสำหรับฉันคือฉันสามารถเรียกใช้ลองเมธอดภายใน if-clause เดียวและไม่จำเป็นต้องกำหนดตัวแปรออกล่วงหน้าไว้ก่อน แต่สามารถทำอินไลน์ได้ในตัวอย่างต่อไปนี้:

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

ดังนั้นฉันจึงคิดวิธีแก้ปัญหาต่อไปนี้ขึ้นมา:

  1. กำหนดโครงสร้างผู้ช่วย:

     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 ลองวิธีการดังนี้:

     public async Task<AsyncOut<bool, string>> TryReceiveAsync()
     {
         string message;
         bool success;
         // ...
         return (success, message);
     }
  3. เรียกใช้ async ลองวิธีนี้:

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

สำหรับพารามิเตอร์หลายค่าคุณสามารถกำหนด struct เพิ่มเติม (เช่น AsyncOut <T, OUT1, OUT2>) หรือคุณสามารถคืนค่า tuple


นี่เป็นทางออกที่ฉลาดมาก!
Theodor Zoulias

2

ข้อ จำกัด ของ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และContinueWithวิธีการนั้นค่อนข้างอึดอัด แต่ไม่มีตัวเลือกอื่นเนื่องจากเราไม่สามารถใช้awaitคำหลักที่สะดวกสบายในวิธีนี้ได้

ตัวอย่างการใช้งาน:

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

อัปเดต:หากตรรกะawaitอะซิงก์ซับซ้อนเกินกว่าที่จะแสดงออกได้มันอาจจะถูกห่อหุ้มภายในตัวแทนที่ไม่ระบุชื่อแบบอะซิงโครนัสซ้อนกัน A TaskCompletionSourceจะยังคงจำเป็นสำหรับ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พารามิเตอร์จะไม่สมบูรณ์


1
นี่เป็นวิธีแก้ปัญหาที่ดีมากสำหรับบางกรณี
Jerry Nixon

1

ฉันคิดว่าการใช้ 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):
}

คุณไม่จำเป็นต้องใช้ NuGet หากใช้. net-4.7 หรือ netstandard-2.0
binki

เฮ้คุณพูดถูก! ฉันเพิ่งถอนการติดตั้งแพคเกจ NuGet และยังคงใช้งานได้ ขอบคุณ!
Paul Marangoni

1

ต่อไปนี้เป็นรหัสของคำตอบของ @ dcastro ที่แก้ไขสำหรับ C # 7.0 ที่มีชื่อ tuples และ tuple deconstruction ซึ่งเพิ่มความคล่องตัวให้กับสัญกรณ์:

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

สำหรับรายละเอียดเกี่ยวกับ tuples ชื่อใหม่ตัวอักษร tuple และ deconstructions ของ tuple โปรดดูที่: https://blogs.msdn.microsoft.com/dotnet/2017/03/09/new-features-in-c-7-0/


-2

คุณสามารถทำได้โดยใช้ TPL (task ขนานไลบรารี) แทนที่จะใช้คำหลักที่รอคอยโดยตรง

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

ไม่เคยใช้. ผลลัพธ์ มันเป็นรูปแบบการต่อต้าน ขอบคุณ!
Ben
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.