Điều gì thực sự xảy ra trong một lần thử {return x; } cuối cùng {x = null; } tuyên bố?


259

Tôi đã thấy mẹo này trong một câu hỏi khác và tự hỏi liệu ai đó có thể giải thích cho tôi cách thức hoạt động trên trái đất này không?

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

Ý tôi là, không những finallykhoản thực sự thực hiện sau các returntuyên bố? Làm thế nào chủ đề không an toàn là mã này? Bạn có thể nghĩ ra bất kỳ bản try-finallyhack bổ sung nào có thể được thực hiện trong bản hack này không?

Câu trả lời:


235

Không - ở cấp độ IL, bạn không thể quay lại từ bên trong một khối được xử lý ngoại lệ. Nó chủ yếu lưu trữ nó trong một biến và trả về sau

tức là tương tự như:

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

ví dụ (sử dụng gương phản xạ):

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

biên dịch thành:

.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
}

Điều này về cơ bản khai báo một biến cục bộ ( CS$1$0000), đặt giá trị vào biến (bên trong khối được xử lý), sau đó sau khi thoát khỏi khối sẽ tải biến đó, sau đó trả về nó. Reflector biểu hiện điều này như sau:

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

10
Đây không phải là chính xác những gì ocdedio đã nói: cuối cùng được thực hiện sau khi tính toán giá trị trả về và trước khi thực sự trở về từ funczion ???
mmmmmmmm

"khối xử lý ngoại lệ" Tôi nghĩ kịch bản này không liên quan gì đến ngoại lệ và xử lý ngoại lệ. Đây là về cách .NET thực hiện cấu trúc bảo vệ tài nguyên cuối cùng .
g.pickardou

361

Câu lệnh cuối cùng được thực thi, nhưng giá trị trả về không bị ảnh hưởng. Trình tự thực hiện là:

  1. Mã trước khi trả lại câu lệnh được thực thi
  2. Biểu thức trong tuyên bố trả lại được đánh giá
  3. khối cuối cùng được thực thi
  4. Kết quả được đánh giá ở bước 2 được trả về

Đây là một chương trình ngắn để chứng minh:

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

Điều này in "thử" (vì đó là những gì được trả lại) và sau đó "cuối cùng" bởi vì đó là giá trị mới của x.

Tất nhiên, nếu chúng tôi trả lại tham chiếu đến một đối tượng có thể thay đổi (ví dụ StringBuilder) thì mọi thay đổi được thực hiện cho đối tượng trong khối cuối cùng sẽ hiển thị khi trả về - điều này không ảnh hưởng đến chính giá trị trả về (chỉ là một tài liệu tham khảo).


Tôi muốn hỏi, Có tùy chọn nào trong phòng thu trực quan để xem ngôn ngữ Trung cấp (IL) được tạo cho mã C # bằng văn bản tại thời điểm thực thi không ....
Trạng thái Enigma

Ngoại lệ cho "nếu chúng tôi trả lại tham chiếu đến một đối tượng có thể thay đổi (ví dụ: StringBuilder) thì mọi thay đổi được thực hiện cho đối tượng trong khối cuối cùng sẽ hiển thị khi trả về" là nếu đối tượng StringBuilder được đặt thành null trong khối cuối cùng, trong trường hợp đó một đối tượng không null được trả về.
Nick

4
@Nick: Đó không phải là thay đổi đối tượng - đó là thay đổi đối với biến . Nó không ảnh hưởng đến đối tượng mà giá trị trước đó của biến được đề cập ở tất cả. Vì vậy, nó không phải là một ngoại lệ.
Jon Skeet

3
@Skeet Điều đó có nghĩa là "tuyên bố trả về trả về một bản sao"?
prabhakara

4
@prabhakaran: Vâng, nó đánh giá biểu thức tại điểm của returncâu lệnh và giá trị đó sẽ được trả về. Biểu thức không được đánh giá là điều khiển rời khỏi phương thức.
Jon Skeet

19

Mệnh đề cuối cùng thực thi sau câu lệnh return nhưng trước khi thực sự quay trở lại từ hàm. Nó có ít liên quan đến an toàn chủ đề, tôi nghĩ. Nó không phải là một hack - cuối cùng được đảm bảo luôn luôn chạy bất kể bạn làm gì trong khối thử hoặc khối bắt của bạn.


13

Thêm vào các câu trả lời được đưa ra bởi Marc Gravell và Jon Skeet, điều quan trọng cần lưu ý là các đối tượng và các loại tham chiếu khác hoạt động tương tự khi trả về nhưng có một số khác biệt.

"Cái gì" được trả về tuân theo logic giống như các loại đơn giản:

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

Tham chiếu đang được trả về đã được đánh giá trước khi biến cục bộ được gán một tham chiếu mới trong khối cuối cùng.

Việc thực hiện về cơ bản là:

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

Sự khác biệt là vẫn có thể sửa đổi các loại có thể thay đổi bằng cách sử dụng các thuộc tính / phương thức của đối tượng có thể dẫn đến các hành vi không mong muốn nếu bạn không cẩn thận.

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

Điều thứ hai cần xem xét về việc thử trả lại-cuối cùng là các tham số được truyền "bằng tham chiếu" vẫn có thể được sửa đổi sau khi trả về. Chỉ có giá trị trả về đã được ước tính và được lưu trữ trong một biến tạm thời đang chờ được trả về, bất kỳ biến nào khác vẫn được sửa đổi theo cách thông thường. Hợp đồng của một tham số out thậm chí có thể không được thực hiện cho đến khi cuối cùng chặn theo cách này.

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

Giống như bất kỳ dòng chảy nào khác, "try-return-cuối cùng" có vị trí của nó và có thể cho phép tìm mã sạch hơn là viết cấu trúc mà nó thực sự biên dịch. Nhưng nó phải được sử dụng cẩn thận để tránh gotcha.


4

Nếu xlà biến cục bộ, tôi không thấy điểm, vì xdù sao thì phương thức này sẽ được đặt thành null và giá trị của giá trị trả về không phải là null (vì nó được đặt trong thanh ghi trước khi lệnh gọi được đặt xthành null).

Tôi chỉ có thể thấy việc này xảy ra nếu bạn muốn đảm bảo thay đổi giá trị của một trường khi trả về (và sau khi giá trị trả về được xác định).


Trừ khi biến cục bộ cũng được nắm bắt bởi một đại biểu :)
Jon Skeet

Sau đó, có một đóng cửa, và đối tượng không thể được thu gom rác, bởi vì vẫn còn một tài liệu tham khảo.
đệ quy

Nhưng tôi vẫn không thấy lý do tại sao bạn sẽ sử dụng var cục bộ trong một đại biểu trừ khi bạn có ý định sử dụng giá trị của nó.
đệ quy
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.