Ý nghĩa của việc mua lại tài nguyên là Khởi tạo (RAII) là gì?
Ý nghĩa của việc mua lại tài nguyên là Khởi tạo (RAII) là gì?
Câu trả lời:
Đó là một cái tên thực sự khủng khiếp cho một khái niệm cực kỳ mạnh mẽ và có lẽ là một trong những điều số 1 mà các nhà phát triển C ++ bỏ lỡ khi họ chuyển sang các ngôn ngữ khác. Đã có một chút phong trào để cố gắng đổi tên khái niệm này thành Quản lý tài nguyên giới hạn phạm vi , mặc dù nó dường như chưa bắt kịp.
Khi chúng ta nói 'Tài nguyên', chúng ta không chỉ có nghĩa là bộ nhớ - đó có thể là xử lý tệp, ổ cắm mạng, xử lý cơ sở dữ liệu, đối tượng GDI ... Tóm lại, những thứ mà chúng ta có nguồn cung cấp hữu hạn và vì vậy chúng ta cần có khả năng kiểm soát việc sử dụng của họ. Khía cạnh 'Phạm vi giới hạn' có nghĩa là thời gian tồn tại của đối tượng bị ràng buộc với phạm vi của một biến, do đó, khi biến đó nằm ngoài phạm vi thì hàm hủy sẽ giải phóng tài nguyên. Một đặc tính rất hữu ích của việc này là nó tạo ra sự an toàn ngoại lệ lớn hơn. Ví dụ, so sánh điều này:
RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation(); // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks
Với RAII
class ManagedResourceHandle {
public:
ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
~ManagedResourceHandle() {delete rawHandle; }
... // omitted operator*, etc
private:
RawResourceHandle* rawHandle;
};
ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();
Trong trường hợp sau này, khi ngoại lệ được ném và ngăn xếp không được xử lý, các biến cục bộ sẽ bị hủy để đảm bảo rằng tài nguyên của chúng ta được dọn sạch và không bị rò rỉ.
Scope-Bound
là lựa chọn tên tốt nhất ở đây vì các chỉ định lớp lưu trữ cùng với phạm vi xác định thời lượng lưu trữ của một thực thể. Thu hẹp nó được thực hiện để giới hạn phạm vi có thể là một sự đơn giản hóa hữu ích, tuy nhiên nó không chính xác 100%
Đây là một thành ngữ lập trình có nghĩa ngắn gọn là bạn
Điều này đảm bảo rằng bất cứ điều gì xảy ra trong khi tài nguyên đang được sử dụng, cuối cùng nó sẽ được giải phóng (cho dù là trả lại bình thường, phá hủy đối tượng chứa hoặc ném ngoại lệ).
Đó là một cách thực hành tốt được sử dụng rộng rãi trong C ++, vì ngoài cách an toàn để xử lý tài nguyên, nó còn giúp mã của bạn sạch hơn rất nhiều vì bạn không cần trộn mã xử lý lỗi với chức năng chính.
*
Cập nhật: "cục bộ" có thể có nghĩa là một biến cục bộ hoặc một biến thành viên không phải là thành viên của một lớp. Trong trường hợp sau, biến thành viên được khởi tạo và hủy với đối tượng chủ sở hữu của nó.
**
Update2: như @sbi đã chỉ ra, tài nguyên - mặc dù thường được phân bổ bên trong hàm tạo - cũng có thể được phân bổ bên ngoài và được truyền vào dưới dạng tham số.
open()
/ close()
phương thức để khởi tạo và giải phóng tài nguyên, chỉ là hàm tạo và hàm hủy, do đó, 'giữ' tài nguyên chỉ là thời gian tồn tại của đối tượng, bất kể thời gian tồn tại đó là được xử lý bởi bối cảnh (ngăn xếp) hoặc rõ ràng (phân bổ động)
"RAII" là viết tắt của "Thu nhận tài nguyên là khởi tạo" và thực sự là một cách hiểu sai, vì nó không phải là mua lại tài nguyên (và khởi tạo một đối tượng) mà nó quan tâm, nhưng giải phóng tài nguyên (bằng cách phá hủy một đối tượng ).
Nhưng RAII là tên chúng tôi có và nó dính vào.
Về cơ bản, thành ngữ này bao gồm các tài nguyên (khối bộ nhớ, tệp mở, mutexes đã mở khóa, tên bạn) trong các đối tượng tự động, cục bộ và có hàm hủy của đối tượng đó giải phóng tài nguyên khi đối tượng bị phá hủy tại cuối phạm vi nó thuộc về:
{
raii obj(acquire_resource());
// ...
} // obj's dtor will call release_resource()
Tất nhiên, các đối tượng không phải lúc nào cũng là đối tượng tự động cục bộ. Họ cũng có thể là thành viên của một lớp:
class something {
private:
raii obj_; // will live and die with instances of the class
// ...
};
Nếu các đối tượng như vậy quản lý bộ nhớ, chúng thường được gọi là "con trỏ thông minh".
Có nhiều biến thể của điều này. Ví dụ, trong đoạn mã đầu tiên, câu hỏi đặt ra điều gì sẽ xảy ra nếu ai đó muốn sao chép obj
. Cách dễ nhất sẽ chỉ đơn giản là không cho phép sao chép. std::unique_ptr<>
, một con trỏ thông minh là một phần của thư viện tiêu chuẩn như đặc trưng của tiêu chuẩn C ++ tiếp theo, thực hiện điều này.
Một con trỏ thông minh như vậy, std::shared_ptr
có tính năng "sở hữu chung" của tài nguyên (một đối tượng được phân bổ động) mà nó nắm giữ. Đó là, nó có thể được sao chép tự do và tất cả các bản sao đề cập đến cùng một đối tượng. Con trỏ thông minh theo dõi có bao nhiêu bản sao đề cập đến cùng một đối tượng và sẽ xóa nó khi bản cuối cùng bị hủy.
Một biến thể thứ ba được đặc trưng bởistd::auto_ptr
trong đó thực hiện một loại ngữ nghĩa di chuyển: Một đối tượng chỉ được sở hữu bởi một con trỏ và cố gắng sao chép một đối tượng sẽ dẫn đến (thông qua cú pháp cú pháp) trong việc chuyển quyền sở hữu đối tượng sang mục tiêu của hoạt động sao chép.
std::auto_ptr
là phiên bản lỗi thời của std::unique_ptr
. std::auto_ptr
loại ngữ nghĩa di chuyển mô phỏng càng nhiều càng tốt trong C ++ 98, std::unique_ptr
sử dụng ngữ nghĩa di chuyển mới của C ++ 11. Lớp mới đã được tạo vì ngữ nghĩa di chuyển của C ++ 11 rõ ràng hơn (yêu cầu std::move
ngoại trừ tạm thời) trong khi nó được mặc định cho bất kỳ bản sao nào từ non-const in std::auto_ptr
.
Tuổi thọ của một đối tượng được xác định bởi phạm vi của nó. Tuy nhiên, đôi khi chúng ta cần, hoặc nó hữu ích, để tạo một đối tượng sống độc lập với phạm vi nơi nó được tạo. Trong C ++, toán tử new
được sử dụng để tạo một đối tượng như vậy. Và để tiêu diệt đối tượng, toán tử delete
có thể được sử dụng. Các đối tượng được tạo bởi toán tử new
được phân bổ động, tức là được phân bổ trong bộ nhớ động (còn được gọi là heap hoặc lưu trữ miễn phí ). Vì vậy, một đối tượng được tạo bởi new
sẽ tiếp tục tồn tại cho đến khi nó bị phá hủy rõ ràng bằng cách sử dụng delete
.
Một số lỗi có thể xảy ra khi sử dụng new
và delete
là:
new
để phân bổ một đối tượng và quên delete
đối tượng.delete
đối tượng và sau đó sử dụng con trỏ khác.delete
một đối tượng hai lần.Nói chung, các biến phạm vi được ưa thích. Tuy nhiên, RAII có thể được sử dụng thay thế new
và delete
để làm cho một đối tượng sống độc lập với phạm vi của nó. Một kỹ thuật như vậy bao gồm việc đưa con trỏ đến đối tượng được phân bổ trên heap và đặt nó vào một đối tượng xử lý / quản lý . Cái sau có một hàm hủy sẽ đảm nhiệm việc tiêu diệt đối tượng. Điều này sẽ đảm bảo rằng đối tượng có sẵn cho bất kỳ chức năng nào muốn truy cập vào nó và đối tượng bị hủy khi thời gian tồn tại của đối tượng xử lý kết thúc mà không cần dọn dẹp rõ ràng.
Các ví dụ từ thư viện chuẩn C ++ sử dụng RAII là std::string
và std::vector
.
Hãy xem xét đoạn mã này:
void fn(const std::string& str)
{
std::vector<char> vec;
for (auto c : str)
vec.push_back(c);
// do something
}
Khi bạn tạo một vectơ và bạn đẩy các phần tử đến nó, bạn không quan tâm đến việc phân bổ và giải quyết các phần tử đó. Vectơ sử dụng new
để phân bổ không gian cho các phần tử của nó trên heap và delete
để giải phóng không gian đó. Bạn là người sử dụng vectơ, bạn không quan tâm đến các chi tiết triển khai và sẽ tin tưởng vectơ không bị rò rỉ. Trong trường hợp này, vectơ là đối tượng xử lý các phần tử của nó.
Những ví dụ khác từ thư viện chuẩn RAII sử dụng nằm std::shared_ptr
, std::unique_ptr
và std::lock_guard
.
Một tên khác cho kỹ thuật này là SBRM , viết tắt của Scope-Bound Resource Management .
Cuốn sách Lập trình C ++ với các mẫu thiết kế được tiết lộ mô tả RAII như sau:
Ở đâu
Các tài nguyên được triển khai như các lớp và tất cả các con trỏ đều có các lớp bao quanh chúng (làm cho chúng trở thành các con trỏ thông minh).
Tài nguyên có được bằng cách gọi các nhà xây dựng của họ và phát hành ngầm (theo thứ tự ngược lại để có được) bằng cách gọi các hàm hủy của chúng.
Có ba phần cho một lớp RAII:
RAII là viết tắt của "Thu nhận tài nguyên là khởi tạo." Phần "thu nhận tài nguyên" của RAII là nơi bạn bắt đầu một cái gì đó phải kết thúc sau đó, chẳng hạn như:
Phần "là khởi tạo" có nghĩa là việc mua lại xảy ra bên trong hàm tạo của một lớp.
Quản lý bộ nhớ thủ công là một cơn ác mộng mà các lập trình viên đã và đang phát minh ra những cách để tránh kể từ khi phát minh ra trình biên dịch. Ngôn ngữ lập trình với bộ thu gom rác giúp cuộc sống dễ dàng hơn, nhưng với chi phí hiệu năng. Trong bài viết này - Loại bỏ người thu gom rác: Cách RAII , kỹ sư Toptal Peter Goodspeed-Niklaus cho chúng ta xem qua lịch sử của người thu gom rác và giải thích cách các khái niệm về quyền sở hữu và vay mượn có thể giúp loại bỏ người thu gom rác mà không ảnh hưởng đến sự đảm bảo an toàn của họ.