เหตุใดผลตอบแทนจึงไม่ปรากฏในบล็อกลองด้วยการจับ


98

ต่อไปนี้ไม่เป็นไร:

try
{
    Console.WriteLine("Before");

    yield return 1;

    Console.WriteLine("After");
}
finally
{
    Console.WriteLine("Done");
}

finallyบล็อกทำงานเมื่อสิ่งที่ทั้งได้เสร็จสิ้นการดำเนินการ ( IEnumerator<T>การสนับสนุนIDisposableเพื่อให้วิธีการที่จะให้แน่ใจว่านี้แม้ในขณะที่นับถูกยกเลิกก่อนที่มันจะเสร็จสิ้น)

แต่ไม่เป็นไร:

try
{
    Console.WriteLine("Before");

    yield return 1;  // error CS1626: Cannot yield a value in the body of a try block with a catch clause

    Console.WriteLine("After");
}
catch (Exception e)
{
    Console.WriteLine(e.Message);
}

สมมติว่า (เพื่อประโยชน์ในการโต้แย้ง) ว่ามีข้อยกเว้นเกิดขึ้นโดยการเรียกหนึ่งหรือสายอื่น ๆWriteLineภายในบล็อกการลอง ปัญหาในการดำเนินการต่อในcatchบล็อกคืออะไร?

แน่นอนว่าส่วนที่ให้ผลตอบแทนคือ (ในปัจจุบัน) ไม่สามารถโยนอะไรได้ แต่เหตุใดจึงควรหยุดเราจากการปิดล้อมtry/ catchจัดการกับข้อยกเว้นที่โยนก่อนหรือหลัง a yield return?

อัปเดต:มีความคิดเห็นที่น่าสนใจจาก Eric Lippert ที่นี่ - ดูเหมือนว่าพวกเขามีปัญหาเพียงพอในการใช้พฤติกรรม try / ในที่สุดอย่างถูกต้องแล้ว!

แก้ไข: หน้า MSDN ในข้อผิดพลาดนี้: http://msdn.microsoft.com/en-us/library/cs1x15az.aspx มันไม่ได้อธิบายว่าทำไม


2
ลิงก์ตรงไปยังความคิดเห็นของ Eric Lippert: blogs.msdn.com/oldnewthing/archive/2008/08/14/…
Roman Starkov

หมายเหตุ: คุณไม่สามารถให้ผลในการดักจับเองได้เช่นกัน :-(
Simon_Weaver

2
ลิงก์ oldnewthing ใช้งานไม่ได้อีกต่อไป
Sebastian Redl

คำตอบ:


50

ฉันสงสัยว่านี่เป็นเรื่องของการปฏิบัติจริงมากกว่าความเป็นไปได้ ฉันสงสัยว่ามีมากน้อยมากเท่าที่ข้อ จำกัด นี้เป็นจริงปัญหาที่ไม่สามารถทำงานรอบ - แต่เพิ่มความซับซ้อนในการคอมไพเลอร์จะมีความสำคัญมาก

มีบางสิ่งเช่นนี้ที่ฉันเคยพบมาแล้ว:

  • แอตทริบิวต์ไม่สามารถเป็นแบบทั่วไปได้
  • ไม่สามารถให้ X ได้มาจาก XY (คลาสที่ซ้อนกันใน X)
  • Iterator บล็อกโดยใช้ฟิลด์สาธารณะในคลาสที่สร้างขึ้น

ในแต่ละกรณีเหล่านี้จะเป็นไปได้ที่จะได้รับอิสระมากขึ้นเล็กน้อยโดยต้องเสียค่าใช้จ่ายที่ซับซ้อนเป็นพิเศษในคอมไพเลอร์ ทีมงานได้เลือกใช้ในทางปฏิบัติซึ่งฉันปรบมือให้พวกเขา - ฉันอยากจะมีภาษาที่เข้มงวดกว่านี้เล็กน้อยพร้อมกับคอมไพเลอร์ที่แม่นยำ 99.9% (ใช่มีข้อบกพร่องฉันพบหนึ่งในวันอื่น ๆ ดังนั้น) มากกว่า ภาษาที่ยืดหยุ่นซึ่งไม่สามารถรวบรวมได้อย่างถูกต้อง

แก้ไข: นี่คือหลักฐานหลอกว่าทำไมถึงเป็นไปได้

พิจารณาว่า:

  • คุณสามารถตรวจสอบให้แน่ใจว่าส่วนที่ให้ผลตอบแทนนั้นไม่ได้ทำให้เกิดข้อยกเว้น (คำนวณค่าล่วงหน้าจากนั้นคุณเพียงแค่ตั้งค่าฟิลด์และส่งกลับ "จริง")
  • คุณได้รับอนุญาตให้ลอง / จับซึ่งไม่ใช้ผลตอบแทนที่ได้รับในบล็อกตัววนซ้ำ
  • ตัวแปรโลคัลทั้งหมดในบล็อกตัววนซ้ำเป็นตัวแปรอินสแตนซ์ในประเภทที่สร้างขึ้นดังนั้นคุณจึงสามารถย้ายโค้ดไปยังวิธีการใหม่ได้อย่างอิสระ

ตอนนี้แปลงร่าง:

try
{
    Console.WriteLine("a");
    yield return 10;
    Console.WriteLine("b");
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

เป็น (ประเภทของรหัสหลอก):

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    __current = 10;
    return true;

case just_after_yield_return:
    try
    {
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        CatchBlock();
    }
    goto case post;

case post;
    Console.WriteLine("Post");


void CatchBlock()
{
    Console.WriteLine("Catch block");
}

การทำซ้ำเพียงอย่างเดียวคือการตั้งค่าบล็อก try / catch - แต่นั่นเป็นสิ่งที่คอมไพเลอร์สามารถทำได้อย่างแน่นอน

ฉันอาจพลาดบางอย่างที่นี่ไป - ถ้าเป็นเช่นนั้นโปรดแจ้งให้เราทราบ!


11
หลักฐานของแนวคิดที่ดี แต่กลยุทธ์นี้ได้รับความเจ็บปวด (อาจจะมากขึ้นสำหรับโปรแกรมเมอร์ C # กว่าสำหรับนักเขียนเรียบเรียง C #) เมื่อคุณคุณเริ่มต้นสร้างขอบเขตกับสิ่งที่ต้องการและusing foreachตัวอย่าง:try{foreach (string s in c){yield return s;}}catch(Exception){}
Brian

ความหมายปกติของ "try / catch" หมายความว่าหากส่วนใดส่วนหนึ่งของบล็อก try / catch ถูกข้ามเนื่องจากมีข้อยกเว้นการควบคุมจะโอนไปยังบล็อก "catch" ที่เหมาะสมหากมีอยู่ น่าเสียดายที่หากมีข้อยกเว้นเกิดขึ้น "ระหว่าง" ผลตอบแทนที่ได้รับจะไม่มีทางที่ตัววนซ้ำจะแยกความแตกต่างของกรณีที่ถูกกำจัดเนื่องจากข้อยกเว้นจากกรณีที่ถูกกำจัดเนื่องจากเจ้าของดึงข้อมูลที่น่าสนใจทั้งหมด
supercat

7
"ฉันสงสัยว่ามีน้อยครั้งมากที่ข้อ จำกัด นี้เป็นปัญหาที่ไม่สามารถแก้ไขได้" นั่นเหมือนกับการบอกว่าคุณไม่ต้องการข้อยกเว้นเพราะคุณสามารถใช้กลยุทธ์การส่งคืนรหัสข้อผิดพลาดที่มักใช้ใน C หลายปีที่ผ่านมา. ฉันยอมรับว่าปัญหาทางเทคนิคอาจมีความสำคัญ แต่yieldในความคิดของฉันยัง จำกัด ประโยชน์อย่างมากเนื่องจากรหัสสปาเก็ตตี้ที่คุณต้องเขียนเพื่อแก้ไข
jpmc26

@ jpmc26: ไม่จริง ๆ มันไม่ได้เป็นอย่างนั้นเลย ฉันจำไม่ได้ว่าสิ่งนี้เคยกัดฉันและฉันใช้ตัววนซ้ำบล็อกหลายครั้ง มันเล็กน้อยจำกัด ประโยชน์ของyieldIMO - มันเป็นทางยาวจากอย่างรุนแรง
Jon Skeet

2
'คุณลักษณะ' นี้ต้องใช้รหัสที่ค่อนข้างน่าเกลียดในบางกรณีในการแก้ไขปัญหาชั่วคราวดูstackoverflow.com/questions/5067188/…
namey

5

yieldคำสั่งทั้งหมดในคำจำกัดความของตัววนซ้ำจะถูกแปลงเป็นสถานะในเครื่องสถานะซึ่งใช้switchคำสั่งเพื่อเลื่อนสถานะได้อย่างมีประสิทธิภาพ ถ้ามันไม่ได้สร้างรหัสสำหรับyieldงบในลอง / จับมันจะต้องทำซ้ำทุกอย่างในtryบล็อกแต่ละ yieldคำสั่งในขณะที่ยังไม่รวมอื่น ๆ ทุกyieldคำสั่งสำหรับบล็อกว่า สิ่งนี้เป็นไปไม่ได้เสมอไปโดยเฉพาะอย่างยิ่งหากyieldคำสั่งหนึ่งขึ้นอยู่กับคำสั่งก่อนหน้านี้


2
ฉันไม่คิดว่าฉันจะซื้อสิ่งนั้น ฉันคิดว่ามันจะเป็นไปได้ทั้งหมด - แต่ซับซ้อนมาก
Jon Skeet

2
ลอง / จับบล็อกใน C # ไม่ได้หมายถึงการเข้าใหม่ หากคุณแยกมันออกเป็นไปได้ที่จะเรียก MoveNext () หลังจากมีข้อยกเว้นและดำเนินการบล็อกต่อไปโดยมีสถานะไม่ถูกต้อง
Mark Cidade

2

ฉันจะคาดเดาว่าเนื่องจากวิธีที่ call stack ได้รับผลกระทบ / คลายออกเมื่อคุณให้ผลตอบแทนจากตัวแจงนับจึงเป็นไปไม่ได้ที่บล็อก try / catch จะ "จับ" ข้อยกเว้นได้จริง (เนื่องจากบล็อกผลตอบแทนผลตอบแทนไม่ได้อยู่ในสแต็กแม้ว่าเขาจะสร้างบล็อกการวนซ้ำ)

เพื่อให้เข้าใจถึงสิ่งที่ฉันกำลังพูดถึงเกี่ยวกับการตั้งค่าบล็อกตัววนซ้ำและ foreach โดยใช้ตัววนซ้ำนั้น ตรวจสอบว่า Call Stack มีลักษณะอย่างไรภายในบล็อก foreach จากนั้นตรวจสอบภายในตัววนซ้ำลอง / บล็อกในที่สุด


ฉันคุ้นเคยกับการคลายสแต็กใน C ++ ซึ่งตัวทำลายถูกเรียกบนวัตถุในพื้นที่ที่อยู่นอกขอบเขต สิ่งที่สอดคล้องกันใน C # จะพยายาม / ในที่สุด แต่การคลี่คลายนั้นจะไม่เกิดขึ้นเมื่อผลตอบแทนเกิดขึ้น และสำหรับ try / catch ไม่จำเป็นต้องโต้ตอบกับผลตอบแทน
Daniel Earwicker

ตรวจสอบสิ่งที่เกิดขึ้นกับ call stack เมื่อวนซ้ำผ่านตัววนซ้ำแล้วคุณจะเข้าใจว่าฉันหมายถึงอะไร
Radu094

@ Radu094: ไม่ฉันแน่ใจว่ามันจะเป็นไปได้ อย่าลืมว่าในที่สุดก็จัดการได้แล้วซึ่งอย่างน้อยก็ค่อนข้างคล้ายกัน
Jon Skeet

2

ฉันยอมรับคำตอบของ THE INVINCIBLE SKEET จนกระทั่งมีคนจาก Microsoft มาเทน้ำเย็นให้กับไอเดียนี้ แต่ฉันไม่เห็นด้วยกับส่วนของความคิดเห็น - แน่นอนว่าคอมไพเลอร์ที่ถูกต้องมีความสำคัญมากกว่าคอมไพเลอร์ที่สมบูรณ์ แต่คอมไพเลอร์ C # นั้นฉลาดมากในการแยกแยะการเปลี่ยนแปลงนี้สำหรับเราเท่าที่จะเป็นไปได้ ความสมบูรณ์ที่มากขึ้นเล็กน้อยในกรณีนี้จะทำให้ภาษาใช้สอนอธิบายได้ง่ายขึ้นโดยมีขอบหรือ gotchas น้อยลง ดังนั้นฉันคิดว่ามันจะคุ้มค่ากับความพยายามพิเศษ ผู้ชายไม่กี่คนในเรดมอนด์เกาหัวเป็นเวลาสองสัปดาห์และด้วยเหตุนี้ผู้เขียนโค้ดหลายล้านคนในทศวรรษหน้าจะสามารถผ่อนคลายได้มากขึ้น

(ฉันยังเก็บงำความปรารถนาอันเลวร้ายที่จะมีวิธีการyield returnโยนข้อยกเว้นที่ถูกยัดเข้าไปในเครื่องของรัฐ "จากภายนอก" โดยรหัสที่ผลักดันการทำซ้ำ แต่เหตุผลของฉันที่ต้องการสิ่งนี้ค่อนข้างคลุมเครือ)

จริงๆแล้วคำถามหนึ่งที่ฉันมีเกี่ยวกับคำตอบของจอนคือการทำกับการโยนนิพจน์ผลตอบแทน

เห็นได้ชัดว่าผลตอบแทน 10 ไม่เลวร้ายนัก แต่สิ่งนี้จะไม่ดี:

yield return File.ReadAllText("c:\\missing.txt").Length;

ดังนั้นจะไม่สมเหตุสมผลกว่าที่จะประเมินสิ่งนี้ภายในบล็อก try / catch ก่อนหน้านี้:

case just_before_try_state:
    try
    {
        Console.WriteLine("a");
        __current = File.ReadAllText("c:\\missing.txt").Length;
    }
    catch (Something e)
    {
        CatchBlock();
        goto case post;
    }
    return true;

ปัญหาต่อไปจะซ้อนทับบล็อก try / catch และแก้ไขข้อยกเว้นใหม่:

try
{
    Console.WriteLine("x");

    try
    {
        Console.WriteLine("a");
        yield return 10;
        Console.WriteLine("b");
    }
    catch (Something e)
    {
        Console.WriteLine("y");

        if ((DateTime.Now.Second % 2) == 0)
            throw;
    }
}
catch (Something e)
{
    Console.WriteLine("Catch block");
}
Console.WriteLine("Post");

แต่ฉันมั่นใจว่าเป็นไปได้ ...


1
ใช่คุณจะนำการประเมินไปทดลอง / จับ ไม่สำคัญว่าคุณจะตั้งค่าตัวแปรไว้ที่ใด ประเด็นหลักคือคุณสามารถแบ่ง try / catch เพียงครั้งเดียวได้อย่างมีประสิทธิภาพโดยให้ผลตอบแทนเป็นสอง try / catches โดยให้ผลตอบแทนระหว่างพวกเขา
Jon Skeet
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.