Tại sao thử {{}} cuối cùng là tốt; thử {'}} bắt {} xấu?


201

Tôi đã thấy mọi người nói rằng đó là hình thức xấu khi sử dụng sản phẩm khai thác mà không có đối số, đặc biệt là nếu sản phẩm khai thác đó không làm gì cả:

StreamReader reader=new  StreamReader("myfile.txt");
try
{
  int i = 5 / 0;
}
catch   // No args, so it will catch any exception
{}
reader.Close();

Tuy nhiên, đây được coi là hình thức tốt:

StreamReader reader=new  StreamReader("myfile.txt");
try
{
  int i = 5 / 0;
}
finally   // Will execute despite any exception
{
  reader.Close();
}

Theo như tôi có thể nói, sự khác biệt duy nhất giữa việc đặt mã dọn dẹp vào khối cuối cùng và đặt mã dọn dẹp sau khối try..catch là nếu bạn có câu lệnh trả về trong khối thử của mình (trong trường hợp đó, mã dọn dẹp cuối cùng sẽ chạy, nhưng mã sau khi thử..catch sẽ không).

Nếu không, cuối cùng có gì đặc biệt?


7
Trước khi bạn cố gắng bắt một con hổ mà bạn không thể xử lý, bạn nên ghi lại những điều ước cuối cùng của mình.

Chủ đề ngoại lệ trong tài liệu có thể cung cấp một số hiểu biết tốt. Ngoài ra hãy xem ví dụ Khối cuối cùng .
Athafoud

Câu trả lời:


357

Sự khác biệt lớn là try...catchsẽ nuốt ngoại lệ, che giấu sự thật rằng đã xảy ra lỗi. try..finallysẽ chạy mã dọn dẹp của bạn và sau đó ngoại lệ sẽ tiếp tục, được xử lý bởi một cái gì đó biết phải làm gì với nó.


11
Bất kỳ mã nào được viết với đóng gói trong tâm trí có khả năng chỉ có thể xử lý ngoại lệ tại điểm được nêu ra. Đơn giản chỉ cần chuyển nó trở lại ngăn xếp cuộc gọi với hy vọng tuyệt vọng rằng một cái gì đó khác sẽ có thể xử lý một số ngoại lệ tùy tiện là một công thức cho thảm họa.
David Arno

3
Trong hầu hết các trường hợp, rõ ràng hơn tại sao một ngoại lệ cụ thể sẽ xảy ra từ cấp ứng dụng (ví dụ: một cài đặt cấu hình nhất định) so với cấp độ thư viện lớp.
Đánh dấu Cidade

88
David - Tôi muốn chương trình thất bại nhanh hơn vì vậy tôi có thể nhận thức được vấn đề thay vì để chương trình chạy ở trạng thái không xác định.
Erik Forbes

6
Nếu chương trình của bạn ở trạng thái không xác định sau một ngoại lệ thì bạn đang thực hiện mã sai.
Zan Lynx

41
@DavidArno, bất kỳ mã nào được viết với sự đóng gói trong tâm trí chỉ nên xử lý ngoại lệ trong phạm vi của chúng. Bất cứ điều gì khác nên được thông qua cho người khác để xử lý. Nếu tôi có một ứng dụng nhận được tên tệp từ người dùng thì đọc tệp và trình đọc tệp của tôi có ngoại lệ mở tệp, nó sẽ chuyển chúng lên (hoặc sử dụng ngoại lệ và ném một cái mới) để ứng dụng có thể nói , hey - tập tin không mở, hãy nhắc người dùng cho một cái khác. Trình đọc tệp không thể nhắc nhở người dùng hoặc thực hiện bất kỳ hành động nào khác để phản hồi. Mục đích duy nhất của nó là đọc các tập tin.
iheanyi

62

"Cuối cùng" là một tuyên bố của "Một cái gì đó bạn phải luôn luôn làm để đảm bảo trạng thái chương trình là lành mạnh". Như vậy, luôn có một hình thức tốt, nếu có bất kỳ khả năng ngoại lệ nào có thể làm mất trạng thái chương trình. Trình biên dịch cũng có độ dài lớn để đảm bảo rằng mã Cuối cùng của bạn được chạy.

"Bắt" là một tuyên bố "Tôi có thể phục hồi từ ngoại lệ này". Bạn chỉ nên khôi phục từ các trường hợp ngoại lệ mà bạn thực sự có thể sửa - bắt mà không cần tranh luận "Này, tôi có thể phục hồi từ bất cứ điều gì!", Điều này gần như luôn luôn không đúng sự thật.

Nếu có thể phục hồi từ mọi ngoại lệ, thì đó thực sự sẽ là một sự ngụy biện về ngữ nghĩa, về những gì bạn đang tuyên bố ý định của mình. Tuy nhiên, không phải như vậy, và gần như chắc chắn các khung phía trên của bạn sẽ được trang bị tốt hơn để xử lý các ngoại lệ nhất định. Như vậy, sử dụng cuối cùng, để mã dọn dẹp của bạn chạy miễn phí, nhưng vẫn để người xử lý hiểu biết hơn giải quyết vấn đề.


1
Tình cảm của bạn rất phổ biến, nhưng không may bỏ qua một trường hợp quan trọng khác: vô hiệu hóa một đối tượng mà bất biến có thể không còn giữ được nữa. Một mô hình phổ biến là mã để có được một khóa, thực hiện một số thay đổi đối với một đối tượng và giải phóng khóa. Nếu một ngoại lệ xảy ra sau khi thực hiện một số nhưng không phải tất cả các thay đổi, đối tượng có thể bị bỏ lại ở trạng thái không hợp lệ. Mặc dù IMHO lựa chọn thay thế tốt hơn nên tồn tại, tôi biết không có cách tiếp cận tốt hơn để đón bất kỳ ngoại lệ xảy ra trong khi tình trạng đối tượng có thể là không hợp lệ, rõ ràng làm mất hiệu lực của nhà nước, và rethrow.
supercat

32

Bởi vì khi một dòng duy nhất ném một ngoại lệ, bạn sẽ không biết nó.

Với khối mã đầu tiên, ngoại lệ sẽ được hấp thụ , chương trình sẽ tiếp tục thực thi ngay cả khi trạng thái của chương trình có thể sai.

Với khối thứ hai, các ngoại lệ sẽ được ném và bong bóng lên nhưng những reader.Close()vẫn được đảm bảo để chạy.

Nếu không có ngoại lệ, thì đừng đặt khối try..catch như vậy, sẽ khó gỡ lỗi sau khi chương trình rơi vào trạng thái xấu và bạn không biết tại sao.


21

Cuối cùng được thực hiện không có vấn đề gì. Vì vậy, nếu khối thử của bạn thành công, nó sẽ thực thi, nếu khối thử của bạn thất bại, thì nó sẽ thực hiện khối bắt và sau đó là khối cuối cùng.

Ngoài ra, tốt hơn hết là thử sử dụng cấu trúc sau:

using (StreamReader reader=new  StreamReader("myfile.txt"))
{
}

Vì câu lệnh sử dụng được tự động gói trong một lần thử / cuối cùng và luồng sẽ được tự động đóng lại. (Bạn sẽ cần đặt thử / bắt xung quanh câu lệnh sử dụng nếu bạn muốn thực sự bắt ngoại lệ).


5
Điều này LAF không đúng. Sử dụng không bao bọc mã bằng thử / bắt, nên nói thử / cuối cùng
pr0nin

8

Trong khi 2 khối mã sau là tương đương, chúng không bằng nhau.

try
{
  int i = 1/0; 
}
catch
{
  reader.Close();
  throw;
}

try
{
  int i = 1/0;
}
finally
{
  reader.Close();
}
  1. "Cuối cùng" là mã tiết lộ ý định. Bạn khai báo với trình biên dịch và cho các lập trình viên khác rằng mã này cần chạy bất kể là gì.
  2. nếu bạn có nhiều khối bắt và bạn có mã dọn dẹp, cuối cùng bạn cần. Cuối cùng, bạn sẽ nhân đôi mã dọn dẹp của mình trong mỗi khối bắt. (Nguyên tắc DRY)

cuối cùng các khối là đặc biệt. CLR nhận ra và xử lý mã với một khối cuối cùng tách biệt với các khối bắt và CLR sẽ có độ dài lớn để đảm bảo rằng khối cuối cùng sẽ luôn thực thi. Nó không chỉ là cú pháp đường từ trình biên dịch.


5

Tôi đồng ý với những gì dường như là sự đồng thuận ở đây - một 'cái bẫy' trống rỗng là xấu bởi vì nó che giấu bất kỳ ngoại lệ nào có thể xảy ra trong khối thử.

Ngoài ra, từ quan điểm dễ đọc, khi tôi thấy khối 'thử' tôi cho rằng sẽ có một tuyên bố 'bắt' tương ứng. Nếu bạn chỉ sử dụng 'thử' để đảm bảo tài nguyên được phân bổ trong khối 'cuối cùng', bạn có thể xem xét câu lệnh 'bằng cách sử dụng' :

using (StreamReader reader = new StreamReader('myfile.txt'))
{
    // do stuff here
} // reader.dispose() is called automatically

Bạn có thể sử dụng câu lệnh 'bằng cách sử dụng' với bất kỳ đối tượng nào thực hiện IDis Dùng một lần. Phương thức dispose () của đối tượng được gọi tự động ở cuối khối.


4

Sử dụng Try..Catch..Finally , nếu phương pháp của bạn biết cách xử lý ngoại lệ cục bộ. Ngoại lệ xảy ra trong Thử, Xử lý trong Bắt và sau đó việc dọn dẹp được thực hiện trong Cuối cùng.

Trong trường hợp nếu phương thức của bạn không biết cách xử lý ngoại lệ nhưng cần dọn dẹp một khi nó đã xảy ra, hãy sử dụng Try..Finally

Do đó, ngoại lệ được truyền đến các phương thức gọi và được xử lý nếu có bất kỳ câu lệnh Catch phù hợp nào trong các phương thức gọi. Nếu không có trình xử lý ngoại lệ nào trong phương thức hiện tại hoặc bất kỳ phương thức gọi nào thì ứng dụng gặp sự cố.

Bởi Try..Finallynó được đảm bảo rằng việc dọn dẹp cục bộ được thực hiện trước khi truyền ngoại lệ cho các phương thức gọi.


1
Cơ bản như câu trả lời này, nó hoàn toàn là câu trả lời hay nhất. Thật tốt khi chỉ có thói quen thử / bắt / cuối cùng, ngay cả khi một trong hai thứ hai bị bỏ trống. Có những trường hợp RẤT HIẾM trong đó một khối bắt có thể tồn tại và trống, nhưng ít nhất nếu bạn luôn viết thử / bắt / cuối cùng, bạn sẽ thấy khối trống khi bạn đọc mã. Có một khối trống cuối cùng là hữu ích theo cùng một cách. Nếu bạn cần dọn dẹp sau hoặc cần gỡ lỗi một trạng thái tại thời điểm ngoại lệ, điều đó cực kỳ hữu ích.
Jesse Williams

3

Việc thử..tất cả cuối cùng vẫn sẽ đưa ra bất kỳ ngoại lệ nào được nêu ra. Tất cả finallyđều đảm bảo rằng mã dọn dẹp được chạy trước khi ném ngoại lệ.

Việc thử..catch với một sản phẩm khai thác trống sẽ hoàn toàn tiêu thụ bất kỳ ngoại lệ nào và che giấu sự thật rằng nó đã xảy ra. Người đọc sẽ bị đóng cửa, nhưng không biết liệu điều đó có đúng hay không. Điều gì nếu ý định của bạn là viết i vào tập tin? Trong trường hợp này, bạn sẽ không làm cho phần đó của mã và myfile.txt sẽ trống. Có phải tất cả các phương pháp hạ nguồn xử lý điều này đúng? Khi bạn nhìn thấy tệp trống, bạn có thể đoán chính xác rằng nó trống không vì một ngoại lệ đã bị ném? Tốt hơn là ném ngoại lệ và cho biết rằng bạn đang làm gì đó sai.

Một lý do khác là try..catch được thực hiện như thế này là hoàn toàn không chính xác. Những gì bạn đang nói bằng cách làm điều này là, "Bất kể điều gì xảy ra, tôi có thể xử lý nó." Thế còn StackOverflowException, bạn có thể dọn dẹp sau đó không? Thế còn OutOfMemoryException? Nói chung, bạn chỉ nên xử lý các trường hợp ngoại lệ mà bạn mong đợi và biết cách xử lý.


2

Nếu bạn không biết loại ngoại lệ nào cần bắt hoặc phải làm gì với loại ngoại lệ đó, sẽ không có lý do gì để có tuyên bố bắt. Bạn chỉ nên để nó cho một người gọi cao hơn có thể có nhiều thông tin hơn về tình huống để biết phải làm gì.

Bạn vẫn nên có một tuyên bố cuối cùng trong đó trong trường hợp có ngoại lệ, để bạn có thể dọn sạch tài nguyên trước khi ngoại lệ đó được ném cho người gọi.


2

Từ góc độ dễ đọc, nó nói rõ hơn với những người đọc mã trong tương lai "công cụ này ở đây rất quan trọng, nó cần phải được thực hiện bất kể điều gì xảy ra." Điều này là tốt

Ngoài ra, các tuyên bố đánh bắt trống có xu hướng có một "mùi" nhất định đối với họ. Chúng có thể là một dấu hiệu cho thấy các nhà phát triển không suy nghĩ thông qua các ngoại lệ khác nhau có thể xảy ra và cách xử lý chúng.


2

Cuối cùng là tùy chọn - không có lý do gì để có khối "Cuối cùng" nếu không có tài nguyên để dọn dẹp.


2

Lấy từ: đây

Tăng và bắt ngoại lệ không nên thường xuyên xảy ra như là một phần của việc thực hiện thành công một phương thức. Khi phát triển thư viện lớp, mã máy khách phải có cơ hội kiểm tra tình trạng lỗi trước khi thực hiện một thao tác có thể dẫn đến ngoại lệ được đưa ra. Ví dụ: System.IO.FileStream cung cấp thuộc tính CanRead có thể được kiểm tra trước khi gọi phương thức Đọc, ngăn chặn một ngoại lệ tiềm năng được nêu ra, như được minh họa trong đoạn mã sau:

Dim str As Stream = GetStream () If (str.CanRead) Sau đó 'mã để đọc luồng End If

Quyết định có kiểm tra trạng thái của một đối tượng trước khi gọi một phương thức cụ thể có thể đưa ra một ngoại lệ hay không phụ thuộc vào trạng thái dự kiến ​​của đối tượng. Nếu một đối tượng FileStream được tạo bằng đường dẫn tệp tồn tại và hàm tạo sẽ trả về một tệp ở chế độ đọc, việc kiểm tra thuộc tính CanRead là không cần thiết; không thể đọc FileStream sẽ vi phạm hành vi dự kiến ​​của các cuộc gọi phương thức được thực hiện và một ngoại lệ sẽ được nêu ra. Ngược lại, nếu một phương thức được ghi lại là trả về tham chiếu FileStream có thể hoặc không thể đọc được, thì nên kiểm tra thuộc tính CanRead trước khi thử đọc dữ liệu.

Để minh họa tác động hiệu suất mà việc sử dụng kỹ thuật mã hóa "chạy cho đến khi có ngoại lệ" có thể gây ra, hiệu suất của một diễn viên, ném một UnlimitedCastException nếu diễn viên thất bại, được so sánh với toán tử C #, sẽ trả về null nếu diễn viên thất bại. Hiệu suất của hai kỹ thuật là giống hệt nhau trong trường hợp diễn viên hợp lệ (xem Kiểm tra 8.05), nhưng đối với trường hợp diễn viên không hợp lệ và sử dụng diễn viên gây ra ngoại lệ, sử dụng diễn viên chậm hơn 600 lần so với sử dụng là toán tử (xem Kiểm tra 8.06). Tác động hiệu suất cao của kỹ thuật ném ngoại lệ bao gồm chi phí phân bổ, ném và bắt ngoại lệ và chi phí thu gom rác tiếp theo của đối tượng ngoại lệ, có nghĩa là tác động tức thời của việc ném ngoại lệ không cao. Khi nhiều ngoại lệ được ném,


2
Scott - nếu văn bản bạn trích dẫn ở trên đứng sau paywall của Expertsexchange.com, có lẽ bạn không nên đăng nó ở đây. Tôi có thể sai về điều này nhưng tôi cá rằng đó không phải là một ý tưởng tốt.
Onorio Catenacci

2

Đó là một thực tế xấu khi thêm một mệnh đề bắt chỉ để lấy lại ngoại lệ.


2

Nếu bạn sẽ đọc C # cho các lập trình viên, bạn sẽ hiểu, khối cuối cùng là thiết kế để tối ưu hóa một ứng dụng và ngăn rò rỉ bộ nhớ.

CLR không loại bỏ hoàn toàn rò rỉ ... rò rỉ bộ nhớ có thể xảy ra nếu chương trình vô tình giữ các tham chiếu đến các đối tượng không mong muốn

Ví dụ: khi bạn mở một kết nối tệp hoặc cơ sở dữ liệu, máy của bạn sẽ phân bổ bộ nhớ để phục vụ giao dịch đó và bộ nhớ đó sẽ được giữ không trừ khi lệnh xử lý hoặc đóng được thực thi. nhưng nếu trong quá trình giao dịch xảy ra lỗi, lệnh tiến hành sẽ bị chấm dứt trừ khi nó nằm trongtry.. finally.. khối.

catchkhác với finallyý nghĩa rằng, Catch là thiết kế để cung cấp cho bạn cách xử lý / quản lý hoặc giải thích lỗi tự nó. Hãy nghĩ về nó như một người nói với bạn "hey tôi đã bắt được một số kẻ xấu, bạn muốn tôi làm gì với chúng?" trong khifinally được thiết kế để đảm bảo rằng tài nguyên của bạn được đặt đúng cách. Hãy nghĩ về ai đó rằng dù có kẻ xấu hay không, anh ta sẽ đảm bảo rằng tài sản của bạn vẫn an toàn.

Và bạn nên cho phép hai người đó làm việc cùng nhau cho tốt.

ví dụ:

try
{
  StreamReader reader=new  StreamReader("myfile.txt");
  //do other stuff
}
catch(Exception ex){
 // Create log, or show notification
 generic.Createlog("Error", ex.message);
}
finally   // Will execute despite any exception
{
  reader.Close();
}

1

Cuối cùng, bạn có thể dọn sạch các tài nguyên, ngay cả khi câu lệnh bắt của bạn ném ngoại lệ lên đến chương trình gọi. Với ví dụ của bạn có chứa câu lệnh bắt trống, có rất ít sự khác biệt. Tuy nhiên, nếu trong lần bắt của bạn, bạn thực hiện một số xử lý và đưa ra lỗi, hoặc thậm chí chỉ không có một lần bắt nào, cuối cùng vẫn sẽ được chạy.


1

Đối với một người, đó là cách thực hành tồi để bắt ngoại lệ mà bạn không bận tâm xử lý. Hãy xem Chương 5 về .Net Performance từ Cải thiện Hiệu suất và Khả năng mở rộng của Ứng dụng .NET . Lưu ý bên cạnh, có lẽ bạn nên tải luồng bên trong khối thử, bằng cách đó, bạn có thể bắt được ngoại lệ thích hợp nếu thất bại. Tạo luồng bên ngoài khối thử đánh bại mục đích của nó.


0

Trong số rất nhiều lý do, các trường hợp ngoại lệ rất chậm thực hiện. Bạn có thể dễ dàng làm tê liệt thời gian thực hiện nếu điều này xảy ra rất nhiều.


0

Vấn đề với các khối try / Catch bắt tất cả các ngoại lệ là chương trình của bạn hiện đang ở trạng thái không xác định nếu xảy ra ngoại lệ không xác định. Điều này hoàn toàn đi ngược lại quy tắc nhanh thất bại - bạn không muốn chương trình của mình tiếp tục nếu có ngoại lệ xảy ra. Thử / bắt ở trên thậm chí sẽ bắt được OutOfMemoryExceptions, nhưng đó chắc chắn là trạng thái mà chương trình của bạn sẽ không chạy.

Các khối thử / cuối cùng cho phép bạn thực thi mã dọn dẹp trong khi vẫn thất bại nhanh. Trong hầu hết các trường hợp, bạn chỉ muốn nắm bắt tất cả các ngoại lệ ở cấp độ toàn cầu, để bạn có thể đăng nhập chúng, sau đó thoát ra.


0

Sự khác biệt hiệu quả giữa các ví dụ của bạn là không đáng kể miễn là không có ngoại lệ nào được đưa ra.

Tuy nhiên, nếu một ngoại lệ được đưa ra trong mệnh đề 'thử', ví dụ đầu tiên sẽ nuốt nó hoàn toàn. Ví dụ thứ hai sẽ đưa ra ngoại lệ cho bước tiếp theo trong ngăn xếp cuộc gọi, do đó, sự khác biệt trong các ví dụ đã nêu là một cái che khuất hoàn toàn mọi ngoại lệ (ví dụ đầu tiên) và cái kia (ví dụ thứ hai) giữ lại thông tin ngoại lệ để xử lý sau vẫn thực hiện nội dung trong mệnh đề 'cuối cùng'.

Ví dụ, nếu bạn đặt mã trong mệnh đề 'bắt' của ví dụ đầu tiên đã đưa ra một ngoại lệ (hoặc là một trong đó ban đầu được nêu ra, hoặc một cái mới), mã dọn dẹp trình đọc sẽ không bao giờ thực thi. Cuối cùng thực thi bất kể điều gì xảy ra trong mệnh đề 'bắt'.

Vì vậy, sự khác biệt chính giữa 'bắt' và 'cuối cùng' là nội dung của khối 'cuối cùng' (với một vài ngoại lệ hiếm) có thể được coi là đảm bảo để thực thi, ngay cả khi có ngoại lệ không mong muốn, trong khi bất kỳ mã nào theo sau một điều khoản 'bắt' (nhưng bên ngoài một điều khoản 'cuối cùng') sẽ không mang lại sự bảo đảm như vậy.

Ngẫu nhiên, Stream và StreamReader đều triển khai IDis Dùng một lần và có thể được gói trong một khối 'sử dụng'. Các khối 'Sử dụng' là tương đương ngữ nghĩa của thử / cuối cùng (không 'bắt'), vì vậy ví dụ của bạn có thể được thể hiện chặt chẽ hơn như sau:

using (StreamReader reader = new  StreamReader("myfile.txt"))
{
  int i = 5 / 0;
}

... sẽ đóng và loại bỏ đối tượng StreamReader khi nó đi ra khỏi phạm vi. Hi vọng điêu nay co ich.


0

thử {'}} bắt {} không phải lúc nào cũng xấu. Nó không phải là một mẫu phổ biến, nhưng tôi có xu hướng sử dụng nó khi tôi cần tắt tài nguyên bất kể điều gì, như đóng một ổ cắm mở (có thể) ở cuối chuỗi.

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.