เกิดอะไรขึ้นในการลอง {return x; } ในที่สุด {x = null; } คำให้การ?


259

ฉันเห็นเคล็ดลับนี้ในอีกคำถามหนึ่งและสงสัยว่ามีใครบางคนอธิบายให้ฉันฟังว่ามันใช้งานได้อย่างไรบนโลกใบนี้?

try { return x; } finally { x = null; }

ผมหมายถึงการที่ไม่finallyเป็นไปตามข้อจริงๆรันหลังจากreturnคำสั่ง? รหัสนี้เป็นวิธีที่ไม่ปลอดภัยหรือไม่ คุณนึกถึงการtry-finallyแฮ็กเพิ่มเติมที่สามารถทำแฮ็คนี้ได้หรือไม่?

คำตอบ:


235

ไม่ - ที่ระดับ IL คุณจะไม่สามารถกลับจากภายในบล็อกที่จัดการโดยมีข้อยกเว้น มันจะเก็บมันไว้ในตัวแปรและส่งกลับหลังจากนั้น

ie คล้ายกับ:

int tmp;
try {
  tmp = ...
} finally {
  ...
}
return tmp;

ตัวอย่างเช่น (โดยใช้ตัวสะท้อนแสง):

static int Test() {
    try {
        return SomeNumber();
    } finally {
        Foo();
    }
}

รวบรวมเพื่อ:

.method private hidebysig static int32 Test() cil managed
{
    .maxstack 1
    .locals init (
        [0] int32 CS$1$0000)
    L_0000: call int32 Program::SomeNumber()
    L_0005: stloc.0 
    L_0006: leave.s L_000e
    L_0008: call void Program::Foo()
    L_000d: endfinally 
    L_000e: ldloc.0 
    L_000f: ret 
    .try L_0000 to L_0008 finally handler L_0008 to L_000e
}

โดยทั่วไปประกาศตัวแปรท้องถิ่น ( CS$1$0000) วางค่าลงในตัวแปร (ภายในบล็อกที่จัดการ) จากนั้นหลังจากออกจากบล็อกโหลดตัวแปรแล้วส่งกลับ ตัวสะท้อนแสงทำให้สิ่งนี้เป็น:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}

10
นี่ไม่ใช่สิ่งที่ ocdedio พูดถึง: สุดท้ายถูกดำเนินการหลังจากการคำนวณค่าส่งคืนและก่อนที่จะกลับมาจาก funczion จริง ๆ
mmmmmmmm

"บล็อกที่มีการจัดการข้อยกเว้น" ฉันคิดว่าสถานการณ์นี้ไม่มีส่วนเกี่ยวข้องกับข้อยกเว้นและการจัดการข้อยกเว้น นี่คือเกี่ยวกับวิธีที่. NET ดำเนินการสร้างResource Guard ในที่สุด
g.pickardou

361

คำสั่งสุดท้ายถูกดำเนินการ แต่ค่าส่งคืนจะไม่ได้รับผลกระทบ ลำดับการดำเนินการคือ:

  1. รหัสก่อนที่จะดำเนินการกับคำสั่ง return
  2. นิพจน์ในคำสั่ง return ถูกประเมินค่า
  3. ในที่สุดก็มีการดำเนินการบล็อก
  4. ผลลัพธ์ที่ประเมินในขั้นตอนที่ 2 จะถูกส่งคืน

นี่เป็นโปรแกรมสั้น ๆ ที่แสดงให้เห็น:

using System;

class Test
{
    static string x;

    static void Main()
    {
        Console.WriteLine(Method());
        Console.WriteLine(x);
    }

    static string Method()
    {
        try
        {
            x = "try";
            return x;
        }
        finally
        {
            x = "finally";
        }
    }
}

สิ่งนี้พิมพ์ว่า "ลอง" (เพราะนั่นคือสิ่งที่ส่งคืน) จากนั้น "สุดท้าย" เพราะนั่นคือค่าใหม่ของ x

แน่นอนถ้าเราส่งคืนการอ้างอิงไปยังวัตถุที่ไม่แน่นอน (เช่น StringBuilder) การเปลี่ยนแปลงใด ๆ ที่เกิดขึ้นกับวัตถุในบล็อกในที่สุดจะสามารถมองเห็นได้เมื่อกลับมา - สิ่งนี้ไม่ได้ส่งผลกระทบต่อมูลค่าที่ส่งคืน (ซึ่งเป็นเพียง อ้างอิง)


ฉันต้องการถามว่ามีตัวเลือกใด ๆ ใน Visual Studio เพื่อดูภาษา Intermediate (IL) ที่สร้างขึ้นสำหรับการเขียนรหัส C # ในขณะดำเนินการ ....
Enigma State

ข้อยกเว้น "ถ้าเราส่งคืนการอ้างอิงไปยังวัตถุที่ไม่แน่นอน (เช่น StringBuilder) การเปลี่ยนแปลงใด ๆ ที่เกิดขึ้นกับวัตถุในบล็อกสุดท้ายจะปรากฏขึ้นเมื่อกลับมา" คือถ้าวัตถุ StringBuilder ถูกตั้งค่าเป็นโมฆะในบล็อกสุดท้าย ซึ่งในกรณีนี้จะส่งคืนวัตถุที่ไม่ใช่นัล
Nick

4
@ Nick: ที่ไม่ได้มีการเปลี่ยนแปลงไปยังวัตถุ - ที่เปลี่ยนแปลงตัวแปร ไม่ส่งผลกระทบต่อวัตถุที่ค่าก่อนหน้าของตัวแปรที่อ้างถึงเลย ไม่เลยไม่ใช่ข้อยกเว้น
Jon Skeet

3
@Skeet นั่นหมายความว่า "คำสั่ง return ส่งคืนสำเนา" หรือไม่?
prabhakaran

4
@prabhakaran: มันประเมินการแสดงออกที่จุดของreturnคำสั่งและค่านั้นจะถูกส่งกลับ การแสดงออกไม่ได้รับการประเมินเป็นตัวควบคุมออกจากวิธีการ
Jon Skeet

19

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


13

การเพิ่มคำตอบที่ Marc Gravell และ Jon Skeet ได้รับคำตอบเป็นสิ่งสำคัญที่จะต้องทราบวัตถุและประเภทการอ้างอิงอื่น ๆ ที่ทำงานคล้ายกันเมื่อถูกส่งคืน แต่มีความแตกต่างบางอย่าง

"อะไร" ที่ได้รับกลับมาตามตรรกะเดียวกับประเภทที่เรียบง่าย:

class Test {
    public static Exception AnException() {
        Exception ex = new Exception("Me");
        try {
            return ex;
        } finally {
            // Reference unchanged, Local variable changed
            ex = new Exception("Not Me");
        }
    }
}

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

การดำเนินการเป็นหลัก:

class Test {
    public static Exception AnException() {
        Exception ex = new Exception("Me");
        Exception CS$1$0000 = null;
        try {
            CS$1$0000 = ex;
        } finally {
            // Reference unchanged, Local variable changed
            ex = new Exception("Not Me");
        }
        return CS$1$0000;
    }
}

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

class Test2 {
    public static System.IO.MemoryStream BadStream(byte[] buffer) {
        System.IO.MemoryStream ms = new System.IO.MemoryStream(buffer);
        try {
            return ms;
        } finally {
            // Reference unchanged, Referenced Object changed
            ms.Dispose();
        }
    }
}

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

class ByRefTests {
    public static int One(out int i) {
        try {
            i = 1;
            return i;
        } finally {
            // Return value unchanged, Store new value referenced variable
            i = 1000;
        }
    }

    public static int Two(ref int i) {
        try {
            i = 2;
            return i;
        } finally {
            // Return value unchanged, Store new value referenced variable
            i = 2000;
        }
    }

    public static int Three(out int i) {
        try {
            return 3;
        } finally {
            // This is not a compile error!
            // Return value unchanged, Store new value referenced variable
            i = 3000;
        }
    }
}

เช่นเดียวกับการสร้างโฟลว์อื่น ๆ "try-return-final" มีที่และสามารถอนุญาตให้ใช้โค้ดที่ดูสะอาดกว่าการเขียนโครงสร้างที่คอมไพล์จริง แต่จะต้องใช้อย่างระมัดระวังเพื่อหลีกเลี่ยงของ gotcha


4

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

ฉันสามารถเห็นได้ว่าทำสิ่งนี้เกิดขึ้นถ้าคุณต้องการรับประกันการเปลี่ยนแปลงมูลค่าของสนามเมื่อกลับมา (และหลังจากกำหนดมูลค่าส่งคืนแล้ว)


เว้นแต่ตัวแปรท้องถิ่นยังถูกจับโดยตัวแทน :)
จอนสกีต

จากนั้นมีการปิดและวัตถุไม่สามารถเก็บขยะได้เนื่องจากยังมีการอ้างอิง
เรียกซ้ำ

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