วัตถุประสงค์ของ“ การรอคืนสินค้า” ใน C # คืออะไร


251

มีใด ๆสถานการณ์ที่เขียนวิธีการเช่นนี้

public async Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return await DoAnotherThingAsync();
}

แทนสิ่งนี้:

public Task<SomeResult> DoSomethingAsync()
{
    // Some synchronous code might or might not be here... //
    return DoAnotherThingAsync();
}

จะทำให้รู้สึก

เหตุใดจึงต้องใช้การreturn awaitสร้างเมื่อคุณสามารถกลับTask<T>จากการDoAnotherThingAsync()เรียกใช้ภายในได้โดยตรง

ฉันเห็นโค้ดreturn awaitในหลาย ๆ ที่ฉันคิดว่าฉันอาจพลาดอะไรบางอย่างไป แต่เท่าที่ฉันเข้าใจไม่ใช้ async / รอคำหลักในกรณีนี้และกลับงานโดยตรงจะเทียบเท่ากับการทำงาน ทำไมต้องเพิ่มค่าใช้จ่ายเพิ่มเติมของawaitเลเยอร์เพิ่มเติม?


2
ฉันคิดว่าเหตุผลเดียวที่คุณเห็นนี้เป็นเพราะผู้คนเรียนรู้จากการเลียนแบบและโดยทั่วไป (หากพวกเขาไม่ต้องการ) พวกเขาใช้วิธีแก้ปัญหาที่ง่ายที่สุดที่พวกเขาสามารถหาได้ ดังนั้นผู้คนจึงเห็นรหัสนั้นใช้รหัสนั้นพวกเขาเห็นว่ามันใช้งานได้และต่อจากนี้ไปสำหรับพวกเขานั่นเป็นวิธีที่ถูกต้องที่จะทำ ... ไม่มีประโยชน์ที่จะรอในกรณีนี้
Fabio Marcolini

7
มีอย่างน้อยหนึ่งแตกต่างที่สำคัญ: การขยายพันธุ์ยกเว้น
noseratio

1
ฉันก็ไม่เข้าใจเหมือนกันไม่สามารถเข้าใจแนวคิดทั้งหมดนี้ได้เลย จากสิ่งที่ฉันได้เรียนรู้ว่าวิธีใดมีประเภทผลตอบแทน IT ต้องมีคำหลักส่งคืนไม่ใช่กฎของภาษา C # ใช่หรือไม่
monstro

@monstro คำถามของ OP ไม่มีข้อความสั่ง return
David Klempfner

คำตอบ:


189

มีหนึ่งกรณีลับ ๆ ล่อ ๆ เมื่อreturnอยู่ในวิธีการปกติและreturn awaitในasyncวิธีการทำงานแตกต่างกัน: เมื่อรวมกับusing(หรือมากกว่าใด ๆreturn awaitในtryบล็อก)

พิจารณาเมธอดทั้งสองเวอร์ชันนี้:

Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return foo.DoAnotherThingAsync();
    }
}

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

วิธีแรกจะDispose()เป็นFooวัตถุทันทีที่DoAnotherThingAsync()วิธีการส่งกลับซึ่งมีแนวโน้มที่จะนานก่อนที่มันจะเสร็จสมบูรณ์จริง ซึ่งหมายความว่ารุ่นแรกอาจมีข้อผิดพลาด (เนื่องจากFooมีการจำหน่ายเร็วเกินไป) ในขณะที่รุ่นที่สองจะทำงานได้ดี


4
เพื่อความสมบูรณ์ในกรณีแรกคุณควรกลับมาfoo.DoAnotherThingAsync().ContinueWith(_ => foo.Dispose());
ghord

7
@ghord ที่จะไม่ทำงานผลตอบแทนDispose() voidคุณต้องการบางสิ่งเช่นreturn foo.DoAnotherThingAsync().ContinueWith(t -> { foo.Dispose(); return t.Result; });นี้ แต่ฉันไม่รู้ว่าทำไมคุณถึงทำอย่างนั้นเมื่อคุณสามารถใช้ตัวเลือกที่สอง
svick

1
@svick { var task = DoAnotherThingAsync(); task.ContinueWith(_ => foo.Dispose()); return task; }คุณกำลังที่เหมาะสมก็ควรจะมีมากขึ้นตามสายของ กรณีการใช้งานค่อนข้างง่าย: ถ้าคุณใช้. NET 4.0 (เหมือนส่วนใหญ่) คุณยังคงสามารถเขียนรหัส async ได้ด้วยวิธีนี้ซึ่งจะใช้งานได้ดีจากแอพ 4.5
ghord

2
@ghord ถ้าคุณอยู่ในสุทธิ 4.0 และคุณต้องการที่จะเขียนโค้ดไม่ตรงกันคุณอาจจะใช้Microsoft.Bcl.Async และโค้ดของคุณจะถูกกำจัดFooหลังจากที่การส่งคืนTaskเสร็จสมบูรณ์ซึ่งฉันไม่ชอบเพราะมันจะแนะนำการเกิดพร้อมกันโดยไม่จำเป็น
svick

1
@svick รหัสของคุณรอจนกว่างานจะเสร็จเช่นกัน นอกจากนี้ Microsoft.Bcl.Async ใช้งานไม่ได้สำหรับฉันเนื่องจากการพึ่งพา KB2468871 และข้อขัดแย้งเมื่อใช้. NET 4.0 async codebase พร้อมกับ 4.5 async code ที่เหมาะสม
ghord

93

ถ้าคุณไม่จำเป็นต้องasync(เช่นคุณสามารถกลับมาTaskโดยตรง) asyncแล้วไม่ได้ใช้

มีบางสถานการณ์ที่return awaitมีประโยชน์เช่นถ้าคุณมีการดำเนินการแบบอะซิงโครนัสสองรายการ :

var intermediate = await FirstAsync();
return await SecondAwait(intermediate);

สำหรับข้อมูลเพิ่มเติมเกี่ยวกับasyncประสิทธิภาพดูบทความและวิดีโอMSDNของ Stephen Toub ในหัวข้อ

อัปเดต:ฉันเขียนบทความในบล็อกซึ่งมีรายละเอียดมากกว่านี้


13
คุณสามารถเพิ่มคำอธิบายว่าทำไมจึงawaitมีประโยชน์ในกรณีที่สอง? ทำไมไม่ทำreturn SecondAwait(intermediate);?
Matt Smith

2
ฉันมีคำถามเดียวกันกับแมตต์จะไม่return SecondAwait(intermediate);บรรลุเป้าหมายในกรณีเช่นนี้หรือไม่? ฉันคิดว่าreturn awaitมีความซ้ำซ้อนที่นี่เช่นกัน ...
TX_

23
@MattSmith นั่นคงไม่รวบรวม หากคุณต้องการใช้awaitในบรรทัดแรกคุณต้องใช้มันในบรรทัดที่สองด้วย
svick

2
@cateyes ผมไม่แน่ใจว่าสิ่งที่“ค่าใช้จ่ายในการแนะนำให้รู้จักเดียวกับพวกเขา” หมายถึง แต่asyncรุ่นจะใช้น้อยทรัพยากร (หัวข้อ) กว่ารุ่นซิงโครของคุณ
svick

4
@TomLint มันไม่ได้รวบรวมจริงๆ สมมติว่าชนิดส่งคืนของSecondAwaitคือ `สตริงข้อความแสดงข้อผิดพลาดคือ:" CS4016: เนื่องจากนี่เป็นวิธีการ async นิพจน์ที่ส่งคืนจะต้องเป็นประเภท 'string' แทนที่จะเป็น 'Task <string>' "
svick

23

เหตุผลเดียวที่คุณต้องการทำคือหากมีบางอย่างawaitในรหัสก่อนหน้าหรือหากคุณกำลังจัดการผลลัพธ์ก่อนที่จะส่งคืน อีกวิธีหนึ่งที่อาจเกิดขึ้นคือการtry/catchเปลี่ยนวิธีการจัดการข้อยกเว้น หากคุณไม่ทำสิ่งใดสิ่งนั้นแสดงว่าคุณถูกต้องไม่มีเหตุผลที่จะเพิ่มค่าใช้จ่ายในการทำวิธีasyncนี้


4
เช่นเดียวกับคำตอบของสตีเฟนผมไม่เข้าใจว่าทำไมจะreturn awaitเป็นสิ่งที่จำเป็น (แทนเพียงกลับมางานของการภาวนาเด็ก) แม้ว่าจะมีบางอย่างที่รอคอยคนอื่น ๆ ในรหัสก่อนหน้านี้ คุณช่วยอธิบายได้ไหม
TX_

10
@TX_ หากคุณต้องการลบasyncคุณจะรองานแรกได้อย่างไร คุณต้องทำเครื่องหมายวิธีราวกับasyncว่าคุณต้องการใช้การรอคอยใด ๆ หากวิธีการทำเครื่องหมายเป็นasyncและคุณมีawaitรหัสก่อนหน้านี้แล้วคุณจะต้องawaitดำเนินการ async ที่สองเพื่อให้เป็นประเภทที่เหมาะสม หากคุณเพิ่งลบออกไปawaitมันจะไม่รวบรวมเนื่องจากค่าส่งคืนจะไม่เป็นประเภทที่เหมาะสม เนื่องจากเมธอดคือasyncผลลัพธ์จะถูกห่อหุ้มอยู่ในงานเสมอ
Servy

11
@Noseratio ลองทั้งสอง คอมไพล์ครั้งแรก ประการที่สองไม่ได้ ข้อความแสดงข้อผิดพลาดจะแจ้งให้คุณทราบถึงปัญหา คุณจะไม่กลับประเภทที่เหมาะสม เมื่ออยู่ในasyncเมธอดที่คุณไม่ส่งคืนภารกิจคุณจะส่งคืนผลลัพธ์ของภารกิจที่จะถูกห่อ
Servy

3
@Servy แน่นอน - คุณพูดถูก ในกรณีหลังเราจะกลับมาTask<Type>อย่างชัดเจนในขณะที่asyncสั่งให้กลับType(ซึ่งตัวแปลจะกลายเป็นTask<Type>)
noseratio

3
@Itsik แน่นอนว่าasyncมันเป็นเพียงน้ำตาลซินแทคติคสำหรับการเดินสายต่อเนื่องอย่างชัดเจน คุณไม่จำเป็นต้อง asyncทำอะไรเลย แต่เมื่อทำงานแบบอะซิงโครนัสที่ไม่สำคัญอะไรเลยมันง่ายกว่ามากที่จะทำงานกับ ตัวอย่างเช่นรหัสที่คุณให้ไม่ได้เผยแพร่ข้อผิดพลาดตามที่คุณต้องการและการทำอย่างถูกต้องในสถานการณ์ที่ซับซ้อนยิ่งขึ้นก็เริ่มยากขึ้น ในขณะที่คุณไม่ต้องการ asyncสถานการณ์ที่ฉันอธิบายคือที่ซึ่งมันเป็นการเพิ่มมูลค่าให้ใช้
Servy

17

อีกกรณีหนึ่งที่คุณอาจต้องรอผลคือ:

async Task<IFoo> GetIFooAsync()
{
    return await GetFooAsync();
}

async Task<Foo> GetFooAsync()
{
    var foo = await CreateFooAsync();
    await foo.InitializeAsync();
    return foo;
}

ในกรณีนี้GetIFooAsync()จะต้องรอผลจากการGetFooAsyncเพราะชนิดของTความแตกต่างกันระหว่างสองวิธีการและไม่ได้มอบหมายโดยตรงกับTask<Foo> Task<IFoo>แต่ถ้าคุณรอผลที่ได้มันก็จะกลายเป็นFooซึ่งเป็นIFooมอบหมายโดยตรงกับ จากนั้นวิธีการ async จะทำการบรรจุผลลัพธ์ภายในTask<IFoo>และออกไป


1
เห็นด้วยนี่มันน่ารำคาญจริงๆ - ฉันเชื่อว่าสาเหตุที่สำคัญTask<>คือไม่เปลี่ยนแปลง
StuartLC

7

การทำให้วิธี "thunk" เป็นอย่างอื่นง่าย ๆ async สร้างเครื่องสถานะ async ในหน่วยความจำในขณะที่ไม่ใช่ async หนึ่งไม่ ในขณะที่สามารถชี้คนที่ใช้รุ่นที่ไม่ใช่แบบอะซิงก์ได้เนื่องจากมีประสิทธิภาพมากกว่า (ซึ่งเป็นจริง) แต่ก็หมายความว่าในกรณีที่เกิดการแฮงค์คุณไม่มีหลักฐานว่าวิธีการนั้นมีส่วนเกี่ยวข้องใน ซึ่งบางครั้งทำให้ยากที่จะเข้าใจการแขวน

ดังนั้นใช่เมื่อ perf ไม่สำคัญ (และมักจะไม่ใช่) ฉันจะโยน async ในวิธีการอันธพาลเหล่านี้ทั้งหมดเพื่อให้ฉันมีเครื่องสถานะ async เพื่อช่วยฉันในการวินิจฉัยแฮงค์ในภายหลังและเพื่อช่วยให้แน่ใจว่าหาก วิธีการ thunk พัฒนาตลอดเวลาพวกเขาจะต้องกลับงานที่ผิดพลาดแทนที่จะโยน


6

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

เมื่อคุณกลับงานวิธีการบรรลุวัตถุประสงค์และออกจากสแตกการโทร เมื่อคุณใช้return awaitคุณจะทิ้งมันไว้ใน call stack

ตัวอย่างเช่น:

Call stack เมื่อใช้ await: A กำลังรองานจาก B => B กำลังรองานจาก C

Call stack เมื่อไม่ได้ใช้งานกำลังรอ: A กำลังรองานจาก C ซึ่ง B ได้กลับมา


2

สิ่งนี้ทำให้ฉันสับสนและฉันรู้สึกว่าคำตอบก่อนหน้านี้มองข้ามคำถามที่แท้จริงของคุณ:

เหตุใดจึงต้องใช้ return รอโครงสร้างเมื่อคุณสามารถส่งคืนงานโดยตรงจากการเรียก DoAnotherThingAsync () ภายใน?

บางครั้งคุณต้องการ a จริงๆTask<SomeType>แต่ส่วนใหญ่เวลาที่คุณต้องการตัวอย่างSomeTypeนั่นคือผลลัพธ์จากภารกิจ

จากรหัสของคุณ:

async Task<SomeResult> DoSomethingAsync()
{
    using (var foo = new Foo())
    {
        return await foo.DoAnotherThingAsync();
    }
}

คนที่ไม่คุ้นเคยกับไวยากรณ์ (ผมเป็นต้น) อาจจะคิดว่าวิธีนี้ควรกลับTask<SomeResult>แต่เนื่องจากมันถูกทำเครื่องหมายด้วยก็หมายความว่าประเภทผลตอบแทนที่เกิดขึ้นจริงasync SomeResultหากคุณเพิ่งใช้return foo.DoAnotherThingAsync()คุณจะต้องส่งคืนภารกิจซึ่งจะไม่รวบรวม return awaitวิธีที่ถูกต้องคือการกลับผลของงานดังนั้น


1
"ประเภทผลตอบแทนจริง" ใช่มั้ย? async / await ไม่เปลี่ยนประเภทผลตอบแทน ในตัวอย่างของคุณvar task = DoSomethingAsync();จะให้งานคุณไม่ใช่T
รองเท้า

2
@ รองเท้าฉันไม่แน่ใจว่าฉันเข้าใจasync/awaitเรื่องนี้ดี เพื่อความเข้าใจของฉันTask task = DoSomethingAsync()ในขณะที่Something something = await DoSomethingAsync()ทั้งสองทำงาน ครั้งแรกให้งานที่เหมาะสมในขณะที่สองเนื่องจากawaitคำหลักจะให้ผลลัพธ์จากงานหลังจากเสร็จสิ้น Task task = DoSomethingAsync(); Something something = await task;ฉันจะยกตัวอย่างเช่นมี
heltonbiker
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.