Bạn nên sử dụng cả hai. Vấn đề là quyết định khi nào nên sử dụng mỗi người .
Có một vài tình huống trong đó các trường hợp ngoại lệ là sự lựa chọn rõ ràng :
Trong một số trường hợp, bạn không thể làm gì với mã lỗi và bạn chỉ cần xử lý nó ở mức cao hơn trong ngăn xếp cuộc gọi , thường chỉ cần ghi nhật ký lỗi, hiển thị nội dung nào đó cho người dùng hoặc đóng chương trình. Trong những trường hợp này, mã lỗi sẽ yêu cầu bạn tạo ra các mã lỗi theo cấp độ theo cách thủ công, điều này rõ ràng dễ thực hiện hơn với các ngoại lệ. Vấn đề là điều này dành cho những tình huống bất ngờ và không thể giải quyết được .
Tuy nhiên, về tình huống 1 (khi có điều gì đó bất ngờ và không thể xử lý được xảy ra, bạn sẽ không đăng nhập nó), ngoại lệ có thể hữu ích vì bạn có thể thêm thông tin theo ngữ cảnh . Ví dụ: nếu tôi nhận được SqlException trong trình trợ giúp dữ liệu cấp thấp hơn, tôi sẽ muốn bắt lỗi đó ở cấp độ thấp (nơi tôi biết lệnh SQL gây ra lỗi) để tôi có thể nắm bắt thông tin đó và suy nghĩ lại với thông tin bổ sung . Xin lưu ý từ ma thuật ở đây: nghĩ lại , và không nuốt .
Nguyên tắc đầu tiên của xử lý ngoại lệ: không nuốt ngoại lệ . Ngoài ra, lưu ý rằng sản phẩm khai thác bên trong của tôi không cần phải ghi nhật ký bất cứ thứ gì vì sản phẩm khai thác bên ngoài sẽ có toàn bộ dấu vết ngăn xếp và có thể ghi nhật ký.
Trong một số trường hợp, bạn có một chuỗi lệnh và nếu bất kỳ lệnh nào thất bại, bạn nên dọn dẹp / xử lý tài nguyên (*), cho dù đây có phải là tình huống không thể phục hồi (nên được ném) hoặc tình huống có thể phục hồi (trong trường hợp đó bạn có thể xử lý cục bộ hoặc trong mã người gọi nhưng bạn không cần ngoại lệ). Rõ ràng việc đặt tất cả các lệnh đó trong một lần thử dễ dàng hơn nhiều, thay vì kiểm tra mã lỗi sau mỗi phương thức và dọn dẹp / xử lý trong khối cuối cùng. Xin lưu ý rằng nếu bạn muốn lỗi nổi lên (có thể là điều bạn muốn), bạn thậm chí không cần phải bắt lỗi - bạn chỉ cần sử dụng cuối cùng để dọn dẹp / xử lý - bạn chỉ nên sử dụng bắt / rút tiền nếu muốn để thêm thông tin theo ngữ cảnh (xem đạn 2).
Một ví dụ sẽ là một chuỗi các câu lệnh SQL bên trong một khối giao dịch. Một lần nữa, đây cũng là một tình huống "không thể giải quyết", ngay cả khi bạn quyết định bắt sớm (xử lý cục bộ thay vì sủi bọt lên trên đỉnh), đó vẫn là một tình huống nghiêm trọng từ đó kết quả tốt nhất là hủy bỏ mọi thứ hoặc ít nhất là hủy bỏ một lượng lớn một phần của quá trình.
(*) Điều này giống như cái on error goto
mà chúng ta đã sử dụng trong Visual Basic cũ
Trong constructor bạn chỉ có thể ném ngoại lệ.
Phải nói rằng, trong tất cả các tình huống khác khi bạn trả lại một số thông tin mà người gọi CÓ THỂ / NÊN thực hiện một số hành động , sử dụng mã trả lại có lẽ là một cách thay thế tốt hơn. Điều này bao gồm tất cả các "lỗi" dự kiến , bởi vì có lẽ chúng nên được xử lý bởi người gọi ngay lập tức và sẽ khó có thể được đưa lên quá nhiều cấp độ trong ngăn xếp.
Tất nhiên, luôn có thể coi các lỗi dự kiến là ngoại lệ và bắt ngay lập tức một cấp ở trên và cũng có thể bao gồm mọi dòng mã trong một lần thử và thực hiện các hành động cho từng lỗi có thể. IMO, đây là thiết kế tồi, không chỉ vì nó dài dòng hơn, mà đặc biệt bởi vì các ngoại lệ có thể bị ném không rõ ràng nếu không đọc mã nguồn - và ngoại lệ có thể được ném từ bất kỳ phương thức sâu nào, tạo ra các hình ảnh vô hình . Chúng phá vỡ cấu trúc mã bằng cách tạo nhiều điểm thoát vô hình khiến mã khó đọc và kiểm tra. Nói cách khác, bạn không bao giờ nên sử dụng ngoại lệ làm kiểm soát luồng , bởi vì đó sẽ là khó để người khác hiểu và duy trì. Thậm chí có thể khó hiểu tất cả các dòng mã có thể để thử nghiệm.
Một lần nữa: để dọn dẹp / vứt bỏ chính xác, bạn có thể sử dụng thử-cuối cùng mà không bắt được gì .
Những lời chỉ trích phổ biến nhất về mã trả về là "ai đó có thể bỏ qua mã lỗi, nhưng theo nghĩa tương tự, ai đó cũng có thể nuốt ngoại lệ. Xử lý ngoại lệ xấu dễ dàng trong cả hai phương pháp. Nhưng viết chương trình dựa trên mã lỗi vẫn dễ dàng hơn nhiều hơn là viết một chương trình dựa trên ngoại lệ . Và nếu vì một lý do nào đó quyết định bỏ qua tất cả các lỗi (cũ on error resume next
), bạn có thể dễ dàng làm điều đó với mã trả về và bạn không thể làm điều đó mà không cần nhiều bản tóm tắt thử.
Lời chỉ trích phổ biến thứ hai về mã trả lại là "rất khó để nổi bong bóng" - nhưng đó là vì mọi người không hiểu rằng các ngoại lệ dành cho các tình huống không thể phục hồi, trong khi mã lỗi thì không.
Quyết định giữa các ngoại lệ và mã lỗi là một khu vực màu xám. Thậm chí có khả năng bạn cần lấy mã lỗi từ một số phương thức kinh doanh có thể sử dụng lại, và sau đó bạn quyết định bọc nó thành một ngoại lệ (có thể thêm thông tin) và để nó nổi lên. Nhưng đó là một lỗi thiết kế khi cho rằng TẤT CẢ các lỗi nên được đưa ra làm ngoại lệ.
Tóm lại:
Tôi thích sử dụng các ngoại lệ khi tôi gặp tình huống không mong muốn, trong đó không có nhiều việc phải làm và thông thường chúng tôi muốn hủy bỏ một khối mã lớn hoặc thậm chí toàn bộ hoạt động hoặc chương trình. Điều này giống như "lỗi goto" cũ.
Tôi thích sử dụng mã trả về khi tôi gặp các tình huống dự kiến trong đó mã người gọi có thể / nên thực hiện một số hành động. Điều này bao gồm hầu hết các phương thức kinh doanh, API, xác nhận, v.v.
Sự khác biệt giữa ngoại lệ và mã lỗi này là một trong những nguyên tắc thiết kế của ngôn ngữ GO, sử dụng "hoảng loạn" cho các tình huống bất ngờ gây tử vong, trong khi các tình huống dự kiến thông thường được trả về là lỗi.
Tuy nhiên, về GO, nó cũng cho phép nhiều giá trị trả về , đây là điều giúp ích rất nhiều cho việc sử dụng mã trả về, vì bạn có thể đồng thời trả lại lỗi và một thứ khác. Trên C # / Java, chúng ta có thể đạt được điều đó với các tham số, Tuples hoặc Generics (yêu thích của tôi), kết hợp với enums có thể cung cấp mã lỗi rõ ràng cho người gọi:
public MethodResult<CreateOrderResultCodeEnum, Order> CreateOrder(CreateOrderOptions options)
{
....
return MethodResult<CreateOrderResultCodeEnum>.CreateError(CreateOrderResultCodeEnum.NO_DELIVERY_AVAILABLE, "There is no delivery service in your area");
...
return MethodResult<CreateOrderResultCodeEnum>.CreateSuccess(CreateOrderResultCodeEnum.SUCCESS, order);
}
var result = CreateOrder(options);
if (result.ResultCode == CreateOrderResultCodeEnum.OUT_OF_STOCK)
// do something
else if (result.ResultCode == CreateOrderResultCodeEnum.SUCCESS)
order = result.Entity; // etc...
Nếu tôi thêm một lợi nhuận mới có thể có trong phương thức của mình, tôi thậm chí có thể kiểm tra tất cả người gọi nếu họ đang bao gồm giá trị mới đó trong câu lệnh chuyển đổi chẳng hạn. Bạn thực sự không thể làm điều đó với ngoại lệ. Khi bạn sử dụng mã trả về, bạn thường sẽ biết trước tất cả các lỗi có thể xảy ra và kiểm tra chúng. Với ngoại lệ, bạn thường không biết điều gì có thể xảy ra. Gói enum bên trong các ngoại lệ (thay vì Generics) là một cách thay thế (miễn là nó rõ ràng loại ngoại lệ mà mỗi phương thức sẽ đưa ra), nhưng IMO vẫn là thiết kế tồi.