2009-12-18 35 views
43

Tôi cảm thấy mình hiểu rõ về bao đóng, cách sử dụng chúng và khi nào chúng có thể hữu ích. Nhưng những gì tôi không hiểu là làm thế nào họ thực sự làm việc đằng sau hậu trường trong bộ nhớ. Một số mã ví dụ:Đóng cửa hoạt động như thế nào? (C#)

public Action Counter() 
{ 
    int count = 0; 
    Action counter =() => 
    { 
     count++; 
    }; 

    return counter; 
} 

Thông thường, nếu {count} không bị chiếm đóng bởi việc đóng cửa, vòng đời của nó sẽ được scoped với phương pháp kê đơn(), và sau khi hoàn tất nó sẽ biến mất với phần còn lại của ngăn xếp phân bổ cho Counter(). Điều gì xảy ra mặc dù nó bị đóng cửa? Liệu toàn bộ phân bổ stack cho cuộc gọi Counter() này có được không? Liệu nó sao chép {count} vào heap? Nó không bao giờ thực sự được phân bổ trên stack, nhưng được công nhận bởi trình biên dịch như được đóng lại và do đó luôn luôn sống trên đống?

Đối với câu hỏi cụ thể này, tôi chủ yếu quan tâm đến cách thức hoạt động trong C#, nhưng sẽ không phản đối so sánh với các ngôn ngữ khác hỗ trợ đóng cửa.

+0

Câu hỏi hay. Tôi không chắc chắn, nhưng có, bạn có thể giữ khung ngăn xếp xung quanh trong C#. Máy phát điện sử dụng nó tất cả các thời gian (điều LINQ cho cấu trúc dữ liệu) mà dựa vào năng suất dưới mui xe. Hy vọng rằng tôi không được đánh dấu. nếu tôi, tôi sẽ học rất nhiều. –

+2

sản lượng biến phương pháp thành một lớp riêng biệt với một máy trạng thái. Ngăn xếp chính nó không được giữ xung quanh, nhưng trạng thái ngăn xếp được chuyển vào trạng thái lớp trong một lớp biên dịch được tạo ra bởi trình biên dịch – thecoop

+0

@thecoop, bạn có một liên kết giải thích điều này không? –

Trả lời

31

Trình biên dịch (trái ngược với thời gian chạy) tạo một lớp/loại khác. Các chức năng với đóng cửa của bạn và bất kỳ biến bạn đóng trên/hoisted/bị bắt được viết lại trong suốt mã của bạn là thành viên của lớp đó. Một đóng trong .Net được thực hiện như một ví dụ của lớp ẩn này.

Điều đó có nghĩa là biến số đếm của bạn là thành viên của một lớp khác hoàn toàn và tuổi thọ của lớp đó hoạt động giống như bất kỳ đối tượng clr nào khác; nó không đủ điều kiện để thu thập rác cho đến khi nó không còn bắt nguồn từ. Điều đó có nghĩa là miễn là bạn có một tham chiếu có thể gọi đến phương pháp nó sẽ không đi đâu cả.

+4

Kiểm tra mã trong câu hỏi với Reflector để xem một ví dụ này – Greg

+5

... chỉ cần tìm lớp được đặt tên xấu nhất trong giải pháp của bạn. – Will

+2

Điều đó có nghĩa là việc đóng cửa sẽ dẫn đến việc phân bổ đống mới, ngay cả khi giá trị bị đóng là nguyên thủy? – Matt

45

Đoán thứ ba của bạn là chính xác. Trình biên dịch sẽ tạo mã như sau:

private class Locals 
{ 
    public int count; 
    public void Anonymous() 
    { 
    this.count++; 
    } 
} 

public Action Counter() 
{ 
    Locals locals = new Locals(); 
    locals.count = 0; 
    Action counter = new Action(locals.Anonymous); 
    return counter; 
} 

Có ý nghĩa?

Ngoài ra, bạn đã yêu cầu so sánh. VB và JScript cả hai tạo ra bao đóng trong khá nhiều theo cùng một cách.

+14

Tại sao chúng ta nên tin bạn? Ồ đúng rồi ... – ChaosPandion

0

Cảm ơn @HenkHolterman. Vì nó đã được giải thích bởi Eric, tôi đã thêm liên kết chỉ để cho thấy trình biên dịch thực sự tạo ra lớp nào để đóng. Tôi muốn thêm vào đó việc tạo ra các lớp hiển thị bằng trình biên dịch C# có thể dẫn đến rò rỉ bộ nhớ. Ví dụ bên trong một hàm có một biến int được bắt bởi một biểu thức lambda và có một biến cục bộ khác chỉ đơn giản là giữ một tham chiếu đến một mảng byte lớn. Trình biên dịch sẽ tạo ra một cá thể lớp hiển thị sẽ giữ tham chiếu đến cả các biến i.e. int và mảng byte. Nhưng mảng byte sẽ không được thu gom rác cho đến khi lambda đang được tham chiếu.

0

Câu trả lời của Eric Lippert thực sự gây ấn tượng. Tuy nhiên nó sẽ là tốt đẹp để xây dựng một hình ảnh về cách ngăn xếp khung và chụp công việc nói chung. Để làm điều này, nó giúp xem xét một ví dụ phức tạp hơn một chút.

Đây là mã chụp:

public class Scorekeeper { 
    int swish = 7; 

    public Action Counter(int start) 
    { 
     int count = 0; 
     Action counter =() => { count += start + swish; } 
     return counter; 
    } 
} 

Và đây là những gì tôi nghĩ là tương đương sẽ là (nếu chúng ta may mắn Eric Lippert sẽ bình luận về việc liệu đây là thực sự đúng hay không):

private class Locals 
{ 
    public Locals(Scorekeeper sk, int st) 
    { 
     this.scorekeeper = sk; 
     this.start = st; 
    } 

    private Scorekeeper scorekeeper; 
    private int start; 

    public int count; 

    public void Anonymous() 
    { 
    this.count += start + scorekeeper.swish; 
    } 
} 

public class Scorekeeper { 
    int swish = 7; 

    public Action Counter(int start) 
    { 
     Locals locals = new Locals(this, start); 
     locals.count = 0; 
     Action counter = new Action(locals.Anonymous); 
     return counter; 
    } 
} 

Vấn đề là lớp địa phương thay thế cho toàn bộ khung ngăn xếp và được khởi tạo cho phù hợp mỗi lần phương thức Counter được gọi. Thông thường khung stack bao gồm một tham chiếu đến 'this', cộng với các đối số phương thức, cộng với các biến cục bộ. (Khung ngăn xếp cũng có hiệu lực mở rộng khi một khối điều khiển được nhập vào.)

Do đó, chúng tôi không chỉ có một đối tượng tương ứng với ngữ cảnh đã chụp, thay vào đó chúng tôi thực sự có một đối tượng trên mỗi khung ngăn xếp được chụp.

Dựa trên điều này, chúng ta có thể sử dụng mô hình tinh thần sau: khung ngăn xếp được lưu trữ trên heap (thay vì trên ngăn xếp), trong khi ngăn xếp chính nó chỉ chứa các con trỏ đến khung ngăn xếp trên đống. Các phương thức Lambda chứa một con trỏ tới khung ngăn xếp. Điều này được thực hiện bằng cách sử dụng bộ nhớ được quản lý, do đó, khung sẽ di chuyển xung quanh trên heap cho đến khi nó không còn cần thiết nữa.

Rõ ràng trình biên dịch có thể thực hiện điều này bằng cách chỉ sử dụng vùng heap khi đối tượng heap được yêu cầu để hỗ trợ đóng cửa lambda.

Điều tôi thích về mô hình này là nó cung cấp hình ảnh tích hợp cho 'lợi tức lãi'. Chúng ta có thể nghĩ về một phương thức iterator (sử dụng return yield) như thể đó là stack frame được tạo trên heap và con trỏ tham chiếu được lưu trữ trong một biến cục bộ trong người gọi, để sử dụng trong quá trình lặp.

+0

Nó không chính xác; làm thế nào có thể tư nhân 'swish' được truy cập từ bên ngoài lớp' Scorekeeper'? Điều gì sẽ xảy ra nếu 'bắt đầu' bị đột biến? Nhưng quan trọng hơn: giá trị trong việc trả lời câu hỏi 8 năm tuổi với câu trả lời được chấp nhận là gì? –

+0

Nếu bạn muốn biết codegen thực là gì, hãy sử dụng ILDASM hoặc bộ tách rời IL-to-source. –

+0

Một cách tốt hơn hoàn toàn để nghĩ về nó là để ngừng suy nghĩ của "khung stack" như một cái gì đó cơ bản. Ngăn xếp chỉ đơn giản là một cấu trúc dữ liệu được sử dụng để thực hiện hai điều: ** kích hoạt ** và ** tiếp tục **. Đó là: các giá trị liên quan đến việc kích hoạt một phương thức là gì, và mã nào sẽ chạy sau khi phương thức này trả về? Nhưng ngăn xếp chỉ là một cấu trúc dữ liệu thích hợp để lưu trữ thông tin kích hoạt/tiếp tục ** nếu thời gian hoạt động của phương thức kích hoạt một cách hợp lý tạo thành một chồng **. –

Các vấn đề liên quan