2015-11-24 21 views
15

tôi đang điều tra một số vấn đề đối tượng đời kỳ lạ, và đi qua hành vi rất khó hiểu này của biên dịch C#:Hành vi kết hợp đóng này có phải là lỗi trình biên dịch C# không?

Hãy xem xét các lớp thử nghiệm sau đây:

class Test 
{ 
    delegate Stream CreateStream(); 

    CreateStream TestMethod(IEnumerable<string> data) 
    { 
     string file = "dummy.txt"; 
     var hashSet = new HashSet<string>(); 

     var count = data.Count(s => hashSet.Add(s)); 

     CreateStream createStream =() => File.OpenRead(file); 

     return createStream; 
    } 
} 

Trình biên dịch tạo ra như sau:

internal class Test 
{ 
    public Test() 
    { 
    base..ctor(); 
    } 

    private Test.CreateStream TestMethod(IEnumerable<string> data) 
    { 
    Test.<>c__DisplayClass1_0 cDisplayClass10 = new Test.<>c__DisplayClass1_0(); 
    cDisplayClass10.file = "dummy.txt"; 
    cDisplayClass10.hashSet = new HashSet<string>(); 
    Enumerable.Count<string>(data, new Func<string, bool>((object) cDisplayClass10, __methodptr(<TestMethod>b__0))); 
    return new Test.CreateStream((object) cDisplayClass10, __methodptr(<TestMethod>b__1)); 
    } 

    private delegate Stream CreateStream(); 

    [CompilerGenerated] 
    private sealed class <>c__DisplayClass1_0 
    { 
    public HashSet<string> hashSet; 
    public string file; 

    public <>c__DisplayClass1_0() 
    { 
     base..ctor(); 
    } 

    internal bool <TestMethod>b__0(string s) 
    { 
     return this.hashSet.Add(s); 
    } 

    internal Stream <TestMethod>b__1() 
    { 
     return (Stream) File.OpenRead(this.file); 
    } 
    } 
} 

Lớp gốc chứa hai lambdas: s => hashSet.Add(s)() => File.OpenRead(file). Lần đầu tiên đóng trên biến cục bộ hashSet, lần thứ hai đóng trên biến cục bộ file. Tuy nhiên, trình biên dịch tạo ra một lớp thực hiện đóng cửa duy nhất <>c__DisplayClass1_0 có chứa cả hai hashSetfile. Kết quả là, đại diện trả lại CreateStream chứa và giữ nguyên tham chiếu đến đối tượng hashSet cần phải có sẵn cho GC khi trả lại TestMethod.

Trong trường hợp thực tế mà tôi đã gặp sự cố này, một đối tượng rất quan trọng (nghĩa là> 100mb) được đính kèm không chính xác.

câu hỏi cụ thể của tôi là:

  1. Đây có phải là một lỗi? Nếu không, tại sao hành vi này được coi là mong muốn?

Cập nhật:

C# 5 đặc tả 7.15.5.1 nói:

Khi một biến ngoài được tham chiếu bởi một chức năng mang tính chất biến bên ngoài được cho là đã bị bắt bởi hàm ẩn danh. Thông thường, tuổi thọ của một biến cục bộ được giới hạn ở mức thực hiện khối hoặc câu lệnh mà nó được liên kết (§5.1.7). Tuy nhiên, tuổi thọ của một biến ngoài bị bắt là được kéo dài ít nhất cho đến khi cây đại diện hoặc biểu thức được tạo từ chức năng ẩn danh sẽ đủ điều kiện để thu thập rác.

Điều này dường như được mở ở một mức độ giải thích nào đó và không cấm rõ ràng lambda từ việc nắm bắt các biến mà nó không tham chiếu. Tuy nhiên, this question bao gồm một kịch bản có liên quan, mà @ eric-lippert được coi là lỗi. IMHO, tôi thấy việc thực hiện đóng cửa kết hợp được cung cấp bởi trình biên dịch là một tối ưu hóa tốt, nhưng tối ưu hóa không nên được sử dụng cho lambdas mà trình biên dịch có thể phát hiện hợp lý có thể có tuổi thọ vượt quá khung ngăn xếp hiện tại.


  1. Làm thế nào để mã chống lại điều này mà không từ bỏ việc sử dụng các lambdas tất cả lại với nhau? Đáng chú ý là làm thế nào để tôi mã chống lại điều này phòng thủ, để thay đổi mã trong tương lai không đột nhiên gây ra một số lambda không thay đổi khác trong cùng một phương pháp để bắt đầu kèm theo một cái gì đó mà nó không nên?

Cập nhật:

Đoạn mã ví dụ tôi đã cung cấp là cần thiết bởi contrived. Rõ ràng, tái cấu trúc lambda tạo ra một phương pháp riêng biệt làm việc xung quanh vấn đề. Câu hỏi của tôi không có ý định về các phương pháp hay nhất về thiết kế (cũng được đề cập bởi @ peter-duniho). Thay vào đó, với nội dung của TestMethod như nó là viết tắt, tôi muốn biết nếu có bất kỳ cách nào để ép buộc trình biên dịch để loại trừ các lambda createStream từ việc thực hiện đóng cửa kết hợp.


Đối với hồ sơ, tôi nhắm mục tiêu NET 4.6 với VS 2015.

+0

Họ chia sẻ cùng phạm vi từ vựng. có lẽ vì điều đó. –

+1

Bản sao có thể có của [Các phương thức ẩn danh riêng biệt chia sẻ một lớp?] (Http://stackoverflow.com/questions/3885106/discrete-anonymous-methods-sharing-a-class). Như là một tiền thưởng thêm, ví dụ này là khá đơn giản, nhưng đã được * không * contrived. – Brian

+0

Đây có phải là lý do cho "đóng cửa hoàn toàn bị bắt" không?Tôi nghĩ rằng tôi hiểu rằng cảnh báo tốt hơn rất nhiều ngay bây giờ. Tôi luôn tự hỏi tại sao trong một số trường hợp một lambda đã bắt được một cái gì đó nó không có gì để làm với. –

Trả lời

12

Đây có phải là lỗi không?

No. Trình biên dịch tuân thủ đặc điểm kỹ thuật tại đây.

Tại sao hành vi này được coi là mong muốn?

Không cần thiết. Đó là sâu sắc không may, khi bạn phát hiện ra ở đây, và như tôi đã mô tả lại trong năm 2007:

http://blogs.msdn.com/b/ericlippert/archive/2007/06/06/fyi-c-and-vb-closures-are-per-scope.aspx

C đội # biên dịch đã xem xét sửa chữa này trong mọi phiên bản kể từ C# 3.0 và nó chưa bao giờ được ưu tiên đủ cao. Xem xét việc nhập một vấn đề trên trang web Roslyn github (nếu chưa có, có thể có).

Cá nhân tôi muốn thấy sự cố này; vì nó đứng nó là một "gotcha" lớn.

Tôi làm cách nào để chống lại điều này mà không bỏ qua việc sử dụng lambdas cùng nhau?

Biến là thứ được chụp. Bạn có thể đặt biến hashset thành null khi bạn hoàn thành nó. Sau đó, bộ nhớ duy nhất được tiêu thụ là bộ nhớ cho biến, bốn byte, và không phải bộ nhớ cho điều mà nó đang đề cập đến, mà sẽ được thu thập.

6

Tôi không biết bất cứ điều gì trong ngôn ngữ C# đặc điểm kỹ thuật mà sẽ dictate chính xác làm thế nào một trình biên dịch là thực hiện phương pháp vô danh và ghi lại biến. Đây là chi tiết triển khai.

Những đặc điểm kỹ thuật không làm là đặt một số quy tắc về cách thức các phương thức ẩn danh và các biến chụp bắt buộc phải hoạt động. Tôi không có bản sao đặc tả C# 6, nhưng đây là văn bản có liên quan từ đặc điểm kỹ thuật C# 5, trong "7.15.5.1 Các biến bên ngoài đã chụp":

& hellip; tuổi thọ của biến bên ngoài bị bắt là mở rộng ít nhất cho đến khi cây đại biểu hoặc biểu thức được tạo từ hàm ẩn danh sẽ đủ điều kiện để thu thập rác. [nhấn mạnh mỏ]

Không có gì trong đặc điểm giới hạn tuổi thọ của biến. Trình biên dịch được yêu cầu đơn giản để đảm bảo biến tồn tại đủ lâu để vẫn hợp lệ nếu cần bằng phương thức ẩn danh.

Vì vậy & hellip;

1.Đây có phải là lỗi không? Nếu không, tại sao hành vi này được coi là mong muốn?

Không phải lỗi. Trình biên dịch tuân thủ các đặc điểm kỹ thuật.

Đối với việc liệu nó có được coi là "mong muốn" hay không, đó là thuật ngữ được tải. Những gì là "mong muốn" phụ thuộc vào ưu tiên của bạn. Điều đó nói rằng, một ưu tiên của một tác giả biên dịch là đơn giản hóa nhiệm vụ của trình biên dịch (và làm như vậy, làm cho nó chạy nhanh hơn và giảm thiểu nguy cơ lỗi). Việc triển khai cụ thể này có thể được coi là "mong muốn" trong ngữ cảnh đó.

Mặt khác, các nhà thiết kế ngôn ngữ và tác giả biên dịch cả hai cũng có mục tiêu chia sẻ giúp các lập trình viên tạo mã làm việc. Inasmuch như là một chi tiết thực hiện có thể can thiệp vào điều này, một chi tiết thực hiện như vậy có thể được coi là "không mong muốn". Cuối cùng, đó là vấn đề làm thế nào mà mỗi ưu tiên được xếp hạng, theo mục tiêu có khả năng cạnh tranh của họ.

2.Làm cách nào để viết mã này mà không bỏ qua việc sử dụng lambdas cùng nhau? Đáng chú ý là làm thế nào để tôi mã chống lại điều này phòng thủ, để thay đổi mã trong tương lai không đột nhiên gây ra một số lambda không thay đổi khác trong cùng một phương pháp để bắt đầu kèm theo một cái gì đó mà nó không nên?

Khó nói mà không có ví dụ ít giả tạo hơn. Nói chung, tôi muốn nói câu trả lời rõ ràng là "đừng trộn thịt cừu của bạn như thế". Trong ví dụ cụ thể của bạn (phải thừa nhận), bạn có một phương pháp dường như đang làm hai thứ hoàn toàn khác nhau. Điều này thường được tán thành vì nhiều lý do, và có vẻ như với tôi rằng ví dụ này chỉ thêm vào danh sách đó.

Tôi không biết cách tốt nhất để khắc phục "hai thứ khác nhau" là gì, nhưng một cách thay thế rõ ràng sẽ ít nhất là tái cấu trúc phương pháp sao cho phương pháp "hai thứ khác nhau" phân bổ công việc hai phương thức khác, mỗi phương thức có tên mô tả (có lợi ích bổ sung là giúp mã tự tài liệu).

Ví dụ:

CreateStream TestMethod(IEnumerable<string> data) 
{ 
    string file = "dummy.txt"; 
    var hashSet = new HashSet<string>(); 

    var count = AddAndCountNewItems(data, hashSet); 

    CreateStream createStream = GetCreateStreamCallback(file); 

    return createStream; 
} 

int AddAndCountNewItems(IEnumerable<string> data, HashSet<string> hashSet) 
{ 
    return data.Count(s => hashSet.Add(s)); 
} 

CreateStream GetCreateStreamCallback(string file) 
{ 
    return() => File.OpenRead(file); 
} 

Bằng cách này, các biến bắt vẫn độc lập. Ngay cả khi trình biên dịch không cho một số lý do kỳ lạ vẫn đặt chúng cả hai vào cùng một loại đóng cửa, nó vẫn không nên kết quả trong cùng một ví dụ của loại được sử dụng giữa hai đóng cửa.

TestMethod() của bạn vẫn làm hai việc khác nhau, nhưng ít nhất nó không tự chứa hai triển khai không liên quan đó. Mã này dễ đọc hơn và được ngăn cách tốt hơn, đó là một điều tốt, ngay cả bên cạnh thực tế là nó sửa vấn đề tồn tại biến.

+0

liên quan đến thông số C#, 7.15.5.1, đoạn đầu tiên bắt đầu _ "Khi biến ngoài được tham chiếu bởi một hàm ẩn danh, biến ngoài được cho là đã bị hàm ẩn danh ghi lại" _. Tuy nhiên, lambda '() => File.OpenRead (file)' không tham chiếu biến ngoài 'hashSet', do đó, thời gian tồn tại của' hashSet' không nên được kéo dài bởi toàn bộ thời gian của lambda này. Về _two những thứ khác nhau - như bạn lưu ý, đây thực sự là một ví dụ giả tạo. Vấn đề này dường như ảnh hưởng đến bất kỳ phương pháp nào sử dụng lambdas bắt giữ làm một số công việc và tạo ra một lambda ghi được lâu đời. – tg73

+0

@ tg73: _ "Vì vậy, tuổi thọ của hashSet không nên được kéo dài bởi thời gian tồn tại của lambda này" _ - IMHO, bạn không đọc kỹ thông số kỹ thuật đủ cẩn thận. Biến 'hashSet' _is_ được bắt bởi biểu thức lambda _other_, và không có gì trong đặc tả đặt một giới hạn _upper_ trong suốt thời gian tồn tại của các biến bị bắt này. Nếu trình biên dịch muốn, nó có thể thực hiện việc capture bằng cách biến biến 'static' và _never_ loại bỏ nó. Trong khi tôi hiểu hành vi này là bất tiện cho các mục đích của bạn, nó hoàn toàn nằm trong các yêu cầu được thiết lập bởi đặc điểm kỹ thuật. –

+0

@ tg73: _ "Vấn đề dường như ảnh hưởng đến bất kỳ phương thức nào ... tạo ra lambda ghi lại" _ - nhưng chỉ khi bạn có hai phương thức ẩn danh không liên quan trong phương thức mà mỗi phương thức nắm bắt một biến cục bộ khác nhau. Phương pháp nên đơn giản; một phương pháp đủ lớn để chứa hai bit độc lập của logic với các vòng đời biến không liên quan là do tái cấu trúc. IMHO nó phải dễ dàng, đủ để làm việc xung quanh vấn đề này trong mọi trường hợp, bằng cách phá vỡ các phương pháp thành những phần nhỏ hơn. Tôi không thể bình luận về những ví dụ mà tôi chưa từng thấy, nhưng sẽ không bình thường khi không thể làm điều này một cách dễ dàng. –

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