Câu hỏi Tại sao ngoại lệ nên được sử dụng một cách thận trọng?


Tôi thường thấy / nghe mọi người nói rằng ngoại lệ chỉ nên được sử dụng hiếm khi, nhưng không bao giờ giải thích tại sao. Trong khi đó có thể đúng, lý do thường là một glib: "nó được gọi là một ngoại lệ vì một lý do" mà, với tôi, dường như là loại giải thích mà không bao giờ nên được chấp nhận bởi một lập trình viên / kỹ sư đáng kính.

Có một loạt các vấn đề mà một ngoại lệ có thể được sử dụng để giải quyết. Tại sao nó không khôn ngoan để sử dụng chúng cho dòng điều khiển? Triết lý đằng sau việc bảo thủ đặc biệt với cách chúng được sử dụng là gì? Ngữ nghĩa? Hiệu suất? Phức tạp? Tính thẩm mỹ? Quy ước?

Tôi đã nhìn thấy một số phân tích về hiệu suất trước đây, nhưng ở một mức độ có liên quan đến một số hệ thống và không liên quan đến những người khác.

Một lần nữa, tôi không nhất thiết không đồng ý rằng họ nên được cứu cho các trường hợp đặc biệt, nhưng tôi tự hỏi lý do đồng thuận là gì (nếu có một điều như vậy).


75
2017-11-16 18:46


gốc


Bản sao: stackoverflow.com/questions/1736146/… - jheddings
Không trùng lặp. Ví dụ được liên kết là về việc xử lý ngoại lệ có hữu ích hay không. Đây là sự khác biệt giữa thời điểm sử dụng ngoại lệ và thời điểm sử dụng cơ chế báo cáo lỗi khác. - Adrian McCarthy
và cái này thì sao stackoverflow.com/questions/1385172/… ? - stijn
Không có lý do đồng thuận. Những người khác nhau có ý kiến ​​khác nhau về "sự phù hợp" của việc ném ngoại lệ, và những ý kiến ​​này thường bị ảnh hưởng bởi ngôn ngữ mà họ phát triển. Bạn đã gắn thẻ câu hỏi này với C ++, nhưng tôi nghi ngờ nếu bạn gắn thẻ nó với Java, bạn sẽ nhận được các ý kiến ​​khác nhau. - Charles Salvia
Không, nó không phải là một wiki. Các câu trả lời ở đây đòi hỏi chuyên môn và phải được thưởng với đại diện. - bobobobo


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


Điểm chính của ma sát là ngữ nghĩa. Nhiều nhà phát triển lạm dụng ngoại lệ và ném họ vào mọi cơ hội. Ý tưởng là sử dụng ngoại lệ cho một số tình huống đặc biệt. Ví dụ: nhập sai người dùng không được tính là ngoại lệ vì bạn mong đợi điều này xảy ra và sẵn sàng cho điều đó. Nhưng nếu bạn đã cố gắng tạo một tệp và không có đủ dung lượng trên đĩa thì có, đây là một ngoại lệ nhất định.

Một vấn đề khác là các ngoại lệ thường bị ném và nuốt. Các nhà phát triển sử dụng kỹ thuật này để chỉ đơn giản là "im lặng" chương trình và để cho nó chạy càng lâu càng tốt cho đến khi hoàn toàn sụp đổ. Điều này rất sai. Nếu bạn không xử lý ngoại lệ, nếu bạn không phản ứng thích hợp bằng cách giải phóng một số tài nguyên, nếu bạn không đăng nhập ngoại lệ hoặc ít nhất không thông báo cho người dùng, thì bạn không sử dụng ngoại lệ cho ý nghĩa của chúng.

Trả lời trực tiếp câu hỏi của bạn. Ngoại lệ hiếm khi được sử dụng vì các tình huống ngoại lệ hiếm và ngoại lệ rất đắt.

Hiếm, bởi vì bạn không mong đợi chương trình của bạn bị sập ở mọi nút nhấn hoặc ở mọi đầu vào người dùng không đúng định dạng. Nói, cơ sở dữ liệu đột nhiên không thể truy cập được, có thể không đủ dung lượng trên đĩa, một số dịch vụ bên thứ ba mà bạn phụ thuộc là ngoại tuyến, tất cả điều này có thể xảy ra, nhưng rất hiếm khi xảy ra.

Đắt tiền, vì việc ném một ngoại lệ sẽ làm gián đoạn luồng chương trình bình thường. Thời gian chạy sẽ giải phóng ngăn xếp cho đến khi nó tìm thấy một trình xử lý ngoại lệ thích hợp có thể xử lý ngoại lệ. Nó cũng sẽ thu thập thông tin cuộc gọi trên đường truyền tới đối tượng ngoại lệ mà trình xử lý sẽ nhận. Tất cả đều có chi phí.

Đây không phải là để nói rằng có thể không có ngoại lệ để sử dụng ngoại lệ (cười). Đôi khi nó có thể đơn giản hóa cấu trúc mã nếu bạn ném một ngoại lệ thay vì chuyển tiếp mã trả về qua nhiều lớp. Như một quy tắc đơn giản, nếu bạn mong đợi một số phương pháp được gọi thường xuyên và khám phá một số tình huống "đặc biệt" một nửa thời gian thì tốt hơn là tìm một giải pháp khác. Tuy nhiên, nếu bạn mong đợi dòng chảy hoạt động bình thường hầu hết trong khi tình trạng "đặc biệt" này chỉ có thể xuất hiện trong một số trường hợp hiếm hoi, thì nó chỉ là tốt để ném một ngoại lệ.

@Comments: Ngoại lệ chắc chắn có thể được sử dụng trong một số tình huống ít ngoại lệ nếu điều đó có thể làm cho mã của bạn đơn giản và dễ dàng hơn. Tùy chọn này là mở nhưng tôi muốn nói nó đến khá hiếm trong thực tế.

Tại sao nó không khôn ngoan để sử dụng chúng cho dòng điều khiển?

Bởi vì các ngoại lệ làm gián đoạn "dòng điều khiển" bình thường. Bạn đưa ra một ngoại lệ và việc thực thi bình thường của chương trình bị bỏ rơi có khả năng để lại các đối tượng trong trạng thái không nhất quán và một số tài nguyên mở không xác định. Chắc chắn, C # có câu lệnh sử dụng sẽ đảm bảo rằng đối tượng sẽ được xử lý ngay cả khi một ngoại lệ được ném ra khỏi cơ thể đang sử dụng. Nhưng hãy để chúng tôi tóm tắt cho thời điểm này từ ngôn ngữ. Giả sử khung sẽ không vứt bỏ các đối tượng cho bạn. Bạn làm điều đó bằng tay. Bạn có một số hệ thống về cách yêu cầu và tài nguyên và bộ nhớ miễn phí. Bạn có thỏa thuận toàn hệ thống, người chịu trách nhiệm giải phóng các đối tượng và tài nguyên trong những tình huống nào. Bạn có các quy tắc về cách xử lý các thư viện bên ngoài. Nó hoạt động tốt nếu chương trình tuân theo quy trình vận hành bình thường. Nhưng đột nhiên ở giữa thực hiện bạn ném một ngoại lệ. Một nửa số tài nguyên còn lại không tồn tại. Một nửa chưa được yêu cầu. Nếu hoạt động này có nghĩa là giao dịch ngay bây giờ thì nó bị hỏng. Các quy tắc xử lý tài nguyên của bạn sẽ không hoạt động vì các phần mã chịu trách nhiệm giải phóng tài nguyên đơn giản sẽ không thực thi. Nếu bất kỳ ai khác muốn sử dụng những tài nguyên đó, họ có thể tìm thấy chúng trong trạng thái không nhất quán và sụp đổ cũng bởi vì họ không thể dự đoán tình huống cụ thể này.

Nói rằng, bạn muốn phương thức M () gọi phương thức N () để thực hiện một số công việc và sắp xếp một số tài nguyên rồi trả về M () để sử dụng nó và sau đó vứt bỏ nó. Khỏe. Bây giờ có gì đó sai trong N () và nó ném một ngoại lệ mà bạn không mong đợi trong M () vì vậy các bong bóng ngoại lệ lên đầu cho đến khi nó có thể bị bắt trong một phương thức C () sẽ không biết điều gì đang xảy ra trong N () và liệu và làm thế nào để giải phóng một số tài nguyên.

Với việc ném ngoại lệ, bạn tạo một cách để đưa chương trình của bạn vào nhiều trạng thái trung gian không thể đoán trước mới khó dự đoán, hiểu và giải quyết. Nó hơi giống với việc sử dụng GOTO. Rất khó để thiết kế một chương trình có thể ngẫu nhiên nhảy thực thi từ vị trí này đến vị trí khác. Nó cũng sẽ khó để duy trì và gỡ lỗi nó. Khi chương trình phát triển phức tạp, bạn sẽ chỉ mất một cái nhìn tổng quan về những gì khi nào và ở đâu đang xảy ra ít hơn để sửa chữa nó.


88
2017-11-16 18:49



Nếu bạn so sánh một hàm ném một ngoại lệ so với một hàm trả về một mã lỗi, thì việc giải phóng stack sẽ giống nhau trừ khi tôi thiếu một thứ gì đó. - Catskul
@Catskul: không nhất thiết. Trả về hàm trả về điều khiển trực tiếp cho người gọi ngay lập tức. Một ngoại lệ ném trả về điều khiển cho trình xử lý bắt đầu tiên cho loại ngoại lệ đó (hoặc một lớp cơ sở của nó hoặc "...") trong toàn bộ ngăn xếp cuộc gọi hiện tại. - jon-hanson
Cũng cần lưu ý rằng các trình gỡ rối thường phá vỡ theo mặc định về ngoại lệ. Trình gỡ rối sẽ phá vỡ rất nhiều nếu bạn đang sử dụng ngoại lệ cho các điều kiện không phải là bất thường. - Qwertie
@jon: Điều đó sẽ chỉ xảy ra nếu nó không bị bắt và cần thiết để thoát khỏi phạm vi hơn để đạt được xử lý lỗi. Trong trường hợp tương tự đó bằng cách sử dụng các giá trị trả về, (và cũng cần phải được truyền qua nhiều phạm vi) cùng một lượng thư giãn sẽ xảy ra. - Catskul
-1 xin lỗi. Nó là vô nghĩa để nói về những gì các cơ sở ngoại lệ là "có nghĩa là" cho. Chúng chỉ là một cơ chế, một cơ chế có thể được sử dụng để xử lý các tình huống đặc biệt như hết bộ nhớ vv - nhưng điều đó không có nghĩa là chúng không nên cũng được sử dụng cho các mục đích khác. Điều tôi muốn thấy là giải thích lý do tại sao họ không nên được sử dụng cho các mục đích khác. Mặc dù câu trả lời của bạn không nói về điều đó một chút, nó được trộn lẫn với rất nhiều cuộc nói chuyện về những ngoại lệ "có nghĩa là" cho. - j_random_hacker


Trong khi "ném ngoại lệ trong cirumstances đặc biệt" là câu trả lời glib, bạn thực sự có thể xác định những trường hợp đó là: khi điều kiện tiên quyết được thỏa mãn, nhưng không thể thỏa mãn điều kiện. Điều này cho phép bạn viết postconditions chặt chẽ hơn, chặt chẽ hơn, và hữu ích hơn mà không bị mất xử lý lỗi; nếu không, không có ngoại lệ, bạn phải thay đổi postcondition để cho phép mọi trạng thái lỗi có thể xảy ra.

  • Điều kiện tiên quyết phải đúng trước gọi một hàm.
  • Postconditionlà chức năng đảm bảo sau nó trả về.
  • An toàn ngoại lệ nêu rõ các ngoại lệ ảnh hưởng như thế nào đến tính nhất quán nội bộ của một hàm hoặc cấu trúc dữ liệu và thường xử lý với hành vi được truyền từ bên ngoài (ví dụ: functor, ctor của tham số mẫu, v.v.).

Constructors

Có rất ít bạn có thể nói về mọi hàm tạo cho mỗi lớp có thể được viết bằng C ++, nhưng có một vài điều. Đứng đầu trong số đó là các đối tượng được xây dựng (tức là đối với công trình xây dựng đã thành công khi trở về) sẽ bị hủy. Bạn không thể sửa đổi postcondition này bởi vì ngôn ngữ giả định nó là đúng, và sẽ tự động gọi destructors.  (Về mặt kỹ thuật, bạn có thể chấp nhận khả năng hành vi không xác định mà ngôn ngữ tạo ra Không đảm bảo về bất cứ điều gì, nhưng đó có lẽ là tốt hơn được bảo hiểm ở nơi khác.)

Cách thay thế duy nhất để ném một ngoại lệ khi một constructor không thể thành công là sửa đổi định nghĩa cơ bản của class ("class invariant") để cho phép các trạng thái "null" hoặc zombie hợp lệ và do đó cho phép constructor "thành công" bằng cách xây dựng một zombie .

Ví dụ về Zombie

Một ví dụ về sửa đổi zombie này là std :: ifstreamvà bạn phải luôn kiểm tra trạng thái của nó trước khi có thể sử dụng. Bởi vì std :: string, ví dụ, không, bạn luôn được đảm bảo rằng bạn có thể sử dụng nó ngay lập tức sau khi xây dựng. Hãy tưởng tượng nếu bạn phải viết mã như ví dụ này và nếu bạn quên kiểm tra trạng thái zombie, bạn sẽ âm thầm nhận được kết quả không chính xác hoặc làm hỏng các phần khác của chương trình:

string s = "abc";
if (s.memory_allocation_succeeded()) {
  do_something_with(s); // etc.
}

Thậm chí đặt tên cho phương thức đó là một ví dụ tốt về cách bạn phải sửa đổi bất biến và giao diện của lớp cho một tình huống chuỗi không thể dự đoán hay tự xử lý.

Ví dụ về xác thực đầu vào

Hãy giải quyết một ví dụ phổ biến: xác thực người dùng nhập vào. Chỉ vì chúng tôi muốn cho phép đầu vào không thành công không có nghĩa là chức năng phân tích cú pháp cần bao gồm điều đó trong hậu kỳ của nó. Điều đó có nghĩa là trình xử lý của chúng tôi cần kiểm tra xem trình phân tích cú pháp có bị lỗi hay không.

// boost::lexical_cast<int>() is the parsing function here
void show_square() {
  using namespace std;
  assert(cin); // precondition for show_square()
  cout << "Enter a number: ";
  string line;
  if (!getline(cin, line)) { // EOF on cin
    // error handling omitted, that EOF will not be reached is considered
    // part of the precondition for this function for the sake of example
    //
    // note: the below Python version throws an EOFError from raw_input
    //  in this case, and handling this situation is the only difference
    //  between the two
  }
  int n;
  try {
    n = boost::lexical_cast<int>(line);
    // lexical_cast returns an int
    // if line == "abc", it obviously cannot meet that postcondition
  }
  catch (boost::bad_lexical_cast&) {
    cout << "I can't do that, Dave.\n";
    return;
  }
  cout << n * n << '\n';
}

Thật không may, điều này cho thấy hai ví dụ về cách C ++ 's phạm vi đòi hỏi bạn phải phá vỡ RAII / SBRM. Một ví dụ trong Python mà không có vấn đề đó và cho thấy một cái gì đó tôi muốn C + + đã có - try-else:

# int() is the parsing "function" here
def show_square():
  line = raw_input("Enter a number: ") # same precondition as above
  # however, here raw_input will throw an exception instead of us
  # using assert
  try:
    n = int(line)
  except ValueError:
    print "I can't do that, Dave."
  else:
    print n * n

Điều kiện tiên quyết

Điều kiện tiên quyết không hoàn toàn phải được kiểm tra - vi phạm một luôn luôn chỉ ra một lỗi logic, và họ là trách nhiệm của người gọi - nhưng nếu bạn kiểm tra chúng, sau đó ném một ngoại lệ là thích hợp. (Trong một số trường hợp, nó thích hợp hơn để trả lại rác hoặc làm hỏng chương trình, mặc dù những hành động đó có thể sai lầm khủng khiếp trong các ngữ cảnh khác. hành vi không xác định là một chủ đề khác.)

Đặc biệt, tương phản std :: logic_error và std :: runtime_error các nhánh của hệ thống phân cấp ngoại lệ stdlib. Trước đây thường được sử dụng cho các vi phạm điều kiện tiên quyết, trong khi sau này là phù hợp hơn cho vi phạm sau điều kiện.


60
2017-11-16 19:06



Câu trả lời rõ ràng, nhưng về cơ bản bạn chỉ nêu rõ quy tắc và không cung cấp lý do. Là hợp lý của bạn sau đó: Công ước và phong cách? - Catskul
+1. Đây là cách các ngoại lệ được thiết kế để sử dụng và sử dụng chúng theo cách này cải thiện khả năng đọc và sử dụng lại mã (IMHO). - Daniel Pryden
Catskul: thứ nhất là định nghĩa về hoàn cảnh đặc biệt, không phải lý do cũng như quy ước. Nếu bạn không thể đảm bảo postconditions bạn không thể trở lại. Không có ngoại lệ, bạn phải thực hiện điều ước vô cùng rộng để bao gồm tất cả các trạng thái lỗi, đến mức nó thực sự vô dụng. Có vẻ như tôi đã không trả lời câu hỏi bằng chữ của bạn về "tại sao họ lại hiếm" vì tôi không nhìn họ theo cách đó. Chúng là một công cụ cho phép tôi thắt chặt postconditions một cách hữu ích trong khi vẫn cho phép các lỗi xảy ra (.. trong những trường hợp ngoại lệ).
+1. Pre / Post condition là lời giải thích TỐT NHẤT mà tôi từng nghe. - KitsuneYMG
Rất nhiều người đang nói "nhưng điều đó chỉ nhún xuống theo quy ước / ngữ nghĩa." Vâng đúng rồi. Nhưng quy ước và ngữ nghĩa quan trọng bởi vì chúng có ý nghĩa sâu sắc đối với sự phức tạp, khả năng sử dụng và tính bảo trì. Sau khi tất cả, không phải là quyết định của việc chính thức xác định trước điều kiện và sau điều kiện cũng chỉ là một vấn đề của quy ước và ngữ nghĩa? Và việc sử dụng chúng có thể làm cho mã của bạn dễ sử dụng và bảo trì dễ dàng hơn. - Darryl


  1. Đắt 
     các cuộc gọi hạt nhân (hoặc các lời gọi API hệ thống khác) để quản lý các giao diện tín hiệu hạt nhân (hệ thống)
  2. Khó phân tích
     Nhiều vấn đề của goto tuyên bố áp dụng cho trường hợp ngoại lệ. Chúng nhảy qua một lượng lớn mã có thể thường xuyên trong nhiều tệp thông thường và nguồn. Điều này không phải lúc nào cũng rõ ràng khi đọc mã nguồn trung gian. (Nó là trong Java.)
  3. Không phải lúc nào cũng được dự đoán bằng mã trung gian
     Mã được nhảy qua có thể hoặc có thể không được viết với khả năng thoát khỏi ngoại lệ. Nếu ban đầu được viết, nó có thể không được duy trì với ý nghĩ đó. Hãy suy nghĩ: rò rỉ bộ nhớ, rò rỉ mô tả tập tin, rò rỉ ổ cắm, những người hiểu biết?
  4. Biến chứng bảo trì
    Khó duy trì mã nhảy xung quanh việc xử lý các ngoại lệ.

39
2017-11-17 03:18



1, lý do tốt nói chung. Nhưng lưu ý rằng chi phí của việc mở thư giãn và gọi hàm hủy (ít nhất) được trả bởi bất kỳ cơ chế xử lý lỗi tương đương nào về mặt chức năng, ví dụ: bằng cách kiểm tra và trả lại mã lỗi như trong C. - j_random_hacker
Tôi sẽ đánh dấu đây là câu trả lời mặc dù tôi đồng ý với j_random. Stack unwinding và tài nguyên de / phân bổ sẽ giống nhau trong mọi cơ chế chức năng tương đương. Nếu bạn đồng ý, khi bạn thấy điều này, chỉ cần cắt những người ra khỏi danh sách để làm cho câu trả lời tốt hơn một chút. - Catskul
Julien: bình luận của j_random chỉ ra rằng không có tiết kiệm so sánh: int f() { char* s = malloc(...); if (some_func() == error) { free(s); return error; } ... } Bạn phải trả tiền để thư giãn ngăn xếp cho dù bạn làm điều đó bằng tay hoặc thông qua ngoại lệ. Bạn không thể so sánh việc sử dụng ngoại lệ để không xử lý lỗi ở tất cả.
1. Đắt tiền: tối ưu hóa sớm là gốc rễ của mọi điều ác. 2. Khó phân tích: so với các lớp lồng nhau của mã trả về lỗi? Tôi trân trọng không đồng ý. 3. Không phải lúc nào cũng được dự đoán bằng mã trung gian: so với các lớp lồng nhau không phải lúc nào cũng xử lý và dịch lỗi một cách hợp lý? Người mới thất bại ở cả hai. 4. Các biến chứng bảo trì: làm thế nào? Các phụ thuộc được trung gian bởi các mã lỗi có dễ duy trì hơn không? ... nhưng điều này dường như là một trong những lĩnh vực mà các nhà phát triển của một trong hai trường gặp khó khăn trong việc chấp nhận lập luận của nhau. Như với bất cứ điều gì, thiết kế xử lý lỗi là một sự cân bằng. - Pontus Gagge
Tôi đồng ý rằng đây là một vấn đề phức tạp và khó trả lời chính xác với các tổng quát. Nhưng hãy nhớ rằng câu hỏi ban đầu được yêu cầu đơn giản tại sao những lời khuyên chống ngoại lệ đã được đưa ra, không phải cho một lời giải thích về thực hành tốt nhất tổng thể. - DigitalRoss


Ném một ngoại lệ, ở một mức độ nào đó, tương tự như một tuyên bố goto. Làm điều đó để kiểm soát dòng chảy, và bạn kết thúc với mã spaghetti không thể hiểu được. Thậm chí tệ hơn, trong một số trường hợp bạn thậm chí không biết chính xác bước nhảy tới đâu (tức là nếu bạn không bắt ngoại lệ trong ngữ cảnh đã cho). Điều này blatantly vi phạm nguyên tắc "ít ngạc nhiên" mà tăng cường bảo trì.


22
2017-11-16 18:52



Làm cách nào để sử dụng ngoại lệ cho bất kỳ mục đích nào khác ngoài kiểm soát luồng? Ngay cả trong một tình huống đặc biệt, chúng vẫn là một cơ chế kiểm soát dòng chảy, với những hậu quả cho sự rõ ràng của mã của bạn (được giả định ở đây: không thể hiểu được). Tôi cho rằng bạn có thể sử dụng ngoại lệ nhưng cấm catch, mặc dù trong trường hợp đó bạn gần như có thể cũng chỉ cần gọi std::terminate() bản thân bạn. Nói chung, lập luận này dường như tôi nói "không bao giờ sử dụng ngoại lệ", thay vì "chỉ sử dụng ngoại lệ hiếm khi". - Steve Jessop
return statement bên trong hàm sẽ đưa bạn ra khỏi hàm. Trường hợp ngoại lệ sẽ đưa bạn biết ai có bao nhiêu lớp - không có cách nào đơn giản để tìm hiểu. - Arkadiy
Steve: Tôi hiểu ý của bạn, nhưng đó không phải là ý tôi. Trường hợp ngoại lệ là ok cho các tình huống bất ngờ. Một số người lạm dụng chúng như "lợi nhuận ban đầu", thậm chí có thể là một loại tuyên bố chuyển đổi. - Erich Kitzmueller
Tôi nghĩ câu hỏi này giả định rằng "bất ngờ" không đủ giải thích. Tôi đồng ý với một mức độ nào đó. Nếu một số điều kiện dừng một chức năng hoàn thành như mong muốn, thì chương trình của bạn sẽ xử lý tình huống đó một cách chính xác hoặc không. Nếu nó xử lý nó, sau đó nó "mong đợi", và mã cần phải được hiểu. Nếu nó không xử lý nó một cách chính xác, bạn đang gặp rắc rối. Trừ khi bạn cho phép ngoại lệ chấm dứt chương trình của bạn, một số cấp mã của bạn phải "mong đợi" nó. Và trong thực tế, như tôi nói trong câu trả lời của tôi, đó là một quy tắc tốt mặc dù về mặt lý thuyết không đạt yêu cầu. - Steve Jessop
Ngoài ra, trình bao bọc khóa học có thể chuyển đổi hàm ném thành một hàm trả về lỗi hoặc ngược lại. Vì vậy, nếu người gọi của bạn không đồng ý với ý tưởng của bạn "bất ngờ", nó không nhất thiết phải phá vỡ khả năng hiểu mã của họ. Nó có thể hy sinh một số hiệu suất mà họ đang kích động rất nhiều điều kiện mà bạn nghĩ là "bất ngờ", nhưng họ nghĩ là bình thường và có thể phục hồi, và do đó bắt và chuyển đổi thành một errno hoặc bất cứ điều gì. - Steve Jessop


Các trường hợp ngoại lệ khiến cho việc lập luận về trạng thái chương trình của bạn khó hơn. Ví dụ, trong C ++, bạn phải làm suy nghĩ thêm để đảm bảo các chức năng của bạn là ngoại lệ mạnh mẽ an toàn, hơn bạn sẽ phải làm nếu chúng không cần.

Lý do là không có ngoại lệ, một cuộc gọi hàm có thể trả về hoặc nó có thể chấm dứt chương trình trước. Với các ngoại lệ, một cuộc gọi hàm có thể trả về hoặc nó có thể kết thúc chương trình, hoặc nó có thể nhảy đến một khối catch ở đâu đó. Vì vậy, bạn không còn có thể theo dõi luồng kiểm soát chỉ bằng cách xem mã trước mặt bạn. Bạn cần phải biết nếu các chức năng gọi là có thể ném. Bạn có thể cần phải biết những gì có thể được ném và nơi nó bị bắt, tùy thuộc vào việc bạn quan tâm nơi kiểm soát đi, hoặc chỉ quan tâm rằng nó rời khỏi phạm vi hiện tại.

Vì lý do này, mọi người nói "không sử dụng ngoại lệ trừ khi tình hình thực sự đặc biệt". Khi bạn nhận được nó, "thực sự đặc biệt" có nghĩa là "một số tình huống đã xảy ra, nơi lợi ích của việc xử lý nó với một giá trị trả về lỗi là lớn hơn bởi các chi phí". Vì vậy, có, đây là một cái gì đó của một tuyên bố trống rỗng, mặc dù một khi bạn có một số bản năng cho "thực sự đặc biệt", nó sẽ trở thành một nguyên tắc tốt của ngón tay cái. Khi mọi người nói về kiểm soát dòng chảy, họ có nghĩa là khả năng lý luận cục bộ (không tham chiếu đến các khối catch) là một lợi ích của các giá trị trả về.

Java có định nghĩa rộng hơn về "thực sự đặc biệt" so với C ++. Lập trình C ++ có nhiều khả năng muốn xem xét giá trị trả về của một hàm hơn là các lập trình viên Java, vì vậy trong Java "thực sự đặc biệt" có thể có nghĩa là "Tôi không thể trả về một đối tượng không null là kết quả của hàm này". Trong C ++, nó có nhiều khả năng có nghĩa là "Tôi rất nghi ngờ người gọi của tôi có thể tiếp tục". Vì vậy, một dòng Java ném nếu nó không thể đọc một tập tin, trong khi một dòng C ++ (theo mặc định) trả về một giá trị cho biết lỗi. Tuy nhiên, trong mọi trường hợp, vấn đề mã bạn sẵn sàng ép buộc người gọi của bạn phải viết là gì. Vì vậy, nó thực sự là một vấn đề của phong cách mã hóa: bạn phải đạt được một sự đồng thuận mã của bạn sẽ như thế nào, và bao nhiêu "kiểm tra lỗi" mã bạn muốn viết chống lại bao nhiêu "ngoại lệ an toàn" lý do bạn muốn làm.

Sự đồng thuận rộng rãi trên tất cả các ngôn ngữ dường như được thực hiện tốt nhất về mức độ có thể phục hồi của lỗi (vì các lỗi không thể phục hồi dẫn đến không có mã nào có ngoại lệ, nhưng vẫn cần phải tự kiểm tra và trả lại lỗi trong mã sử dụng trả về lỗi). Vì vậy, mọi người đến mong đợi "chức năng này tôi gọi ném một ngoại lệ" để có nghĩa là "tôi không thể tiếp tục ", không chỉ" Không phải là vốn có trong trường hợp ngoại lệ, nó chỉ là một tùy chỉnh, nhưng cũng giống như bất kỳ thực hành lập trình tốt nào, đó là một người ủng hộ thông minh bởi những người thông minh đã thử nó theo cách khác và không được hưởng kết quả. Vì vậy, cá nhân, tôi nghĩ về "thực sự đặc biệt", trừ khi một cái gì đó về tình hình làm cho một ngoại lệ đặc biệt hấp dẫn.

Btw, hoàn toàn ngoài lý do về trạng thái mã của bạn, cũng có những hàm ý hiệu suất. Các trường hợp ngoại lệ thường rẻ ở các ngôn ngữ mà bạn có quyền quan tâm đến hiệu suất. Chúng có thể nhanh hơn nhiều cấp độ của "oh, kết quả là một lỗi, tốt nhất tôi nên thoát khỏi bản thân mình với một lỗi quá, sau đó". Trong những ngày xưa tồi tệ, có những nỗi sợ thực sự ném một ngoại lệ, bắt nó, và tiếp tục với điều tiếp theo, sẽ làm cho những gì bạn đang làm chậm đến mức vô dụng. Vì vậy, trong trường hợp đó, "thực sự đặc biệt" có nghĩa là, "tình hình là xấu như vậy mà hiệu suất khủng khiếp không còn quan trọng". Đó không còn là trường hợp (mặc dù một ngoại lệ trong một vòng lặp chặt chẽ vẫn còn đáng chú ý) và hy vọng cho thấy lý do tại sao định nghĩa "thực sự đặc biệt" cần phải linh hoạt.


16
2017-11-16 19:28



"Trong C ++ chẳng hạn, bạn phải làm suy nghĩ thêm để đảm bảo các chức năng của bạn là ngoại lệ mạnh mẽ an toàn, hơn bạn sẽ phải làm gì nếu chúng không cần." - cho rằng các cấu trúc C ++ cơ bản (chẳng hạn như new) và thư viện chuẩn tất cả ném ngoại lệ, tôi không thấy cách không sử dụng ngoại lệ trong mã của bạn sẽ giúp bạn viết mã ngoại lệ an toàn bất kể. - Pavel Minaev
Bạn sẽ phải hỏi Google. Nhưng có, điểm, nếu chức năng của bạn không phải là không có, sau đó người gọi của bạn sẽ phải làm một số công việc, và thêm điều kiện thêm gây ra ngoại lệ không làm cho nó "thêm ngoại lệ ném". Nhưng ngoài bộ nhớ là một chút của một trường hợp đặc biệt anyway. Nó phổ biến để có các chương trình mà không xử lý nó, và chỉ cần tắt để thay thế. Bạn có thể có một tiêu chuẩn mã hóa mà nói, "không sử dụng ngoại lệ, không đảm bảo ngoại lệ, nếu mới ném thì chúng tôi sẽ chấm dứt, và chúng tôi sẽ sử dụng trình biên dịch mà không thư giãn ngăn xếp". Không phải là khái niệm cao, nhưng nó sẽ bay. - Steve Jessop
Một khi bạn sử dụng một trình biên dịch mà không thư giãn ngăn xếp (hoặc nếu không vô hiệu hóa ngoại lệ trong nó), nó là, về mặt kỹ thuật nói, không còn C ++ :) - Pavel Minaev
Không thư giãn các ngăn xếp trên một ngoại lệ uncaught, tôi có nghĩa là. - Steve Jessop
Làm thế nào để nó biết nó vô ích trừ khi nó bung ra ngăn xếp để tìm một khối catch? - jmucchiello


Có thực sự là không có sự đồng thuận. Toàn bộ vấn đề là hơi chủ quan, bởi vì "sự phù hợp" của việc ném một ngoại lệ thường được đề xuất bởi các thực hành hiện có trong thư viện chuẩn của ngôn ngữ. Thư viện chuẩn C ++ ném ngoại lệ ít hơn nhiều so với nói, thư viện chuẩn Java, mà gần như luôn luôn thích ngoại lệ, ngay cả đối với các lỗi dự kiến ​​như đầu vào của người dùng không hợp lệ (ví dụ: Scanner.nextInt). Điều này, tôi tin rằng, ảnh hưởng đáng kể đến ý kiến ​​của nhà phát triển về thời điểm thích hợp để ném một ngoại lệ.

Là một lập trình viên C ++, cá nhân tôi thích dự trữ ngoại lệ cho các trường hợp rất "đặc biệt", ví dụ: ra khỏi bộ nhớ, ra khỏi không gian đĩa, sự khải huyền đã xảy ra, vv Nhưng tôi không nhấn mạnh rằng đây là cách chính xác tuyệt đối để làm việc.


11
2017-11-16 18:56



Tôi nghĩ rằng có một sự đồng thuận về các loại, nhưng có lẽ sự đồng thuận dựa trên quy ước hơn là lý luận âm thanh. Có thể có lý do âm thanh chỉ sử dụng ngoại lệ cho các trường hợp ngoại lệ, nhưng hầu hết các nhà phát triển không thực sự nhận thức được lý do. - Qwertie
Đồng ý - bạn nghe nói về "trường hợp ngoại lệ dành cho những trường hợp ngoại lệ" thường xuyên, nhưng không ai bận tâm để xác định chính xác "tình huống đặc biệt" là gì - phần lớn chỉ là tùy chỉnh và chắc chắn là ngôn ngữ cụ thể. Heck, trong Python, các trình vòng lặp sử dụng ngoại lệ để báo hiệu kết thúc chuỗi, và nó được coi là hoàn toàn bình thường! - Pavel Minaev
+1. Không có quy tắc cứng nhắc và nhanh chóng, chỉ là các quy ước - nhưng nó rất hữu ích để tuân thủ các quy ước của những người khác sử dụng ngôn ngữ của bạn, vì nó giúp các lập trình viên dễ hiểu mã của nhau hơn. - j_random_hacker
"sự khải huyền đã xảy ra" - không bao giờ nhớ ngoại lệ, nếu bất cứ điều gì biện minh cho U.B, điều đó ;-) - Steve Jessop
Catskul: Hầu như tất cả các chương trình đều là quy ước. Về mặt kỹ thuật, chúng tôi thậm chí không cần ngoại lệ, hoặc thực sự nhiều. Nếu nó không liên quan đến NP-đầy đủ, big-O / theta / little-o, hoặc Universal Turing Machines, nó có thể là quy ước. :-) - Ken


Tôi không nghĩ, ngoại lệ đó hiếm khi được sử dụng. Nhưng.

Không phải tất cả các nhóm và dự án đều sẵn sàng sử dụng ngoại lệ. Việc sử dụng các ngoại lệ đòi hỏi trình độ cao của các lập trình viên, kỹ thuật đặc biệt và thiếu mã kế thừa không an toàn lớn. Nếu bạn có codebase cũ rất lớn, sau đó nó hầu như luôn luôn không phải là ngoại lệ an toàn. Tôi chắc chắn rằng bạn không muốn viết lại nó.

Nếu bạn sẽ sử dụng ngoại lệ rộng rãi, thì:

  • được chuẩn bị để dạy cho mọi người biết về sự an toàn ngoại lệ là gì
  • bạn không nên sử dụng quản lý bộ nhớ thô
  • sử dụng RAII rộng rãi

Mặt khác, việc sử dụng ngoại lệ trong các dự án mới với nhóm mạnh có thể làm cho mã sạch hơn, dễ bảo trì hơn và thậm chí nhanh hơn:

  • bạn sẽ không bỏ lỡ hoặc bỏ qua lỗi
  • bạn không phải viết kiểm tra mã trả lại đó, mà không thực sự biết phải làm gì với mã sai ở mức thấp
  • khi bạn buộc phải viết mã an toàn ngoại lệ, nó trở nên có cấu trúc hơn

7
2017-11-16 20:36



1, một số lý do thực tế tốt để tránh sử dụng ngoại lệ. - j_random_hacker
Tôi đặc biệt thích đề cập đến thực tế là "không phải tất cả các đội ... đều sẵn sàng sử dụng ngoại lệ". Ngoại lệ chắc chắn là điều gì đó hình như dễ làm, nhưng cực kỳ khó làm đúng, và đó là một phần của những gì làm cho chúng nguy hiểm. - j_random_hacker
1 cho "khi bạn buộc phải viết mã ngoại lệ an toàn, nó trở nên có cấu trúc hơn". Mã dựa trên ngoại lệ là buộc để có nhiều cấu trúc hơn, và tôi thấy nó thực sự dễ dàng hơn để lý luận về các đối tượng và bất biến khi nó không thể bỏ qua chúng. Trong thực tế, tôi tin rằng sự an toàn ngoại lệ mạnh mẽ là tất cả về viết mã gần như có thể đảo ngược, điều này khiến cho việc tránh tình trạng không xác định thật dễ dàng. - Tom


CHỈNH SỬA 11/20/2009:

Tôi vừa mới đọc bài viết MSDN này về cải thiện hiệu suất mã được quản lý và phần này nhắc tôi về câu hỏi này:

Chi phí hoạt động của việc ném một ngoại lệ là đáng kể. Mặc dù xử lý ngoại lệ có cấu trúc là cách được khuyến nghị để xử lý các điều kiện lỗi, hãy đảm bảo bạn chỉ sử dụng ngoại lệ trong các trường hợp ngoại lệ khi xảy ra các điều kiện lỗi. Không sử dụng ngoại lệ cho luồng điều khiển thông thường.

Tất nhiên, điều này chỉ dành cho .NET, và nó cũng hướng đến những ứng dụng phát triển hiệu năng cao (như bản thân tôi); vì vậy nó rõ ràng không phải là một sự thật phổ quát. Tuy nhiên, có rất nhiều chúng tôi. NET phát triển ra khỏi đó, vì vậy tôi cảm thấy nó đáng chú ý.

CHỈNH SỬA:

OK, trước hết, hãy nói thẳng một chút: Tôi không có ý định chọn một cuộc chiến với bất kỳ ai trong câu hỏi về hiệu suất. Nói chung, trên thực tế, tôi có khuynh hướng đồng ý với những người tin rằng tối ưu hóa sớm là một tội lỗi. Tuy nhiên, hãy để tôi chỉ thực hiện hai điểm:

  1. Các poster là yêu cầu cho một lý do khách quan đằng sau sự khôn ngoan thông thường mà ngoại lệ nên được sử dụng một cách tiết kiệm. Chúng ta có thể thảo luận về khả năng đọc và thiết kế phù hợp tất cả những gì chúng ta muốn; nhưng đây là những vấn đề chủ quan với những người sẵn sàng tranh luận ở hai bên. Tôi nghĩ rằng các poster là nhận thức được điều này. Thực tế là việc sử dụng các ngoại lệ để kiểm soát luồng chương trình thường là một cách làm không hiệu quả. Không, không phải luôn luôn, nhưng thường xuyên. Đây là lý do tại sao nó là lời khuyên hợp lý để sử dụng ngoại lệ một cách tiết kiệm, giống như lời khuyên tốt để ăn thịt đỏ hoặc uống rượu một cách tiết kiệm.

  2. Có sự khác biệt giữa tối ưu hóa không có lý do chính đáng và viết mã hiệu quả. Hệ quả của điều này là có một sự khác biệt giữa việc viết một cái gì đó mạnh mẽ, nếu không được tối ưu hóa, và cái gì đó chỉ đơn giản là không hiệu quả. Đôi khi tôi nghĩ rằng khi mọi người tranh luận về những thứ như xử lý ngoại lệ, họ thực sự chỉ nói chuyện với nhau, bởi vì họ đang thảo luận về những điều cơ bản khác nhau.

Để minh họa quan điểm của tôi, hãy xem xét các ví dụ mã C # sau đây.

Ví dụ 1: Phát hiện đầu vào người dùng không hợp lệ

Đây là một ví dụ về những gì tôi muốn gọi là ngoại lệ sự lạm dụng.

int value = -1;
string input = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        value = int.Parse(input);
        inputChecksOut = true;

    } catch (FormatException) {
        input = GetInput();
    }
}

Mã này, với tôi, vô lý. Tất nhiên là nó công trinh. Không ai tranh luận với điều đó. Nhưng nó Nên giống như:

int value = -1;
string input = GetInput();

while (!int.TryParse(input, out value)) {
    input = GetInput();
}

Ví dụ 2: Kiểm tra sự tồn tại của một tệp

Tôi nghĩ kịch bản này thực sự rất phổ biến. Nó chắc chắn có vẻ nhiều hơn "chấp nhận được" cho rất nhiều người, vì nó đề cập đến tập tin I / O:

string text = null;
string path = GetInput();
bool inputChecksOut = false;

while (!inputChecksOut) {
    try {
        using (FileStream fs = new FileStream(path, FileMode.Open)) {
            using (StreamReader sr = new StreamReader(fs)) {
                text = sr.ReadToEnd();
            }
        }

        inputChecksOut = true;

    } catch (FileNotFoundException) {
        path = GetInput();
    }
}

Điều này có vẻ hợp lý, phải không? Chúng tôi đang cố mở một tệp; nếu nó không có ở đó, chúng ta bắt được ngoại lệ đó và cố mở một tập tin khác ... Có chuyện gì với điều đó?

Không có gì thực sự. Nhưng hãy xem xét giải pháp thay thế này, không ném bất kỳ ngoại lệ nào:

string text = null;
string path = GetInput();

while (!File.Exists(path)) path = GetInput();

using (FileStream fs = new FileStream(path, FileMode.Open)) {
    using (StreamReader sr = new StreamReader(fs)) {
        text = sr.ReadToEnd();
    }
}

Tất nhiên, nếu hiệu suất của hai phương pháp này thực sự giống nhau, điều này thực sự sẽ hoàn toàn là một vấn đề về giáo lý. Vì vậy, chúng ta hãy xem. Đối với ví dụ mã đầu tiên, tôi đã tạo danh sách 10000 chuỗi ngẫu nhiên, không có chuỗi nào trong số đó đại diện cho một số nguyên thích hợp và sau đó thêm chuỗi số nguyên hợp lệ vào cuối. Sử dụng cả hai phương pháp trên, đây là kết quả của tôi:

Sử dụng try/catch block: 25.455 giây
Sử dụng int.TryParse: 1.637 mili giây

Đối với ví dụ thứ hai, tôi đã làm điều tương tự về cơ bản: tạo danh sách 10000 chuỗi ngẫu nhiên, không có chuỗi nào trong số đó là một đường dẫn hợp lệ, sau đó thêm đường dẫn hợp lệ vào cuối. Đây là kết quả:

Sử dụng try/catch block: 29.989 giây
Sử dụng File.Exists: 22,820 mili giây

Rất nhiều người sẽ phản ứng với điều này bằng cách nói, "Vâng, vâng, vứt và bắt được 10.000 ngoại lệ là vô cùng phi thực tế; điều này làm tăng thêm kết quả." Tất nhiên nó. Sự khác biệt giữa việc ném một ngoại lệ và xử lý đầu vào xấu của riêng bạn sẽ không đáng chú ý đối với người dùng. Thực tế vẫn là sử dụng ngoại lệ, trong hai trường hợp này, từ 1.000 đến hơn 10.000 lần chậm hơn hơn các phương pháp thay thế có thể đọc được - nếu không nhiều hơn.

Đó là lý do tại sao tôi đưa vào ví dụ về GetNine() phương pháp dưới đây. Nó không phải là nó không thể chấp nhận chậm hoặc không thể chấp nhận chậm; nó chậm hơn nó Nên ... Vì không có lý do chính đáng.

Một lần nữa, đây chỉ là hai ví dụ. Của khóa học sẽ có những lúc hiệu quả khi sử dụng ngoại lệ không phải là nghiêm trọng (quyền của Pavel; sau tất cả, nó phụ thuộc vào việc thực hiện). Tất cả những gì tôi nói là: hãy đối mặt với sự thật, các bạn - trong những trường hợp như trên, việc ném và bắt một ngoại lệ tương tự như GetNine(); nó chỉ là một cách không hiệu quả để làm điều gì đó có thể dễ dàng thực hiện tốt hơn.


Bạn đang yêu cầu một lý do như thể đây là một trong những tình huống mà mọi người đã nhảy vào một bandwagon mà không biết tại sao. Nhưng trên thực tế câu trả lời là hiển nhiên, và tôi nghĩ bạn đã biết rồi. Xử lý ngoại lệ có hiệu suất khủng khiếp.

OK, có thể nó tốt cho kịch bản kinh doanh đặc biệt của bạn, nhưng nói tương đối, ném / bắt một ngoại lệ giới thiệu cách chi phí cao hơn là cần thiết trong nhiều, nhiều trường hợp. Bạn biết điều đó, tôi biết điều đó: hầu hết thời gian, nếu bạn đang sử dụng ngoại lệ để kiểm soát luồng chương trình, bạn chỉ cần viết mã chậm.

Bạn cũng có thể hỏi: tại sao mã này xấu?

private int GetNine() {
    for (int i = 0; i < 10; i++) {
        if (i == 9) return i;
    }
}

Tôi sẽ đặt cược rằng nếu bạn lược tả chức năng này, bạn sẽ thấy nó hoạt động khá nhanh chóng chấp nhận được cho ứng dụng kinh doanh thông thường của bạn. Điều đó không thay đổi thực tế rằng đó là một cách khủng khiếp không hiệu quả để hoàn thành một cái gì đó mà có thể được thực hiện tốt hơn rất nhiều.

Đó là những gì mọi người có ý nghĩa khi họ nói về ngoại lệ "lạm dụng".


7
2017-11-16 19:57



"Xử lý ngoại lệ có hiệu suất khủng khiếp." - đó là một chi tiết thực hiện, và không giữ đúng cho tất cả các ngôn ngữ, và ngay cả đối với C ++ cụ thể, cho tất cả các triển khai. - Pavel Minaev
"đó là một chi tiết thực hiện" Tôi không thể nhớ thường xuyên như thế nào tôi nghe nói đối số này - và nó chỉ stinks. Hãy nhớ: Tất cả các công cụ máy tính là về tổng chi tiết triển khai. Và cũng có thể: Không nên sử dụng trình điều khiển vít thay thế cho một cái búa - đó là những gì mọi người làm, mà nói nhiều về "chi tiết thực hiện". - Juergen
Tuy nhiên, Python vui vẻ sử dụng các ngoại lệ cho kiểm soát luồng chung, bởi vì cú đánh hoàn hảo cho việc triển khai của họ không phải ở bất cứ nơi nào xấu. Vì vậy, nếu búa của bạn trông giống như một tuốc nơ vít, tốt, đổ lỗi cho búa ... - Pavel Minaev
@j_random_hacker: Bạn nói đúng, GetNine công việc của nó tốt. Quan điểm của tôi là không có lý do gì để sử dụng nó. Nó không phải là cách "nhanh chóng và dễ dàng" để hoàn thành công việc, trong khi cách tiếp cận "đúng" hơn sẽ tốn nhiều công sức hơn. Theo kinh nghiệm của tôi, thường là trường hợp phương pháp "đúng" thực sự ít hơn nỗ lực, hoặc về tương tự. Dù sao, với tôi hiệu suất là chỉ mục tiêu lý do tại sao sử dụng ngoại lệ trái và phải không được khuyến khích. Đối với việc mất hiệu suất là "đánh giá quá cao", đó là trong thực tế chủ quan. - Dan Tao
@j_random_hacker: Bên cạnh bạn là rất thú vị, và thực sự thúc đẩy điểm của Pavel về hiệu suất hit tùy thuộc vào việc thực hiện. Một điều tôi rất nghi ngờ là ai đó có thể tìm thấy một kịch bản trong đó sử dụng ngoại lệ tốt hơn sự thay thế (nơi mà một sự thay thế tồn tại, ít nhất, chẳng hạn như trong các ví dụ của tôi). - Dan Tao


Tất cả các quy tắc của ngón tay cái về trường hợp ngoại lệ đi xuống đến các điều khoản chủ quan. Bạn không nên mong đợi để có được định nghĩa cứng và nhanh chóng khi sử dụng chúng và khi nào thì không. "Chỉ trong những trường hợp đặc biệt". Định nghĩa tròn đẹp: ngoại lệ là trường hợp ngoại lệ.

Khi nào sử dụng ngoại lệ rơi vào cùng một nhóm với "làm cách nào để biết mã này là một hay hai lớp?" Đó là một phần vấn đề về phong cách, một phần là một sở thích. Ngoại lệ là một công cụ. Chúng có thể được sử dụng và lạm dụng, và việc tìm ra mối quan hệ giữa hai phần là một phần của nghệ thuật và kỹ năng lập trình.

Có rất nhiều ý kiến, và sự cân bằng sẽ được thực hiện. Tìm một cái gì đó mà nói với bạn, và làm theo nó.


6
2017-11-16 19:15



1, không khí trong lành hơn, cảm ơn bạn. - j_random_hacker