Tôi ngạc nhiên là không ai đề xuất phương án thay thế này, vì vậy mặc dù câu hỏi đã xuất hiện một thời gian, tôi sẽ thêm nó vào: một cách tốt để giải quyết vấn đề này là sử dụng các biến để theo dõi trạng thái hiện tại. Đây là một kỹ thuật có thể được sử dụng cho dù có goto
được sử dụng để đến mã dọn dẹp hay không. Giống như bất kỳ kỹ thuật mã hóa nào, nó có ưu và nhược điểm và sẽ không phù hợp với mọi tình huống, nhưng nếu bạn đang chọn một phong cách thì đáng để cân nhắc - đặc biệt nếu bạn muốn tránh goto
mà không kết thúc với các if
s lồng nhau sâu sắc .
Ý tưởng cơ bản là, đối với mọi hành động dọn dẹp có thể cần thực hiện, có một biến từ giá trị của nó mà chúng ta có thể cho biết liệu hoạt động dọn dẹp có cần thực hiện hay không.
Tôi sẽ hiển thị goto
phiên bản trước, vì nó gần với mã trong câu hỏi ban đầu hơn.
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
/*
* Prepare
*/
if (do_something(bar)) {
something_done = 1;
} else {
goto cleanup;
}
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
goto cleanup;
}
if (prepare_stuff(bar)) {
stufF_prepared = 1;
} else {
goto cleanup;
}
/*
* Do the thing
*/
return_value = do_the_thing(bar);
/*
* Clean up
*/
cleanup:
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Một ưu điểm của điều này so với một số kỹ thuật khác là, nếu thứ tự của các hàm khởi tạo bị thay đổi, việc dọn dẹp chính xác sẽ vẫn xảy ra - ví dụ: sử dụng switch
phương pháp được mô tả trong một câu trả lời khác, nếu thứ tự khởi tạo thay đổi, thì switch
phải được chỉnh sửa rất cẩn thận để tránh cố gắng xóa một cái gì đó không thực sự được khởi tạo ngay từ đầu.
Bây giờ, một số người có thể tranh luận rằng phương pháp này thêm rất nhiều biến phụ - và thực sự trong trường hợp này, điều đó đúng - nhưng trong thực tế, một biến hiện có đã theo dõi hoặc có thể được thực hiện để theo dõi, trạng thái cần thiết. Ví dụ: nếu prepare_stuff()
thực sự là một lệnh gọi đến malloc()
hoặc đến open()
, thì biến giữ con trỏ trả về hoặc bộ mô tả tệp có thể được sử dụng - ví dụ:
int fd = -1;
....
fd = open(...);
if (fd == -1) {
goto cleanup;
}
...
cleanup:
if (fd != -1) {
close(fd);
}
Bây giờ, nếu chúng tôi theo dõi thêm trạng thái lỗi với một biến, chúng tôi có thể tránh goto
hoàn toàn và vẫn dọn dẹp một cách chính xác, mà không có thụt lề ngày càng sâu hơn khi chúng tôi cần khởi tạo nhiều hơn:
int foo(int bar)
{
int return_value = 0;
int something_done = 0;
int stuff_inited = 0;
int stuff_prepared = 0;
int oksofar = 1;
/*
* Prepare
*/
if (oksofar) { /* NB This "if" statement is optional (it always executes) but included for consistency */
if (do_something(bar)) {
something_done = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (init_stuff(bar)) {
stuff_inited = 1;
} else {
oksofar = 0;
}
}
if (oksofar) {
if (prepare_stuff(bar)) {
stuff_prepared = 1;
} else {
oksofar = 0;
}
}
/*
* Do the thing
*/
if (oksofar) {
return_value = do_the_thing(bar);
}
/*
* Clean up
*/
if (stuff_prepared) {
unprepare_stuff();
}
if (stuff_inited) {
uninit_stuff();
}
if (something_done) {
undo_something();
}
return return_value;
}
Một lần nữa, có những lời chỉ trích tiềm ẩn về điều này:
- Không phải tất cả những "nếu" của hiệu suất làm tổn thương? Không - bởi vì trong trường hợp thành công, bạn phải thực hiện tất cả các kiểm tra (nếu không, bạn sẽ không kiểm tra tất cả các trường hợp lỗi); và trong trường hợp không thành công, hầu hết các trình biên dịch sẽ tối ưu hóa chuỗi
if (oksofar)
kiểm tra không thành công cho một lần chuyển đến mã dọn dẹp (GCC chắc chắn có) - và trong mọi trường hợp, trường hợp lỗi thường ít quan trọng hơn đối với hiệu suất.
Đây không phải là thêm một biến khác? Trong trường hợp này là có, nhưng thường thì return_value
biến có thể được sử dụng để đóng vai trò oksofar
ở đây. Nếu bạn cấu trúc các hàm của mình để trả về lỗi một cách nhất quán, bạn thậm chí có thể tránh lỗi thứ hai if
trong mỗi trường hợp:
int return_value = 0;
if (!return_value) {
return_value = do_something(bar);
}
if (!return_value) {
return_value = init_stuff(bar);
}
if (!return_value) {
return_value = prepare_stuff(bar);
}
Một trong những lợi thế của việc viết mã như vậy là tính nhất quán có nghĩa là bất kỳ nơi nào mà lập trình viên ban đầu đã quên kiểm tra giá trị trả về sẽ nhô ra như một ngón tay cái đau, khiến việc tìm thấy (một loại) lỗi dễ dàng hơn nhiều.
Vì vậy - đây là (chưa) một phong cách nữa có thể được sử dụng để giải quyết vấn đề này. Được sử dụng đúng cách, nó cho phép tạo ra mã rất rõ ràng, nhất quán - và giống như bất kỳ kỹ thuật nào, nếu dùng sai nó có thể tạo ra mã dài dòng và khó hiểu :-)