Menu

Câu lệnh using trong C#: IDisposable, Dispose và using var

Câu lệnh using bảo đảm Dispose được gọi trên tệp, stream, kết nối và các đối tượng IDisposable khác, kể cả khi có ngoại lệ được ném ra. Tìm hiểu nó được biên dịch thành gì, thứ tự các đối tượng được dispose, using declaration của C# 8, và cách cài đặt IDisposable trong lớp của bạn.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Một số đối tượng giữ những tài nguyên mà bộ thu gom rác (garbage collector) không quản lý: tệp đang mở, socket mạng, kết nối cơ sở dữ liệu, handle của hệ điều hành. Chúng cài đặt IDisposable, và bạn giải phóng tài nguyên bằng cách gọi Dispose(). Câu lệnh using gọi phương thức này cho bạn tại một điểm cố định, kể cả khi có ngoại lệ được ném ra.

Trang này nói về câu lệnh using. Các dòng using System; ở đầu tệp là một tính năng khác, using directive, dùng để import một namespace.

Khối using

using (declaration) { body } tạo đối tượng, chạy phần thân, rồi gọi Dispose. Một lớp nhỏ in ra từ Dispose cho thấy chính xác khi nào việc đó xảy ra:

Đầu ra:

open db
db sends SELECT 1
close db
open api
api sends GET /orders
close api
error handled

Khối thứ hai ném ngoại lệ, nhưng "close api" vẫn được in ra, trước khi catch xử lý ngoại lệ. Sự bảo đảm đó là lý do nên dùng using thay vì tự gọi Dispose() ở dòng cuối: một lời gọi thủ công sẽ bị bỏ qua nếu có bất kỳ ngoại lệ nào được ném ra trước nó.

using được biên dịch thành gì

Trình biên dịch mở rộng khối using thành một cặp try/finally. Hai đoạn sau là tương đương:

using (var writer = new StreamWriter("log.txt"))
{
    writer.WriteLine("started");
}

// is compiled as:
{
    var writer = new StreamWriter("log.txt");
    try
    {
        writer.WriteLine("started");
    }
    finally
    {
        if (writer != null) ((IDisposable)writer).Dispose();
    }
}

Từ cách mở rộng này suy ra ba chi tiết. Biến chỉ có phạm vi trong khối lệnh và là chỉ đọc bên trong khối (bạn không thể gán lại writer). Đối tượng phải cài đặt IDisposable, nếu không code sẽ không biên dịch được. Và giá trị null được phép: khi đó Dispose đơn giản là bị bỏ qua, rất tiện khi một factory có thể không trả về gì.

Tệp và stream

Các lớp tệp và stream là trường hợp gặp hằng ngày. Một StreamWriter giữ văn bản trong bộ đệm ở bộ nhớ, và Dispose đẩy bộ đệm xuống đĩa rồi đóng file handle:

Đầu ra:

id,total
1001,59.90
1002,12.50

Không có Dispose, những dòng cuối của writer có thể vẫn nằm trong bộ đệm, và trên Windows handle đang mở sẽ ngăn các chương trình khác (kể cả lời gọi File.Delete tiếp theo của chính bạn) chạm vào tệp. Các helper như File.WriteAllText và File.ReadAllLines tự mở và dispose bên trong, nên không cần using. Xem tệp để biết thêm.

Nhiều tài nguyên: thứ tự dispose

Xếp chồng các câu lệnh using mà không có dấu ngoặc nhọn ở giữa để mở nhiều tài nguyên trong một khối. Chúng được dispose theo thứ tự ngược với thứ tự tạo:

Đầu ra:

acquire file
acquire buffer
acquire writer
working
release writer
release buffer
release file
acquire a
acquire b
both open
release b
release a

Thứ tự ngược là đúng thứ các tài nguyên xếp lớp cần: một writer bọc một bộ đệm, bộ đệm lại bọc một tệp, nên writer phải đẩy dữ liệu vào bộ đệm trước khi bộ đệm ghi xuống tệp, và trước khi tệp được đóng.

Using declaration (C# 8)

C# 8 thêm một dạng không có dấu ngoặc nhọn. Biến được dispose khi phạm vi bao quanh kết thúc, thường là cuối phương thức:

static void ExportReport(string path, IEnumerable<string> rows)
{
    using var writer = new StreamWriter(path);
    writer.WriteLine("Report");

    foreach (string row in rows)
    {
        writer.WriteLine(row);
    }
} // writer.Dispose() runs here, at the end of the method

Cách này bớt được một cấp thụt lề, và điều đó đáng kể khi một phương thức dùng hai ba tài nguyên. Quy tắc về thứ tự dispose vẫn giữ nguyên: các khai báo trong cùng một phạm vi được dispose theo thứ tự ngược. Cái giá phải trả là tài nguyên vẫn mở cho tới khi phạm vi kết thúc, nên khi cần đóng tệp trước khi phương thức làm thêm việc khác, hãy giữ dạng khối hoặc đặt khai báo trong một khối { } riêng.

Với các đối tượng có bước dọn dẹp bất đồng bộ (IAsyncDisposable, như nhiều kết nối cơ sở dữ liệu và stream), C# 8 còn có await using, chờ DisposeAsync() tại đúng vị trí đó.

Cài đặt IDisposable trong lớp của bạn

Hãy cài đặt IDisposable khi lớp của bạn sở hữu một thứ cần dispose (nó đã tạo một stream, một timer, một kết nối) và vì thế đến lượt nó phải dispose thứ đó. Với trường hợp thông thường, đó chỉ là một phương thức ngắn:

Đầu ra:

1. user ana logged in
2. order 1001 created
AuditLog disposed
Caught ObjectDisposedException

Các quy tắc mà đoạn code tuân theo: Dispose dispose mọi thứ đối tượng sở hữu; gọi nó nhiều lần thì từ lần thứ hai không làm gì cả; và dùng đối tượng sau khi đã dispose sẽ ném ObjectDisposedException. using (log) cũng cho thấy khối lệnh có thể nhận một biến có sẵn thay vì một khai báo.

"Dispose pattern" dài hơn, với phương thức protected virtual void Dispose(bool disposing) và một finalizer, chỉ cần khi một lớp trực tiếp giữ một unmanaged handle (một con trỏ thô từ native code). Trong .NET hiện đại, hãy bọc những handle như vậy trong một lớp con của SafeHandle, và phiên bản đơn giản ở trên là tất cả những gì lớp của bạn cần.

Lỗi thường gặp

  • Trả về một đối tượng được tạo trong khối using. Nó bị dispose khi khối kết thúc, nên nơi gọi nhận được một stream đã đóng. Hãy trả về dữ liệu, hoặc để nơi gọi tự quản lý using.
  • Quên using với stream và writer. Dữ liệu trong bộ đệm có thể bị mất và tệp bị khóa.
  • Dispose một đối tượng dùng chung. Chỉ dispose những gì bạn sở hữu. Ví dụ, một HttpClient sống lâu được thiết kế để dùng chung và tái sử dụng, không phải tạo rồi dispose cho mỗi request.
  • Dùng một đối tượng sau khối lệnh. Các phương thức trên đối tượng đã dispose sẽ ném ObjectDisposedException.
  • Trông cậy vào bộ thu gom rác. Nó giải phóng bộ nhớ, không phải tệp hay socket, và không bao giờ gọi Dispose.

Câu hỏi thường gặp

Câu lệnh using trong C# dùng để làm gì?

using (var x = ...) { ... } gọi x.Dispose() khi khối lệnh kết thúc, dù nó kết thúc bình thường, qua return, hay vì có ngoại lệ được ném ra. Trình biên dịch biến nó thành một cặp try/finally với lời gọi Dispose nằm trong finally, nên tài nguyên được giải phóng tại một thời điểm biết trước thay vì lúc nào bộ thu gom rác chạy.

IDisposable trong C# là gì?

IDisposable là một interface có đúng một phương thức, void Dispose(). Một lớp cài đặt nó khi lớp đó giữ thứ gì cần được giải phóng một cách tường minh: một file handle, một socket mạng, một kết nối cơ sở dữ liệu, một timer. Mọi đối tượng có kiểu cài đặt IDisposable đều nên được dispose khi bạn dùng xong, thường là bằng using.

using và using var khác nhau thế nào?

using (var x = ...) { } dispose ở cuối khối lệnh của nó. Using declaration using var x = ...; (từ C# 8) không có khối lệnh riêng: nó dispose khi phạm vi bao quanh kết thúc, thường là cuối phương thức. Cách này đỡ phải lồng thêm một cấp khi tài nguyên cần sống đến hết phương thức.

Nhiều đối tượng using được dispose theo thứ tự nào?

Theo thứ tự ngược với thứ tự tạo. Với using (var a = ...) using (var b = ...) { }, b được dispose trước rồi mới đến a. Đó chính là thứ tự bạn cần khi b phụ thuộc vào a, chẳng hạn một StreamWriter bọc một FileStream.

Bộ thu gom rác có gọi Dispose không?

Không. Bộ thu gom rác giải phóng bộ nhớ, và có thể chạy finalizer vào một lúc nào đó không đoán trước được, nhưng nó không bao giờ gọi Dispose. Một tệp không được dispose có thể bị khóa mãi, và dữ liệu còn trong bộ đệm có thể không bao giờ được ghi ra. Đó là lý do using tồn tại.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu