Bạn thích mã Ngoại lệ hay Mã trả lại nào và tại sao?


92

Câu hỏi của tôi là hầu hết các nhà phát triển thích xử lý lỗi, Ngoại lệ hoặc Mã trả lại lỗi nào. Vui lòng nêu ngôn ngữ (hoặc họ ngôn ngữ) cụ thể và tại sao bạn thích ngôn ngữ này hơn ngôn ngữ kia.

Tôi hỏi điều này vì tò mò. Cá nhân tôi thích Mã trả lại lỗi vì chúng ít bùng nổ hơn và không buộc mã người dùng phải trả hình phạt hiệu suất ngoại lệ nếu họ không muốn.

cập nhật: cảm ơn cho tất cả các câu trả lời! Tôi phải nói rằng mặc dù tôi không thích sự khó đoán của dòng mã với các ngoại lệ. Câu trả lời về mã trả lại (và người anh em của họ xử lý) làm thêm nhiều tiếng ồn vào mã.


1
Vấn đề về con gà hay quả trứng trong thế giới phần mềm ... là vấn đề tranh cãi muôn thuở. :)
Gishu 19-08

Xin lỗi về điều đó, nhưng hy vọng có nhiều ý kiến ​​khác nhau sẽ giúp mọi người (bao gồm cả tôi) lựa chọn phù hợp.
Robert Gould 19-08

Câu trả lời:


107

Đối với một số ngôn ngữ (tức là C ++) Rò rỉ tài nguyên không phải là lý do

C ++ dựa trên RAII.

Nếu bạn có mã mà có thể thất bại, trả lại hoặc ném (có nghĩa là, hầu hết các mã bình thường), sau đó bạn nên có con trỏ của bạn được bao bọc bên trong một con trỏ thông minh (giả sử bạn có một rất tốt lý do gì để không có đối tượng của bạn tạo ra trên stack).

Mã trả lại dài hơn

Chúng dài dòng và có xu hướng phát triển thành một cái gì đó như:

if(doSomething())
{
   if(doSomethingElse())
   {
      if(doSomethingElseAgain())
      {
          // etc.
      }
      else
      {
         // react to failure of doSomethingElseAgain
      }
   }
   else
   {
      // react to failure of doSomethingElse
   }
}
else
{
   // react to failure of doSomething
}

Cuối cùng, mã của bạn là một tập hợp các hướng dẫn được nhận dạng (tôi đã thấy loại mã này trong mã sản xuất).

Mã này cũng có thể được dịch thành:

try
{
   doSomething() ;
   doSomethingElse() ;
   doSomethingElseAgain() ;
}
catch(const SomethingException & e)
{
   // react to failure of doSomething
}
catch(const SomethingElseException & e)
{
   // react to failure of doSomethingElse
}
catch(const SomethingElseAgainException & e)
{
   // react to failure of doSomethingElseAgain
}

Việc tách biệt rõ ràng mã và xử lý lỗi, đó có thể là một điều tốt .

Mã trả lại giòn hơn

Nếu không phải là một số cảnh báo khó hiểu từ một trình biên dịch (xem bình luận của "phjr"), chúng có thể dễ dàng bị bỏ qua.

Với các ví dụ trên, giả sử hơn ai đó quên xử lý lỗi có thể xảy ra (điều này xảy ra ...). Lỗi được bỏ qua khi "trả về", và có thể sẽ phát nổ sau đó (tức là con trỏ NULL). Vấn đề tương tự sẽ không xảy ra ngoại lệ.

Lỗi sẽ không được bỏ qua. Đôi khi, bạn muốn nó không phát nổ, nhưng bạn phải lựa chọn cẩn thận.

Mã trở lại đôi khi phải được dịch

Giả sử chúng ta có các chức năng sau:

  • doSomething, có thể trả về một int được gọi là NOT_FOUND_ERROR
  • doSomethingElse, có thể trả về bool "false" (không thành công)
  • doSomethingElseAgain, có thể trả về đối tượng Lỗi (với cả __LINE__, __FILE__ và một nửa các biến ngăn xếp.
  • doTryToDoSomethingWithAllThisMess ... Sử dụng các hàm trên và trả về mã lỗi thuộc loại ...

Kiểu trả về của doTryToDoSomethingWithAllThisMess là gì nếu một trong các hàm được gọi của nó bị lỗi?

Mã trở lại không phải là một giải pháp phổ biến

Các nhà khai thác không thể trả lại mã lỗi. Các hàm tạo C ++ cũng không thể.

Mã trả lại có nghĩa là bạn không thể chuỗi các biểu thức

Hệ quả của điểm trên. Nếu tôi muốn viết:

CMyType o = add(a, multiply(b, c)) ;

Tôi không thể, vì giá trị trả về đã được sử dụng (và đôi khi, không thể thay đổi được). Vì vậy, giá trị trả về trở thành tham số đầu tiên, được gửi dưới dạng tham chiếu ... Hoặc không.

Ngoại lệ được nhập

Bạn có thể gửi các lớp khác nhau cho từng loại ngoại lệ. Các ngoại lệ Ressources (tức là hết bộ nhớ) phải nhẹ, nhưng bất kỳ thứ gì khác cũng có thể nặng đến mức cần thiết (Tôi thích Java Exception cho tôi toàn bộ ngăn xếp).

Mỗi lần đánh bắt sau đó có thể được chuyên biệt hóa.

Đừng bao giờ sử dụng catch (...) mà không ném lại

Thông thường, bạn không nên ẩn lỗi. Nếu bạn không ném lại, ít nhất, hãy ghi lại lỗi trong một tệp, mở hộp thư, bất cứ điều gì ...

Ngoại lệ là ... NUKE

Vấn đề với ngoại lệ là lạm dụng chúng sẽ tạo ra mã đầy thử / bắt. Nhưng vấn đề nằm ở chỗ khác: Ai thử / bắt mã của anh ấy / cô ấy bằng cách sử dụng STL container? Tuy nhiên, những vùng chứa đó có thể gửi một ngoại lệ.

Tất nhiên, trong C ++, đừng bao giờ để một ngoại lệ thoát khỏi hàm hủy.

Ngoại lệ là ... đồng bộ

Hãy chắc chắn nắm bắt chúng trước khi chúng đưa chuỗi của bạn ra ngoài hoặc lan truyền bên trong vòng lặp thông báo Windows của bạn.

Giải pháp có thể là trộn chúng?

Vì vậy, tôi đoán giải pháp là ném khi điều gì đó không nên xảy ra. Và khi điều gì đó có thể xảy ra, hãy sử dụng mã trả về hoặc một tham số để cho phép người dùng phản ứng với nó.

Vì vậy, câu hỏi duy nhất là "điều gì không nên xảy ra?"

Nó phụ thuộc vào hợp đồng của chức năng của bạn. Nếu hàm chấp nhận một con trỏ, nhưng chỉ định con trỏ phải không phải là NULL, thì có thể ném một ngoại lệ khi người dùng gửi một con trỏ NULL (câu hỏi là, trong C ++, tác giả hàm đã không sử dụng tham chiếu thay thế khi nào của con trỏ, nhưng ...)

Một giải pháp khác là hiển thị lỗi

Đôi khi, vấn đề của bạn là bạn không muốn có lỗi. Sử dụng ngoại lệ hoặc mã trả về lỗi rất tuyệt, nhưng ... Bạn muốn biết về nó.

Trong công việc của tôi, chúng tôi sử dụng một loại "Khẳng định". Tùy thuộc vào giá trị của tệp cấu hình, nó sẽ tùy thuộc vào các tùy chọn biên dịch gỡ lỗi / phát hành:

  • ghi lại lỗi
  • mở hộp tin nhắn với thông báo "Này, bạn gặp sự cố"
  • mở một hộp thư với thông báo "Này, bạn gặp sự cố, bạn có muốn gỡ lỗi không"

Trong cả quá trình phát triển và thử nghiệm, điều này cho phép người dùng xác định chính xác vấn đề khi nó được phát hiện chứ không phải sau khi (khi một số mã quan tâm đến giá trị trả về hoặc bên trong một lệnh bắt).

Nó rất dễ dàng để thêm vào mã kế thừa. Ví dụ:

void doSomething(CMyObject * p, int iRandomData)
{
   // etc.
}

dẫn một loại mã tương tự như:

void doSomething(CMyObject * p, int iRandomData)
{
   if(iRandomData < 32)
   {
      MY_RAISE_ERROR("Hey, iRandomData " << iRandomData << " is lesser than 32. Aborting processing") ;
      return ;
   }

   if(p == NULL)
   {
      MY_RAISE_ERROR("Hey, p is NULL !\niRandomData is equal to " << iRandomData << ". Will throw.") ;
      throw std::some_exception() ;
   }

   if(! p.is Ok())
   {
      MY_RAISE_ERROR("Hey, p is NOT Ok!\np is equal to " << p->toString() << ". Will try to continue anyway") ;
   }

   // etc.
}

(Tôi có các macro tương tự chỉ hoạt động khi gỡ lỗi).

Lưu ý rằng trên sản xuất, tệp cấu hình không tồn tại, vì vậy khách hàng không bao giờ nhìn thấy kết quả của macro này ... Nhưng có thể dễ dàng kích hoạt nó khi cần.

Phần kết luận

Khi bạn viết mã bằng mã trả lại, bạn đang chuẩn bị cho sự thất bại và hy vọng pháo đài kiểm tra của bạn đủ an toàn.

Khi bạn viết mã bằng cách sử dụng ngoại lệ, bạn biết rằng mã của bạn có thể bị lỗi và thường đặt chốt chặn phản công ở vị trí chiến lược đã chọn trong mã của bạn. Nhưng thông thường, mã của bạn thiên về "những gì nó phải làm" rồi "những gì tôi sợ sẽ xảy ra".

Nhưng khi bạn viết mã, bạn phải sử dụng công cụ tốt nhất theo ý của mình, và đôi khi, đó là "Không bao giờ ẩn lỗi, và hiển thị nó càng sớm càng tốt". Cái vĩ mô mà tôi đã nói ở trên tuân theo triết lý này.


3
Yeah, NHƯNG về ví dụ đầu tiên của bạn, điều đó có thể dễ dàng viết như sau: if( !doSomething() ) { puts( "ERROR - doSomething failed" ) ; return ; // or react to failure of doSomething } if( !doSomethingElse() ) { // react to failure of doSomethingElse() }
bobobobo

Tại sao không ... Nhưng sau đó, tôi vẫn thấy doSomething(); doSomethingElse(); ...tốt hơn vì nếu tôi cần thêm if / while / etc. cho các mục đích thực thi thông thường, tôi không muốn chúng bị trộn lẫn với if / while / etc. các câu lệnh được thêm vào cho các mục đích đặc biệt ... Và như quy tắc thực sự về việc sử dụng các ngoại lệ là ném , không bắt , các câu lệnh try / catch thường không xâm lấn.
paercebal

1
Điểm đầu tiên của bạn cho thấy vấn đề với các trường hợp ngoại lệ là gì. Luồng kiểm soát của bạn trở nên kỳ lạ và tách biệt khỏi vấn đề thực tế. Nó đang thay thế một số cấp độ nhận dạng bằng một loạt các sản phẩm đánh bắt. Tôi sẽ sử dụng cả hai, trả về mã (hoặc trả về các đối tượng có nhiều thông tin) cho các lỗi có thể xảy ra và các trường hợp ngoại lệ cho những thứ không mong đợi .
Peter

2
@Peter Weber: Không có gì lạ đâu. Nó được tách ra khỏi vấn đề thực tế vì nó không phải là một phần của quy trình thực thi thông thường . Đó là một cuộc thực hiện đặc biệt . Và sau đó, một lần nữa, điểm về ngoại lệ là, trong trường hợp có lỗi đặc biệt , hãy ném thường xuyênhiếm khi bắt được , nếu có. Vì vậy, các khối bắt thậm chí hiếm khi xuất hiện trong mã.
paercebal

Ví dụ trong loại tranh luận này rất đơn giản. Thông thường, "doSomething ()" hoặc "doSomethingElse ()" thực sự thực hiện một cái gì đó, chẳng hạn như thay đổi trạng thái của một số đối tượng. Mã ngoại lệ không đảm bảo trả đối tượng về trạng thái trước đó và thậm chí ít hơn khi bắt được rất xa lần ném ... Ví dụ, hãy tưởng tượng doSomething được gọi hai lần và tăng một bộ đếm trước khi ném. Làm thế nào để bạn biết khi bắt được ngoại lệ rằng bạn nên giảm một hoặc hai lần? Nói chung, viết mã an toàn ngoại lệ cho bất cứ thứ gì không phải là ví dụ về đồ chơi là rất khó (không thể?).
xryl669 27/09/2016

36

Tôi thực sự sử dụng cả hai.

Tôi sử dụng mã trả lại nếu đó là lỗi đã biết, có thể xảy ra. Nếu đó là một kịch bản mà tôi biết có thể và sẽ xảy ra, thì sẽ có một mã được gửi lại.

Ngoại lệ chỉ được sử dụng cho những thứ mà tôi KHÔNG mong đợi.


+1 cho cách thực hiện rất đơn giản. Ít nhất là nó đủ ngắn để tôi có thể đọc nó rất nhanh. :-)
Ashish Gupta

1
" Ngoại lệ chỉ được sử dụng cho những thứ mà tôi KHÔNG mong đợi. " Nếu bạn không mong đợi chúng, thì tại sao bạn phải làm hoặc làm thế nào bạn có thể sử dụng chúng?
anar khalilov

5
Chỉ vì tôi không mong điều đó xảy ra, không có nghĩa là tôi không thể thấy nó có thể xảy ra như thế nào. Tôi mong đợi Máy chủ SQL của tôi được bật và phản hồi. Nhưng tôi vẫn viết mã các kỳ vọng của mình để có thể thất bại một cách duyên dáng nếu xảy ra thời gian chết không mong muốn.
Stephen Wrighton

Máy chủ SQL không phản hồi sẽ không dễ dàng được phân loại theo "lỗi đã biết, có thể xảy ra"?
tkburbidge

21

Theo Chương 7 có tiêu đề "Ngoại lệ" trong Nguyên tắc Thiết kế Khung: Quy ước, Thành ngữ và Mẫu cho Thư viện .NET có thể tái sử dụng , nhiều lý do được đưa ra cho lý do tại sao việc sử dụng ngoại lệ đối với giá trị trả về là cần thiết cho các khung OO như C #.

Có lẽ đây là lý do thuyết phục nhất (trang 179):

"Các ngoại lệ tích hợp tốt với ngôn ngữ hướng đối tượng. Ngôn ngữ hướng đối tượng có xu hướng áp đặt các ràng buộc đối với chữ ký thành viên không được áp đặt bởi các hàm trong ngôn ngữ không phải OO. Ví dụ: trong trường hợp hàm tạo, quá tải toán tử và thuộc tính, không có lựa chọn nào trong giá trị trả về. Vì lý do này, không thể chuẩn hóa báo cáo lỗi dựa trên giá trị trả về cho các khuôn khổ hướng đối tượng. Một phương pháp báo cáo lỗi, chẳng hạn như ngoại lệ, nằm ngoài phạm vi của chữ ký phương pháp là lựa chọn duy nhất. "


10

Sở thích của tôi (trong C ++ và Python) là sử dụng các ngoại lệ. Các phương tiện được cung cấp bằng ngôn ngữ làm cho nó trở thành một quy trình được xác định rõ ràng để nâng cao, bắt và (nếu cần) loại bỏ các ngoại lệ, làm cho mô hình dễ nhìn và dễ sử dụng. Về mặt khái niệm, nó rõ ràng hơn mã trả lại, trong đó các ngoại lệ cụ thể có thể được xác định bằng tên của chúng và có thông tin bổ sung đi kèm. Với mã trả về, bạn chỉ giới hạn ở giá trị lỗi (trừ khi bạn muốn xác định đối tượng ReturnStatus hoặc thứ gì đó).

Trừ khi mã bạn đang viết là quan trọng về thời gian, chi phí liên quan đến việc mở cuộn ngăn xếp không đủ đáng kể để lo lắng.


2
Hãy nhớ rằng việc sử dụng các ngoại lệ làm cho việc phân tích chương trình khó hơn.
Paweł Hajdan 19-08

7

Ngoại lệ chỉ nên được trả lại khi có điều gì đó xảy ra mà bạn không mong đợi.

Về mặt lịch sử, một điểm ngoại lệ khác là các mã trả về vốn là độc quyền, đôi khi số 0 có thể được trả về từ một hàm C để biểu thị thành công, đôi khi là -1 hoặc một trong hai mã này là thất bại với 1 để thành công. Ngay cả khi chúng được liệt kê, các phép liệt kê có thể mơ hồ.

Các ngoại lệ cũng có thể cung cấp thêm nhiều thông tin và đặc biệt viết rõ 'Đã có điều gì đó sai, đây là thông tin, dấu vết ngăn xếp và một số thông tin hỗ trợ cho ngữ cảnh'

Điều đó đang được nói, một mã trả về được liệt kê tốt có thể hữu ích cho một tập hợp các kết quả đã biết, một 'heres n kết quả đơn giản của hàm, và nó chỉ chạy theo cách này'


6

Trong Java, tôi sử dụng (theo thứ tự sau):

  1. Thiết kế theo hợp đồng (đảm bảo đáp ứng các điều kiện tiên quyết trước khi thử bất cứ điều gì có thể thất bại). Điều này nắm bắt hầu hết mọi thứ và tôi trả lại mã lỗi cho điều này.

  2. Trả lại mã lỗi trong khi xử lý (và thực hiện khôi phục nếu cần).

  3. Những trường hợp ngoại lệ, nhưng những điều này chỉ được sử dụng cho những việc không mong muốn.


1
Sẽ đúng hơn một chút nếu sử dụng xác nhận cho các hợp đồng? Nếu hợp đồng bị phá vỡ, không có gì để cứu bạn.
Paweł Hajdan 19-08

@ PawełHajdan, tôi tin rằng xác nhận bị tắt theo mặc định. Điều này có vấn đề tương tự như assertcông cụ của C ở chỗ nó sẽ không gặp sự cố trong mã sản xuất trừ khi bạn luôn chạy với các xác nhận. Tôi có xu hướng xem xác nhận là một cách để nắm bắt các vấn đề trong quá trình phát triển, nhưng chỉ đối với những thứ sẽ khẳng định hoặc không khẳng định một cách nhất quán (chẳng hạn như những thứ có hằng số, không phải những thứ có biến hoặc bất kỳ thứ gì khác có thể thay đổi trong thời gian chạy).
paxdiablo

Và mười hai năm để trả lời câu hỏi của bạn. Tôi nên điều hành bàn trợ giúp :-)
paxdiablo

6

Đối với tôi hầu như mọi lúc, mã trả lại đều không đạt được bài kiểm tra " Pit of Success ".

  • Thật quá dễ dàng nếu bạn quên kiểm tra mã trả lại và sau đó gặp lỗi red-herring.
  • Mã trả lại không có bất kỳ thông tin gỡ lỗi tuyệt vời nào trên chúng như ngăn xếp cuộc gọi, ngoại lệ bên trong.
  • Mã trả về không phổ biến, cùng với điểm trên, có xu hướng thúc đẩy quá trình ghi nhật ký chẩn đoán đan xen và quá mức thay vì đăng nhập ở một nơi tập trung (trình xử lý ngoại lệ cấp ứng dụng và luồng).
  • Các mã trả về có xu hướng tạo ra mã lộn xộn dưới dạng các khối 'nếu' lồng nhau
  • Thời gian của nhà phát triển đã dành để gỡ lỗi một vấn đề không xác định mà nếu không sẽ là một ngoại lệ hiển nhiên (thành công) thì rất tốn kém.
  • Nếu nhóm đằng sau C # không có ý định cho các ngoại lệ để chi phối luồng điều khiển, thì các ngoại lệ sẽ không được nhập, sẽ không có bộ lọc "khi" trên các câu lệnh catch và sẽ không cần câu lệnh 'ném' ít tham số .

Về hiệu suất:

  • Các trường hợp ngoại lệ có thể tốn kém về mặt tính toán LIÊN QUAN ĐẾN hoàn toàn không ném ra, nhưng chúng được gọi là NGOẠI LỆ là có lý do. So sánh tốc độ luôn quản lý để giả định tỷ lệ ngoại lệ 100%, điều này không bao giờ nên xảy ra. Ngay cả khi một ngoại lệ chậm hơn 100 lần, điều đó thực sự quan trọng bao nhiêu nếu nó chỉ xảy ra 1% thời gian?
  • Trừ khi chúng ta đang nói về số học dấu phẩy động cho các ứng dụng đồ họa hoặc thứ gì đó tương tự, chu kỳ CPU rất rẻ so với thời gian của nhà phát triển.
  • Chi phí từ quan điểm thời gian cũng có lý lẽ tương tự. Liên quan đến các truy vấn cơ sở dữ liệu hoặc cuộc gọi dịch vụ web hoặc tải tệp, thời gian ứng dụng thông thường sẽ giảm thời gian ngoại lệ. Các trường hợp ngoại lệ gần như dưới MICRO giây trong năm 2006
    • Tôi dám bất kỳ ai làm việc trong .net, đặt trình gỡ lỗi của bạn phá vỡ tất cả các ngoại lệ và chỉ vô hiệu hóa mã của tôi và xem có bao nhiêu ngoại lệ đang xảy ra mà bạn thậm chí không biết.

4

Một lời khuyên tuyệt vời mà tôi nhận được từ The Pragmatic Programmer là một điều gì đó dọc theo dòng "chương trình của bạn sẽ có thể thực hiện tất cả các chức năng chính của nó mà không cần sử dụng ngoại lệ".


4
Bạn đang hiểu sai về nó. Ý của họ là "nếu chương trình của bạn ném ra các ngoại lệ trong các luồng thông thường của nó, thì đó là sai". Nói cách khác "chỉ sử dụng ngoại lệ cho những điều ngoại lệ".
Paweł Hajdan 19-08

4

Tôi đã viết một bài blog về điều này một thời gian trước đây.

Hiệu suất của việc ném một ngoại lệ sẽ không đóng bất kỳ vai trò nào trong quyết định của bạn. Rốt cuộc, nếu bạn đang làm đúng, một ngoại lệ là đặc biệt .


Bài đăng trên blog được liên kết thiên về cách nhìn trước khi bạn nhảy (kiểm tra) so với việc dễ dàng yêu cầu sự tha thứ hơn là sự cho phép (ngoại lệ hoặc mã trả lại). Tôi đã trả lời ở đó với suy nghĩ của mình về vấn đề đó (gợi ý: TOCTTOU). Nhưng câu hỏi này là về một vấn đề khác, cụ thể là trong những điều kiện nào để sử dụng cơ chế ngoại lệ của một ngôn ngữ hơn là trả về một giá trị có các thuộc tính đặc biệt.
Damian Yerrick

Tôi hoàn toàn đồng ý. Có vẻ như tôi đã học được một điều hay hai trong chín năm qua;)
Thomas

4

Tôi không thích mã trả lại vì chúng làm cho mẫu sau trở thành nấm trong toàn bộ mã của bạn

CRetType obReturn = CODE_SUCCESS;
obReturn = CallMyFunctionWhichReturnsCodes();
if (obReturn == CODE_BLOW_UP)
{
  // bail out
  goto FunctionExit;
}

Ngay sau đó, một cuộc gọi phương thức bao gồm 4 lời gọi hàm sẽ nổi lên với 12 dòng xử lý lỗi .. Một số trong số đó sẽ không bao giờ xảy ra. Nếu và chuyển đổi trường hợp rất nhiều.

Các ngoại lệ sẽ sạch hơn nếu bạn sử dụng chúng tốt ... để báo hiệu các sự kiện ngoại lệ .. sau đó đường dẫn thực thi không thể tiếp tục. Chúng thường mang tính mô tả và thông tin nhiều hơn là mã lỗi.

Nếu bạn có nhiều trạng thái sau cuộc gọi phương thức cần được xử lý khác nhau (và không phải là trường hợp ngoại lệ), hãy sử dụng mã lỗi hoặc tham số ngoài. Mặc dù Personaly tôi thấy điều này là hiếm ..

Tôi đã tìm hiểu một chút về đối số phân cấp 'hình phạt hiệu suất' .. nhiều hơn nữa trong thế giới C ++ / COM nhưng trong các ngôn ngữ mới hơn, tôi nghĩ sự khác biệt không nhiều lắm. Trong bất kỳ trường hợp nào, khi một thứ gì đó nổ tung, những lo ngại về hiệu suất sẽ được xếp vào hàng hậu bối :)


3

Với bất kỳ ngoại lệ trình biên dịch hoặc thời gian chạy tốt nào không phải chịu một hình phạt đáng kể. Nó ít nhiều giống như một câu lệnh GOTO chuyển đến trình xử lý ngoại lệ. Ngoài ra, việc có các ngoại lệ bị bắt bởi môi trường thời gian chạy (như JVM) giúp cô lập và sửa lỗi dễ dàng hơn rất nhiều. Tôi sẽ thực hiện một NullPointerException trong Java thay vì một segfault trong C bất kỳ ngày nào.


2
Các trường hợp ngoại lệ là cực kỳ tốn kém. Họ phải đi bộ để tìm các trình xử lý ngoại lệ tiềm năng. Chuyến đi bộ này không hề rẻ. Nếu một dấu vết ngăn xếp được tạo, nó thậm chí còn đắt hơn, vì sau đó toàn bộ ngăn xếp phải được phân tích cú pháp.
Công viên Derek

Tôi ngạc nhiên rằng các trình biên dịch không thể xác định nơi mà ngoại lệ sẽ được bắt gặp ít nhất trong một thời gian. Thêm vào đó, thực tế là một ngoại lệ làm thay đổi luồng mã giúp dễ dàng xác định chính xác vị trí xảy ra lỗi hơn, theo ý kiến ​​của tôi, sẽ tạo ra một hình phạt hiệu suất.
Kyle Cronin 19-08

Ngăn xếp cuộc gọi có thể trở nên cực kỳ phức tạp trong thời gian chạy và các trình biên dịch thường không thực hiện loại phân tích đó. Ngay cả khi họ đã làm vậy, bạn vẫn phải đi bộ để tìm dấu vết. Bạn cũng sẽ phải giải phóng ngăn xếp để xử lý finallycác khối và trình hủy cho các đối tượng được phân bổ ngăn xếp.
Công viên Derek 19-08

Tuy nhiên, tôi đồng ý rằng lợi ích gỡ lỗi của các ngoại lệ thường bù đắp cho chi phí hiệu suất.
Công viên Derek

1
Công viên Derak, ngoại lệ là tốn kém khi chúng xảy ra. Đây là lý do không nên quá lạm dụng chúng. Nhưng khi chúng không xảy ra, chúng hầu như không tốn kém gì.
paercebal 21-08

3

Tôi có một bộ quy tắc đơn giản:

1) Sử dụng mã trả lại cho những thứ bạn mong đợi người gọi ngay lập tức phản ứng.

2) Sử dụng ngoại lệ cho các lỗi có phạm vi rộng hơn và có thể hợp lý được dự kiến ​​sẽ được xử lý bởi một thứ gì đó ở nhiều cấp trên người gọi để nhận thức về lỗi không phải thấm qua nhiều lớp, làm cho mã phức tạp hơn.

Trong Java, tôi chỉ sử dụng các ngoại lệ không được kiểm tra, các ngoại lệ được kiểm tra cuối cùng chỉ là một dạng mã trả lại khác và theo kinh nghiệm của tôi, tính hai mặt của những gì có thể được "trả về" bởi một cuộc gọi phương thức thường là một trở ngại hơn là một trợ giúp.


3

Tôi sử dụng Exceptions trong python trong cả trường hợp Ngoại lệ và không Ngoại lệ.

Thông thường, rất hay khi có thể sử dụng Ngoại lệ để chỉ ra "yêu cầu không thể được thực hiện", trái ngược với việc trả về giá trị Lỗi. Nó có nghĩa là bạn / luôn luôn / biết rằng giá trị trả về là loại phù hợp, thay vì tùy tiện là None hoặc NotFoundSingleton hoặc thứ gì đó. Đây là một ví dụ điển hình về việc tôi muốn sử dụng trình xử lý ngoại lệ thay vì điều kiện đối với giá trị trả về.

try:
    dataobj = datastore.fetch(obj_id)
except LookupError:
    # could not find object, create it.
    dataobj = datastore.create(....)

Tác dụng phụ là khi chạy datastore.fetch (obj_id), bạn không bao giờ phải kiểm tra xem giá trị trả về của nó có phải là Không, bạn sẽ nhận được lỗi đó ngay lập tức miễn phí. Điều này trái ngược với lập luận, "chương trình của bạn phải có thể thực hiện tất cả các chức năng chính của nó mà không cần sử dụng ngoại lệ".

Đây là một ví dụ khác về nơi các ngoại lệ 'đặc biệt' hữu ích, để viết mã xử lý hệ thống tệp không tuân theo các điều kiện chủng tộc.

# wrong way:
if os.path.exists(directory_to_remove):
    # race condition is here.
    os.path.rmdir(directory_to_remove)

# right way:
try: 
    os.path.rmdir(directory_to_remove)
except OSError:
    # directory didn't exist, good.
    pass

Một cuộc gọi hệ thống thay vì hai, không có điều kiện chủng tộc. Đây là một ví dụ kém vì rõ ràng điều này sẽ không thành công với OSError trong nhiều trường hợp hơn là thư mục không tồn tại, nhưng đó là một giải pháp 'đủ tốt' cho nhiều trường hợp được kiểm soát chặt chẽ.


Ví dụ thứ hai là gây hiểu lầm. Được cho là sai cách là sai vì mã os.path.rmdir được thiết kế để ném ngoại lệ. Triển khai đúng trong luồng mã trả lại sẽ là 'if rmdir (...) == FAILED: pass'
MaR

3

Tôi tin rằng các mã trả lại làm tăng thêm nhiễu mã. Ví dụ, tôi luôn ghét giao diện của mã COM / ATL do các mã trả về. Phải kiểm tra HRESULT cho mọi dòng mã. Tôi coi mã trả về lỗi là một trong những quyết định tồi của các kiến ​​trúc sư của COM. Nó làm cho việc phân nhóm mã hợp lý trở nên khó khăn, do đó việc xem xét mã trở nên khó khăn.

Tôi không chắc chắn về việc so sánh hiệu suất khi có kiểm tra rõ ràng cho mã trả lại mỗi dòng.


1
COM wad được thiết kế để có thể sử dụng được bằng các ngôn ngữ không hỗ trợ ngoại lệ.
Kevin 19-08

Đó là một điểm tốt. Nó có ý nghĩa khi đối phó với mã lỗi cho ngôn ngữ kịch bản. Ít nhất VB6 cũng che giấu tốt các chi tiết mã lỗi bằng cách đóng gói chúng trong đối tượng Err, điều này phần nào giúp mã sạch hơn.
rpattabi 20/09/08

Tôi không đồng ý: VB6 chỉ ghi lại lỗi cuối cùng. Kết hợp với "tiếp tục lỗi tiếp theo" khét tiếng, bạn sẽ hoàn toàn bỏ lỡ nguồn gốc của vấn đề, vào thời điểm bạn nhìn thấy nó. Lưu ý rằng đây là cơ sở để xử lý lỗi trong Win32 APII (xem chức năng GetLastError)
paercebal

2

Tôi thích sử dụng các ngoại lệ để xử lý lỗi và trả về giá trị (hoặc tham số) như là kết quả bình thường của một hàm. Điều này mang lại một sơ đồ xử lý lỗi dễ dàng và nhất quán và nếu được thực hiện đúng, nó sẽ tạo ra mã trông sạch hơn nhiều.


2

Một trong những điểm khác biệt lớn là các ngoại lệ buộc bạn phải xử lý lỗi, trong khi các mã trả về lỗi có thể không được chọn.

Mã trả về lỗi, nếu được sử dụng nhiều, cũng có thể gây ra mã rất xấu với nhiều nếu kiểm tra tương tự như biểu mẫu này:

if(function(call) != ERROR_CODE) {
    do_right_thing();
}
else {
    handle_error();
}

Cá nhân tôi thích sử dụng ngoại lệ cho các lỗi NÊN hoặc PHẢI được xử lý bởi mã gọi và chỉ sử dụng mã lỗi cho "lỗi dự kiến" trong đó trả về một cái gì đó thực sự hợp lệ và có thể.


Ít nhất trong C / C ++ và gcc, bạn có thể cung cấp cho hàm một thuộc tính sẽ tạo ra cảnh báo khi giá trị trả về của nó bị bỏ qua.
Paweł Hajdan 19-08

phjr: Mặc dù tôi không đồng ý với mẫu "mã lỗi trả về", nhận xét của bạn có lẽ nên trở thành câu trả lời đầy đủ. Tôi thấy nó đủ thú vị. Ít nhất, nó đã cung cấp cho tôi một thông tin hữu ích.
paercebal 21-08

2

Có nhiều lý do để thích Ngoại lệ hơn mã trả lại:

  • Thông thường, để dễ đọc, mọi người cố gắng giảm thiểu số lượng câu lệnh trả về trong một phương thức. Làm như vậy, các trường hợp ngoại lệ sẽ ngăn không cho thực hiện thêm một số công việc khi ở trạng thái không hoạt động, và do đó ngăn chặn khả năng làm hỏng nhiều dữ liệu hơn.
  • Các ngoại lệ thường dài hơn arn dễ mở rộng hơn giá trị trả về. Giả sử rằng một phương thức trả về số tự nhiên và bạn sử dụng số âm làm mã trả về khi xảy ra lỗi, nếu phạm vi phương thức của bạn thay đổi và bây giờ trả về số nguyên, bạn sẽ phải sửa đổi tất cả các lệnh gọi phương thức thay vì chỉ chỉnh sửa một chút sự ngoại lệ.
  • Các ngoại lệ cho phép dễ dàng hơn trong việc phân tách việc xử lý lỗi đối với hành vi bình thường. Chúng cho phép đảm bảo rằng một số hoạt động thực hiện bằng cách nào đó như một hoạt động nguyên tử.

2

Ngoại lệ không dành cho xử lý lỗi, IMO. Ngoại lệ chỉ có vậy; sự kiện đặc biệt mà bạn không mong đợi. Sử dụng một cách thận trọng, tôi nói.

Mã lỗi có thể ổn, nhưng trả về 404 hoặc 200 từ một phương thức là không hợp lệ, IMO. Thay vào đó, hãy sử dụng enums (.Net) để làm cho mã dễ đọc hơn và dễ sử dụng hơn cho các nhà phát triển khác. Ngoài ra, bạn không cần phải duy trì một bảng trên các con số và mô tả.

Cũng thế; mô hình thử-bắt-cuối cùng là một mô hình phản đối trong sách của tôi. Cố gắng cuối cùng có thể tốt, cố gắng nắm bắt cũng có thể tốt nhưng cố gắng nắm bắt cuối cùng không bao giờ tốt. try-last thường có thể được thay thế bằng câu lệnh "using" (mẫu IDispose), là IMO tốt hơn. Và Hãy thử bắt khi bạn thực sự bắt được một ngoại lệ mà bạn có thể xử lý là tốt, hoặc nếu bạn làm điều này:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Vì vậy, miễn là bạn để ngoại lệ tiếp tục bong bóng là OK. Một ví dụ khác là:

try{
    dbHasBeenUpdated = db.UpdateAll(somevalue); // true/false
}
catch (ConnectionException ex) {
    logger.Exception(ex, "Connection failed");
    dbHasBeenUpdated = false;
}

Ở đây tôi thực sự xử lý ngoại lệ; những gì tôi làm ngoài lần thử bắt khi phương pháp cập nhật không thành công là một câu chuyện khác, nhưng tôi nghĩ rằng quan điểm của tôi đã được thực hiện. :)

Tại sao try-catch-cuối cùng lại là anti-pattern? Đây là lý do tại sao:

try{
    db.UpdateAll(somevalue);
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}
finally {
    db.Close();
}

Điều gì xảy ra nếu đối tượng db đã được đóng? Một ngoại lệ mới được ném ra và nó phải được xử lý! Điều này tốt hơn:

try{
    using(IDatabase db = DatabaseFactory.CreateDatabase()) {
        db.UpdateAll(somevalue);
    }
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Hoặc, nếu đối tượng db không triển khai IDisposable, hãy làm như sau:

try{
    try {
        IDatabase db = DatabaseFactory.CreateDatabase();
        db.UpdateAll(somevalue);
    }
    finally{
        db.Close();
    }
}
catch (DatabaseAlreadyClosedException dbClosedEx) {
    logger.Exception(dbClosedEx, "Database connection was closed already.");
}
catch (Exception ex) {
    logger.Exception(ex, "UpdateAll method failed");
    throw;
}

Đó là 2 xu của tôi dù sao! :)


Nó sẽ là kỳ lạ nếu một đối tượng có .Close () nhưng không có .Dispose ()
abatishchev

Tôi chỉ sử dụng Close () làm ví dụ. Hãy nghĩ về nó như một cái gì đó khác. Như tôi nói; nên sử dụng mẫu sử dụng (doh!) nếu có. Điều này tất nhiên ngụ ý rằng lớp thực hiện IDisposable và như vậy bạn có thể gọi Dispose.
noocyte

1

Tôi chỉ sử dụng ngoại lệ, không có mã trả lại. Tôi đang nói về Java ở đây.

Nguyên tắc chung mà tôi tuân theo là nếu tôi có một phương thức được gọi doFoo()thì nó sẽ theo sau rằng nếu nó không "do foo", như nó đã xảy ra, thì một điều gì đó đặc biệt đã xảy ra và một Ngoại lệ sẽ được ném ra.


1

Một điều tôi lo sợ về các ngoại lệ là việc ném một ngoại lệ sẽ làm hỏng dòng mã. Ví dụ nếu bạn làm

void foo()
{
  MyPointer* p = NULL;
  try{
    p = new PointedStuff();
    //I'm a module user and  I'm doing stuff that might throw or not

  }
  catch(...)
  {
    //should I delete the pointer?
  }
}

Hoặc thậm chí tệ hơn nếu tôi xóa một cái gì đó mà tôi không nên có, nhưng lại bị bắt trước khi tôi thực hiện phần còn lại của quá trình dọn dẹp. Ném đặt rất nhiều trọng lượng lên người dùng kém IMHO.


Đây là câu lệnh <code> cuối cùng </code> dùng để làm gì. Nhưng, than ôi, nó không có trong tiêu chuẩn C ++ ...
Thomas

Trong C ++, bạn nên tuân thủ quy tắc ngón tay cái "Thu thập tài nguyên trong construcotor và giải phóng chúng trong Destucotor. Đối với trường hợp cụ thể này, auto_ptr sẽ thực hiện hoàn hảo.
Serge

Thomas, bạn sai rồi. C ++ đã không cuối cùng vì nó không cần nó. Nó có RAII để thay thế. Giải pháp của Serge là một trong những giải pháp sử dụng RAII.
paercebal 21/09/08

Robert, hãy sử dụng giải pháp của Serge, và bạn sẽ thấy vấn đề của mình sẽ biến mất. Bây giờ, nếu bạn viết nhiều thử / bắt hơn ném, thì (bằng cách đánh giá nhận xét của bạn) có lẽ bạn có vấn đề trong mã của mình. Tất nhiên, việc sử dụng catch (...) mà không ném lại thường là không tốt, vì nó ẩn lỗi để tốt hơn là bỏ qua nó.
paercebal 21-08

1

Quy tắc chung của tôi trong đối số mã trả về ngoại lệ:

  • Sử dụng mã lỗi khi bạn cần bản địa hóa / quốc tế hóa - trong .NET, bạn có thể sử dụng các mã lỗi này để tham chiếu đến tệp tài nguyên mà sau đó sẽ hiển thị lỗi bằng ngôn ngữ thích hợp. Nếu không, hãy sử dụng các ngoại lệ
  • Chỉ sử dụng ngoại lệ cho các lỗi thực sự đặc biệt. Nếu đó là điều gì đó xảy ra khá thường xuyên, hãy sử dụng mã lỗi boolean hoặc enum.

Không có lý do gì trong khi bạn không thể sử dụng một ngoại lệ khi bạn làm l10n / i18n. Các trường hợp ngoại lệ cũng có thể chứa thông tin bản địa hóa.
gizmo

1

Tôi không thấy mã trả lại ít xấu hơn các trường hợp ngoại lệ. Với ngoại lệ, bạn có ở try{} catch() {} finally {}đâu cũng như với mã trả lại bạn có if(){}. Tôi từng sợ ngoại lệ vì những lý do được đưa ra trong bài đăng; bạn không biết nếu con trỏ cần được xóa, những gì bạn có. Nhưng tôi nghĩ rằng bạn có cùng một vấn đề khi nói đến mã trả lại. Bạn không biết trạng thái của các tham số trừ khi bạn biết một số chi tiết về hàm / phương thức được đề cập.

Bất kể, bạn phải xử lý lỗi nếu có thể. Bạn có thể dễ dàng để một ngoại lệ truyền lên cấp cao nhất như bỏ qua mã trả về và để chương trình mặc định.

Tôi thích ý tưởng trả về một giá trị (liệt kê?) Cho các kết quả và một ngoại lệ cho một trường hợp ngoại lệ.


0

Đối với một ngôn ngữ như Java, tôi sẽ sử dụng Ngoại lệ vì trình biên dịch đưa ra lỗi thời gian biên dịch nếu các ngoại lệ không được xử lý. Điều này buộc hàm gọi phải xử lý / ném các ngoại lệ.

Đối với Python, tôi thấy mâu thuẫn hơn. Không có trình biên dịch nên có thể người gọi không xử lý ngoại lệ do hàm ném ra dẫn đến ngoại lệ thời gian chạy. Nếu bạn sử dụng mã trả lại, bạn có thể có hành vi không mong muốn nếu không được xử lý đúng cách và nếu bạn sử dụng ngoại lệ, bạn có thể nhận được ngoại lệ thời gian chạy.


0

Tôi thường thích mã trả lại hơn vì chúng cho phép người gọi quyết định xem lỗi có phải là ngoại lệ hay không .

Cách tiếp cận này là điển hình trong ngôn ngữ Elixir.

# I care whether this succeeds. If it doesn't return :ok, raise an exception.
:ok = File.write(path, content)

# I don't care whether this succeeds. Don't check the return value.
File.write(path, content)

# This had better not succeed - the path should be read-only to me.
# If I get anything other than this error, raise an exception.
{:error, :erofs} = File.write(path, content)

# I want this to succeed but I can handle its failure
case File.write(path, content) do
  :ok => handle_success()
  error => handle_error(error)
end

Mọi người đã đề cập rằng mã trả lại có thể khiến bạn có nhiều ifcâu lệnh lồng nhau , nhưng điều đó có thể được xử lý bằng cú pháp tốt hơn. Trong Elixir, withcâu lệnh cho phép chúng ta dễ dàng tách một loạt giá trị trả về đường dẫn hạnh phúc khỏi bất kỳ lỗi nào.

with {:ok, content} <- get_content(),
  :ok <- File.write(path, content) do
    IO.puts "everything worked, happy path code goes here"
else
  # Here we can use a single catch-all failure clause
  # or match every kind of failure individually
  # or match subsets of them however we like
  _some_error => IO.puts "one of those steps failed"
  _other_error => IO.puts "one of those steps failed"
end

Elixir vẫn có các chức năng nâng cao ngoại lệ. Quay trở lại ví dụ đầu tiên của tôi, tôi có thể thực hiện một trong hai cách này để đưa ra một ngoại lệ nếu tệp không thể được ghi.

# Raises a generic MatchError because the return value isn't :ok
:ok = File.write(path, content)

# Raises a File.Error with a descriptive error message - eg, saying
# that the file is read-only
File.write!(path, content)

Nếu tôi, với tư cách là người gọi, biết rằng tôi muốn thông báo lỗi nếu ghi không thành công, tôi có thể chọn gọi File.write!thay thế File.write. Hoặc tôi có thể chọn cách gọi điện File.writevà xử lý từng lý do có thể gây ra lỗi khác nhau.

Tất nhiên luôn có thể có rescuemột ngoại lệ nếu chúng ta muốn. Nhưng so với việc xử lý một giá trị trả về đầy đủ thông tin, thì nó có vẻ khó xử đối với tôi. Nếu tôi biết rằng một lệnh gọi hàm có thể không thành công hoặc thậm chí sẽ thất bại, thì lỗi của nó không phải là một trường hợp ngoại lệ.

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.