Trình thu gom rác sẽ gọi IDis Dùng một lần. Mục đích cho tôi?


134

Mẫu .NET IDis Dùng một lần ngụ ý rằng nếu bạn viết một bộ hoàn thiện và triển khai IDis Dùng một lần, thì bộ hoàn thiện của bạn cần gọi một cách rõ ràng là Dispose. Điều này là hợp lý và là điều tôi luôn làm trong những tình huống hiếm hoi mà người hoàn thiện được bảo hành.

Tuy nhiên, điều gì xảy ra nếu tôi chỉ làm điều này:

class Foo : IDisposable
{
     public void Dispose(){ CloseSomeHandle(); }
}

và không thực hiện một quyết toán, hoặc bất cứ điều gì. Khung sẽ gọi phương thức Vứt bỏ cho tôi?

Vâng, tôi nhận ra điều này nghe thật ngu ngốc, và tất cả logic đều ngụ ý rằng nó sẽ không xảy ra, nhưng tôi luôn có 2 thứ ở phía sau đầu khiến tôi không chắc chắn.

  1. Một vài năm trước đây đã từng nói với tôi rằng trên thực tế nó sẽ làm điều này và người đó đã có một hồ sơ theo dõi rất chắc chắn về việc "biết công cụ của họ".

  2. Trình biên dịch / khung làm những việc 'ma thuật' khác tùy thuộc vào giao diện bạn triển khai (ví dụ: foreach, phương thức mở rộng, tuần tự hóa dựa trên các thuộc tính, v.v.), do đó, điều này có nghĩa là điều này cũng có thể là 'ma thuật'.

Trong khi tôi đã đọc rất nhiều thứ về nó, và có được rất nhiều điều ngụ ý, tôi chưa bao giờ có thể tìm thấy một dứt khoát Có hoặc Không câu trả lời cho câu hỏi này.

Câu trả lời:


121

Trình thu gom rác .Net gọi phương thức Object.Finalize của một đối tượng trên bộ sưu tập rác. Theo mặc định, điều này không có gì và phải được ghi đè nếu bạn muốn giải phóng tài nguyên bổ sung.

Vứt bỏ là không tự động gọi và phải explicity gọi nếu nguồn lực sẽ được phát hành, chẳng hạn như trong một 'sử dụng' hoặc 'cố gắng cuối cùng' khối

xem http://msdn.microsoft.com/en-us/l Library / system.object.finalize.aspx để biết thêm thông tin


35
Trên thực tế, tôi không tin rằng GC gọi Object.Finalize ở tất cả nếu nó không bị ghi đè. Đối tượng được xác định là không có bộ hoàn thiện một cách hiệu quả và quyết toán bị loại bỏ - điều này làm cho nó hiệu quả hơn, vì đối tượng không cần phải nằm trong hàng đợi quyết toán / có thể giải quyết được.
Jon Skeet

7
Theo MSDN: msdn.microsoft.com/en-us/l Library /, bạn thực sự không thể "ghi đè" phương thức Object.Finalize trong C #, trình biên dịch tạo ra lỗi: Không ghi đè đối tượng.Finalize. Thay vào đó, cung cấp một hàm hủy. ; tức là bạn phải thực hiện một hàm hủy hoạt động hiệu quả như Finalizer. [chỉ cần thêm vào đây để hoàn thiện vì đây là câu trả lời được chấp nhận và rất có thể sẽ được đọc]
Sudhanshu Mishra

1
GC không làm gì đối với một đối tượng không ghi đè lên Finalizer. Nó không được đưa vào hàng đợi Quyết toán - và không có Trình hoàn thiện nào được gọi.
Dave Black

1
@dotnetguy - mặc dù thông số kỹ thuật C # ban đầu đề cập đến "công cụ hủy diệt", nó thực sự được gọi là Finalizer - và cơ chế của nó hoàn toàn khác với cách "công cụ hủy diệt" thực sự hoạt động đối với các ngôn ngữ không được quản lý.
Dave Black

67

Tôi muốn nhấn mạnh quan điểm của Brian trong bình luận của anh ấy, bởi vì nó rất quan trọng.

Các công cụ hoàn thiện không phải là các hàm hủy xác định như trong C ++. Như những người khác đã chỉ ra, không có đảm bảo khi nó sẽ được gọi, và thực sự nếu bạn có đủ bộ nhớ, nếu nó sẽ không bao giờ được gọi.

Nhưng điều tồi tệ về người hoàn thiện là, như Brian nói, nó khiến đối tượng của bạn sống sót trong một bộ sưu tập rác. Điều này có thể là xấu. Tại sao?

Như bạn có thể biết hoặc không, GC được chia thành các thế hệ - Gen 0, 1 và 2, cộng với Heap đối tượng lớn. Chia là một thuật ngữ lỏng lẻo - bạn có được một khối bộ nhớ, nhưng có những con trỏ về nơi các đối tượng Gen 0 bắt đầu và kết thúc.

Quá trình suy nghĩ là bạn sẽ có khả năng sử dụng nhiều đối tượng sẽ tồn tại trong thời gian ngắn. Vì vậy, những thứ đó phải dễ dàng và nhanh chóng để các đối tượng GC chuyển đến - Gen 0. Vì vậy, khi có áp lực bộ nhớ, điều đầu tiên nó làm là bộ sưu tập Gen 0.

Bây giờ, nếu điều đó không giải quyết đủ áp lực, thì nó sẽ quay trở lại và thực hiện quét Gen 1 (làm lại Gen 0), và sau đó nếu vẫn không đủ, nó sẽ thực hiện quét Gen 2 (làm lại Gen 1 và Gen 0). Vì vậy, việc dọn dẹp các vật thể tồn tại lâu có thể mất một thời gian và khá tốn kém (vì các luồng của bạn có thể bị treo trong quá trình hoạt động).

Điều này có nghĩa là nếu bạn làm một cái gì đó như thế này:

~MyClass() { }

Đối tượng của bạn, không có vấn đề gì, sẽ sống ở Thế hệ 2. Điều này là do GC không có cách nào gọi bộ hoàn thiện trong quá trình thu gom rác. Vì vậy, các đối tượng phải được hoàn thành sẽ được chuyển đến một hàng đợi đặc biệt để được làm sạch bằng một luồng khác (luồng hoàn thiện - nếu bạn giết sẽ làm cho tất cả các loại điều xấu xảy ra). Điều này có nghĩa là các đối tượng của bạn treo xung quanh lâu hơn và có khả năng buộc nhiều bộ sưu tập rác hơn.

Vì vậy, tất cả những điều đó chỉ là lái xe về nhà mà bạn muốn sử dụng IDis Dùng để dọn dẹp tài nguyên bất cứ khi nào có thể và nghiêm túc cố gắng tìm cách sử dụng bộ hoàn thiện. Đó là lợi ích tốt nhất của ứng dụng của bạn.


8
Tôi đồng ý rằng bạn muốn sử dụng IDis Dùng bất cứ khi nào có thể, nhưng bạn cũng nên có một bộ hoàn thiện gọi phương thức xử lý. Bạn có thể gọi GC.SuppressFinalize () trong IDispose.Dispose sau khi gọi phương thức xử lý của bạn để đảm bảo đối tượng của bạn không bị đưa vào hàng đợi trình hoàn thiện.
jColeson

2
Các thế hệ được đánh số 0-2, không phải 1-3, nhưng bài viết của bạn thì tốt. Tuy nhiên, tôi sẽ thêm vào rằng mọi đối tượng được tham chiếu bởi đối tượng của bạn hoặc bất kỳ đối tượng nào được tham chiếu bởi các đối tượng đó, v.v. cũng sẽ được bảo vệ chống lại việc thu gom rác (mặc dù không chống lại quyết toán) ở thế hệ khác. Do đó, các đối tượng có bộ hoàn thiện không nên giữ các tham chiếu đến bất cứ thứ gì không cần thiết cho việc hoàn thiện.
supercat


3
Về "Đối tượng của bạn, không có vấn đề gì, sẽ sống ở Thế hệ 2." Đây là thông tin cơ bản RẤT! Nó đã tiết kiệm rất nhiều thời gian gỡ lỗi của một hệ thống, trong đó có rất nhiều đối tượng Gen2 tồn tại trong thời gian ngắn "chuẩn bị" để hoàn thiện, nhưng chưa bao giờ hoàn thiện đã gây ra OutOfMemoryException vì sử dụng rất nhiều. Loại bỏ bộ hoàn thiện (thậm chí trống) và di chuyển (làm việc xung quanh) mã ở nơi khác, vấn đề biến mất và GC có thể xử lý tải.
mài

@CoryFoy "Đối tượng của bạn, không có vấn đề gì, sẽ sống ở Thế hệ 2" Có tài liệu nào cho việc này không?
Ashish Negi

33

Có rất nhiều cuộc thảo luận tốt ở đây, và tôi đến bữa tiệc muộn một chút, nhưng tôi muốn tự mình thêm một vài điểm.

  • Trình thu gom rác sẽ không bao giờ trực tiếp thực hiện phương thức Vứt bỏ cho bạn.
  • GC sẽ thực hiện quyết toán khi cảm thấy thích nó.
  • Một mô hình phổ biến được sử dụng cho các đối tượng có bộ hoàn thiện là gọi nó là một phương thức theo quy ước được định nghĩa là Vứt bỏ (bool dispose) chuyển sai để chỉ ra rằng cuộc gọi được thực hiện do quyết toán chứ không phải là một cuộc gọi Vứt bỏ rõ ràng.
  • Điều này là do không an toàn khi đưa ra bất kỳ giả định nào về các đối tượng được quản lý khác trong khi hoàn thiện một đối tượng (chúng có thể đã được hoàn thiện).

class SomeObject : IDisposable {
 IntPtr _SomeNativeHandle;
 FileStream _SomeFileStream;

 // Something useful here

 ~ SomeObject() {
  Dispose(false);
 }

 public void Dispose() {
  Dispose(true);
 }

 protected virtual void Dispose(bool disposing) {
  if(disposing) {
   GC.SuppressFinalize(this);
   //Because the object was explicitly disposed, there will be no need to 
   //run the finalizer.  Suppressing it reduces pressure on the GC

   //The managed reference to an IDisposable is disposed only if the 
   _SomeFileStream.Dispose();
  }

  //Regardless, clean up the native handle ourselves.  Because it is simple a member
  // of the current instance, the GC can't have done anything to it, 
  // and this is the onlyplace to safely clean up

  if(IntPtr.Zero != _SomeNativeHandle) {
   NativeMethods.CloseHandle(_SomeNativeHandle);
   _SomeNativeHandle = IntPtr.Zero;
  }
 }
}

Đó là phiên bản đơn giản, nhưng có rất nhiều sắc thái có thể khiến bạn gặp phải vấn đề này.

  • Hợp đồng cho IDis Dùng một lần. Mục đích chỉ ra rằng phải gọi an toàn nhiều lần (gọi Vứt bỏ trên một đối tượng đã được xử lý sẽ không làm gì cả)
  • Việc quản lý đúng cách một hệ thống phân cấp thừa kế của các đối tượng dùng một lần, đặc biệt nếu các lớp khác nhau giới thiệu các tài nguyên dùng một lần và không được quản lý. Trong mẫu ở trên, Dispose (bool) là ảo để cho phép nó bị ghi đè để có thể quản lý được, nhưng tôi thấy nó dễ bị lỗi.

Theo tôi, tốt hơn hết là tránh hoàn toàn việc có bất kỳ loại nào chứa trực tiếp cả tài liệu tham khảo dùng một lần và tài nguyên bản địa có thể yêu cầu hoàn thiện. SafeHandles cung cấp một cách rất rõ ràng để thực hiện điều này bằng cách đóng gói các tài nguyên bản địa thành dùng một lần mà bên trong cung cấp quyết định riêng của họ (cùng với một số lợi ích khác như xóa cửa sổ trong P / Gọi nơi xử lý riêng có thể bị mất do ngoại lệ không đồng bộ) .

Chỉ cần xác định SafeHandle sẽ tạo ra tầm thường này:


private class SomeSafeHandle
 : SafeHandleZeroOrMinusOneIsInvalid {
 public SomeSafeHandle()
  : base(true)
  { }

 protected override bool ReleaseHandle()
 { return NativeMethods.CloseHandle(handle); }
}

Cho phép bạn đơn giản hóa loại chứa thành:


class SomeObject : IDisposable {
 SomeSafeHandle _SomeSafeHandle;
 FileStream _SomeFileStream;
 // Something useful here
 public virtual void Dispose() {
  _SomeSafeHandle.Dispose();
  _SomeFileStream.Dispose();
 }
}

1
Lớp SafeHandleZeroOrMinusOneIsInvalid đến từ đâu? Đây có phải là một loại .net được xây dựng?
Orion Edwards

+1 cho // Theo tôi, tốt hơn hết là tránh hoàn toàn có bất kỳ loại nào chứa trực tiếp cả tài liệu tham khảo dùng một lần và tài nguyên bản địa có thể yêu cầu hoàn thiện .// Các lớp không được bảo vệ duy nhất nên có phần cuối là những mục đích tập trung vào quyết toán.
supercat

1
@OrionEdwards có, xem msdn.microsoft.com/en-us/l
Library / trộm

1
Về cuộc gọi đến GC.SuppressFinalizetrong ví dụ này. Trong bối cảnh này, SuppressFinalize chỉ nên được gọi nếu Dispose(true)thực thi thành công. Nếu Dispose(true)thất bại tại một số thời điểm sau khi quyết toán bị triệt tiêu nhưng trước khi tất cả các tài nguyên (đặc biệt là không được quản lý) được dọn sạch, thì bạn vẫn muốn hoàn tất xảy ra để dọn dẹp càng nhiều càng tốt. Tốt hơn để di chuyển GC.SuppressFinalizecuộc gọi vào Dispose()phương thức sau cuộc gọi đến Dispose(true). Xem Nguyên tắc thiết kế khungbài đăng này .
BitMask777

6

Tôi không nghĩ vậy. Bạn có quyền kiểm soát khi Dispose được gọi, điều đó có nghĩa là về mặt lý thuyết bạn có thể viết mã xử lý đưa ra các giả định về (ví dụ) sự tồn tại của các đối tượng khác. Bạn không có quyền kiểm soát khi nào trình hoàn thiện được gọi, do đó, sẽ là iffy để trình hoàn thiện tự động gọi Vứt bỏ thay cho bạn.


EDIT: Tôi đã đi xa và thử nghiệm, chỉ để đảm bảo:

class Program
{
    static void Main(string[] args)
    {
        Fred f = new Fred();
        f = null;
        GC.Collect();
        GC.WaitForPendingFinalizers();
        Console.WriteLine("Fred's gone, and he's not coming back...");
        Console.ReadLine();
    }
}

class Fred : IDisposable
{
    ~Fred()
    {
        Console.WriteLine("Being finalized");
    }

    void IDisposable.Dispose()
    {
        Console.WriteLine("Being Disposed");
    }
}

Giả định về các đối tượng có thể có cho bạn trong quá trình xử lý có thể nguy hiểm và khó khăn, đặc biệt là trong quá trình hoàn thiện.
Scott Dorman

3

Không phải trong trường hợp bạn mô tả, nhưng GC sẽ gọi Finalizer cho bạn, nếu bạn có.

TUY NHIÊN. Bộ sưu tập rác tiếp theo, thay vì được thu thập, đối tượng sẽ đi vào hàng đợi hoàn thiện, mọi thứ sẽ được thu thập, sau đó là bộ hoàn thiện được gọi. Bộ sưu tập tiếp theo sau đó sẽ được giải phóng.

Tùy thuộc vào áp lực bộ nhớ của ứng dụng của bạn, bạn có thể không có gc cho việc tạo đối tượng đó trong một thời gian. Vì vậy, trong trường hợp nói, một luồng tệp hoặc kết nối db, bạn có thể phải chờ một lúc để tài nguyên không được quản lý được giải phóng trong lệnh gọi bộ hoàn thiện trong một thời gian, gây ra một số vấn đề.


1

Không, nó không được gọi.

Nhưng điều này làm cho dễ dàng đừng quên vứt bỏ đồ vật của bạn. Chỉ cần sử dụng usingtừ khóa.

Tôi đã làm bài kiểm tra sau đây cho việc này:

class Program
{
    static void Main(string[] args)
    {
        Foo foo = new Foo();
        foo = null;
        Console.WriteLine("foo is null");
        GC.Collect();
        Console.WriteLine("GC Called");
        Console.ReadLine();
    }
}

class Foo : IDisposable
{
    public void Dispose()
    {

        Console.WriteLine("Disposed!");
    }

1
Đây là một ví dụ về cách nếu bạn KHÔNG sử dụng từ khóa <code> bằng cách sử dụng từ khóa </ code> thì nó sẽ không được gọi là ... và đoạn trích này có 9 năm, chúc mừng sinh nhật!
penyaskito

1

Các GC sẽ không gọi xử lý. Nó có thể gọi bộ hoàn thiện của bạn, nhưng ngay cả điều này không được đảm bảo trong mọi trường hợp.

Xem bài viết này để thảo luận về cách tốt nhất để xử lý việc này.


0

Tài liệu về IDis Dùng để giải thích khá rõ ràng và chi tiết về hành vi, cũng như mã ví dụ. GC sẽ KHÔNG gọi Dispose()phương thức trên giao diện, nhưng nó sẽ gọi bộ hoàn thiện cho đối tượng của bạn.


0

Mẫu IDis Dùng được tạo chủ yếu để được nhà phát triển gọi, nếu bạn có một đối tượng thực hiện IDispose thì nhà phát triển nên triển khai usingtừ khóa xung quanh ngữ cảnh của đối tượng hoặc gọi trực tiếp phương thức Vứt bỏ.

Lỗi an toàn cho mẫu là triển khai bộ hoàn thiện gọi phương thức Dispose (). Nếu bạn không làm điều đó, bạn có thể tạo ra một số rò rỉ bộ nhớ, ví dụ: Nếu bạn tạo một số trình bao bọc COM và không bao giờ gọi System.R nb.Interop.Marshall.ReleaseComObject (comObject) (sẽ được đặt trong phương thức Vứt bỏ).

Không có phép thuật nào trong clr để gọi các phương thức Vứt bỏ tự động ngoài việc theo dõi các đối tượng có chứa bộ hoàn thiện và lưu trữ chúng trong bảng Finalizer bởi GC và gọi chúng khi một số heuristic dọn dẹp bằng cách sử dụng.

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.