Câu hỏi Sử dụng hợp lý giao diện IDisposable


Tôi biết từ đọc tài liệu MSDN rằng việc sử dụng "chính" của IDisposable giao diện là dọn sạch tài nguyên không được quản lý.

Với tôi, "không được quản lý" có nghĩa là những thứ như kết nối cơ sở dữ liệu, ổ cắm, tay cầm cửa sổ, v.v. Nhưng tôi đã thấy mã nơi Dispose() phương pháp được triển khai miễn phí được quản lý tài nguyên, mà dường như dư thừa với tôi, vì người thu gom rác nên chăm sóc điều đó cho bạn.

Ví dụ:

public class MyCollection : IDisposable
{
    private List<String> _theList = new List<String>();
    private Dictionary<String, Point> _theDict = new Dictionary<String, Point>();

    // Die, clear it up! (free unmanaged resources)
    public void Dispose()
    {
        _theList.clear();
        _theDict.clear();
        _theList = null;
        _theDict = null;
    }

Câu hỏi của tôi là, điều này làm cho bộ nhớ rác miễn phí bộ nhớ được sử dụng bởi MyCollection nhanh hơn bình thường?

chỉnh sửa: Cho đến nay mọi người đã đăng một số ví dụ tốt về việc sử dụng IDisposable để xóa các tài nguyên không được quản lý như kết nối cơ sở dữ liệu và bitmap. Nhưng giả sử rằng _theList trong đoạn mã trên chứa một triệu chuỗi và bạn muốn giải phóng bộ nhớ đó hiện nay, thay vì đợi cho người thu gom rác. Đoạn mã trên có thực hiện được điều đó không?


1382
2018-02-11 18:12


gốc


Tôi thích câu trả lời được chấp nhận bởi vì nó cho bạn biết 'mẫu' chính xác của việc sử dụng IDisposable, nhưng giống như OP nói trong bản chỉnh sửa của mình, nó không trả lời câu hỏi dự định của anh ấy. IDisposable không 'gọi' GC, nó chỉ 'đánh dấu' một đối tượng như là hủy diệt. Nhưng cách thực sự để giải phóng bộ nhớ là gì 'ngay bây giờ' thay vì chờ GC bắt đầu? Tôi nghĩ câu hỏi này xứng đáng được thảo luận nhiều hơn. - Punit Vora
IDisposable không đánh dấu gì cả. Các Dispose phương pháp làm những gì nó phải làm để làm sạch các tài nguyên được sử dụng bởi cá thể. Điều này không liên quan gì đến GC. - John Saunders
@John. tôi hiểu IDisposable. Và đó là lý do tại sao tôi nói rằng câu trả lời được chấp nhận không trả lời câu hỏi dự định của OP (và chỉnh sửa tiếp theo) về việc liệu IDisposable sẽ giúp giải phóng bộ nhớ </ i>. Vì IDisposable không có gì để làm với giải phóng bộ nhớ, chỉ tài nguyên, như bạn đã nói, không cần thiết lập các tham chiếu được quản lý thành null ở tất cả những gì mà OP đang làm trong ví dụ của mình. Vì vậy, câu trả lời đúng cho câu hỏi của ông là "Không, nó không giúp bộ nhớ trống nhanh hơn. Thực tế, nó không giúp bộ nhớ trống chút nào, chỉ có tài nguyên". Nhưng dù sao, cảm ơn cho đầu vào của bạn. - Punit Vora
@desigeek: nếu đây là trường hợp, sau đó bạn không nên nói "IDisposable không 'gọi' GC, nó chỉ 'đánh dấu' một đối tượng là hủy diệt" - John Saunders
@desigeek: Không có cách nào đảm bảo giải phóng bộ nhớ một cách xác định. Bạn có thể gọi GC.Collect (), nhưng đó là một yêu cầu lịch sự, không phải là một nhu cầu. Tất cả các chủ đề đang chạy phải được tạm ngưng để thu thập rác để tiếp tục - đọc lên khái niệm về các điểm truy cập .NET. Nếu bạn muốn tìm hiểu thêm, ví dụ: msdn.microsoft.com/en-us/library/678ysw69(v=vs.110).aspx . Nếu chuỗi không thể bị tạm ngưng, ví dụ: bởi vì có một cuộc gọi vào mã không được quản lý, GC.Collect () có thể không làm gì cả. - Concrete Gannet


Các câu trả lời:


Các điểm của Vứt Bỏ  để giải phóng tài nguyên không được quản lý. Nó cần phải được thực hiện tại một số điểm, nếu không họ sẽ không bao giờ được làm sạch. Người thu gom rác không biết làm sao để gọi DeleteHandle() trên một loại biến IntPtr, nó không biết liệu hay không nó cần phải gọi DeleteHandle().

chú thích: Cái gì là tài nguyên không được quản lý? Nếu bạn tìm thấy nó trong Microsoft .NET Framework: nó được quản lý. Nếu bạn đã đi xung quanh MSDN mình, nó không được quản lý. Bất cứ điều gì bạn đã sử dụng P / Gọi các cuộc gọi để có được bên ngoài của thế giới comfy tốt đẹp của tất cả mọi thứ có sẵn cho bạn trong. Framwork NET là không được quản lý - và bây giờ bạn có trách nhiệm làm sạch nó lên.

Đối tượng mà bạn đã tạo cần phải hiển thị một số phương pháp, mà thế giới bên ngoài có thể gọi, để làm sạch tài nguyên không được quản lý. Phương thức này có thể được đặt tên theo bất cứ thứ gì bạn thích:

public void Cleanup()

public void Shutdown()

Nhưng thay vào đó, có một tên được chuẩn hóa cho phương thức này:

public void Dispose()

Thậm chí còn có một giao diện được tạo ra, IDisposable, chỉ có một phương thức:

public interface IDisposable
{
   void Dispose()
}

Vì vậy, bạn làm cho đối tượng của bạn phơi bày IDisposable giao diện, và theo cách đó bạn hứa rằng bạn đã viết một phương pháp duy nhất để dọn sạch tài nguyên không được quản lý của bạn:

public void Dispose()
{
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);
}

Và bạn đã hoàn tất. Ngoại trừ bạn có thể làm tốt hơn.


Điều gì sẽ xảy ra nếu đối tượng của bạn đã cấp 250 MB System.Drawing.Bitmap (tức là lớp Bitmap được quản lý .NET) như một số loại bộ đệm khung? Chắc chắn, đây là một đối tượng .NET được quản lý và bộ thu gom rác sẽ giải phóng nó. Nhưng bạn có thực sự muốn để lại 250MB bộ nhớ chỉ cần ngồi ở đó - chờ cho bộ thu gom rác đến cuối cùng đi cùng và giải phóng nó? Điều gì sẽ xảy ra nếu có mở kết nối cơ sở dữ liệu? Chắc chắn chúng tôi không muốn kết nối đó đang mở, chờ cho GC kết thúc đối tượng.

Nếu người dùng đã gọi Dispose() (có nghĩa là họ không còn có kế hoạch sử dụng đối tượng) tại sao không loại bỏ các bitmap lãng phí và các kết nối cơ sở dữ liệu đó?

Vì vậy, bây giờ chúng tôi sẽ:

  • loại bỏ các tài nguyên không được quản lý (vì chúng tôi phải), và
  • loại bỏ các tài nguyên được quản lý (vì chúng tôi muốn hữu ích)

Vì vậy, hãy cập nhật Dispose() phương pháp để loại bỏ các đối tượng được quản lý đó:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose();
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose();
      this.frameBufferImage = null;
   }
}

Và tất cả đều tốt, ngoại trừ bạn có thể làm tốt hơn!


Nếu người đó quên mất để gọi Dispose() về đối tượng của bạn? Sau đó, họ sẽ rò rỉ một số không được quản lý tài nguyên!

Chú thích: Họ sẽ không bị rò rỉ được quản lý tài nguyên, bởi vì cuối cùng bộ thu gom rác sẽ chạy, trên một luồng nền và giải phóng bộ nhớ liên kết với bất kỳ đối tượng không được sử dụng nào. Điều này sẽ bao gồm đối tượng của bạn và bất kỳ đối tượng được quản lý nào bạn sử dụng (ví dụ: Bitmap và DbConnection).

Nếu người đó quên gọi Dispose(), chúng ta có thể vẫn cứu thịt xông khói của họ! Chúng ta vẫn có cách gọi nó cho họ: khi người thu gom rác cuối cùng cũng được giải phóng (tức là hoàn thành) đối tượng của chúng tôi.

Chú thích: Bộ gom rác cuối cùng sẽ giải phóng tất cả các đối tượng được quản lý.   Khi đó, nó gọi Finalize   phương pháp trên đối tượng. GC không biết hoặc   quan tâm, về của bạn  Vứt bỏ phương pháp.   Đó chỉ là một cái tên mà chúng tôi đã chọn cho   một phương pháp mà chúng tôi gọi khi chúng tôi muốn   loại bỏ những thứ không được quản lý.

Sự hủy diệt đối tượng của chúng ta bởi nhà sưu tập rác là hoàn hảo thời gian để giải phóng những tài nguyên không được quản lý. Chúng tôi làm điều này bằng cách ghi đè Finalize() phương pháp.

Chú thích: Trong C #, bạn không ghi đè rõ ràng Finalize() phương pháp.   Bạn viết một phương thức giống như một C ++ destructor, và   trình biên dịch sẽ thực hiện điều đó để bạn thực hiện Finalize() phương pháp:

~MyObject()
{
    //we're being finalized (i.e. destroyed), call Dispose in case the user forgot to
    Dispose(); //<--Warning: subtle bug! Keep reading!
}

Nhưng có một lỗi trong mã đó. Bạn thấy đấy, bộ thu gom rác chạy trên một chủ đề nền; bạn không biết thứ tự mà trong đó hai vật thể bị phá hủy. Hoàn toàn có thể là trong Dispose() mã, được quản lý đối tượng bạn đang cố gắng loại bỏ (vì bạn muốn hữu ích) không còn ở đó nữa:

public void Dispose()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.gdiCursorBitmapStreamFileHandle);

   //Free managed resources too
   if (this.databaseConnection != null)
   {
      this.databaseConnection.Dispose(); //<-- crash, GC already destroyed it
      this.databaseConnection = null;
   }
   if (this.frameBufferImage != null)
   {
      this.frameBufferImage.Dispose(); //<-- crash, GC already destroyed it
      this.frameBufferImage = null;
   }
}

Vì vậy, những gì bạn cần là một cách để Finalize() nói Dispose() rằng nó nên không chạm vào bất kỳ quản lý nào tài nguyên (vì chúng có thể không có nữa), trong khi vẫn giải phóng tài nguyên không được quản lý.

Mẫu chuẩn để làm điều này là có Finalize() và Dispose() cả hai đều gọi thứ ba(!) phương pháp; nơi bạn vượt qua một câu nói Boolean nếu bạn đang gọi nó từ Dispose() (như trái ngược với Finalize()), có nghĩa là an toàn để giải phóng tài nguyên được quản lý.

Điều này nội bộ phương pháp có thể được cung cấp một số tên tùy ý như "CoreDispose" hoặc "MyInternalDispose", nhưng truyền thống gọi nó là Dispose(Boolean):

protected void Dispose(Boolean disposing)

Nhưng tên thông số hữu ích hơn có thể là:

protected void Dispose(Boolean itIsSafeToAlsoFreeManagedObjects)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //Free managed resources too, but only if I'm being called from Dispose
   //(If I'm being called from Finalize then the objects might not exist
   //anymore
   if (itIsSafeToAlsoFreeManagedObjects)  
   {    
      if (this.databaseConnection != null)
      {
         this.databaseConnection.Dispose();
         this.databaseConnection = null;
      }
      if (this.frameBufferImage != null)
      {
         this.frameBufferImage.Dispose();
         this.frameBufferImage = null;
      }
   }
}

Và bạn thay đổi việc triển khai IDisposable.Dispose() phương pháp để:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
}

và finalizer của bạn để:

~MyObject()
{
   Dispose(false); //I am *not* calling you from Dispose, it's *not* safe
}

chú thích: Nếu đối tượng của bạn xuống từ một đối tượng thực hiện Dispose, đừng quên gọi cho họ căn cứ Vứt bỏ phương pháp khi bạn ghi đè Vứt bỏ:

public Dispose()
{
    try
    {
        Dispose(true); //true: safe to free managed resources
    }
    finally
    {
        base.Dispose();
    }
}

Và tất cả đều tốt, ngoại trừ bạn có thể làm tốt hơn!


Nếu người dùng gọi Dispose() trên đối tượng của bạn, sau đó mọi thứ đã được làm sạch. Sau đó, khi người thu gom rác đến và gọi Finalize, nó sẽ gọi Dispose lần nữa.

Không chỉ là lãng phí, nhưng nếu đối tượng của bạn có tham chiếu đến các đối tượng bạn đã xử lý từ Cuối cùng gọi tới Dispose(), bạn sẽ cố gắng vứt bỏ chúng một lần nữa!

Bạn sẽ nhận thấy trong mã của tôi, tôi đã cẩn thận để loại bỏ các tham chiếu đến các đối tượng mà tôi đã xử lý, vì vậy tôi không cố gắng gọi Dispose trên một tham chiếu đối tượng rác. Nhưng điều đó đã không ngăn chặn một lỗi tinh tế từ leo vào.

Khi người dùng gọi Dispose(): tay cầm CursorFileBitmapIconServiceHandle bị phá hủy. Sau đó khi bộ thu gom rác chạy, nó sẽ cố gắng phá hủy cùng một tay cầm lần nữa.

protected void Dispose(Boolean iAmBeingCalledFromDisposeAndNotFinalize)
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle); //<--double destroy 
   ...
}

Cách bạn khắc phục điều này là nói cho người thu gom rác rằng nó không cần phải bận tâm hoàn thiện đối tượng - tài nguyên của nó đã được dọn sạch và không cần thêm công việc nữa. Bạn làm điều này bằng cách gọi GC.SuppressFinalize() bên trong Dispose() phương pháp:

public void Dispose()
{
   Dispose(true); //I am calling you from Dispose, it's safe
   GC.SuppressFinalize(this); //Hey, GC: don't bother calling finalize later
}

Bây giờ người dùng đã gọi Dispose(), chúng ta có:

  • giải phóng tài nguyên không được quản lý
  • tài nguyên được quản lý miễn phí

Không có điểm trong GC chạy finalizer - tất cả mọi thứ được đưa về chăm sóc.

Tôi không thể sử dụng Finalize để dọn dẹp tài nguyên không được quản lý?

Tài liệu cho Object.Finalize nói:

Phương thức Finalize được sử dụng để thực hiện các hoạt động dọn dẹp trên các tài nguyên không được quản lý được giữ bởi đối tượng hiện tại trước khi đối tượng bị hủy.

Nhưng tài liệu MSDN cũng cho biết, IDisposable.Dispose:

Thực hiện các tác vụ do ứng dụng xác định liên quan đến giải phóng, phát hành hoặc đặt lại tài nguyên không được quản lý.

Vì vậy, đó là nó? Cái nào là nơi để tôi dọn dẹp tài nguyên không được quản lý? Câu trả lời là:

Đó là lựa chọn của bạn! Nhưng chọn Dispose.

Bạn chắc chắn có thể đặt dọn dẹp không được quản lý của bạn trong finalizer:

~MyObject()
{
   //Free unmanaged resources
   Win32.DestroyHandle(this.CursorFileBitmapIconServiceHandle);

   //A C# destructor automatically calls the destructor of its base class.
}

Vấn đề với điều đó là bạn không có ý tưởng khi các nhà sưu tập rác sẽ nhận được xung quanh để hoàn thiện đối tượng của bạn. Tài nguyên gốc không được quản lý, không cần thiết, không được sử dụng của bạn sẽ được lưu lại cho đến khi bộ thu gom rác cuối cùng chạy. Sau đó, nó sẽ gọi phương thức finalizer của bạn; dọn dẹp tài nguyên không được quản lý. Tài liệu của Object.Finalize chỉ ra điều này:

Thời gian chính xác khi finalizer thực hiện là undefined. Để đảm bảo phát hành các tài nguyên xác định cho các cá thể của lớp học của bạn, hãy triển khai Gần phương pháp hoặc cung cấp IDisposable.Dispose thực hiện.

Đây là đức hạnh của việc sử dụng Dispose để dọn dẹp tài nguyên không được quản lý; bạn có thể biết và kiểm soát, khi tài nguyên không được quản lý được dọn dẹp. Sự hủy diệt của họ là "xác định".


Để trả lời câu hỏi ban đầu của bạn: Tại sao không phát hành bộ nhớ ngay bây giờ, thay vì khi GC quyết định làm điều đó? Tôi có phần mềm nhận diện khuôn mặt nhu cầu để loại bỏ 530 MB hình ảnh nội bộ hiện nay, vì chúng không còn cần thiết nữa. Khi chúng ta không: máy nghiền thành dừng lại hoán đổi.

Đọc thêm

Đối với bất kỳ ai thích phong cách của câu trả lời này (giải thích tại sao, nên làm sao trở nên rõ ràng), tôi đề nghị bạn đọc Chương Một của Essential Essential Don Box:

Trong 35 trang, ông giải thích các vấn đề của việc sử dụng các đối tượng nhị phân, và phát minh ra COM trước mắt bạn. Một khi bạn nhận ra tại sao của COM, 300 trang còn lại là hiển nhiên, và chỉ cần thực hiện chi tiết của Microsoft.

Tôi nghĩ rằng mọi lập trình viên đã từng xử lý các đối tượng hoặc COM nên, ít nhất, hãy đọc chương đầu tiên. Đó là lời giải thích tốt nhất của bất cứ điều gì bao giờ hết.

Đọc thêm tiền thưởng

Khi mọi thứ bạn biết là sai bởi Eric Lippert

Do đó, thực sự rất khó để viết một finalizer chính xác,   và lời khuyên tốt nhất tôi có thể cho bạn là không thử.


2287
2018-02-11 18:20



Bạn có thể làm tốt hơn - bạn cần thêm một cuộc gọi vào GC.SuppressFinalize () trong Dispose. - plinth
@Daniel Earwicker: Đúng vậy. Microsoft rất thích bạn ngừng sử dụng Win32 hoàn toàn, và gắn bó với các cuộc gọi .NET Framework độc lập, di động, độc lập với thiết bị. Nếu bạn muốn đi xung quanh hệ điều hành bên dưới; bởi vì bạn suy nghĩ bạn biết những gì hệ điều hành đang chạy: bạn đang dùng cuộc sống của bạn trong tay của riêng bạn. Không phải mọi ứng dụng .NET đều chạy trên Windows hoặc trên máy tính để bàn. - Ian Boyd
Đây là một câu trả lời tuyệt vời nhưng tôi nghĩ rằng nó sẽ được hưởng lợi từ một danh sách mã cuối cùng cho một trường hợp tiêu chuẩn và cho một trường hợp mà lớp xuất phát từ một baseclass đã thực hiện Dispose. ví dụ: đọc ở đây (msdn.microsoft.com/en-us/library/aa720161%28v=vs.71%29.aspx) cũng như tôi đã nhầm lẫn về những gì tôi nên làm khi bắt nguồn từ lớp đã thực hiện Vứt bỏ (hey tôi mới đến này). - integra753
@GregS, và những người khác: Nói chung tôi sẽ không bận tâm đặt tham chiếu đến null. Trước hết, nó có nghĩa là bạn không thể làm cho họ readonlyvà thứ hai, bạn phải làm rất xấu !=null kiểm tra (như trong mã ví dụ). Bạn có thể có một lá cờ disposed, nhưng dễ dàng hơn khi không bận tâm về nó. .NET GC đủ mạnh để tham chiếu đến một trường x sẽ không còn được tính 'đã sử dụng' vào thời điểm nó vượt qua x.Dispose() hàng. - porges
Trong trang thứ hai của cuốn sách Don Box bạn đã đề cập, ông sử dụng ví dụ về một O (1) thực hiện các thuật toán tìm kiếm, có "chi tiết còn lại như là một tập thể dục cho người đọc". Tôi bật cười. - wil


IDisposable thường được sử dụng để khai thác using tuyên bố và tận dụng lợi thế của một cách dễ dàng để làm sạch xác định các đối tượng được quản lý.

public class LoggingContext : IDisposable {
    public Finicky(string name) {
        Log.Write("Entering Log Context {0}", name);
        Log.Indent();
    }
    public void Dispose() {
        Log.Outdent();
    }

    public static void Main() {
        Log.Write("Some initial stuff.");
        try {
            using(new LoggingContext()) {
                Log.Write("Some stuff inside the context.");
                throw new Exception();
            }
        } catch {
            Log.Write("Man, that was a heavy exception caught from inside a child logging context!");
        } finally {
            Log.Write("Some final stuff.");
        }
    }
}

54
2018-02-11 18:42



Tôi thích điều đó, cá nhân, nhưng nó không thực sự thích thú với các hướng dẫn thiết kế khung. - mquander
Tôi sẽ xem xét nó thiết kế phù hợp bởi vì nó cho phép dễ dàng xác định phạm vi và phạm vi công trình xây dựng / dọn dẹp, đặc biệt là khi trộn với ngoại lệ xử lý, khóa, và không được quản lý tài nguyên bằng cách sử dụng khối theo những cách phức tạp. Ngôn ngữ cung cấp tính năng này như là một tính năng hạng nhất. - yfeldblum
Nó không chính xác theo các quy tắc được chỉ định trong FDG nhưng nó chắc chắn là một sử dụng hợp lệ của mẫu vì nó được yêu cầu để được sử dụng bởi các "sử dụng tuyên bố". - Scott Dorman
Miễn là Log.Outdent không ném, chắc chắn không có gì sai với điều này. - Daniel Earwicker
Các câu trả lời khác nhau cho Có lạm dụng để sử dụng IDisposable và "sử dụng" như là một phương tiện để nhận được "hành vi phạm vi" cho an toàn ngoại lệ? đi vào chi tiết hơn một chút về lý do tại sao những người khác thích / không thích kỹ thuật này. Nó có phần gây tranh cãi. - Brian


Mục đích của mẫu Vứt bỏ là cung cấp một cơ chế để dọn sạch cả tài nguyên được quản lý và không được quản lý và khi điều đó xảy ra phụ thuộc vào cách thức gọi phương thức Vứt bỏ. Trong ví dụ của bạn, việc sử dụng Dispose không thực sự làm bất cứ điều gì liên quan đến việc vứt bỏ, vì việc xóa danh sách không ảnh hưởng đến bộ sưu tập đó đang được xử lý. Tương tự như vậy, các cuộc gọi để thiết lập các biến thành null cũng không ảnh hưởng đến GC.

Bạn có thể xem cái này bài báo để biết thêm chi tiết về cách triển khai mẫu vứt bỏ, nhưng về cơ bản, nó trông giống như sau:

public class SimpleCleanup : IDisposable
{
    // some fields that require cleanup
    private SafeHandle handle;
    private bool disposed = false; // to detect redundant calls

    public SimpleCleanup()
    {
        this.handle = /*...*/;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposed)
        {
            if (disposing)
            {
                // Dispose managed resources.
                if (handle != null)
                {
                    handle.Dispose();
                }
            }

            // Dispose unmanaged managed resources.

            disposed = true;
        }
    }

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
}

Phương thức quan trọng nhất ở đây là Dispose (bool), thực sự chạy dưới hai trường hợp khác nhau:

  • disposing == true: phương thức đã được gọi trực tiếp hoặc gián tiếp bởi mã của người dùng. Tài nguyên được quản lý và không được quản lý có thể được xử lý.
  • disposing == false: phương thức đã được gọi bởi runtime từ bên trong finalizer, và bạn không nên tham chiếu đến các đối tượng khác. Chỉ tài nguyên không được quản lý mới có thể được xử lý.

Vấn đề chỉ đơn giản là để GC thực hiện việc dọn dẹp là bạn không có quyền kiểm soát thực sự khi GC sẽ chạy một chu kỳ thu thập (bạn có thể gọi GC.Collect (), nhưng bạn thực sự không nên) để các tài nguyên có thể ở lại xung quanh lâu hơn cần thiết. Hãy nhớ rằng, gọi Dispose () không thực sự gây ra một chu kỳ thu thập hoặc theo bất kỳ cách nào khiến cho GC thu thập / giải phóng đối tượng; nó đơn giản cung cấp các phương tiện để dọn dẹp các tài nguyên được sử dụng một cách xác định và cho GC biết rằng việc dọn dẹp này đã được thực hiện.

Toàn bộ điểm IDisposable và mẫu vứt bỏ không phải là về giải phóng bộ nhớ ngay lập tức. Thời gian duy nhất một cuộc gọi đến Vứt bỏ thực sự thậm chí sẽ có cơ hội giải phóng bộ nhớ ngay lập tức là khi nó xử lý việc xóa bỏ kịch bản == sai và thao tác các tài nguyên không được quản lý. Đối với mã được quản lý, bộ nhớ sẽ không thực sự được khôi phục cho đến khi GC chạy một chu kỳ thu thập, mà bạn thực sự không có quyền kiểm soát (ngoài gọi GC.Collect (), mà tôi đã đề cập không phải là một ý tưởng tốt).

Kịch bản của bạn không thực sự hợp lệ vì các chuỗi trong .NET không sử dụng bất kỳ tài nguyên chưa được gắn kết nào và không triển khai IDisposable, không có cách nào buộc chúng được "dọn sạch".


36
2018-02-11 20:21



Bạn không quên thực hiện finalizer? - Budda
@Budda: Không, anh ấy đang sử dụng SafeHandle. Không cần một destructor. - Henk Holterman
+1 để thêm mạng an toàn cho nhiều cuộc gọi tới Dispose (). Thông số kỹ thuật cho biết nhiều cuộc gọi phải an toàn. Quá nhiều lớp Microsoft không thực hiện được điều đó, và bạn nhận được sự khó chịu của ObjectDisposedException. - Jesse Chisholm
Nhưng Dispose (bool disposing) là phương thức của riêng bạn trên lớp SimpleCleanup của bạn và sẽ không bao giờ được gọi bởi framework. Vì bạn chỉ gọi nó là "true" như một tham số, 'disposing' sẽ không bao giờ sai. Mã của bạn rất giống với ví dụ MSDN cho IDisposable, nhưng thiếu trình hoàn thiện, như @Budda đã chỉ ra, đó là nơi mà cuộc gọi với việc xử lý = false sẽ đến từ đó. - yoyo


Không nên có thêm các cuộc gọi đến các phương thức của đối tượng sau khi Dispose đã được gọi trên nó (mặc dù một đối tượng nên chịu đựng thêm các cuộc gọi đến Dispose). Do đó ví dụ trong câu hỏi là ngớ ngẩn. Nếu Dispose được gọi, thì chính đối tượng đó có thể bị loại bỏ. Vì vậy, người dùng chỉ nên loại bỏ tất cả các tham chiếu đến toàn bộ đối tượng đó (đặt chúng thành null) và tất cả các đối tượng liên quan bên trong nó sẽ tự động được dọn sạch.

Đối với câu hỏi chung về quản lý / không được quản lý và thảo luận trong các câu trả lời khác, tôi nghĩ rằng bất kỳ câu trả lời nào cho câu hỏi này đều phải bắt đầu bằng định nghĩa về tài nguyên không được quản lý.

Những gì nó nắm được là có một chức năng mà bạn có thể gọi để đưa hệ thống vào trạng thái, và có một chức năng khác mà bạn có thể gọi để đưa nó trở lại trạng thái đó. Bây giờ, trong ví dụ điển hình, ví dụ đầu tiên có thể là một hàm trả về một trình xử lý tệp và lệnh thứ hai có thể là cuộc gọi đến CloseHandle.

Nhưng - và đây là chìa khóa - chúng có thể là bất kỳ cặp hàm nào phù hợp. Một xây dựng một nhà nước, những giọt nước mắt khác nó xuống. Nếu trạng thái đã được xây dựng nhưng chưa bị rách, thì một thể hiện của tài nguyên tồn tại. Bạn phải sắp xếp cho các teardown để xảy ra vào đúng thời điểm - tài nguyên không được quản lý bởi CLR. Loại tài nguyên được quản lý tự động duy nhất là bộ nhớ. Có hai loại: GC và ngăn xếp. Các kiểu giá trị được quản lý bởi ngăn xếp (hoặc bằng cách kéo một chuyến đi bên trong các loại tham chiếu), và các kiểu tham chiếu được quản lý bởi GC.

Các hàm này có thể gây ra các thay đổi trạng thái có thể được tự do xen kẽ, hoặc có thể cần được lồng ghép hoàn toàn. Thay đổi trạng thái có thể là chủ đề an toàn, hoặc có thể không.

Nhìn vào ví dụ trong câu hỏi của Công lý. Các thay đổi đối với thụt lề của tệp nhật ký phải được lồng ghép hoàn toàn hoặc tất cả đều sai. Ngoài ra họ không có khả năng là chủ đề an toàn.

Có thể cản trở một chuyến đi với bộ thu gom rác để làm sạch tài nguyên không được quản lý của bạn. Nhưng chỉ khi các hàm thay đổi trạng thái là luồng an toàn và hai trạng thái có thể có thời gian tồn tại chồng lên nhau theo bất kỳ cách nào. Vì vậy, ví dụ của Công lý về một nguồn lực KHÔNG phải có một finalizer! Nó sẽ không giúp được ai.

Đối với những loại tài nguyên đó, bạn chỉ có thể triển khai IDisposable, mà không có một finalizer. Trình hoàn thiện là hoàn toàn tùy chọn - nó phải là. Điều này được đánh bóng trên hoặc thậm chí không được đề cập trong nhiều sách.

Sau đó, bạn phải sử dụng using tuyên bố để có cơ hội đảm bảo rằng Dispose được gọi là. Về cơ bản, điều này giống như việc bỏ đi một chuyến đi với ngăn xếp (để finalizer là GC, using là ngăn xếp).

Phần còn thiếu là bạn phải viết thủ công Dispose và làm cho nó gọi lên các trường của bạn và lớp cơ sở của bạn. Lập trình viên C ++ / CLI không phải làm như vậy. Trình biên dịch viết nó cho họ trong hầu hết các trường hợp.

Có một thay thế, mà tôi thích cho các tiểu bang làm tổ hoàn hảo và không phải là chủ đề an toàn (ngoài bất cứ điều gì khác, tránh né tránh IDisposable bạn có vấn đề về việc tranh luận với một người không thể cưỡng lại việc thêm trình hoàn thiện cho mọi lớp triển khai IDisposable) .

Thay vì viết một lớp, bạn viết một hàm. Hàm này chấp nhận một đại biểu để gọi lại:

public static void Indented(this Log log, Action action)
{
    log.Indent();
    try
    {
        action();
    }
    finally
    {
        log.Outdent();
    }
}

Và sau đó một ví dụ đơn giản sẽ là:

Log.Write("Message at the top");
Log.Indented(() =>
{
    Log.Write("And this is indented");

    Log.Indented(() =>
    {
        Log.Write("This is even more indented");
    });
});
Log.Write("Back at the outermost level again");

Các lambda được thông qua trong phục vụ như là một khối mã, do đó, nó giống như bạn làm cho cấu trúc điều khiển của riêng bạn để phục vụ cùng một mục đích như using, ngoại trừ việc bạn không còn bất kỳ nguy hiểm nào khi người gọi lạm dụng nó. Không có cách nào họ có thể không làm sạch tài nguyên.

Kỹ thuật này ít hữu ích hơn nếu tài nguyên là loại có thể có thời gian tồn tại chồng chéo, vì sau đó bạn muốn có thể xây dựng tài nguyên A, sau đó là tài nguyên B, sau đó giết tài nguyên A và sau đó giết tài nguyên B. Bạn không thể làm điều đó nếu bạn buộc người dùng phải hoàn toàn làm tổ như thế này. Nhưng sau đó bạn cần phải sử dụng IDisposable (nhưng vẫn không có finalizer, trừ khi bạn đã thực hiện an toàn chủ đề, mà không phải là miễn phí).


17
2018-02-11 19:31



re: "Không nên có thêm các cuộc gọi đến các phương thức của đối tượng sau khi Dispose đã được gọi vào nó". "Nên" là từ tác dụng. Nếu bạn có các hành động không đồng bộ đang chờ xử lý, chúng có thể xuất hiện sau khi đối tượng của bạn đã được xử lý. Gây ra một ObjectDisposedException. - Jesse Chisholm
Của bạn dường như là câu trả lời duy nhất khác với tôi mà chạm vào ý tưởng rằng các nguồn lực không được quản lý đóng gói nhà nước mà GC không hiểu. Tuy nhiên, khía cạnh quan trọng của tài nguyên không được quản lý là một hoặc nhiều thực thể có trạng thái cần dọn sạch trạng thái của nó có thể tiếp tục tồn tại ngay cả khi đối tượng "sở hữu" tài nguyên không. Bạn thích định nghĩa của tôi như thế nào? Khá giống, nhưng tôi nghĩ nó làm cho "tài nguyên" thêm một chút danh từ-ish (đó là "thỏa thuận" của đối tượng bên ngoài để thay đổi hành vi của nó, để đổi lấy thông báo khi các dịch vụ của nó không còn cần thiết nữa) - supercat
@supercat - nếu bạn quan tâm, tôi đã viết bài đăng sau đây một vài ngày sau khi tôi đã viết câu trả lời ở trên: smellegantcode.wordpress.com/2009/02/13/… - Daniel Earwicker
@DanielEarwicker: Bài viết thú vị, mặc dù tôi có thể nghĩ đến ít nhất một loại tài nguyên không được quản lý mà bạn không thực sự đề cập: đăng ký các sự kiện từ các đối tượng tồn tại lâu dài. Đăng ký sự kiện có thể bị nhiễm nấm, nhưng ngay cả khi bộ nhớ bị lỗi không giới hạn để vứt bỏ chúng có thể tốn kém. Ví dụ, một điều tra viên cho một bộ sưu tập cho phép sửa đổi trong điều tra có thể cần phải đăng ký để cập nhật thông báo từ bộ sưu tập, và một bộ sưu tập có thể được cập nhật nhiều lần trong suốt cuộc đời của nó. Nếu các điều tra viên bị hủy bỏ mà không hủy đăng ký ... - supercat
... danh sách người đăng ký sẽ tiếp tục ngày càng lớn hơn. Ngay cả khi bộ nhớ không giới hạn, thời gian để xử lý đơn yêu cầu đăng ký có thể tăng lên mà không bị ràng buộc. Tôi không nghĩ rằng mô hình của bạn phù hợp rất tốt với mô hình này, vì nó không rõ ràng chính xác những gì đang được "phân bổ". Trong một số ý nghĩa, đăng ký sự kiện thậm chí còn nhiều hơn so với bộ nhớ không được quản lý. Mặt khác, đăng ký sự kiện không đủ điều kiện là "một thứ gì đó mà một đối tượng đã yêu cầu một thực thể khác làm thay mặt cho nó". - supercat


Kịch bản tôi sử dụng IDisposable: dọn sạch tài nguyên không được quản lý, hủy đăng ký sự kiện, kết nối chặt chẽ

Thành ngữ tôi sử dụng để triển khai IDisposable (không phải chủ đề):

class MyClass : IDisposable {
    // ...

    #region IDisposable Members and Helpers
    private bool disposed = false;

    public void Dispose() {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    private void Dispose(bool disposing) {
        if (!this.disposed) {
            if (disposing) {
                // cleanup code goes here
            }
            disposed = true;
        }
    }

    ~MyClass() {
        Dispose(false);
    }
    #endregion
}

14
2018-02-11 18:20



Bạn có thể tìm thấy giải thích đầy đủ về mẫu tại msdn.microsoft.com/en-us/library/b1yfkh5e.aspx - LicenseQ
không bao giờ nên có một finalizer bao gồm trừ khi bạn có tài nguyên không được quản lý. Thậm chí sau đó, triển khai được ưu tiên là bọc tài nguyên không được quản lý trong SafeHandle. - Dave Black


Nếu MyCollection sẽ được thu gom rác, tuy nhiên bạn không cần phải vứt bỏ nó. Làm như vậy sẽ chỉ khuấy động CPU nhiều hơn mức cần thiết và thậm chí có thể làm mất hiệu lực một số phân tích được tính toán trước mà bộ thu gom rác đã thực hiện.

tôi sử dụng IDisposable để làm những việc như đảm bảo luồng được xử lý đúng cách, cùng với tài nguyên không được quản lý.

CHỈNH SỬA Đáp lại lời bình luận của Scott:

Thời gian duy nhất chỉ số hiệu suất GC bị ảnh hưởng là khi cuộc gọi [sic] GC.Collect () được thực hiện "

Khái niệm, GC duy trì một cái nhìn của đồ thị tham chiếu đối tượng, và tất cả các tham chiếu đến nó từ các khung stack của chủ đề. Heap này có thể khá lớn và có nhiều trang bộ nhớ. Là một tối ưu hóa, GC lưu trữ phân tích các trang không có khả năng thay đổi thường xuyên để tránh việc quét lại trang không cần thiết. GC nhận được thông báo từ hạt nhân khi dữ liệu trong một trang thay đổi, do đó, nó biết rằng trang là bẩn và đòi hỏi một rescan. Nếu bộ sưu tập nằm trong Gen0 thì có khả năng những thứ khác trong trang cũng đang thay đổi, nhưng điều này ít có khả năng trong Gen1 và Gen2. Theo giai thoại, các móc này không có sẵn trong Mac OS X cho nhóm đã chuyển GC sang Mac để có được trình cắm Silverlight hoạt động trên nền tảng đó.

Một điểm khác chống lại việc xử lý tài nguyên không cần thiết: hãy tưởng tượng một tình huống mà một quá trình được dỡ xuống. Hãy tưởng tượng cũng rằng quá trình đã được chạy một thời gian. Rất có thể là nhiều trang bộ nhớ của quá trình đó đã được hoán đổi vào đĩa. Ít nhất chúng không còn trong bộ nhớ cache L1 hoặc L2 nữa. Trong tình huống như vậy, không có điểm nào cho một ứng dụng được dỡ bỏ để trao đổi tất cả các dữ liệu và các trang mã đó trở lại bộ nhớ để giải phóng các tài nguyên sẽ được hệ điều hành giải phóng khi quá trình chấm dứt. Điều này áp dụng cho các tài nguyên được quản lý và thậm chí một số tài nguyên không được quản lý. Chỉ những tài nguyên giữ các luồng không phải nền tảng còn sống mới được xử lý, nếu không quy trình sẽ vẫn còn sống.

Bây giờ, trong quá trình thực hiện bình thường, có các tài nguyên tạm thời phải được dọn sạch chính xác (như @fezmonkey chỉ ra kết nối cơ sở dữ liệu, ổ cắm, tay nắm cửa sổ) để tránh rò rỉ bộ nhớ không được quản lý. Đây là những thứ phải được xử lý. Nếu bạn tạo một số lớp sở hữu một chủ đề (và bằng sở hữu tôi có nghĩa là nó tạo ra nó và do đó chịu trách nhiệm đảm bảo nó dừng lại, ít nhất là theo kiểu mã hóa của tôi), thì lớp đó rất có thể phải triển khai IDisposable và xé sợi chỉ trong Dispose.

Khuôn khổ .NET sử dụng IDisposable giao diện như một tín hiệu, thậm chí cảnh báo, cho các nhà phát triển rằng lớp này phải được xử lý. Tôi không thể nghĩ ra bất kỳ loại nào trong khung công tác triển khai IDisposable (không bao gồm việc triển khai giao diện rõ ràng) trong đó việc xử lý là tùy chọn.


11
2018-02-11 18:19



Gọi Dispose là hoàn toàn hợp lệ, hợp pháp, và khuyến khích. Các đối tượng thực hiện IDisposable thường làm như vậy vì một lý do. Thời gian duy nhất chỉ số hiệu suất GC bị ảnh hưởng là khi cuộc gọi GC.Collect () được thực hiện. - Scott Dorman
Đối với nhiều lớp .net, xử lý là "hơi" tùy chọn, có nghĩa là bỏ qua các trường hợp "thường" sẽ không gây ra bất kỳ sự cố nào miễn là người ta không phát điên tạo ra các phiên bản mới và từ bỏ chúng. Ví dụ, mã trình biên dịch cho các điều khiển dường như tạo phông chữ khi các điều khiển được khởi tạo và từ bỏ chúng khi các biểu mẫu được xử lý; nếu một người tạo ra và phân phối hàng ngàn điều khiển, điều này có thể buộc hàng ngàn xử lý GDI, nhưng trong hầu hết các trường hợp, các điều khiển không được tạo và phá hủy nhiều. Tuy nhiên, người ta vẫn nên cố gắng tránh bỏ rơi như vậy. - supercat
Trong trường hợp phông chữ, tôi nghi ngờ vấn đề là Microsoft không bao giờ thực sự xác định thực thể nào chịu trách nhiệm xử lý đối tượng "font" được gán cho một điều khiển; trong một số trường hợp, một điều khiển có thể chia sẻ một phông chữ với một đối tượng tồn tại lâu hơn, do đó, có sự kiểm soát Vứt bỏ phông chữ sẽ là xấu. Trong các trường hợp khác, một phông chữ sẽ được gán cho một điều khiển và không nơi nào khác, vì vậy nếu điều khiển không vứt bỏ nó thì không ai sẽ làm điều đó. Ngẫu nhiên, khó khăn với phông chữ này có thể tránh được là có một lớp FontTemplate không dùng một lần riêng biệt, vì các điều khiển dường như không sử dụng phông chữ GDI của Font của chúng. - supercat
Về chủ đề tùy chọn Dispose() cuộc gọi, xem: stackoverflow.com/questions/913228/… - RJ Cuthbertson


Đúng, mã đó hoàn toàn dư thừa và không cần thiết và nó không làm cho bộ thu gom rác làm bất cứ điều gì nó sẽ không làm khác (một khi một thể hiện của MyCollection đi ra khỏi phạm vi, đó là.) Đặc biệt là .Clear() cuộc gọi.

Trả lời chỉnh sửa của bạn: Sắp xếp. Nếu tôi làm điều này:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has no Dispose() method
    instance.FillItWithAMillionStrings();
}

// 1 million strings are in memory, but marked for reclamation by the GC

Nó có chức năng giống với điều này cho mục đích quản lý bộ nhớ:

public void WasteMemory()
{
    var instance = new MyCollection(); // this one has your Dispose()
    instance.FillItWithAMillionStrings();
    instance.Dispose();
}

// 1 million strings are in memory, but marked for reclamation by the GC

Nếu bạn thực sự thực sự thực sự cần phải giải phóng bộ nhớ này ngay lập tức, hãy gọi GC.Collect(). Không có lý do để làm điều này ở đây, mặc dù. Bộ nhớ sẽ được giải phóng khi cần thiết.


10
2018-06-03 21:07



lại: "Bộ nhớ sẽ được giải phóng khi cần." Thay vào đó, "khi GC quyết định cần thiết". Bạn có thể thấy các vấn đề về hiệu năng hệ thống trước khi GC quyết định bộ nhớ đó có thật không cần thiết. Giải phóng nó hiện nay có thể không cần thiết, nhưng có thể hữu ích. - Jesse Chisholm
Có một số trường hợp góc trong đó nulling ra tài liệu tham khảo trong một bộ sưu tập có thể tiến hành thu gom rác của các mục được đề cập đến. Ví dụ, nếu một mảng lớn được tạo và chứa các tham chiếu tới các mục mới được tạo ra nhỏ hơn, nhưng không cần thiết lâu sau đó, bỏ qua mảng có thể khiến các mục đó được giữ xung quanh cho đến Cấp 2 tiếp theo, trong khi zeroing nó ra đầu tiên có thể làm cho các mục đủ điều kiện cho cấp độ tiếp theo 0 hoặc cấp 1 GC. Để chắc chắn, có các đối tượng ngắn ngủi lớn trên Heap đối tượng lớn là icky anyway (tôi không thích thiết kế) nhưng ... - supercat
... zeroing ra mảng như vậy trước khi từ bỏ chúng đôi khi của tôi làm giảm tác động GC. - supercat


Nếu bạn muốn xóa ngay bây giờ, sử dụng bộ nhớ không được quản lý.

Xem:


7
2018-02-11 21:08





Tôi sẽ không lặp lại các công cụ thông thường về Sử dụng hoặc giải phóng tài nguyên không được quản lý, tất cả đã được đề cập đến. Nhưng tôi muốn chỉ ra những gì có vẻ là một quan niệm sai lầm phổ biến.
Với mã sau

Lớp công khai LargeStuff
  Thực hiện IDisposable
  Riêng _Large dưới dạng chuỗi ()

  'Một số mã lạ có nghĩa là _Large giờ chứa vài triệu chuỗi dài.

  Công khai Sub Dispose () Thực hiện IDisposable.Dispose
    _Large = Không có gì
  Kết thúc phụ

Tôi nhận ra rằng việc thực hiện Dùng một lần không tuân theo các hướng dẫn hiện hành, nhưng hy vọng tất cả các bạn có được ý tưởng.
Bây giờ, khi Dispose được gọi, bao nhiêu bộ nhớ được giải phóng?

Trả lời: Không.
Gọi Dispose có thể giải phóng tài nguyên không được quản lý, nó KHÔNG THỂ lấy lại bộ nhớ được quản lý, chỉ có GC mới có thể thực hiện điều đó. Thats không nói rằng ở trên không phải là một ý tưởng tốt, theo mô hình trên vẫn là một ý tưởng tốt trong thực tế. Sau khi Dispose đã được chạy, không có gì ngăn GC tái xác nhận lại bộ nhớ đang được sử dụng bởi _Large, mặc dù thể hiện của LargeStuff vẫn có thể nằm trong phạm vi. Các chuỗi trong _Large cũng có thể ở gen 0 nhưng thể hiện của LargeStuff có thể là gen 2, do đó, một lần nữa, bộ nhớ sẽ được xác nhận lại sớm hơn.
Không có điểm trong việc thêm một finaliser để gọi phương thức Dispose được hiển thị ở trên mặc dù. Điều đó sẽ chỉ DELAY việc yêu cầu lại bộ nhớ để cho phép finaliser chạy.


6
2018-02-11 21:07



Nếu một thể hiện của LargeStuff đã tồn tại đủ lâu để biến nó thành Thế hệ 2 và nếu _Large giữ một tham chiếu đến một chuỗi mới được tạo ra trong thế hệ 0, sau đó nếu thể hiện của LargeStuff bị bỏ rơi mà không rỗng _Large, sau đó chuỗi được gọi bởi _Large sẽ được giữ lại cho đến khi bộ sưu tập Gen2 tiếp theo. Giảm dần _Large có thể để chuỗi bị loại bỏ trong bộ sưu tập Gen0 tiếp theo. Trong hầu hết các trường hợp, việc loại trừ tham chiếu không có ích, nhưng có những trường hợp nó có thể mang lại một số lợi ích. - supercat


Trong ví dụ bạn đã đăng, nó vẫn không "giải phóng bộ nhớ ngay bây giờ". Tất cả bộ nhớ là rác được thu thập, nhưng nó có thể cho phép bộ nhớ được thu thập trong một thế hệ. Bạn sẽ phải chạy một số xét nghiệm để chắc chắn.


Hướng dẫn thiết kế khung là các nguyên tắc và không phải là quy tắc. Họ cho bạn biết giao diện chủ yếu là gì, khi nào sử dụng giao diện, cách sử dụng và khi không sử dụng nó.

Tôi một lần đọc mã đó là một RollBack đơn giản () trên thất bại sử dụng IDisposable. Lớp MiniTx dưới đây sẽ kiểm tra một lá cờ trên Dispose () và nếu Commit cuộc gọi không bao giờ xảy ra sau đó nó sẽ gọi Rollback trên chính nó. Nó đã thêm một lớp hướng dẫn làm cho mã gọi dễ hiểu hơn nhiều và dễ bảo trì hơn. Kết quả trông giống như sau:

using( MiniTx tx = new MiniTx() )
{
    // code that might not work.

    tx.Commit();
} 

Tôi cũng thấy thời gian / mã đăng nhập làm điều tương tự. Trong trường hợp này, phương thức Dispose () đã ngừng bộ hẹn giờ và ghi lại rằng khối đã thoát.

using( LogTimer log = new LogTimer("MyCategory", "Some message") )
{
    // code to time...
}

Vì vậy, đây là một vài ví dụ cụ thể mà không làm bất kỳ việc dọn dẹp tài nguyên không được quản lý nào, nhưng đã sử dụng thành công IDisposable để tạo mã sạch hơn.


5
2018-02-11 20:32



Hãy xem ví dụ của @Daniel Earwicker bằng cách sử dụng các hàm bậc cao hơn. Đối với điểm chuẩn, thời gian, đăng nhập, vv Có vẻ như đơn giản hơn nhiều. - Aluan Haddad