Rò rỉ bộ nhớ có được tạo ra nếu một dòng MemoryStream trong .NET không được đóng không?


112

Tôi có mã sau:

MemoryStream foo(){
    MemoryStream ms = new MemoryStream();
    // write stuff to ms
    return ms;
}

void bar(){
    MemoryStream ms2 = foo();
    // do stuff with ms2
    return;
}

Có khả năng nào mà MemoryStream mà tôi đã cấp phát bằng cách nào đó sẽ không được xử lý sau này không?

Tôi đã nhận được một đánh giá ngang hàng yêu cầu tôi đóng nó theo cách thủ công và tôi không thể tìm thấy thông tin để biết liệu anh ta có điểm hợp lệ hay không.


41
Hỏi người đánh giá của bạn chính xác lý do tại sao họ nghĩ rằng bạn nên đóng nó. Nếu anh ấy nói về thực hành tốt nói chung, có lẽ anh ấy là người thông minh. Nếu anh ấy nói về việc giải phóng bộ nhớ sớm hơn, anh ấy đã sai.
Jon Skeet

Câu trả lời:


60

Nếu thứ gì đó dùng một lần, bạn luôn nên vứt bỏ nó. Bạn nên sử dụng một usingcâu lệnh trong bar()phương thức của mình để đảm bảo ms2được Xử lý.

Cuối cùng nó sẽ được dọn dẹp bởi bộ thu gom rác, nhưng cách gọi là Vứt bỏ luôn luôn là một thông lệ tốt. Nếu bạn chạy FxCop trên mã của mình, nó sẽ gắn cờ nó như một cảnh báo.


16
Các cuộc gọi khối đang sử dụng sẽ giải quyết cho bạn.
Nick

20
@Grauenwolf: khẳng định của bạn phá vỡ tính đóng gói. Là một người tiêu dùng, bạn không nên quan tâm xem nó có phải là không: nếu nó là IDisposable, nhiệm vụ của bạn là Vứt bỏ () nó.
Marc Gravell

4
Điều này không đúng với lớp StreamWriter: Nó sẽ loại bỏ luồng được kết nối chỉ khi bạn loại bỏ StreamWriter - nó sẽ không bao giờ loại bỏ luồng nếu nó được thu gom rác và trình hoàn thiện của nó được gọi - đây là do thiết kế.
springy76

4
Tôi biết câu hỏi này là từ năm 2008, nhưng ngày nay chúng ta có thư viện Tác vụ .NET 4.0. Dispose () là không cần thiết trong hầu hết các trường hợp khi sử dụng Task. Mặc dù tôi đồng ý rằng IDisposable nên có nghĩa là "Tốt hơn bạn nên vứt bỏ cái này khi bạn hoàn thành," nó không thực sự có nghĩa như vậy nữa.
Phil

7
Một ví dụ khác mà bạn không nên vứt bỏ đối tượng IDisposable là HttpClient aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong Chỉ một ví dụ khác từ BCL trong đó có đối tượng IDisposable và bạn không cần (hoặc thậm chí không nên) vứt bỏ nó. Điều này chỉ để nhớ rằng thường có một số ngoại lệ so với quy tắc chung, ngay cả trong BCL;)
Mariusz Pawelski

166

Bạn sẽ không bị rò rỉ bất cứ thứ gì - ít nhất là trong quá trình triển khai hiện tại.

Gọi Dispose sẽ không làm sạch bộ nhớ được sử dụng bởi MemoryStream nhanh hơn. Nó sẽ ngăn luồng của bạn khả thi cho các cuộc gọi Đọc / Viết sau cuộc gọi, điều này có thể hữu ích hoặc không hữu ích cho bạn.

Nếu bạn hoàn toàn chắc chắn rằng bạn không bao giờ muốn chuyển từ MemoryStream sang một loại luồng khác, việc không gọi Dispose sẽ không gây hại gì cho bạn. Tuy nhiên, đó là thực tế chung là tốt một phần là bởi vì nếu bạn đã bao giờ làm thay đổi sử dụng một Stream khác nhau, bạn không muốn bị cắn bởi một lỗi khó tìm vì bạn đã chọn một cách dễ dàng ra về sớm. (Mặt khác, có đối số YAGNI ...)

Lý do khác để làm điều đó dù sao đi nữa là một triển khai mới có thể giới thiệu các tài nguyên sẽ được giải phóng trên Dispose.


Trong trường hợp này, hàm đang trả về một dòng MemoryStream vì nó cung cấp "dữ liệu có thể được diễn giải khác nhau tùy thuộc vào việc gọi tham số", vì vậy nó có thể là một mảng byte, nhưng dễ dàng hơn vì các lý do khác để thực hiện như một dòng MemoryStream. Vì vậy, nó chắc chắn sẽ không phải là một lớp Stream khác.
Coderer

Trong trường hợp đó, tôi vẫn sẽ cố gắng loại bỏ nó theo nguyên tắc chung - xây dựng thói quen tốt, v.v. - nhưng tôi sẽ không lo lắng quá nếu nó trở nên phức tạp.
Jon Skeet

1
Nếu một người thực sự lo lắng về việc giải phóng tài nguyên càng sớm càng tốt, hãy hủy tham chiếu ngay sau khi khối "sử dụng" của bạn, để các tài nguyên chưa được quản lý (nếu có) được dọn sạch và đối tượng đủ điều kiện để thu gom rác. Nếu phương thức trả về ngay lập tức, nó có thể không tạo ra nhiều khác biệt, nhưng nếu bạn tiếp tục làm những việc khác trong phương thức như yêu cầu thêm bộ nhớ, thì chắc chắn nó có thể tạo ra sự khác biệt.
Triynko

@Triynko Không thực sự đúng: Xem: stackoverflow.com/questions/574019/… để biết chi tiết.
George Stocker

10
Lập luận YAGNI có thể được thực hiện theo cả hai cách - vì quyết định không vứt bỏ thứ gì đó thực hiện IDisposablelà một trường hợp đặc biệt đi ngược lại với phương pháp hay nhất thông thường, bạn có thể lập luận rằng đó là trường hợp bạn không nên làm cho đến khi bạn thực sự cần, theo YAGNI nguyên tắc.
Jon Hanna

26

Có, có một rò rỉ , tùy thuộc vào cách bạn định nghĩa LEAK và mức độ LATER mà bạn muốn nói ...

Nếu do rò rỉ, bạn có nghĩa là "bộ nhớ vẫn được cấp phát, không có sẵn để sử dụng, mặc dù bạn đã sử dụng xong" và ý bạn là bất cứ lúc nào sau khi gọi vứt bỏ, thì có thể có một rò rỉ, mặc dù nó không vĩnh viễn (tức là thời gian chạy ứng dụng của bạn).

Để giải phóng bộ nhớ được quản lý được sử dụng bởi MemoryStream, bạn cần loại bỏ nó , bằng cách vô hiệu hóa tham chiếu của bạn đến nó, để nó đủ điều kiện để thu gom rác ngay lập tức. Nếu bạn không làm được điều này, thì bạn sẽ tạo ra một rò rỉ tạm thời kể từ khi bạn sử dụng xong nó, cho đến khi tham chiếu của bạn vượt ra ngoài phạm vi, vì trong thời gian này, bộ nhớ sẽ không có sẵn để cấp phát.

Lợi ích của câu lệnh using (chỉ đơn giản là gọi vứt bỏ) là bạn có thể KHAI BÁO tham chiếu của mình trong câu lệnh using. Khi câu lệnh using kết thúc, không chỉ được gọi, mà tham chiếu của bạn sẽ vượt ra ngoài phạm vi, vô hiệu hóa tham chiếu một cách hiệu quả và làm cho đối tượng của bạn đủ điều kiện để thu gom rác ngay lập tức mà không yêu cầu bạn phải nhớ viết mã "reference = null".

Mặc dù việc không can thiệp vào điều gì đó ngay lập tức không phải là lỗi rò rỉ bộ nhớ "vĩnh viễn" cổ điển, nhưng nó chắc chắn có tác dụng tương tự. Ví dụ: nếu bạn giữ tham chiếu của mình tới Dòng bộ nhớ (ngay cả sau khi gọi lệnh hủy), và xa hơn một chút trong phương thức của mình, bạn cố gắng cấp thêm bộ nhớ ... bộ nhớ đang được sử dụng bởi dòng bộ nhớ vẫn tham chiếu của bạn sẽ không có sẵn cho bạn cho đến khi bạn vô hiệu hóa tham chiếu hoặc nó vượt ra khỏi phạm vi, mặc dù bạn đã gọi là vứt bỏ và đã sử dụng xong.


6
Tôi thích phản hồi này. Đôi khi mọi người quên nhiệm vụ kép của việc sử dụng: háo hức cải tạo tài nguyên háo hức tham khảo.
Kit

1
Thật vậy, mặc dù tôi nghe nói rằng không giống như Java, trình biên dịch C # phát hiện "lần sử dụng cuối cùng có thể", vì vậy nếu biến được định sẵn ra khỏi phạm vi sau lần tham chiếu cuối cùng của nó, nó có thể đủ điều kiện để thu gom rác ngay sau lần sử dụng cuối cùng có thể .. . trước khi nó thực sự vượt ra khỏi phạm vi. Xem stackoverflow.com/questions/680550/explicit-nulling
Triynko vào

2
Bộ thu gom rác và bộ rung không hoạt động theo cách đó. Phạm vi là một cấu trúc ngôn ngữ và không phải là thứ mà thời gian chạy sẽ tuân theo. Trên thực tế, có thể bạn đang kéo dài thời gian tham chiếu trong bộ nhớ, bằng cách thêm lệnh gọi .Dispose () khi khối kết thúc. Xem ericlippert.com/2015/05/18/…
Pablo Montilla

8

Không cần gọi .Dispose()(hoặc gói bằng Using).

Lý do bạn gọi .Dispose()là để giải phóng tài nguyên càng sớm càng tốt .

Hãy nghĩ về máy chủ Stack Overflow, nơi chúng tôi có một bộ nhớ giới hạn và hàng nghìn yêu cầu đến. Chúng tôi không muốn chờ đợi cho quá trình thu gom rác theo lịch trình, chúng tôi muốn giải phóng bộ nhớ đó càng sớm càng tốt để nó có sẵn cho các yêu cầu mới đến.


24
Mặc dù vậy, việc gọi Dispose trên MemoryStream sẽ không giải phóng bất kỳ bộ nhớ nào. Trong thực tế, bạn vẫn có thể có được ở các dữ liệu trong một MemoryStream sau khi bạn đã gọi Vứt bỏ - hãy thử nó :)
Jon Skeet

12
-1 Mặc dù nó đúng với MemoryStream, nhưng theo lời khuyên chung thì điều này hoàn toàn sai. Vứt bỏ là giải phóng các tài nguyên không được quản lý , chẳng hạn như xử lý tệp hoặc kết nối cơ sở dữ liệu. Bộ nhớ không thuộc loại đó. Bạn hầu như luôn phải đợi thu dọn rác theo lịch trình để giải phóng bộ nhớ.
Joe

1
Ưu điểm của việc áp dụng một kiểu mã hóa để phân bổ và sắp xếp FileStreamcác đối tượng và một kiểu khác cho MemoryStreamcác đối tượng là gì?
Robert Rossney

3
FileStream liên quan đến các tài nguyên không được quản lý thực sự có thể được giải phóng ngay lập tức khi gọi Dispose. Mặt khác, MemoryStream lưu trữ một mảng byte được quản lý trong biến _buffer của nó, biến này không được giải phóng tại thời điểm xử lý. Trên thực tế, bộ đệm _buffer thậm chí không được đặt null trong phương thức MemoryStream's Dispose, đây là một IMO NÚT LỖI vì vô hiệu hóa tham chiếu có thể làm cho bộ nhớ đủ điều kiện cho GC ngay tại thời điểm xử lý. Thay vào đó, một tham chiếu MemoryStream kéo dài (nhưng đã được xử lý) vẫn còn lưu giữ trong bộ nhớ. Do đó, một khi bạn loại bỏ nó, bạn cũng nên vô hiệu nó nếu nó vẫn còn trong phạm vi.
Triynko

@Triynko - "Do đó, một khi bạn vứt bỏ nó, bạn cũng nên vô hiệu nó nếu nó vẫn còn trong phạm vi" - Tôi không đồng ý. Nếu nó được sử dụng lại sau lệnh gọi Dispose, điều này sẽ gây ra NullReferenceException. Nếu nó không được sử dụng lại sau khi Vứt bỏ, không cần phải hủy nó; GC đủ thông minh.
Joe

8

Điều này đã được trả lời, nhưng tôi sẽ chỉ nói thêm rằng nguyên tắc ẩn thông tin cổ điển có nghĩa là bạn có thể muốn tái cấu trúc lại:

MemoryStream foo()
{    
    MemoryStream ms = new MemoryStream();    
    // write stuff to ms    
    return ms;
}

đến:

Stream foo()
{    
   ...
}

Điều này nhấn mạnh rằng người gọi không nên quan tâm đến loại Luồng nào đang được trả về và có thể thay đổi việc triển khai nội bộ (ví dụ: khi chế nhạo để thử nghiệm đơn vị).

Sau đó, bạn sẽ gặp rắc rối nếu bạn chưa sử dụng Dispose trong triển khai thanh của mình:

void bar()
{    
    using (Stream s = foo())
    {
        // do stuff with s
        return;
    }
}

5

Tất cả các luồng đều triển khai IDisposable. Kết hợp dòng Bộ nhớ của bạn trong một câu lệnh sử dụng và bạn sẽ ổn và đẹp đẽ. Việc sử dụng chặn sẽ đảm bảo luồng của bạn bị đóng và bị xử lý bất kể điều gì.

bất cứ nơi nào bạn gọi Foo, bạn có thể thực hiện bằng cách sử dụng (MemoryStream ms = foo ()) và tôi nghĩ bạn vẫn sẽ ổn.


1
Một vấn đề mà tôi gặp phải với thói quen này là bạn phải chắc chắn rằng luồng không được sử dụng ở bất kỳ nơi nào khác. Ví dụ: tôi đã tạo một JpegBitmapDecoder trỏ đến một dòng MemoryStream và trả về Frames [0] (nghĩ rằng nó sẽ sao chép dữ liệu vào cửa hàng nội bộ của chính nó) nhưng nhận thấy rằng bitmap sẽ chỉ hiển thị 20% thời gian - hóa ra là do Tôi đã loại bỏ dòng ký ức.
defos1

Nếu luồng bộ nhớ của bạn vẫn tồn tại (nghĩa là một khối đang sử dụng không có ý nghĩa), thì bạn nên gọi Dispose và ngay lập tức đặt biến thành null. Nếu bạn vứt bỏ nó, thì nó sẽ không còn được sử dụng nữa, vì vậy bạn cũng nên đặt nó thành null ngay lập tức. Những gì chaiguy đang mô tả nghe có vẻ giống như một vấn đề quản lý tài nguyên, bởi vì bạn không nên đưa ra một tham chiếu đến một thứ gì đó trừ khi thứ mà bạn đang giao nó chịu trách nhiệm xử lý nó và thứ đưa ra tham chiếu biết rằng nó không còn trách nhiệm làm như vậy.
Triynko

2

Bạn sẽ không bị rò rỉ bộ nhớ, nhưng người đánh giá mã của bạn đã chính xác để chỉ ra rằng bạn nên đóng luồng của mình. Thật lịch sự khi làm như vậy.

Tình huống duy nhất mà bạn có thể bị rò rỉ bộ nhớ là khi bạn vô tình để lại một tham chiếu đến luồng và không bao giờ đóng nó. Bạn vẫn không thực sự rò rỉ bộ nhớ, nhưng bạn đang không cần thiết mở rộng số lượng thời gian mà bạn yêu cầu bồi thường được sử dụng nó.


1
> Bạn vẫn không thực sự bị rò rỉ bộ nhớ, nhưng bạn không cần thiết phải kéo dài khoảng thời gian mà bạn tuyên bố là đang sử dụng nó. Bạn có chắc không? Việc vứt bỏ không giải phóng bộ nhớ và việc gọi nó muộn trong hàm thực sự có thể kéo dài thời gian không thể thu thập được.
Jonathan Allen

2
Vâng, Jonathan có lý. Đặt lệnh gọi Dispose trễ trong hàm thực sự có thể khiến trình biên dịch nghĩ rằng bạn cần truy cập vào cá thể luồng (để đóng nó) rất muộn trong hàm. Điều này có thể tệ hơn việc không gọi vứt bỏ (do đó tránh được tham chiếu trễ trong hàm đến biến dòng), vì trình biên dịch có thể tính toán điểm phát hành tối ưu (còn gọi là "điểm có thể sử dụng cuối cùng") sớm hơn trong hàm .
Triynko

2

Tôi khuyên bạn nên gói MemoryStream bar()trong một usingcâu lệnh chủ yếu để có tính nhất quán:

  • Hiện tại MemoryStream không giải phóng bộ nhớ .Dispose(), nhưng có thể vào một thời điểm nào đó trong tương lai, hoặc bạn (hoặc người khác ở công ty của bạn) có thể thay thế nó bằng MemoryStream tùy chỉnh của riêng bạn, v.v.
  • Nó giúp thiết lập một mô hình trong dự án của bạn để đảm bảo tất cả các Luồng đều được xử lý - dòng này được vẽ chắc chắn hơn bằng cách nói "tất cả các Luồng phải được xử lý" thay vì "một số Luồng phải được xử lý, nhưng một số Luồng nhất định không cần phải xử lý" ...
  • Nếu bạn từng thay đổi mã để cho phép trả lại các loại Luồng khác, bạn vẫn cần phải thay đổi mã đó để loại bỏ.

Một điều khác mà tôi thường làm trong các trường hợp như foo()khi tạo và trả lại IDisposable là đảm bảo rằng bất kỳ lỗi nào giữa việc xây dựng đối tượng và đối tượng returnđều bị bắt bởi một ngoại lệ, loại bỏ đối tượng và ném lại ngoại lệ:

MemoryStream x = new MemoryStream();
try
{
    // ... other code goes here ...
    return x;
}
catch
{
    // "other code" failed, dispose the stream before throwing out the Exception
    x.Dispose();
    throw;
}

1

Nếu một đối tượng triển khai IDisposable, bạn phải gọi phương thức .Dispose khi hoàn tất.

Trong một số đối tượng, Dispose có nghĩa giống như Close và ngược lại, trong trường hợp đó, cũng tốt.

Bây giờ, đối với câu hỏi cụ thể của bạn, không, bạn sẽ không bị rò rỉ bộ nhớ.


3
"Phải" là một từ rất mạnh. Bất cứ khi nào có các quy tắc, bạn nên biết hậu quả của việc vi phạm chúng. Đối với MemoryStream, có rất ít hậu quả.
Jon Skeet

-1

Tôi không phải là chuyên gia .net, nhưng có lẽ vấn đề ở đây là tài nguyên, cụ thể là trình xử lý tệp, chứ không phải bộ nhớ. Tôi đoán rằng trình thu gom rác cuối cùng sẽ giải phóng luồng và đóng tay cầm, nhưng tôi nghĩ rằng cách tốt nhất là đóng nó một cách rõ ràng, để đảm bảo bạn xả hết nội dung vào đĩa.


MemoryStream là tất cả trong bộ nhớ - không có trình xử lý tệp nào ở đây.
Jon Skeet

-2

Việc thải bỏ các tài nguyên không được quản lý là không thể xác định trong các ngôn ngữ thu gom rác. Ngay cả khi bạn gọi Dispose một cách rõ ràng, bạn hoàn toàn không kiểm soát được thời điểm bộ nhớ sao lưu thực sự được giải phóng. Dispose được gọi một cách ngầm định khi một đối tượng vượt ra khỏi phạm vi, cho dù đó là bằng cách thoát khỏi câu lệnh using hoặc bật lên callstack từ một phương thức cấp dưới. Tất cả điều này đang được nói, đôi khi đối tượng thực sự có thể là một trình bao bọc cho một tài nguyên được quản lý (ví dụ: tệp). Đây là lý do tại sao bạn nên đóng một cách rõ ràng trong các câu lệnh cuối cùng hoặc sử dụng câu lệnh using. Chúc mừng


1
Không hoàn toàn đúng. Dispose được gọi khi thoát khỏi một câu lệnh using. Dispose không được gọi khi một đối tượng vừa đi ra khỏi phạm vi.
Alexander Abramov

-3

MemorySteram không là gì ngoài mảng byte, là đối tượng được quản lý. Quên vứt bỏ hoặc đóng cửa này không có tác dụng phụ nào khác ngoài việc vượt qua giai đoạn hoàn thiện đầu.
Chỉ cần kiểm tra hằng số hoặc phương thức tuôn ra của MemoryStream trong bộ phản xạ và sẽ rõ tại sao bạn không cần phải lo lắng về việc đóng hoặc loại bỏ nó ngoài vấn đề thực hành tốt.


6
-1: Nếu bạn định đăng một câu hỏi dành cho lứa tuổi 4+ với câu trả lời được chấp nhận, hãy cố gắng làm cho nó trở nên hữu ích.
Tieson T.
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.