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


80

Tôi thường thấy / nghe mọi người nói rằng hiếm khi sử dụng ngoại lệ, nhưng không bao giờ giải thích tại sao. Mặc dù điều đó có thể đúng, nhưng lý do chính đáng thường là một câu nói lấp lửng: "nó được gọi là ngoại lệ vì một lý do" , theo tôi, dường như là kiểu giải thích không bao giờ đượ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 không khôn ngoan khi sử dụng chúng để kiểm soát luồng? Triết lý đằng sau việc đặc biệt bảo thủ với cách chúng được sử dụng là gì? Ngữ nghĩa học? Hiệu suất? Độ phức tạp? Tính thẩm mỹ? Quy ước?

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

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



32
Không phải là một bản sao. 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ự phân biệt giữa khi nào sử dụng ngoại lệ và khi nào sử dụng cơ chế báo cáo lỗi khác.
Adrian McCarthy

1
và làm thế nào về cái này stackoverflow.com/questions/1385172/… ?
stijn

1
Không có cơ sở lý luận đồ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 đưa ra các 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 bằng C ++, nhưng tôi nghi ngờ nếu bạn gắn thẻ nó với Java, bạn sẽ nhận được ý kiến ​​khác.
Charles Salvia,

5
Không, nó không nên là một wiki. Các câu trả lời ở đây yêu cầu chuyên môn và nên được thưởng bằng đại diện.
bobobobo

Câu trả lời:


92

Điểm chính của ma sát là ngữ nghĩa. Nhiều nhà phát triển lạm dụng các ngoại lệ và ném chúng vào mọi cơ hội. Ý tưởng là sử dụng ngoại lệ cho tình huống hơi đặc biệt. Ví dụ: người dùng nhập sai 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ỉ để "im lặng" chương trình và để nó chạy càng lâu càng tốt cho đến khi hoàn toàn sụp đổ. Điều này là rất sai lầm. Nếu bạn không xử lý các ngoại lệ, nếu bạn không phản ứng một cách 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 ghi lại sự xuất hiện ngoại lệ hoặc ít nhất là 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. Các ngoại lệ hiếm khi được sử dụng vì các trường hợp ngoại lệ rất hiếm và các ngoại lệ rất đắt.

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

Đắt, vì việc ném một ngoại lệ sẽ làm gián đoạn dò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 trong suốt quá trình được chuyển đến đối tượng ngoại lệ mà trình xử lý sẽ nhận được. Tất cả đều có chi phí.

Điều này không có nghĩa là không thể có ngoại lệ để sử dụng ngoại lệ (nụ 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 các mã trả về qua nhiều lớp. Theo nguyên tắc đơn giản, nếu bạn mong đợi một phương pháp nào đó được gọi thường xuyên và phát hiện ra một số tình huống "ngoại lệ" trong nửa thời gian thì tốt hơn là bạn nên tìm một giải pháp khác. Tuy nhiên, nếu bạn mong đợi luồng hoạt động bình thường hầu hết thời gian trong khi tình huố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ì bạn chỉ cần 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ố trường hợp ít đặc biệt hơn 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 được mở nhưng tôi muốn nói rằng nó khá hiếm trong thực tế.

Tại sao không khôn ngoan khi sử dụng chúng để kiểm soát luồng?

Bởi vì các trường hợp ngoại lệ làm gián đoạn "luồng điều khiển" bình thường. Bạn đưa ra một ngoại lệ và quá trình thực thi bình thường của chương trình bị bỏ qua có khả năng khiến các đối tượng ở trạng thái không nhất quán và một số tài nguyên mở không được tự do. Chắc chắn, C # có câu lệnh using sẽ đảm bảo đối tượng sẽ được xử lý ngay cả khi một ngoại lệ được ném ra khỏi phần thân using. Nhưng chúng ta hãy trừu tượng hóa trong thời điểm này từ ngôn ngữ. Giả sử khung công tác sẽ không xử lý các đối tượng cho bạn. Bạn làm điều đó một cách thủ công. Bạn có một số hệ thống để yêu cầu và giải phóng tài nguyên và bộ nhớ. Bạn có thỏa thuận trê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 làm thế nào để đối phó với 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 hoạt động bình thường. Nhưng đột nhiên trong khi thực hiện, bạn ném ra một ngoại lệ. Một nửa số tài nguyên không được sử dụng. Một nửa vẫn chưa được yêu cầu. Nếu thao tác được dùng để giao dịch thì bây giờ 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ì những phần mã đó chịu trách nhiệm giải phóng tài nguyên đơn giản là 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ể thấy chúng ở trạng thái không nhất quán và cũng bị sập vì họ không thể đoán trước được tình huống cụ thể này.

Giả sử, bạn muốn một 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 cho một số tài nguyên sau đó trả lại nó cho M () sẽ sử dụng nó và sau đó loại bỏ nó. Khỏe. Bây giờ có điều gì đó không ổn trong N () và nó ném ra một ngoại lệ mà bạn không mong đợi trong M () vì vậy ngoại lệ bong bóng lên trên cùng cho đến khi nó có thể bị mắc vào một số phương thức C () mà sẽ không biết điều gì đang xảy ra ở sâu bên dưới trong N () và có hay không và cách giải phóng một số tài nguyên.

Với việc ném các ngoại lệ, bạn tạo ra một cách để đưa chương trình của mình vào nhiều trạng thái trung gian mới không thể đoán trước mà khó có thể tiên lượng, hiểu và xử lý. 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ể nhảy ngẫu nhiên việc thực thi của nó từ vị trí này sang vị trí khác. Nó cũng sẽ khó để duy trì và gỡ lỗi nó. Khi chương trình ngày càng trở nên phức tạp, bạn sẽ mất đi cái nhìn tổng quan về những gì đang xảy ra khi nào và ở đâu ít hơn để sửa chữa nó.


5
Nếu bạn so sánh một hàm ném ngoại lệ với một hàm trả về mã lỗi, thì việc mở ngăn xếp sẽ giống nhau trừ khi tôi thiếu thứ gì đó.
Catskul

9
@Catskul: không nhất thiết. Một hàm trả về trả lại quyền điều khiển trực tiếp cho người gọi ngay lập tức. Một ngoại lệ được ném trả lại quyền đ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

5
Cũng cần lưu ý rằng trình gỡ lỗi thường phá vỡ theo mặc định trong các trường hợp ngoại lệ. Trình gỡ lỗi sẽ bị hỏng nhiều nếu bạn đang sử dụng các ngoại lệ cho các điều kiện không bất thường.
Qwertie

5
@jon: Điều đó sẽ chỉ xảy ra nếu nó không được giải quyết và cần phải thoát khỏi nhiều phạm vi hơn để tiếp cận xử lý lỗi. Trong cùng trường hợp đó, sử dụng các giá trị trả về, (và cũng cần được chuyển xuống qua nhiều phạm vi) cùng một số lượng ngăn xếp sẽ xảy ra.
Catskul

3
- Xin lỗi. Thật vô nghĩa khi nói về những cơ sở ngoại lệ được "dành 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ớ, v.v. - nhưng điều đó không có nghĩa là chúng cũng không nên được sử dụng cho các mục đích khác. Những gì tôi muốn xem là lời giải thích tại sao chúng 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 có nói về điều đó một chút, nhưng nó xen lẫn với rất nhiều cuộc thảo luận về những trường hợp ngoại lệ "có nghĩa là" để làm gì.
j_random_hacker

61

Mặc dù "ném các ngoại lệ trong các trường hợp ngoại lệ" là câu trả lời hay, nhưng bạn thực sự có thể xác định các trường hợp đó là gì: khi các điều kiện trước được thỏa mãn, nhưng các điều kiện sau không thể được thỏa mãn . Điều này cho phép bạn viết các điều kiện hậu kỳ chặt chẽ hơn, chặt chẽ hơn và hữu ích hơn mà không phải hy sinh việc xử lý lỗi; nếu không, không có ngoại lệ, bạn phải thay đổi điều kiện sau để cho phép mọi trạng thái lỗi có thể xảy ra.

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

Người xây dựng

Bạn có thể nói rất ít 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 số điều. Chủ yếu trong số đó là các đối tượng được xây dựng (tức là mà hàm tạo đã thành công bằng cách quay lại) sẽ bị hủy. Bạn không thể sửa đổi điều kiện hậu này vì ngôn ngữ giả định điều đó là đúng và sẽ tự động gọi hàm hủy. (Về mặt kỹ thuật, bạn có thể chấp nhận khả năng xảy ra hành vi không xác định mà ngôn ngữ không đảm bảo về bất cứ điều gì, nhưng điều đó có lẽ nên được đề cập ở những nơi khác.)

Cách thay thế duy nhất để ném một ngoại lệ khi một hàm tạo không thể thành công là sửa đổi định nghĩa cơ bản của lớp ("bất biến của lớp") để cho phép các trạng thái "null" hoặc zombie hợp lệ và do đó cho phép hàm tạo "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 :: ifstream và bạn phải luôn kiểm tra trạng thái của nó trước khi có thể sử dụng. Vì std :: string chẳng hạn, nên bạn luôn được đảm bảo rằng bạn có thể sử dụng nó ngay sau khi xây dựng. Hãy tưởng tượng nếu bạn phải viết mã chẳng hạn như ví dụ này và nếu bạn quên kiểm tra trạng thái thây ma, 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.
}

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

Xác thực ví dụ đầu vào

Hãy giải quyết một ví dụ phổ biến: xác thực đầu vào của người dùng. 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à hàm phân tích cú pháp cần bao gồm điều đó trong điều kiện sau của nó. Tuy nhiên, nó có nghĩa là trình xử lý của chúng ta cần kiểm tra xem trình phân tích cú pháp có bị lỗi 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 phạm vi của C ++ yêu cầu bạn phá vỡ RAII / SBRM. Một ví dụ trong Python không gặp vấn đề đó và hiển thị điều gì đó mà tôi ước 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 nhất thiết phải được kiểm tra - vi phạm một điều kiện luôn chỉ ra lỗi logic và chúng là trách nhiệm của người gọi - nhưng nếu bạn kiểm tra chúng, thì việc đưa ra một ngoại lệ là phù hợp. (Trong một số trường hợp, trả về rác hoặc làm hỏng chương trình sẽ thích hợp hơn; mặc dù những hành động đó có thể sai khủng khiếp trong các ngữ cảnh khác. Làm thế nào để xử lý tốt nhất hành vi không xác định là một chủ đề khác.)

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


5
Câu trả lời khéo léo, nhưng về cơ bản bạn chỉ đang nêu một quy tắc và không đưa ra cơ sở lý luận. Vậy thì lý trí của bạn có phải là: Quy ước và phong cách?
Catskul

6
+1. Đây là cách các ngoại lệ được thiết kế để sử dụng và việc sử dụng chúng theo cách này thực sự cải thiện khả năng đọc và khả năng tái sử dụng của mã (IMHO).
Daniel Pryden

15
Catskul: thứ nhất là định nghĩa về các trường hợp ngoại lệ, không phải lý do hay quy ước. Nếu bạn không thể đảm bảo các điều kiện sau, bạn không thể trở lại . Không có ngoại lệ, bạn phải làm cho điều kiện sau cực kỳ 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 theo nghĩa đen của bạn về "tại sao chúng phải hiếm" bởi vì tôi không nhìn chúng theo cách đó. Chúng là một công cụ cho phép tôi thắt chặt các điều kiện hậu kỳ một cách hữu ích trong khi vẫn để xảy ra lỗi (.. trong các trường hợp ngoại lệ :).

9
+1. Điều kiện trước / sau là lời giải thích TỐT NHẤT mà tôi từng nghe.
KitsuneYMG

6
Nhiều người đang nói "nhưng điều đó chỉ đơn giản là quy ước / ngữ nghĩa." Vâng đúng rồi. Nhưng quy ước và ngữ nghĩa quan trọng vì chúng có ý nghĩa sâu sắc về độ phức tạp, khả năng sử dụng và khả năng bảo trì. Rốt cuộc, không phải quyết định có chính thức xác định điều kiện trước và điều kiện sau hay không cũng chỉ là vấn đề quy ước và ngữ nghĩa? Tuy nhiên, 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ì hơn đáng kể.
Darryl

40
  1. Lệnh
    gọi hạt nhân đắt tiền (hoặc lệnh gọi API hệ thống khác) để quản lý các giao diện tín hiệu của hạt nhân (hệ thống)
  2. Khó phân tích
    Nhiều vấn đề củagototuyên bố áp dụng cho các trường hợp ngoại lệ. Họ thường nhảy qua một lượng lớn mã tiềm năng trong nhiều quy trình và tệp 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ó bằng Java.)
  3. Không phải lúc nào mã trung gian cũng đoán trước được Mã
    bị nhảy qua có thể đã được viết hoặc không với khả năng xuất hiện ngoại lệ. Nếu ban đầu được viết như vậy, nó có thể không được duy trì với ý nghĩ đó. Hãy suy nghĩ: rò rỉ bộ nhớ, rò rỉ bộ mô tả tệp, rò rỉ ổ cắm, ai biết được?
  4. Các biến chứng bảo trì
    Khó hơn để duy trì mã nhảy xung quanh các ngoại lệ xử lý.

24
+1, lý do chính đáng nói chung. Nhưng lưu ý rằng chi phí của chồng tháo và gọi Destructor (ít nhất) được thanh toán bằng bất kỳ cơ chế lỗi xử lý chức năng tương đương, ví dụ như bằng cách kiểm tra cho và gửi 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. Giải nén ngăn xếp và gỡ bỏ / phân bổ tài nguyên 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 chúng ra khỏi danh sách để làm cho câu trả lời tốt hơn một chút.
Catskul

14
Julien: bình luận của j_random chỉ ra rằng không có khoản tiết kiệm nào so sánh được: int f() { char* s = malloc(...); if (some_func() == error) { free(s); return error; } ... }Bạn phải trả tiền để mở rộng ngăn xếp cho dù bạn làm thủ công hay thông qua các trường hợp ngoại lệ. Bạn không thể so sánh bằng cách sử dụng ngoại lệ với không xử lý lỗi nào cả .

14
1. Đắt tiền: tối ưu hóa quá sớm là gốc rễ của mọi điều xấu xa. 2. Khó phân tích: so với các lớp mã trả về lỗi lồng nhau? Tôi trân trọng không đồng ý. 3. Không phải lúc nào mã trung gian cũng đoán trước được: 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 hợp lý? Người mới không thành công ở cả hai. 4. Biến chứng bảo dưỡng: như thế nào? Các phần phụ thuộc được dàn xếp bằng mã lỗi có dễ duy trì hơn không? ... nhưng đâ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 phái khó chấp nhận các 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 Ghism

1
Tôi đồng ý rằng đây là một vấn đề phức tạp và khó có thể trả lời chính xác bằng những điều chung chung. Nhưng hãy nhớ rằng câu hỏi ban đầu chỉ đơn giản là hỏi tại sao lời khuyên chống ngoại lệ được đưa ra, không phải để giải thích về phương pháp hay nhất nói chung.
DigitalRoss

22

Việc ném một ngoại lệ, ở một mức độ nào đó, tương tự như một câu lệnh goto. Làm điều đó để kiểm soát dòng chảy, và bạn kết thúc bằng mã spaghetti khó hiểu. 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 vị trí của bước nhảy (tức là nếu bạn không bắt được ngoại lệ trong ngữ cảnh nhất định). Điều này vi phạm trắng trợn nguyên tắc "ít bất ngờ nhất" giúp tăng cường khả năng bảo trì.


5
Làm thế nào để có thể sử dụng một ngoại lệ cho bất kỳ mục đích nào khác ngoài điều khiển luồng? Ngay cả trong một tình huống ngoại lệ, chúng vẫn là một cơ chế kiểm soát luồng, với hậu quả là sự rõ ràng của mã của bạn (giả sử ở đây: không thể hiểu được). Tôi cho rằng bạn có thể sử dụng các ngoại lệ nhưng cấm catch, mặc dù trong trường hợp đó, bạn gần như cũng có thể gọi std::terminate()cho chính mình. Nhìn chung, lập luận này dường như đối với tôi để nói rằng "không bao giờ sử dụng ngoại lệ", thay vì "chỉ sử dụng ngoại lệ hiếm khi".
Steve Jessop

5
câu lệnh return bên trong hàm sẽ đưa bạn ra khỏi hàm. Các trường hợp ngoại lệ sẽ giúp bạn biết có bao nhiêu lớp - không có cách nào đơn giản để tìm ra.

2
Steve: Tôi hiểu quan điểm của bạn, nhưng đó không phải là ý của tôi. 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à "trở lại sớm", thậm chí có thể như một số loại tuyên bố chuyển đổi.
Erich Kitzmueller

2
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 ngăn một chức năng hoàn thành như mong muốn, thì chương trình của bạn có thể xử lý tình huống đó một cách chính xác hoặc không. Nếu nó xử lý nó, thì nó là "mong đợi" và mã cần phải dễ 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 để ngoại lệ chấm dứt chương trình của mình, 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 mình, đó là một quy tắc tốt mặc dù về mặt lý thuyết là không đạt yêu cầu.
Steve Jessop

2
Tất nhiên, các trình bao bọc có thể chuyển đổi một 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 "bất ngờ" của bạn, điều đó không nhất thiết phá vỡ khả năng hiểu mã của họ. Nó có thể hy sinh một số hiệu suất khi chúng gây ra nhiều điều kiện mà bạn nghĩ là "không mong muốn", nhưng chúng lại cho là bình thường và có thể phục hồi, do đó bắt và chuyển đổi thành lỗi hoặc bất cứ điều gì.
Steve Jessop

16

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

Lý do là không có ngoại lệ, một lệnh gọi hàm có thể trả về hoặc nó có thể kết thúc chương trình trước. Với các ngoại lệ, một lệnh 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 bắt ở đâu đó. Vì vậy, bạn không thể tuân theo quy trình kiểm soát chỉ bằng cách nhìn vào mã trước mặt bạn. Bạn cần biết liệu các hàm được gọi có thể ném hay không. Bạn có thể cần biết thứ gì có thể được ném ra và nơi bị bắt, tùy thuộc vào việc bạn quan tâm đến việc quyền kiểm soát đi đến đâu hay chỉ quan tâm đến việc nó rời khỏi phạm vi hiện tại.

Vì lý do này, người ta nói "không sử dụng ngoại lệ trừ khi tình huống thực sự đặc biệt". Khi bạn nhận ra nó, "thực sự đặc biệt" có nghĩa là "một số tình huống đã xảy ra trong đó lợi ích của việc xử lý nó với giá trị trả về lỗi lớn hơn chi phí". Vì vậy, có, đây là một điều 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 quy tắc ngón tay cái. Khi mọi người nói về điều khiển luồng, họ có nghĩa là khả năng suy luận cục bộ (không tham chiếu để bắt khối) là một lợi ích của giá trị trả về.

Java có một định nghĩa rộng hơn về "thực sự đặc biệt" hơn C ++. Các lập trình viên C ++ có nhiều khả năng muốn xem xét giá trị trả về của một hàm hơn 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 phải null do hàm này". Trong C ++, nhiều khả năng nó 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 luồng Java sẽ ném nếu nó không thể đọc một tệp, trong khi một luồng C ++ (theo mặc định) trả về một giá trị chỉ ra lỗi. Tuy nhiên, trong mọi trường hợp, vấn đề là bạn sẵn sàng bắt người gọi của mình phải viết mã nào. Vì vậy, nó thực sự là vấn đề của phong cách mã hóa: bạn phải đạt được sự đồng thuận về mã của bạn trông như thế nào và bạn muốn viết bao nhiêu mã "kiểm tra lỗi" so với "ngoại lệ-an toàn"

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

Btw, ngoài lý luận về trạng thái mã của bạn, còn có các hàm ý về hiệu suất. Các ngoại lệ hiện nay thường rẻ, bằng 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 độ "ồ, kết quả là một lỗi, tốt nhất tôi nên tự thoát ra ngoài với một lỗi, vậy thì". Ngày xưa, có những nỗi sợ hãi thực sự rằng việc ném một ngoại lệ, bắt nó và tiếp tục điều tiếp theo, sẽ khiến những gì bạn đang làm chậm đến mức trở nên vô ích. Vì vậy, trong trường hợp đó, "thực sự đặc biệt" có nghĩa là, "tình hình tồi tệ đến mức hiệu suất khủng khiếp không còn là vấn đề nữa". Điều đó không còn xảy ra nữa (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 chỉ ra lý do tại sao định nghĩa "thực sự đặc biệt" cần phải linh hoạt.


2
"Ví dụ, trong C ++, bạn phải suy nghĩ thêm để đảm bảo các hàm của bạn hoàn toàn an toàn ngoại lệ, so với việc bạn phải làm nếu chúng không cần thiết." - do các cấu trúc C ++ cơ bản (chẳng hạn như new) và thư viện tiêu chuẩn đều ném ra các ngoại lệ, tôi không hiểu việc không sử dụng ngoại lệ trong mã của bạn giúp bạn không phải viết mã an toàn ngoại lệ như thế nào.
Pavel Minaev

1
Bạn phải hỏi Google. Nhưng có, thực tế là, nếu hàm của bạn không tăng trưởng, thì người gọi của bạn sẽ phải thực hiện một số công việc và việc thêm các điều kiện bổ sung gây ra ngoại lệ không làm cho nó "ném ngoại lệ". Nhưng dù sao hết bộ nhớ cũng là một trường hợp đặc biệt. Thường có những chương trình không xử lý được nó và thay vào đó chỉ cần tắt. Bạn có thể có một tiêu chuẩn mã hóa nói rằng, "không sử dụng các ngoại lệ, không đưa ra các đả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 các trình biên dịch không giải phóng ngăn xếp". Không phải là khái niệm cao, nhưng nó sẽ bay.
Steve Jessop

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 trường hợp ngoại lệ khác vô hiệu hóa trong nó), nó là, nói về mặt kỹ thuật, không còn C ++ :)
Pavel Minaev

Ý tôi là không giải phóng ngăn xếp trên một ngoại lệ không cần thiết.
Steve Jessop

Làm thế nào nó biết nó không có giá trị trừ khi nó mở ngăn xếp để tìm một khối bắt?
jmucchiello

11

Thực sự là không có sự đồng thuận. Toàn bộ vấn đề có phần chủ quan, bởi vì "sự phù hợp" của việc đưa ra một ngoại lệ thường được đề xuất bởi các thực tiễn hiện có trong thư viện tiêu chuẩn của chính ngôn ngữ đó. Thư viện tiêu chuẩn C ++ ném ra các ngoại lệ ít thường xuyên hơn nhiều so với thư viện chuẩn Java, hầu như luôn ưu tiên các 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). Tôi tin rằng điều này ảnh hưởng đáng kể đến ý kiến ​​của nhà phát triển về thời điểm thích hợp để đưa ra một ngoại lệ.

Là một lập trình viên C ++, cá nhân tôi thích dành các ngoại lệ cho những trường hợp rất "đặc biệt", ví dụ như hết bộ nhớ, hết dung lượng đĩa, ngày tận thế đã xảy ra, v.v. Nhưng tôi không nhấn mạnh rằng đây là cách hoàn toàn chính xác để làm nhiều thứ.


2
Tôi nghĩ rằng có nhiều loại đồng thuận, nhưng có lẽ sự đồng thuận đó dựa nhiều hơn vào quy ước hơn là lý luận đúng đắn. Có thể có những lý do chính đáng để chỉ sử dụng ngoại lệ cho những 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

4
Đồng ý - bạn thường nghe về "ngoại lệ dành cho những trường hợp đặc biệt", nhưng không ai bận tâm để định nghĩa đúng "tình huống ngoại lệ" là gì - nó phần lớn chỉ là tùy chỉnh và chắc chắn là ngôn ngữ cụ thể. Rất tiếc, 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

2
+1. Không có quy tắc cứng và nhanh, chỉ là các quy ước - nhưng sẽ hữu ích khi 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 hiểu mã của nhau dễ dàng hơn.
j_random_hacker

1
"ngày tận thế xảy ra" - không bao giờ nhớ trường hợp ngoại lệ, nếu bất cứ điều gì biện minh cho UB, mà không ;-)
Steve Jessop

3
Catskul: Hầu hết mọi lập 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-completeness, big-O / theta / little-o hoặc Universal Turing Machines, thì có lẽ đó là quy ước. :-)
Ken

7

Tôi không nghĩ rằng 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 các ngoại lệ. Việc sử dụng các ngoại lệ yêu cầu trình độ cao của lập trình viên, kỹ thuật đặc biệt và thiếu mã an toàn không ngoại lệ kế thừa lớn. Nếu bạn có cơ sở mã cũ khổng lồ, thì hầu như nó luôn không phải là ngoại lệ an toàn. Tôi chắc rằng bạn không muốn viết lại nó.

Nếu bạn định sử dụng rộng rãi các ngoại lệ, thì:

  • chuẩn bị để dạy mọi người của bạn 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 các ngoại lệ trong các dự án mới với đội ngũ 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ỏ sót hoặc bỏ qua lỗi
  • bạn không được 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 ở cấp thấp
  • khi bạn buộc phải viết mã an toàn ngoại lệ, nó sẽ trở nên có cấu trúc hơn

1
Tôi đặc biệt thích việc bạn đề 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 các trường hợp ngoại lệ". Trường hợp ngoại lệ là chắc chắn cái gì đó dường như dễ dàng để làm, nhưng rất khó có thể làm đúng , và đó là một phần của những gì làm cho họ nguy hiểm.
j_random_hacker

1
+1 cho "khi bạn buộc phải viết mã an toàn ngoại lệ, mã đó sẽ trở nên có cấu trúc hơn". Mã dựa trên ngoại lệ buộc phải có nhiều cấu trúc hơn và tôi thấy thực sự dễ dàng hơn để lập luận về các đối tượng và các bất biến khi không thể bỏ qua chúng. Trên thực tế, tôi tin rằng an toàn ngoại lệ mạnh mẽ là tất cả về việc viết mã gần như có thể đảo ngược, điều này giúp bạn thực sự dễ dàng tránh trạng thái không xác định.
Tom

7

CHỈNH SỬA 20/11/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í thực hiện 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 rằng bạn chỉ sử dụng ngoại lệ trong các trường hợp ngoại lệ khi điều kiện lỗi xảy ra. Không sử dụng ngoại lệ cho luồng điều khiển thường xuyên.

Tất nhiên, điều này chỉ dành cho .NET và nó cũng hướng đặc biệt vào những người đang phát triển các ứng dụng hiệu suất cao (như 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 nhà phát triển .NET của chúng tôi ngoài kia, vì vậy tôi cảm thấy điều đó đáng chú ý.

CHỈNH SỬA :

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

  1. Người đăng đang yêu cầu một lý do khách quan đằng sau sự khôn ngoan thông thường rằng các 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 ở cả hai bên. Tôi nghĩ rằng người đăng 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 lời khuyên hợp lý là sử dụng các trường hợp ngoại lệ một cách tiết kiệm, giống như lời khuyên hữu ích là ăn thịt đỏ hoặc uống rượu vang 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ó sự khác biệt giữa việc viết một thứ gì đó mạnh mẽ, nếu không được tối ưu hóa và một thứ gì đó đơn giản là không hiệu quả. Đôi khi tôi nghĩ khi mọi người tranh luận về những thứ như xử lý ngoại lệ, họ thực sự chỉ đang 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 của 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à lạm dụng ngoại lệ .

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, đối với tôi, thật nực cười. Tất nhiên là nó hoạt động . Không ai tranh cãi với điều đó. Nhưng nó phải là một cái gì đó 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ẻ "dễ chấp nhận" hơn đối với nhiều người, vì nó xử lý I / O tệp:

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ố gắng mở một tệp; nếu nó không có ở đó, chúng tôi bắt ngoại lệ đó và cố gắng mở một tệp khác ... Điều đó có gì sai?

Không có gì thực sự. Nhưng hãy xem xét sự thay thế này, điều này không gây ra 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 cách tiếp cận này thực sự giống nhau, thì đây thực sự sẽ là một vấn đề hoàn toàn mang tính học thuyết. Vì vậy, chúng ta hãy xem xét. Đối với ví dụ mã đầu tiên, tôi đã tạo một danh sách gồm 10000 chuỗi ngẫu nhiên, không có chuỗi nào đại diện cho một số nguyên thích hợp, và sau đó thêm một 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/ catchkhối: 25,455 giây
Sử dụng int.TryParse: 1,637 mili giây

Đối với ví dụ thứ hai, về cơ bản tôi đã làm điều tương tự: tạo danh sách 10000 chuỗi ngẫu nhiên, không có chuỗi nào là đườ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/ catchkhối: 29,989 giây
Sử dụng File.Exists: 22,820 mili giây

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

Đó là lý do tại sao tôi bao gồm ví dụ của GetNine()phương pháp bên dưới. Nó không phải là nó intolerably chậm hoặc không thể chấp nhận chậm; nó chậm hơn mức đáng lẽ ... không vì lý do chính đáng .

Một lần nữa, đây chỉ là hai ví dụ. Tất nhiên sẽ có những lúc hiệu quả hoạt động của việc sử dụng các ngoại lệ không nghiêm trọng đến mức này (Pavel là đúng; xét cho cùng, nó phụ thuộc vào việc triển khai). Tất cả những gì tôi đang 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, ném và bắt một ngoại lệ cũng tương tự như vậy GetNine(); đó chỉ là một cách làm không hiệu quả để làm một điều gì đó có thể dễ dàng được thực hiện tốt hơn .


Bạn đang yêu cầu một cơ sở lý luận như thể đây là một trong những tình huống mà mọi người đã nhảy vào một nhóm 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 cũng biết rồi. Xử lý ngoại lệ có hiệu suất khủng khiếp.

Được rồi, có thể điều đó tốt cho tình huống kinh doanh đặc biệt của bạn, nhưng nói một cách tương đối , việc ném / bắt một ngoại lệ sẽ gây tốn kém hơn mức cần thiết trong 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ỉ đang 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 dám cá rằng nếu bạn mô tả chức năng này, bạn sẽ thấy nó hoạt động khá nhanh có thể chấp nhận được đối với ứ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 kém hiệu quả khủng khiếp để hoàn thành một việc mà có thể được thực hiện tốt hơn rất nhiều.

Đó là ý của mọi người khi họ nói về "lạm dụng" ngoại lệ.


2
"Xử lý ngoại lệ có hiệu suất khủng khiếp." - đó là một chi tiết triển khai và không đúng với tất cả các ngôn ngữ và ngay cả đối với C ++ đặc biệt, đối với tất cả các triển khai.
Pavel Minaev

"đó là một chi tiết triển khai" Tôi không thể nhớ mình đã nghe lập luận này bao lâu một lần - và nó chỉ bốc mùi. Hãy nhớ rằng: Tất cả nội dung Máy tính là tổng hợp các chi tiết triển khai. Và nữa: Sẽ không khôn ngoan nếu sử dụng tua vít thay thế một cái búa - đó là những gì mọi người làm, nói nhiều về "chi tiết thực hiện".
Juergen

2
Tuy nhiên, Python vui vẻ sử dụng các ngoại lệ để kiểm soát luồng chung, bởi vì hiệu quả hoàn thiện cho việc triển khai của chúng không ở đâu tệ như vậy. Vì vậy, nếu nhìn cái búa của bạn giống như một tuốc nơ vít, tốt, đổ lỗi cho búa ...
Pavel Minaev

1
@j_random_hacker: Bạn nói đúng, GetNinecông việc của nó rất tốt. Quan điểm của tôi là không có lý do gì để sử dụng nó. Nó không giống như cách "nhanh chóng và dễ dàng" để hoàn thành một việc gì đó, 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 thì cách tiếp cận "đúng" sẽ thực sự tốn ít nỗ lực hơn , hoặc tương tự. Dù sao, với tôi, hiệu suất là lý do khách quan duy nhất khiến việc sử dụng các ngoại lệ trái và phải không được khuyến khích. Đối với việc liệu tổn thất hiệu suất có được "đánh giá quá cao" hay không, đó là thực tế chủ quan.
Dan Tao

1
@j_random_hacker: Bên cạnh đó của bạn rất thú vị và thực sự thúc đẩy quan điểm của Pavel về hiệu suất đạt được tùy thuộc vào việc triển khai. Có một điều mà 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ệ thực sự tốt hơn phương án thay thế (ít nhất là trong đó một phương án tồn tại, chẳng hạn như trong các ví dụ của tôi).
Dan Tao

6

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

Khi nào sử dụng ngoại lệ rơi vào cùng một nhóm như "làm cách nào để biết mã này là một hay hai lớp?" Đó một phần là vấn đề về phong cách, một phần là 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 ranh giới giữa chúng 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 được đưa ra. Tìm một cái gì đó nói với bạn và làm theo nó.


6

Không phải là ngoại lệ hiếm khi được sử dụng. Chỉ là chúng chỉ nên ném trong những trường hợp ngoại lệ. Ví dụ: nếu người dùng nhập sai mật khẩu, điều đó không phải là ngoại lệ.

Lý do rất đơn giản: các ngoại lệ thoát đột ngột một hàm và truyền lên ngăn xếp thành một catchkhối. Quá trình này rất tốn kém về mặt tính toán: C ++ xây dựng hệ thống ngoại lệ của nó để có ít chi phí cho các cuộc gọi hàm "bình thường", vì vậy khi một ngoại lệ được đưa ra, nó phải làm rất nhiều việc để tìm nơi cần đến. Hơn nữa, vì mỗi dòng mã có thể có một ngoại lệ. Nếu chúng ta có một số hàm fthường làm tăng các ngoại lệ, thì bây giờ chúng ta phải quan tâm đến việc sử dụng các khối try/ của chúng ta catchxung quanh mỗi lần gọi của f. Đó là một giao diện / khớp nối triển khai khá tệ.


8
Về cơ bản, bạn đã lặp lại lý do "chúng được gọi là ngoại lệ vì một lý do". Tôi đang tìm mọi người biến điều này thành một thứ gì đó quan trọng hơn.
Catskul

4
"Hoàn cảnh ngoại lệ" nghĩa là gì? Trong thực tế, chương trình của tôi phải xử lý IOExceptions thực tế thường xuyên hơn so với mật khẩu người dùng xấu. Điều đó có nghĩa là bạn nghĩ rằng tôi nên đặt BadUserPassword thành một ngoại lệ, hay những kẻ stdlib không nên đặt IOException thành một Ngoại lệ? "Rất tốn kém về mặt tính toán" không thể là lý do thực tế, bởi vì tôi chưa bao giờ thấy một chương trình (và chắc chắn không phải của tôi) mà xử lý mật khẩu không chính xác, thông qua bất kỳ cơ chế kiểm soát nào, là một nút thắt hiệu suất.
Ken

5

Tôi đã đề cập đến vấn đề này trong một bài viết về các ngoại lệ C ++ .

Phần liên quan:

Hầu như luôn luôn, sử dụng các ngoại lệ để ảnh hưởng đến dòng chảy "bình thường" là một ý tưởng tồi. Như chúng ta đã thảo luận trong phần 3.1, các ngoại lệ tạo ra các đường dẫn mã vô hình. Các đường dẫn mã này được cho là có thể chấp nhận được nếu chúng chỉ được thực thi trong các tình huống xử lý lỗi. Tuy nhiên, nếu chúng ta sử dụng các ngoại lệ cho bất kỳ mục đích nào khác, việc thực thi mã "bình thường" của chúng ta được chia thành một phần hữu hình và vô hình và nó làm cho mã rất khó đọc, hiểu và mở rộng.


1
Bạn thấy đấy, tôi đến với C ++ từ C và tôi nghĩ rằng "các tình huống xử lý lỗi" là bình thường. Chúng có thể không được thực thi thường xuyên, nhưng tôi dành nhiều thời gian hơn để nghĩ về chúng hơn là về các đường dẫn mã không lỗi. Vì vậy, một lần nữa, tôi nói chung đồng ý với quan điểm này, nhưng nó không giúp chúng ta tiến gần hơn đến định nghĩa "bình thường" và cách chúng ta có thể suy luận từ định nghĩa "bình thường" rằng sử dụng ngoại lệ là một lựa chọn tồi.
Steve Jessop

Tôi cũng đã đến với C ++ từ C, nhưng ngay cả trong CI đã phân biệt giữa việc xử lý lỗi kết thúc luồng "bình thường". Nếu bạn gọi ie fopen (), có một nhánh xử lý trường hợp thành công và đó là "bình thường", và một nhánh xử lý các nguyên nhân có thể gây ra thất bại, và đó là xử lý lỗi.
Nemanja Trifunovic

2
Đúng, nhưng tôi không hiểu một cách bí ẩn nào khi lập luận về mã lỗi. Vì vậy, nếu "đường dẫn mã vô hình" rất khó đọc, hiểu và mở rộng cho mã "bình thường", thì tôi không hiểu tại sao việc áp dụng từ "lỗi" lại khiến chúng "được cho là có thể chấp nhận được". Theo kinh nghiệm của tôi, các tình huống lỗi luôn xảy ra và điều này khiến chúng trở nên "bình thường". Nếu bạn có thể tìm ra các ngoại lệ trong các tình huống lỗi, thì bạn có thể tìm ra chúng trong các tình huống không lỗi. Nếu bạn không thể, bạn không thể, và tất cả những gì bạn đã làm là khiến mã lỗi của bạn không thể vượt qua nhưng hãy bỏ qua thực tế đó vì nó "chỉ có lỗi".
Steve Jessop

1
Ví dụ: bạn có một chức năng cần phải hoàn thành một số chức năng, nhưng bạn sẽ thử toàn bộ các cách tiếp cận khác nhau và bạn không biết cách nào sẽ thành công và mỗi người trong số họ có thể thử một số cách tiếp cận phụ bổ sung, và cứ như vậy, cho đến khi một cái gì đó hoạt động. Sau đó, tôi cho rằng thất bại là "bình thường", và thành công là "ngoại lệ" mặc dù rõ ràng không phải là một lỗi. Việc ném một ngoại lệ vào thành công không khó hiểu hơn là ném một ngoại lệ cho một lỗi khủng khiếp trong hầu hết các chương trình - bạn đảm bảo rằng chỉ một bit mã cần biết phải làm gì tiếp theo và bạn sẽ nhảy thẳng tới đó.
Steve Jessop

1
@onebyone: Nhận xét thứ hai của bạn thực sự tạo ra một điểm tốt mà tôi chưa thấy ở nơi khác. Tôi nghĩ câu trả lời cho điều đó là (như bạn đã nói trong câu trả lời của chính mình) sử dụng các ngoại lệ cho các điều kiện lỗi đã được đưa vào "các thông lệ chuẩn" của nhiều ngôn ngữ, khiến nó trở thành một hướng dẫn hữu ích để tuân thủ ngay cả khi lý do ban đầu làm điều đó là sai lầm.
j_random_hacker

5

Cách tiếp cận của tôi để xử lý lỗi là có ba loại lỗi cơ bản:

  • Một tình huống kỳ lạ có thể được xử lý tại vị trí lỗi. Điều này có thể xảy ra nếu người dùng nhập một đầu vào không hợp lệ tại dấu nhắc dòng lệnh. Hành vi chính xác chỉ đơn giản là phàn nàn với người dùng và lặp lại trong trường hợp này. Một tình huống khác có thể là số chia cho không. Những tình huống này không thực sự là tình huống lỗi và thường là do đầu vào bị lỗi.
  • Một tình huống giống như loại trước, nhưng không thể xử lý được tại trang web lỗi. Ví dụ: nếu bạn có một hàm lấy tên tệp và phân tích cú pháp tệp bằng tên đó, nó có thể không mở được tệp. Trong trường hợp này, nó không thể giải quyết lỗi. Đây là lúc các ngoại lệ tỏa sáng. Thay vì sử dụng cách tiếp cận C (trả về giá trị không hợp lệ dưới dạng cờ và đặt một biến lỗi toàn cục để chỉ ra sự cố), mã có thể đưa ra một ngoại lệ thay thế. Mã gọi sau đó sẽ có thể xử lý ngoại lệ - ví dụ: để nhắc người dùng nhập tên tệp khác.
  • Một tình huống không nên xảy ra. Đây là khi một bất biến của lớp bị vi phạm hoặc một hàm nhận được một tham số không hợp lệ hoặc tương tự. Điều này cho thấy lỗi logic trong mã. Tùy thuộc vào mức độ hư hỏng, một ngoại lệ có thể phù hợp hoặc buộc chấm dứt ngay lập tức có thể được ưu tiên (như asserthiện nay). Nói chung, những tình huống này chỉ ra rằng một cái gì đó đã bị hỏng ở đâu đó trong mã và bạn thực sự không thể tin tưởng bất kỳ điều gì khác là đúng - có thể có hiện tượng hỏng bộ nhớ tràn lan. Tàu của bạn đang chìm, hãy xuống tàu.

Để diễn giải, các trường hợp ngoại lệ là khi bạn gặp vấn đề mà bạn có thể giải quyết, nhưng bạn không thể giải quyết ở nơi bạn nhận thấy nó. Các vấn đề bạn không thể giải quyết chỉ đơn giản là giết chương trình; vấn đề bạn có thể giải quyết ngay lập tức nên được giải quyết một cách đơn giản.


2
Bạn đang trả lời câu hỏi sai. Chúng tôi không muốn biết lý do tại sao chúng tôi nên (hoặc không nên) xem xét sử dụng các ngoại lệ để xử lý các tình huống lỗi - chúng tôi muốn biết tại sao chúng tôi nên (hoặc không nên) sử dụng chúng cho các tình huống không xử lý lỗi.
j_random_hacker

5

Tôi đọc một số câu trả lời ở đây. Tôi vẫn ngạc nhiên về những gì mà tất cả sự nhầm lẫn này là về. Tôi thực sự không đồng ý với tất cả các ngoại lệ này == mã spagetty. Ý tôi là với sự nhầm lẫn, rằng có những người không đánh giá cao việc xử lý ngoại lệ C ++. Tôi không chắc mình đã học cách xử lý ngoại lệ trong C ++ như thế nào - nhưng tôi đã hiểu ý nghĩa của nó trong vòng vài phút. Đây là khoảng năm 1996 và tôi đang sử dụng trình biên dịch borland C ++ cho OS / 2. Tôi không bao giờ có vấn đề để quyết định, khi nào sử dụng ngoại lệ. Tôi thường gói các hành động do-undo có thể thực hiện được vào các lớp C ++. Các hành động hoàn tác như vậy bao gồm:

  • tạo / phá hủy một xử lý hệ thống (đối với tệp, bản đồ bộ nhớ, tay cầm GUI WIN32, ổ cắm, v.v.)
  • cài đặt / bỏ thiết lập trình xử lý
  • phân bổ / phân bổ bộ nhớ
  • tuyên bố / phát hành mutex
  • tăng / giảm số lượng tham chiếu
  • hiển thị / ẩn cửa sổ

Hơn có các trình bao bọc chức năng. Để gói lời gọi hệ thống (không thuộc loại trước đây) vào C ++. Ví dụ: đọc / ghi từ / vào một tệp. Nếu một cái gì đó không thành công, một ngoại lệ sẽ được ném ra, trong đó có đầy đủ thông tin về lỗi.

Sau đó, có các ngoại lệ bắt / ném lại để bổ sung thêm thông tin cho một thất bại.

Nhìn chung, việc xử lý ngoại lệ C ++ dẫn đến mã sạch hơn. Số lượng mã giảm mạnh. Cuối cùng, người ta có thể sử dụng một phương thức khởi tạo để phân bổ các tài nguyên có thể thay đổi được và vẫn duy trì một môi trường không có tham nhũng sau một thất bại như vậy.

Người ta có thể xâu chuỗi các lớp như vậy thành các lớp phức tạp. Khi một hàm tạo của một đối tượng thành viên / cơ sở nào đó được thực thi, người ta có thể dựa vào đó mà tất cả các hàm tạo khác của cùng một đối tượng (được thực thi trước đó) được thực thi thành công.


3

Ngoại lệ là một phương pháp điều khiển luồng rất khác thường so với các cấu trúc truyền thống (vòng lặp, ifs, hàm, v.v.) Các cấu trúc luồng điều khiển thông thường (vòng lặp, ifs, lệnh gọi hàm, v.v.) có thể xử lý tất cả các tình huống thông thường. Nếu bạn thấy mình đạt đến một ngoại lệ cho một sự kiện thường xuyên, thì có lẽ bạn cần xem xét cách cấu trúc mã của mình.

Nhưng có một số loại lỗi không thể được xử lý dễ dàng với các cấu trúc bình thường. Các lỗi nghiêm trọng (như lỗi phân bổ tài nguyên) có thể được phát hiện ở mức độ thấp nhưng có lẽ không thể xử lý được ở đó, vì vậy câu lệnh if đơn giản là không đủ. Những loại lỗi này thường cần được xử lý ở cấp độ cao hơn nhiều (ví dụ: lưu tệp, ghi lại lỗi, thoát). Việc cố gắng thông báo lỗi như thế này thông qua các phương pháp truyền thống (như giá trị trả về) là một việc tẻ nhạt và dễ xảy ra lỗi. Hơn nữa, nó đưa chi phí vào các lớp API cấp trung để xử lý lỗi kỳ lạ, bất thường này. Phần mềm làm mất tập trung khách hàng của các API này và yêu cầu họ lo lắng về các vấn đề nằm ngoài tầm kiểm soát của họ. Ngoại lệ cung cấp một cách để thực hiện xử lý không cục bộ đối với các lỗi lớn '

Nếu khách hàng gọi ParseIntbằng một chuỗi và chuỗi không chứa số nguyên, thì người gọi ngay lập tức có thể quan tâm đến lỗi và biết phải làm gì với lỗi đó. Vì vậy, bạn sẽ thiết kế ParseInt để trả về mã lỗi cho một cái gì đó tương tự.

Mặt khác, nếu ParseIntthất bại vì nó không thể cấp phát bộ đệm vì bộ nhớ bị phân mảnh khủng khiếp, thì người gọi sẽ không biết phải làm gì với điều đó. Nó sẽ phải làm bong bóng lỗi bất thường này lên và lên đến một số lớp xử lý những lỗi cơ bản này. Điều đó đánh thuế tất cả mọi người ở giữa (vì họ phải điều chỉnh cơ chế chuyển lỗi trong các API của riêng họ). Một ngoại lệ khiến bạn có thể bỏ qua các lớp đó (trong khi vẫn đảm bảo việc dọn dẹp cần thiết diễn ra).

Khi bạn đang viết mã cấp thấp, có thể khó quyết định khi nào nên sử dụng các phương pháp truyền thống và khi nào nên sử dụng các ngoại lệ. Mã cấp thấp phải đưa ra quyết định (ném hay không). Nhưng mã cấp cao hơn mới thực sự biết những gì được mong đợi và những gì đặc biệt.


1
Tuy nhiên, trong Java, parseIntthực sự một ngoại lệ. Vì vậy, tôi muốn nói rằng các ý kiến ​​về sự phù hợp của việc ném các ngoại lệ phụ thuộc nhiều vào các thực hành đã có từ trước trong các thư viện tiêu chuẩn của ngôn ngữ phát triển mà họ đã chọn.
Charles Salvia,

Dù sao thì thời gian chạy của Java cũng phải theo dõi thông tin, do đó dễ dàng tạo ra các ngoại lệ ... Mã c ++ có xu hướng không có thông tin đó trong tay, vì vậy nó có một hình phạt hiệu suất để tạo ra nó. Bằng cách không giữ nó trên tay, nó có xu hướng nhanh hơn / nhỏ hơn / thân thiện với bộ nhớ cache hơn, v.v. Ngoài ra, mã java có các kiểm tra động trên những thứ như mảng, v.v., một tính năng không có trong c ++, vì vậy java bổ sung lớp ngoại lệ mà nhà phát triển đã quan tâm đến rất nhiều điều này với các khối thử / bắt, vậy tại sao không sử dụng các ngoại trừ cho MỌI THỨ?
Ape-inago

1
@Charles - Câu hỏi được gắn thẻ C ++, vì vậy tôi đã trả lời từ góc độ đó. Tôi chưa bao giờ nghe một lập trình viên Java (hoặc C #) nói rằng ngoại lệ chỉ dành cho các điều kiện ngoại lệ. Các lập trình viên C ++ nói điều này thường xuyên và câu hỏi đặt ra là tại sao.
Adrian McCarthy

1
Trên thực tế, các ngoại lệ trong C # cũng nói lên điều đó, và lý do là các ngoại lệ rất tốn kém để ném vào .NET (hơn là trong Java).
Pavel Minaev

1
@Adrian: đúng, câu hỏi được gắn thẻ C ++, mặc dù triết lý xử lý ngoại lệ hơi giống một hộp thoại đa ngôn ngữ, vì các ngôn ngữ chắc chắn ảnh hưởng lẫn nhau. Dù sao, Boost.lexical_cast ném một ngoại lệ. Nhưng một chuỗi không hợp lệ có thực sự "đặc biệt" không? Có thể là không, nhưng các nhà phát triển Boost nghĩ rằng việc có cú pháp ép kiểu thú vị đó đáng để sử dụng các ngoại lệ. Điều này chỉ cho thấy toàn bộ sự việc chủ quan như thế nào.
Charles Salvia

3

Có một số lý do trong C ++.

Đầu tiên, thường rất khó để biết các ngoại lệ đến từ đâu (vì chúng có thể được ném ra từ hầu hết mọi thứ) và vì vậy khối bắt là thứ gì đó của một câu lệnh COME FROM. Nó tồi tệ hơn ĐI ĐẾN, vì trong ĐI ĐẾN, bạn biết bạn đến từ đâu (câu lệnh, không phải một số lệnh gọi hàm ngẫu nhiên) và bạn đang đi đâu (nhãn). Về cơ bản chúng là phiên bản an toàn về tài nguyên của C's setjmp () và longjmp () và không ai muốn sử dụng chúng.

Thứ hai, C ++ không tích hợp bộ thu gom rác, vì vậy các lớp C ++ sở hữu tài nguyên sẽ loại bỏ chúng trong trình hủy của chúng. Do đó, trong C ++ xử lý ngoại lệ, hệ thống phải chạy tất cả các hàm hủy trong phạm vi. Trong các ngôn ngữ có GC và không có hàm tạo thực, như Java, việc ném các ngoại lệ sẽ bớt nặng nề hơn rất nhiều.

Thứ ba, cộng đồng C ++, bao gồm Bjarne Stroustrup và Ủy ban Tiêu chuẩn và các tác giả trình biên dịch khác nhau, đã giả định rằng các ngoại lệ phải là đặc biệt. Nói chung, nó không đáng để đi ngược lại văn hóa ngôn ngữ. Việc triển khai dựa trên giả định rằng các trường hợp ngoại lệ sẽ rất hiếm. Những cuốn sách tốt hơn coi các trường hợp ngoại lệ là đặc biệt. Mã nguồn tốt sử dụng ít ngoại lệ. Các nhà phát triển C ++ giỏi coi các ngoại lệ là đặc biệt. Để đi ngược lại điều đó, bạn muốn có một lý do chính đáng, và tất cả những lý do tôi thấy đều ở khía cạnh giữ cho chúng đặc biệt.


1
"C ++ không tích hợp sẵn bộ thu thập rác, vì vậy các lớp C ++ sở hữu tài nguyên sẽ loại bỏ chúng trong bộ hủy của chúng. Do đó, trong việc xử lý ngoại lệ C ++, hệ thống phải chạy tất cả bộ hủy trong phạm vi. Trong ngôn ngữ có GC và không có hàm tạo thực , giống như Java, việc ném các ngoại lệ sẽ bớt nặng nề hơn rất nhiều. " - Các trình hủy phải được chạy khi rời khỏi phạm vi bình thường và các finallykhối Java không khác gì so với các trình hủy C ++ về chi phí triển khai.
Pavel Minaev

Tôi thích câu trả lời này nhưng, giống như Pavel, tôi không nghĩ điểm thứ hai là hợp pháp. Khi phạm vi kết thúc vì bất kỳ lý do nào, bao gồm các loại xử lý lỗi khác hoặc chỉ tiếp tục chương trình, các hàm hủy sẽ được gọi bằng mọi cách.
Catskul

+1 để đề cập đến văn hóa ngôn ngữ như là lý do để đi theo dòng chảy. Câu trả lời đó không làm hài lòng một số người, nhưng đó là một lý do chính đáng (và tôi tin rằng đó là lý do chính xác nhất).
j_random_hacker

@Pavel: Vui lòng bỏ qua nhận xét không chính xác của tôi ở trên (hiện đã bị xóa) nói rằng các finallykhối Java không được đảm bảo chạy - tất nhiên là như vậy. Tôi đã nhầm lẫn với finalize()phương thức Java , phương thức này không được đảm bảo sẽ chạy.
j_random_hacker

Nhìn lại câu trả lời này, vấn đề với các hàm hủy là tất cả chúng phải được gọi ngay lúc đó, thay vì có khả năng bị cách ly với mỗi hàm trả về. Đó vẫn chưa phải là một lý do chính đáng, nhưng tôi nghĩ nó có một chút giá trị.
David Thornley

2

Đây là một ví dụ xấu về việc sử dụng ngoại lệ làm luồng điều khiển:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   try {
      totalIncome= calculateIncomeAsTypeA();
   } catch (IncorrectIncomeTypeException& e) {
      totalIncome= calculateIncomeAsTypeB();
   }

   return totalIncome;
}

Điều đó rất tệ, nhưng bạn nên viết:

int getTotalIncome(int incomeType) {
   int totalIncome= 0;
   if (incomeType == A) {
      totalIncome= calculateIncomeAsTypeA();
   } else if (incomeType == B) {
      totalIncome= calculateIncomeAsTypeB();
   }
   return totalIncome;
}

Ví dụ thứ hai này rõ ràng cần một số cấu trúc lại (như sử dụng chiến lược mẫu thiết kế), nhưng minh họa rõ ràng rằng các ngoại lệ không dành cho luồng điều khiển.

Các trường hợp ngoại lệ cũng có một số hình phạt liên quan đến hiệu suất, nhưng các vấn đề về hiệu suất nên tuân theo quy tắc: "tối ưu hóa quá sớm là căn nguyên của mọi điều xấu"


Có vẻ như một trường hợp khi đa hình hơn sẽ tốt, thay vì kiểm tra incomeType.
Sarah Vessels

@Sarah vâng, tôi biết nó sẽ tốt. Nó ở đây chỉ cho mục đích minh họa.
Edison Gustavo Muenz

3
Tại sao nó xấu? Tại sao bạn nên viết nó theo cách thứ 2? Người hỏi muốn biết lý do cho các quy tắc, không phải các quy tắc. -1.
j_random_hacker

2
  1. Khả năng bảo trì: Như những người đã đề cập ở trên, ném các trường hợp ngoại lệ khi rơi mũ cũng giống như sử dụng gotos.
  2. Khả năng tương tác: Bạn không thể giao tiếp các thư viện C ++ với các mô-đun C / Python (ít nhất là không dễ dàng) nếu bạn đang sử dụng các ngoại lệ.
  3. Suy giảm hiệu suất: RTTI được sử dụng để thực sự tìm ra loại ngoại lệ áp đặt chi phí bổ sung. Do đó, các ngoại lệ không phù hợp để xử lý các trường hợp sử dụng thường xảy ra (người dùng nhập int thay vì chuỗi, v.v.).

2
1. Nhưng trong một số ngôn ngữ, ngoại lệ được ném xuống một cái mũ. 3. Đôi khi hiệu suất không quan trọng. (80% thời gian?)
UncleBens

2
2. Các chương trình C ++ hầu như luôn sử dụng các ngoại lệ, vì rất nhiều thư viện chuẩn sử dụng chúng. Các lớp vùng chứa ném. Lớp chuỗi ném. Các lớp luồng ném.
David Thornley

Các hoạt động vùng chứa và chuỗi ném, ngoại trừ at()? Điều đó nói rằng, bất kỳ newcó thể ném, vì vậy ...
Pavel Minaev

RTTI không hoàn toàn cần thiết cho Thử / Ném / Bắt theo như tôi hiểu. Sự suy giảm hiệu suất luôn được đề cập và tôi tin điều đó, nhưng không ai đề cập đến thang đo hoặc liên kết đến bất kỳ tài liệu tham khảo nào.
Catskul

UncleBens: (1) là để bảo trì không phải hiệu suất. Việc sử dụng quá nhiều để thử / bắt / ném làm giảm khả năng đọc của mã. Quy tắc ngón tay cái (và tôi có thể bị kích thích vì điều này nhưng đó chỉ là ý kiến ​​của tôi), càng ít điểm vào và ra, thì mã càng dễ đọc. David Thornley: Không phải khi bạn cần khả năng tương tác. Cờ -fno-exceptions trong gcc vô hiệu hóa các ngoại lệ khi bạn đang biên dịch một thư viện được gọi từ mã C.
Sridhar Iyer

2

Tôi muốn nói rằng ngoại lệ là một cơ chế giúp bạn thoát khỏi bối cảnh hiện tại (ra khỏi khung ngăn xếp hiện tại theo nghĩa đơn giản nhất, nhưng nó còn hơn thế nữa) một cách an toàn. Đó là thứ gần nhất mà lập trình có cấu trúc đạt đến một goto. Để sử dụng các ngoại lệ theo cách chúng được dự định sử dụng, bạn phải gặp tình huống khi bạn không thể tiếp tục công việc hiện tại và bạn không thể xử lý nó tại thời điểm hiện tại. Vì vậy, ví dụ: khi mật khẩu của người dùng sai, bạn có thể tiếp tục bằng cách trả về sai. Nhưng nếu hệ thống con giao diện người dùng báo cáo rằng nó thậm chí không thể nhắc người dùng, chỉ cần trả lại "đăng nhập không thành công" sẽ là sai. Mức độ hiện tại của mã chỉ đơn giản là không biết phải làm gì. Vì vậy, nó sử dụng một cơ chế ngoại lệ để ủy thác trách nhiệm cho người nào đó ở trên, người có thể biết phải làm gì.


2

Một lý do rất thực tế là khi gỡ lỗi một chương trình, tôi thường bật Ngoại lệ Cơ hội Đầu tiên (Debug -> Ngoại lệ) để gỡ lỗi một ứng dụng. Nếu có nhiều trường hợp ngoại lệ xảy ra, rất khó để tìm ra chỗ nào đó đã "sai".

Ngoài ra, nó dẫn đến một số phản đối như kiểu "bắt quả tang" khét tiếng và làm rối loạn các vấn đề thực sự. Để biết thêm thông tin về điều đó, hãy xem một bài đăng trên blog tôi đã thực hiện về chủ đề này.


2

Tôi thích sử dụng ngoại lệ càng ít càng tốt. Các trường hợp ngoại lệ buộc nhà phát triển phải xử lý một số điều kiện có thể có hoặc không phải là lỗi thực sự. Định nghĩa về việc liệu trường hợp ngoại lệ được đề cập là một vấn đề nghiêm trọng hay một vấn đề phải được xử lý ngay lập tức.

Lập luận phản bác rằng nó chỉ yêu cầu những người lười biếng phải gõ nhiều hơn để tự bắn vào chân mình.

Chính sách mã hóa của Google nói rằng không bao giờ sử dụng các ngoại lệ , đặc biệt là trong C ++. Ứng dụng của bạn không được chuẩn bị để xử lý các trường hợp ngoại lệ hoặc đúng như vậy. Nếu không, thì ngoại lệ có thể sẽ lan truyền cho đến khi ứng dụng của bạn chết.

Không bao giờ thú vị khi phát hiện ra một số thư viện mà bạn đã sử dụng các ngoại lệ ném và bạn không chuẩn bị để xử lý chúng.


1

Trường hợp hợp pháp để ném một ngoại lệ:

  • Bạn cố gắng mở một tệp, nó không có ở đó, một FileNotFoundException được ném ra;

Trường hợp bất hợp pháp:

  • Bạn chỉ muốn làm điều gì đó nếu tệp không tồn tại, bạn cố gắng mở tệp rồi thêm một số mã vào khối bắt.

Tôi sử dụng các ngoại lệ khi tôi muốn phá vỡ luồng của ứng dụng đến một thời điểm nhất định . Điểm này là nơi bắt (...) cho ngoại lệ đó. Ví dụ: rất phổ biến là chúng ta phải xử lý một lượng dự án và mỗi dự án nên được xử lý độc lập với những dự án khác. Vì vậy, vòng lặp xử lý các dự án có khối try ... catch, và nếu một số ngoại lệ được ném ra trong quá trình xử lý dự án, mọi thứ sẽ được khôi phục cho dự án đó, lỗi được ghi lại và dự án tiếp theo được xử lý. Cuộc sống vẫn tiếp diễn.

Tôi nghĩ rằng bạn nên sử dụng ngoại lệ cho những thứ như tệp không tồn tại, biểu thức không hợp lệ và những thứ tương tự. Bạn không nên sử dụng các ngoại lệ cho kiểm tra phạm vi / kiểm tra kiểu dữ liệu / tồn tại tệp / bất kỳ điều gì khác nếu có một giải pháp thay thế dễ dàng / rẻ cho nó. Bạn không nên sử dụng các ngoại lệ cho kiểm tra phạm vi / kiểm tra kiểu dữ liệu / tồn tại tệp / bất cứ điều gì khác nếu có một giải pháp thay thế dễ dàng / rẻ tiền vì loại logic này khiến mã khó hiểu:

RecordIterator<MyObject> ri = createRecordIterator();
try {
   MyObject myobject = ri.next();
} catch(NoSuchElement exception) {
   // Object doesn't exist, will create it
}

Điều này sẽ tốt hơn:

RecordIterator<MyObject> ri = createRecordIterator();
if (ri.hasNext()) {
   // It exists! 
   MyObject myobject = ri.next();
} else {
   // Object doesn't exist, will create it
}

BÌNH LUẬN THÊM VÀO CÂU TRẢ LỜI:

Có thể ví dụ của tôi không tốt lắm - ri.next () không nên đưa ra một ngoại lệ trong ví dụ thứ hai, và nếu nó có, có một cái gì đó thực sự đặc biệt và một số hành động khác nên được thực hiện ở một nơi khác. Khi ví dụ 1 được sử dụng nhiều, các nhà phát triển sẽ bắt một ngoại lệ chung thay vì một ngoại lệ cụ thể và cho rằng ngoại lệ đó là do lỗi mà họ đang mong đợi, nhưng nó có thể do một cái gì đó khác. Cuối cùng, điều này dẫn đến các ngoại lệ thực sự bị bỏ qua vì các ngoại lệ trở thành một phần của luồng ứng dụng và không phải là ngoại lệ đối với nó.

Các nhận xét về điều này có thể bổ sung nhiều hơn câu trả lời của tôi.


1
Tại sao không? Tại sao không sử dụng ngoại lệ cho trường hợp thứ 2 bạn đề cập? Người hỏi muốn biết lý do cho các quy tắc, không phải các quy tắc.
j_random_hacker

Cảm ơn bạn đã xây dựng kỹ lưỡng, nhưng IMHO 2 đoạn mã của bạn có độ phức tạp gần như giống hệt nhau - cả hai đều sử dụng logic điều khiển được bản địa hóa cao. Nơi mà mức độ phức tạp của các ngoại lệ vượt quá rõ ràng nhất của nó / then / else là khi bạn có một loạt các câu lệnh bên trong khối try, bất kỳ câu lệnh nào trong số đó cũng có thể ném ra - bạn có đồng ý không?
j_random_hacker

Có thể ví dụ của tôi không tốt lắm - ri.next () không nên đưa ra một ngoại lệ trong ví dụ thứ hai, và nếu nó có, có một cái gì đó thực sự đặc biệt và một số hành động khác nên được thực hiện ở một nơi khác. Khi ví dụ 1 được sử dụng nhiều, các nhà phát triển sẽ bắt một ngoại lệ chung thay vì một ngoại lệ cụ thể và cho rằng ngoại lệ đó là do lỗi mà họ đang mong đợi, nhưng nó có thể do một cái gì đó khác. Cuối cùng, điều này dẫn đến các ngoại lệ thực sự bị bỏ qua vì các ngoại lệ trở thành một phần của luồng ứng dụng và không phải là ngoại lệ đối với nó.
Ravi Wallau

Vì vậy, bạn đang nói: Theo thời gian, các câu lệnh khác có thể tích tụ bên trong trykhối và sau đó bạn không còn chắc chắn rằng catchkhối có thực sự bắt được thứ mà bạn nghĩ rằng nó đang bắt - đúng không? Mặc dù việc sử dụng sai cách tiếp cận if / then / else theo cách tương tự sẽ khó hơn vì bạn chỉ có thể kiểm tra 1 thứ tại một thời điểm thay vì một tập hợp nhiều thứ cùng một lúc, vì vậy cách tiếp cận ngoại lệ có thể dẫn đến mã dễ hỏng hơn. Nếu vậy, vui lòng thảo luận điều này trong câu trả lời của bạn và tôi sẽ vui lòng +1, vì tôi nghĩ tính dễ hỏng của mã là một lý do chính đáng.
j_random_hacker

0

Về cơ bản, các ngoại lệ là một dạng điều khiển luồng không có cấu trúc và khó hiểu. Điều này là cần thiết khi xử lý các điều kiện lỗi không phải là một phần của luồng chương trình thông thường, để tránh việc xử lý lỗi logic làm lộn xộn quá nhiều điều khiển luồng bình thường của mã của bạn.

Ngoại lệ IMHO nên được sử dụng khi bạn muốn cung cấp một mặc định lành mạnh trong trường hợp người gọi sơ ý viết mã xử lý lỗi hoặc nếu lỗi tốt nhất có thể được xử lý trong ngăn xếp cuộc gọi hơn so với người gọi ngay lập tức. Mặc định lành mạnh là thoát khỏi chương trình với một thông báo lỗi chẩn đoán hợp lý. Giải pháp thay thế điên rồ là chương trình bị khập khiễng ở trạng thái có lỗi và bị treo hoặc âm thầm tạo ra kết quả xấu ở một số điểm sau đó, khó chẩn đoán hơn. Nếu "lỗi" là một phần bình thường của dòng chương trình mà người gọi không thể quên kiểm tra nó một cách hợp lý, thì không nên sử dụng ngoại lệ.


0

Tôi nghĩ, "sử dụng nó hiếm khi" không phải là câu đúng. Tôi muốn "chỉ ném trong những tình huống đặc biệt".

Nhiều người đã giải thích, tại sao các ngoại lệ không nên sử dụng trong các tình huống bình thường. Các trường hợp ngoại lệ có quyền xử lý lỗi và hoàn toàn có quyền xử lý lỗi.

Tôi sẽ tập trung vào một điểm khác:

Một điều khác là vấn đề hiệu suất. Các trình biên dịch đã vật lộn rất lâu để tải chúng nhanh chóng. Tôi không chắc, trạng thái chính xác bây giờ như thế nào, nhưng khi bạn sử dụng ngoại lệ cho luồng điều khiển, bạn sẽ gặp một rắc rối khác: Chương trình của bạn sẽ trở nên chậm chạp!

Lý do là, các trường hợp ngoại lệ không chỉ là các câu lệnh goto rất mạnh, họ còn phải giải phóng ngăn xếp cho tất cả các khung hình mà họ để lại. Do đó, các đối tượng trên ngăn xếp cũng phải được giải cấu trúc, v.v. Vì vậy, nếu không nhận thức được điều đó, một lần ném ngoại lệ sẽ thực sự khiến cả một nhóm cơ học tham gia. Bộ vi xử lý sẽ phải làm rất nhiều.

Vì vậy, bạn sẽ kết thúc, thanh lịch ghi bộ xử lý của bạn mà không biết.

Vì vậy: chỉ sử dụng ngoại lệ trong những trường hợp ngoại lệ - Ý nghĩa: Khi xảy ra lỗi thực sự!


1
"Lý do là, các ngoại lệ không chỉ là các câu lệnh goto rất mạnh, chúng còn phải giải phóng ngăn xếp cho tất cả các khung mà chúng để lại. Do đó, các đối tượng trên ngăn xếp cũng phải được giải cấu trúc, v.v." - nếu bạn xuống một số khung ngăn xếp trên ngăn xếp, thì tất cả các khung ngăn xếp đó (và các đối tượng trên chúng) cuối cùng sẽ phải bị hủy - không thành vấn đề nếu điều đó xảy ra vì bạn quay lại bình thường hoặc vì bạn ném một ngoại lệ. Cuối cùng, gọi ngắn gọn std::terminate(), bạn vẫn phải hủy.
Pavel Minaev

Tất nhiên là bạn đúng. Nhưng hãy nhớ rằng: Mỗi khi bạn sử dụng ngoại lệ, hệ thống phải cung cấp cơ sở hạ tầng để thực hiện tất cả điều này một cách kỳ diệu. Tôi chỉ muốn làm rõ một chút rằng nó không chỉ là một goto. Ngoài ra khi tháo cuộn, hệ thống phải tìm đúng nơi đánh bắt, những gì sẽ tốn thêm thời gian, mã và việc sử dụng RTTI. Vì vậy, nó không chỉ là một bước nhảy - hầu hết mọi người chỉ không biết hiểu điều này.
Juergen

Vì vậy, miễn là bạn sử dụng C ++ tuân thủ tiêu chuẩn, RTTI là không thể tránh khỏi. Nếu không, tất nhiên chi phí là ở đó. Chỉ là tôi thường thấy nó bị phóng đại đáng kể.
Pavel Minaev

1
-1. "Các trường hợp ngoại lệ có quyền xử lý lỗi và hoàn toàn là xử lý lỗi" - ai nói? Tại sao? Hiệu suất không thể là lý do duy nhất. (A) Hầu hết mã trên thế giới không nằm trong vòng lặp trong cùng của công cụ kết xuất trò chơi hoặc chức năng nhân ma trận, vì vậy việc sử dụng các ngoại lệ sẽ không có sự khác biệt về hiệu suất có thể nhận thấy được. (B) Như một người đã lưu ý trong một bình luận ở đâu đó, tất cả việc tháo cuộn ngăn xếp đó cuối cùng vẫn cần phải diễn ra ngay cả khi lỗi kiểm tra-lỗi-trả-giá-trị-và-và-trả-lại-nếu-cần-kiểu C cũ phương pháp xử lý đã được sử dụng.
j_random_hacker

Nói I. Trong chủ đề này có rất nhiều lý do được trích dẫn. Chỉ cần đọc chúng. Tôi chỉ muốn mô tả một. Nếu đó không phải là người bạn cần, đừng trách tôi.
Juergen

0

Mục đích của các ngoại lệ là làm cho phần mềm có khả năng chịu lỗi. Tuy nhiên, việc phải cung cấp một phản hồi cho mọi ngoại lệ do một hàm ném ra sẽ dẫn đến việc triệt tiêu. Các ngoại lệ chỉ là một cấu trúc chính thức buộc các lập trình viên phải thừa nhận rằng một số điều nhất định có thể xảy ra sai với một quy trình và rằng lập trình viên khách hàng cần phải nhận thức được những điều kiện này và phục vụ chúng khi cần thiết.

Thành thật mà nói, các ngoại lệ là một phần mềm được thêm vào ngôn ngữ lập trình để cung cấp cho các nhà phát triển một số yêu cầu chính thức nhằm chuyển trách nhiệm xử lý các trường hợp lỗi từ nhà phát triển trực tiếp sang một số nhà phát triển trong tương lai.

Tôi tin rằng một ngôn ngữ lập trình tốt không hỗ trợ các ngoại lệ như chúng ta biết trong C ++ và Java. Bạn nên chọn ngôn ngữ lập trình có thể cung cấp luồng thay thế cho tất cả các loại giá trị trả về từ các hàm. Lập trình viên phải chịu trách nhiệm dự đoán tất cả các dạng kết quả đầu ra của một quy trình và xử lý chúng trong một tệp mã riêng biệt nếu tôi có thể làm theo cách của mình.


0

Tôi sử dụng ngoại lệ nếu:

  • đã xảy ra lỗi không thể khôi phục được từ AND cục bộ
  • nếu lỗi không được phục hồi từ chương trình sẽ chấm dứt.

Nếu lỗi có thể được khôi phục từ (người dùng đã nhập "apple" thay vì một số) thì hãy khôi phục (yêu cầu nhập lại, thay đổi thành giá trị mặc định, v.v.).

Nếu lỗi không thể được khôi phục từ cục bộ nhưng ứng dụng có thể tiếp tục (người dùng đã cố gắng mở tệp nhưng tệp không tồn tại) thì mã lỗi là phù hợp.

Nếu lỗi không thể được khôi phục từ cục bộ và ứng dụng không thể tiếp tục mà không khôi phục (bạn đã hết bộ nhớ / dung lượng đĩa / v.v.), thì một ngoại lệ là cách thích hợp để thực hiện.


1
-1. Vui lòng đọc kỹ câu hỏi. Hầu hết các lập trình viên đều nghĩ rằng các ngoại lệ phù hợp với một số kiểu xử lý lỗi hoặc không bao giờ thích hợp - họ thậm chí không xem xét khả năng sử dụng chúng cho các hình thức kiểm soát luồng khác lạ hơn. Câu hỏi đặt ra là: tại sao vậy?
j_random_hacker

Bạn cũng nên đọc kỹ. Tôi đã trả lời "Triết lý đằng sau việc đặc biệt bảo thủ với cách chúng được sử dụng là gì?" với triết lý của tôi đằng sau là bảo thủ với cách chúng được sử dụng.
Hóa đơn

IMHO bạn vẫn chưa giải thích tại sao sự bảo thủ là cần thiết. Tại sao đôi khi chúng chỉ "thích hợp"? Tại sao không phải mọi lúc? (BTW Tôi nghĩ rằng cách tiếp cận của bạn đề nghị là tốt, nó là nhiều hơn hoặc ít hơn những gì tôi làm bản thân mình, tôi chỉ không nghĩ rằng nó được ở hầu hết các lý do tại sao các câu hỏi.)
j_random_hacker

OP đã hỏi bảy câu hỏi khác nhau. Tôi chỉ chọn một câu trả lời. Tôi xin lỗi vì bạn cảm thấy điều đó đáng để bỏ phiếu.
Hóa đơn

0

Ai nói chúng nên được sử dụng một cách thận trọng? Chỉ cần không bao giờ sử dụng ngoại lệ để kiểm soát luồng và đó là điều đó. Và ai quan tâm đến chi phí của ngoại lệ khi nó đã được ném?


0

Theo quan điểm của tôi:

Tôi thích sử dụng các ngoại lệ, vì nó cho phép tôi lập trình như thể không có lỗi nào xảy ra. Vì vậy, mã của tôi vẫn có thể đọc được, không bị phân tán với tất cả các loại xử lý lỗi. Tất nhiên, việc xử lý lỗi (xử lý ngoại lệ) được chuyển đến cuối (khối bắt) hoặc được coi là khả năng đáp ứng của mức gọi.

Một ví dụ tuyệt vời đối với tôi, là xử lý tệp hoặc xử lý cơ sở dữ liệu. Giả sử mọi thứ đều ổn và đóng tệp của bạn ở cuối hoặc nếu một số ngoại lệ xảy ra. Hoặc khôi phục giao dịch của bạn khi xảy ra ngoại lệ.

Vấn đề với các ngoại lệ, là nó nhanh chóng trở nên dài dòng. Mặc dù nó được sử dụng để cho phép mã của bạn vẫn rất dễ đọc và chỉ tập trung vào quy trình bình thường của mọi thứ, nhưng nếu được sử dụng một cách nhất quán thì hầu như mọi lệnh gọi hàm đều cần được bao bọc trong một khối try / catch và nó bắt đầu đánh bại mục đích.

Đối với một ParseInt như đã đề cập trước đây, tôi thích ý tưởng về các ngoại lệ. Chỉ cần trả lại giá trị. Nếu tham số không thể phân tích cú pháp, hãy ném một ngoại lệ. Một mặt nó làm cho mã của bạn sạch hơn. Ở cấp độ gọi điện, bạn cần thực hiện một số việc như

try 
{
   b = ParseInt(some_read_string);
} 
catch (ParseIntException &e)
{
   // use some default value instead
   b = 0;
}

Mã sạch. Khi tôi nhận được ParseInt như thế này rải rác khắp nơi, tôi tạo các hàm wrapper để xử lý các ngoại lệ và trả về cho tôi các giá trị mặc định. Ví dụ

int ParseIntWithDefault(String stringToConvert, int default_value=0)
{
   int result = default_value;
   try
   {
     result = ParseInt(stringToConvert);
   }
   catch (ParseIntException &e) {}

   return result;
}

Vì vậy, để kết luận: những gì tôi đã bỏ lỡ trong quá trình thảo luận là thực tế là các ngoại lệ cho phép tôi làm cho mã của mình dễ đọc hơn / dễ đọc hơn vì tôi có thể bỏ qua các điều kiện lỗi nhiều hơn. Các vấn đề:

  • các trường hợp ngoại lệ vẫn cần được xử lý ở đâu đó. Vấn đề bổ sung: c ++ không có cú pháp cho phép nó chỉ định ngoại lệ nào mà một hàm có thể ném ra (giống như java). Vì vậy, cấp độ gọi không biết những ngoại lệ nào có thể cần được xử lý.
  • đôi khi mã có thể rất dài dòng, nếu mọi hàm cần được gói trong một khối try / catch. Nhưng đôi khi điều này vẫn có ý nghĩa.

Vì vậy, đôi khi khó tìm được sự cân bằng tốt.


-1

Tôi xin lỗi nhưng câu trả lời là "chúng được gọi là ngoại lệ vì một lý do." Giải thích đó là một "quy tắc ngón tay cái". Bạn không thể đưa ra một tập hợp đầy đủ các trường hợp mà các ngoại lệ nên hoặc không nên được sử dụng vì ngoại lệ nghiêm trọng (định nghĩa tiếng Anh) dành cho một miền sự cố là quy trình hoạt động bình thường cho một miền sự cố khác. Quy tắc ngón tay cái không được thiết kế để tuân theo một cách mù quáng. Thay vào đó, chúng được thiết kế để hướng dẫn bạn điều tra giải pháp. "Chúng được gọi là ngoại lệ vì một lý do" cho bạn biết rằng bạn nên xác định trước đâu là lỗi bình thường mà người gọi có thể xử lý và đâu là trường hợp bất thường mà người gọi không thể xử lý nếu không có mã hóa đặc biệt (khối bắt).

Về mọi quy tắc lập trình thực sự là một hướng dẫn nói rằng "Đừng làm điều này trừ khi bạn có lý do thực sự chính đáng": "Không bao giờ sử dụng goto", "Tránh các biến toàn cục", "Biểu thức chính quy tăng trước số vấn đề của bạn lên một ”, v.v… Các trường hợp ngoại lệ cũng không ngoại lệ….


... và người hỏi muốn biết tại sao đó là quy tắc ngón tay cái, thay vì nghe (một lần nữa) rằng đó là quy tắc ngón tay cái. -1.
j_random_hacker

Tôi thừa nhận điều đó trong phản hồi của tôi. Không có lý do rõ ràng. Quy tắc ngón tay cái là mơ hồ theo định nghĩa. Nếu có một lý do rõ ràng, đó sẽ là một quy tắc chứ không phải quy tắc chung. Mọi lời giải thích trong mọi câu trả lời khác ở trên đều chứa những cảnh báo nên chúng cũng không giải thích tại sao.
jmucchiello

Có thể không có "tại sao" rõ ràng, nhưng có một phần "tại sao" mà những người khác đề cập đến, ví dụ: "bởi vì đó là những gì mọi người khác đang làm" (IMHO một lý do đáng buồn nhưng thực sự) hoặc "hiệu suất" (IMHO lý do này thường được phóng đại , nhưng dù sao đó cũng là một lý do). Điều này cũng đúng với các quy tắc ngón tay cái khác như tránh goto (thông thường, nó làm phức tạp phân tích luồng điều khiển hơn một vòng lặp tương đương) và tránh các biến toàn cục (chúng có khả năng tạo ra nhiều khớp nối, khiến việc thay đổi mã sau này trở nên khó khăn và thường giống nhau mục tiêu có thể đạt được với ít cách ghép nối khác).
j_random_hacker

Và tất cả những lý do đó đều có danh sách dài những cảnh báo là câu trả lời của tôi. Không có thực tế tại sao ngoài kinh nghiệm rộng rãi trong lập trình. Có một số quy tắc ngón tay cái vượt ra ngoài lý do tại sao. Cùng một quy tắc ngón tay cái có thể khiến các "chuyên gia" không đồng ý về lý do. Bạn tự nhận "màn trình diễn" với một hạt muối. Đó sẽ là danh sách hàng đầu của tôi. Phân tích kiểm soát luồng thậm chí không đăng ký với tôi vì (như "không có goto") Tôi thấy các vấn đề về điều khiển luồng bị phóng đại quá mức. Tôi cũng sẽ chỉ ra rằng câu trả lời của tôi cố gắng giải thích CÁCH bạn sử dụng câu trả lời "sáo mòn".
jmucchiello

Tôi đồng ý với bạn vì tất cả các quy tắc ngón tay cái đều có danh sách dài những điều cần lưu ý. Điểm tôi không đồng ý là tôi nghĩ rằng điều đáng giá là cố gắng xác định những lý do ban đầu cho quy tắc, cũng như những lưu ý cụ thể. (Ý tôi là trong một thế giới lý tưởng, nơi chúng ta có vô hạn thời gian để suy ngẫm về những điều này và tất nhiên là không có thời hạn;)) Tôi nghĩ đó là những gì OP đã yêu cầu.
j_random_hacker
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.