Có lý do chính đáng để trả lại các đối tượng ngoại lệ thay vì ném chúng?


45

Câu hỏi này nhằm áp dụng cho bất kỳ ngôn ngữ lập trình OO nào hỗ trợ xử lý ngoại lệ; Tôi đang sử dụng C # cho mục đích minh họa.

Các ngoại lệ thường được nêu ra khi có vấn đề phát sinh mà mã không thể xử lý ngay lập tức và sau đó bị bắt trong một catchmệnh đề ở một vị trí khác (thường là khung ngăn xếp bên ngoài).

H: Có bất kỳ tình huống hợp pháp nào mà các ngoại lệ không bị ném và bị bắt, mà chỉ được trả về từ một phương thức và sau đó được chuyển qua dưới dạng các đối tượng lỗi không?

Câu hỏi này được đặt ra cho tôi bởi vì System.IObserver<T>.OnErrorphương pháp của .NET 4 gợi ý rằng: các ngoại lệ được truyền xung quanh dưới dạng các đối tượng lỗi.

Hãy xem xét một kịch bản khác, xác nhận. Giả sử tôi đang theo sự khôn ngoan thông thường và do đó tôi đang phân biệt giữa một loại đối tượng lỗi IValidationErrorvà một loại ngoại lệ riêng biệt ValidationExceptionđược sử dụng để báo cáo các lỗi không mong muốn:

partial interface IValidationError { }

abstract partial class ValidationException : System.Exception
{
    public abstract IValidationError[] ValidationErrors { get; }
}

(Không System.Component.DataAnnotationsgian tên làm một cái gì đó khá giống nhau.)

Những loại này có thể được sử dụng như sau:

partial interface IFoo { }  // an immutable type

partial interface IFooBuilder  // mutable counterpart to prepare instances of above type
{
    bool IsValid(out IValidationError[] validationErrors);  // true if no validation error occurs
    IFoo Build();  // throws ValidationException if !IsValid(…)
}

Bây giờ tôi đang tự hỏi, tôi có thể không đơn giản hóa những điều trên để này:

partial class ValidationError : System.Exception { }  // = IValidationError + ValidationException

partial interface IFoo { }  // (unchanged)

partial interface IFooBuilder
{
    bool IsValid(out ValidationError[] validationErrors);
    IFoo Build();  // may throw ValidationError or sth. like AggregateException<ValidationError>
}

Q: Những lợi thế và bất lợi của hai phương pháp khác nhau này là gì?


Nếu bạn đang tìm kiếm hai câu trả lời riêng biệt, bạn nên hỏi hai câu hỏi riêng biệt. Kết hợp chúng chỉ làm cho nó khó trả lời hơn nhiều.
Bobson

2
@Bobson: Tôi thấy hai câu hỏi này là bổ sung: Câu hỏi trước được cho là xác định bối cảnh chung của vấn đề theo nghĩa rộng, trong khi câu hỏi sau yêu cầu thông tin cụ thể cho phép người đọc tự rút ra kết luận. Một câu trả lời không phải đưa ra câu trả lời rõ ràng, riêng biệt cho cả hai câu hỏi; câu trả lời "ở giữa" cũng được chào đón.
stakx

5
Tôi không rõ ràng về điều gì đó: lý do dẫn đến Xác thựcError từ Ngoại lệ là gì, nếu bạn sẽ không ném nó?
pdr

@pdr: Ý bạn là trong trường hợp ném AggregateExceptionthay thế? Điểm tốt.
stakx

3
Không hẳn vậy. Ý tôi là nếu bạn định ném nó, hãy sử dụng một ngoại lệ. Nếu bạn định trả lại nó, hãy sử dụng một đối tượng lỗi. Nếu bạn đã dự kiến ​​các ngoại lệ thực sự có lỗi, hãy bắt chúng và đưa chúng vào đối tượng lỗi của bạn. Việc một trong hai có cho phép nhiều lỗi hay không hoàn toàn không liên quan.
pdr

Câu trả lời:


31

Có bất kỳ tình huống hợp pháp nào mà các ngoại lệ không bị ném và bị bắt, mà chỉ được trả về từ một phương thức và sau đó được chuyển qua dưới dạng các đối tượng lỗi không?

Nếu nó không bao giờ được ném thì đó không phải là ngoại lệ. Nó là một đối tượng derivedtừ một lớp Exception, mặc dù nó không tuân theo hành vi. Gọi nó là một ngoại lệ hoàn toàn là ngữ nghĩa vào thời điểm này, nhưng tôi thấy không có gì sai khi không ném nó. Từ quan điểm của tôi, không ném một ngoại lệ là điều tương tự chính xác như một chức năng bên trong bắt nó và ngăn không cho nó lan truyền.

Có hợp lệ cho một hàm để trả về các đối tượng ngoại lệ không?

Chắc chắn rồi. Dưới đây là danh sách ngắn các ví dụ có thể phù hợp:

  • Một nhà máy ngoại lệ.
  • Một đối tượng ngữ cảnh báo cáo nếu có lỗi trước đó là ngoại lệ sẵn sàng sử dụng.
  • Một chức năng giữ một ngoại lệ bị bắt trước đó.
  • API của bên thứ ba tạo ngoại lệ của loại bên trong.

Không phải là ném nó xấu?

Ném một ngoại lệ là một kiểu như: "Đi tù. Đừng vượt qua Go!" trong trò chơi hội đồng độc quyền. Nó báo cho trình biên dịch nhảy qua tất cả mã nguồn cho đến khi bắt mà không thực hiện bất kỳ mã nguồn nào. Nó không có gì để làm với lỗi, báo cáo hoặc dừng lỗi. Tôi thích nghĩ về việc ném một ngoại lệ như một câu lệnh "siêu lợi nhuận" cho một hàm. Nó trả về việc thực thi mã đến một nơi nào đó cao hơn nhiều so với người gọi ban đầu.

Điều quan trọng ở đây là phải hiểu rằng giá trị thực của các ngoại lệ nằm trong try/catchmẫu chứ không phải là sự khởi tạo của đối tượng ngoại lệ. Đối tượng ngoại lệ chỉ là một thông điệp trạng thái.

Trong câu hỏi của bạn, bạn dường như bối rối trong việc sử dụng hai điều này: nhảy sang xử lý ngoại lệ và lỗi nêu trạng thái ngoại lệ. Chỉ vì bạn đã lấy trạng thái lỗi và gói nó trong một ngoại lệ không có nghĩa là bạn đang theo try/catchmô hình hoặc lợi ích của nó.


4
"Nếu nó không bao giờ bị ném thì đó không phải là ngoại lệ. Nó là một đối tượng xuất phát từ một Exceptionlớp nhưng không tuân theo hành vi." - Và đó chính xác là vấn đề: Có hợp pháp không khi sử dụng một Exceptionđối tượng được tạo ra trong tình huống mà nó sẽ không bị ném, bởi nhà sản xuất của đối tượng, cũng như bất kỳ phương thức gọi nào; nơi mà nó chỉ đóng vai trò là một "thông điệp trạng thái" được truyền qua, mà không có luồng điều khiển "siêu lợi nhuận"? Hoặc tôi đang vi phạm một hợp đồng bất thành văn try/ catch(Exception)hợp đồng nghiêm trọng đến mức tôi không bao giờ nên làm điều này, và thay vào đó sử dụng một loại riêng biệt, không Exceptionloại?
stakx

10
Các lập trình viên @stakx đã và sẽ tiếp tục làm những việc gây nhầm lẫn cho các lập trình viên khác, nhưng không đúng về mặt kỹ thuật. Đó là lý do tại sao tôi nói đó là ngữ nghĩa. Câu trả lời pháp lý là Nobạn không phá vỡ bất kỳ hợp đồng nào. Điều popular opinionđó sẽ là bạn không nên làm điều đó. Lập trình có đầy đủ các ví dụ trong đó mã nguồn của bạn không làm gì sai nhưng mọi người đều không thích nó. Mã nguồn phải luôn được viết để nó rõ ràng mục đích là gì. Bạn có thực sự giúp đỡ một lập trình viên khác bằng cách sử dụng một ngoại lệ theo cách đó không? Nếu có, sau đó làm như vậy. Nếu không, đừng ngạc nhiên nếu nó không thích.
Phản ứng

@cgTag Việc bạn sử dụng các khối mã nội tuyến cho chữ nghiêng khiến não tôi bị tổn thương ..
Frayt

51

Trả lại các ngoại lệ thay vì ném chúng có thể có ý nghĩa về ngữ nghĩa khi bạn có phương pháp trợ giúp để phân tích tình huống và trả lại một ngoại lệ thích hợp mà người gọi ném ra (bạn có thể gọi đây là "nhà máy ngoại lệ"). Ném một ngoại lệ trong hàm phân tích lỗi này có nghĩa là có lỗi xảy ra trong quá trình phân tích, trong khi trả về một ngoại lệ có nghĩa là loại lỗi được phân tích thành công.

Một trường hợp sử dụng có thể có thể là một hàm biến mã phản hồi HTTP thành ngoại lệ:

Exception analyzeHttpError(int errorCode) {
    if (errorCode < 400) {
         throw new NotAnErrorException();
    }
    switch (errorCode) {
        case 403:
             return new ForbiddenException();
        case 404:
             return new NotFoundException();
        case 500:
             return new InternalServerErrorException();
        …
        default:
             throw new UnknownHttpErrorCodeException(errorCode);
     }
}

Lưu ý rằng việc ném một ngoại lệ có nghĩa là phương thức đã được sử dụng sai hoặc có lỗi nội bộ, trong khi trả lại một ngoại lệ có nghĩa là mã lỗi được xác định thành công.


Điều này cũng giúp trình biên dịch hiểu được dòng mã: nếu một phương thức không bao giờ trả về đúng (vì nó ném ngoại lệ "mong muốn") và trình biên dịch không biết về điều đó, thì đôi khi bạn có thể cần các returncâu lệnh thừa để làm cho trình biên dịch ngừng phàn nàn.
Joachim Sauer

@JoachimSauer Vâng. Hơn một lần tôi ước họ sẽ thêm một kiểu trả về "không bao giờ" vào C # - nó sẽ chỉ ra chức năng phải thoát bằng cách ném, đặt trả lại trong đó là một lỗi. Đôi khi bạn có mã mà công việc duy nhất của họ là một ngoại lệ.
Loren Pechtel

2
@LorenPechtel Một chức năng không bao giờ trả lại không phải là một chức năng. Đó là một kẻ hủy diệt. Gọi một chức năng như thế sẽ xóa ngăn xếp cuộc gọi. Nó tương tự như gọi điện thoại System.Exit(). Mã sau khi gọi hàm không được thực thi. Điều đó không giống như một mẫu thiết kế tốt cho một chức năng.
Phản ứng

5
@MathewFoscarini Hãy xem xét một lớp có nhiều phương thức có thể lỗi theo những cách tương tự. Bạn muốn bỏ một số thông tin trạng thái như là một phần của ngoại lệ để chỉ ra những gì nó đã được nhai khi nó bùng nổ. Bạn muốn có một bản sao của mã đẹp vì DRY. Điều đó để lại hoặc gọi một chức năng làm đẹp và sử dụng kết quả trong lần ném của bạn hoặc nó có nghĩa là một chức năng xây dựng văn bản được nâng cấp và sau đó ném nó. Tôi thường tìm thấy những cấp trên.
Loren Pechtel

1
@LorenPechtel ah, vâng tôi cũng đã làm điều đó. Được rồi, bây giờ tôi đã hiểu.
Phản ứng

5

Về mặt khái niệm, nếu đối tượng ngoại lệ là kết quả mong đợi của hoạt động, thì có. Tuy nhiên, những trường hợp tôi có thể nghĩ ra luôn liên quan đến việc ném bóng ở một số điểm:

  • Biến thể trên mẫu "Thử" (trong đó một phương thức gói gọn một phương pháp ném ngoại lệ thứ hai, nhưng bắt được ngoại lệ và thay vào đó trả về một boolean biểu thị thành công). Bạn có thể, thay vì trả lại Boolean, trả lại bất kỳ ngoại lệ nào được ném (null nếu thành công), cho phép người dùng cuối có thêm thông tin trong khi vẫn giữ được dấu hiệu thành công hoặc thất bại.

  • Xử lý lỗi công việc. Trong thực tế tương tự như đóng gói phương thức "thử mẫu", bạn có thể có một bước tiến trình công việc được trừu tượng hóa trong một mẫu chuỗi lệnh. Nếu một liên kết trong chuỗi ném ra một ngoại lệ, thì thường sẽ bắt được ngoại lệ đó trong đối tượng trừu tượng hóa, sau đó trả lại kết quả của hoạt động đã nói để công cụ dòng công việc và mã thực tế chạy như một bước tiến trình công việc không yêu cầu thử rộng rãi logic -catch của riêng họ.


Tôi không nghĩ đã thấy bất kỳ người đáng chú ý nào ủng hộ một biến thể như vậy trên mẫu "Thử", nhưng cách tiếp cận "được đề xuất" khi trả lại Boolean có vẻ khá thiếu máu. Tôi nghĩ sẽ tốt hơn nếu có một OperationResultlớp trừu tượng , với một thuộc booltính "Thành công", một GetExceptionIfAnyphương thức và một AssertSuccessfulphương thức (sẽ ném ngoại lệ từ GetExceptionIfAnynếu không null). Có GetExceptionIfAnymột phương thức chứ không phải là một thuộc tính sẽ cho phép sử dụng các đối tượng lỗi bất biến tĩnh cho các trường hợp không có nhiều điều hữu ích để nói.
supercat

5

Hãy nói rằng bạn đã chinh phục một số nhiệm vụ được thực thi trong một số nhóm luồng. Nếu tác vụ này ném ngoại lệ, nó là luồng khác nhau vì vậy bạn không bắt nó. Chủ đề nơi nó đã được thực hiện chỉ cần chết.

Bây giờ hãy xem xét một cái gì đó (hoặc mã trong tác vụ đó hoặc thực thi nhóm luồng) bắt ngoại lệ đó lưu trữ nó cùng với tác vụ và xem xét nhiệm vụ đã hoàn thành (không thành công). Bây giờ bạn có thể hỏi liệu tác vụ đã kết thúc chưa (và có thể hỏi liệu nó đã ném chưa, hoặc nó có thể được ném lại trong luồng của bạn không (hoặc tốt hơn, ngoại lệ mới với nguyên nhân là nguyên nhân)).

Nếu bạn thực hiện thủ công, bạn có thể nhận thấy rằng bạn đang tạo ngoại lệ mới, ném nó và bắt nó, sau đó lưu trữ nó, trong các luồng khác nhau lấy ra ném, bắt và phản ứng với nó. Vì vậy, nó có thể có ý nghĩa để bỏ qua ném và bắt và chỉ cần lưu trữ nó và hoàn thành nhiệm vụ và sau đó chỉ cần hỏi liệu có ngoại lệ và phản ứng với nó. Nhưng điều này có thể dẫn đến mã phức tạp hơn nếu ở cùng một nơi có thể thực sự có ngoại lệ.

PS: điều này được viết bằng kinh nghiệm với Java, trong đó thông tin theo dõi ngăn xếp được tạo khi ngoại lệ được tạo, (không giống như C # nơi nó được tạo khi ném). Vì vậy, không ném phương pháp tiếp cận ngoại lệ trong Java sẽ chậm hơn trong C # (trừ khi được xử lý trước và sử dụng lại), nhưng sẽ có sẵn thông tin theo dõi ngăn xếp.

Nói chung, tôi sẽ tránh xa việc tạo ra ngoại lệ và không bao giờ ném nó (trừ khi tối ưu hóa hiệu suất sau khi định hình nó như là nút cổ chai). Ít nhất là trong java, trong đó việc tạo ngoại lệ là tốn kém (stack stack). Trong C # có thể, nhưng IMO đáng ngạc nhiên và do đó nên tránh.


Điều này thường xảy ra với các đối tượng nghe lỗi là tốt. Một hàng đợi sẽ thực thi tác vụ, bắt bất kỳ ngoại lệ nào, sau đó chuyển ngoại lệ đó cho người nghe lỗi chịu trách nhiệm. Nó thường không có ý nghĩa để giết chuỗi nhiệm vụ mà không biết nhiều về công việc trong trường hợp này. Loại ý tưởng này cũng được tích hợp vào các API Java nơi một luồng có thể vượt qua các ngoại lệ từ một luồng
Lance Nanek

1

Nó phụ thuộc vào thiết kế của bạn, thông thường tôi sẽ không trả lại ngoại lệ cho người gọi, thay vào đó tôi sẽ ném và bắt họ và để nó ở đó. Thông thường mã được viết để thất bại sớm và nhanh chóng. Ví dụ, hãy xem xét trường hợp mở tệp và xử lý tệp (Đây là C # PsuedoCode):

        private static void ProcessFileFailFast()
        {
            try
            {
                using (var file = new System.IO.StreamReader("c:\\test.txt"))
                {
                    string line;
                    while ((line = file.ReadLine()) != null)
                    {
                        ProcessLine(line);
                    }
                }
            }
            catch (Exception ex) 
            {
                LogException(ex);
            }
        }

        private static void ProcessLine(string line)
        {
            //TODO:  Process Line
        }

        private static void LogException(Exception ex)
        {
            //TODO:  Log Exception
        }

Trong trường hợp này, chúng tôi sẽ báo lỗi và ngừng xử lý tệp ngay khi gặp phải một bản ghi xấu.

Nhưng nói yêu cầu là chúng tôi muốn tiếp tục xử lý tệp ngay cả khi một hoặc nhiều dòng có lỗi. Mã có thể bắt đầu trông giống như thế này:

    private static void ProcessFileFailAndContinue()
    {
        try
        {
            using (var file = new System.IO.StreamReader("c:\\test.txt"))
            {
                string line;
                while ((line = file.ReadLine()) != null)
                {
                    Exception ex = ProcessLineReturnException(line);
                    if (ex != null)
                    {
                        _Errors.Add(ex);
                    }
                }
            }

            //Do something with _Errors Here
        }
        //Catch errors specifically around opening the file
        catch (System.IO.FileNotFoundException fnfe) 
        { 
            LogException(fnfe);
        }    
    }

    private static Exception ProcessLineReturnException(string line)
    {
        try
        {
            //TODO:  Process Line
        }
        catch (Exception ex)
        {
            LogException(ex);
            return ex;
        }

        return null;
    }

Vì vậy, trong trường hợp này, chúng tôi trả lại ngoại lệ cho người gọi, mặc dù tôi có thể sẽ không trả lại ngoại lệ mà thay vào đó là một loại đối tượng lỗi do ngoại lệ đã bị bắt và đã được xử lý. Điều này không có hại khi trả lại một ngoại lệ, nhưng những người gọi khác có thể ném lại ngoại lệ có thể không được mong muốn vì đối tượng ngoại lệ có hành vi đó. Nếu bạn muốn người gọi có khả năng ném lại thì trả lại một ngoại lệ, nếu không, hãy lấy thông tin ra khỏi đối tượng ngoại lệ và tạo một đối tượng có trọng lượng nhẹ hơn và thay vào đó trả lại. Các thất bại nhanh thường là mã sạch hơn.

Đối với ví dụ xác thực của bạn, tôi có thể sẽ không kế thừa từ một lớp ngoại lệ hoặc ném ngoại lệ vì lỗi xác thực có thể khá phổ biến. Sẽ là ít chi phí hơn khi trả lại một đối tượng thay vì ném ngoại lệ nếu 50% người dùng của tôi không điền đúng biểu mẫu trong lần thử đầu tiên.


1

H: Có bất kỳ tình huống hợp pháp nào mà các ngoại lệ không bị ném và bị bắt, mà chỉ được trả về từ một phương thức và sau đó được chuyển qua dưới dạng các đối tượng lỗi không?

Đúng. Ví dụ, gần đây tôi đã gặp phải một tình huống trong đó tôi thấy rằng đã ném Ngoại lệ trong một donot Dịch vụ web ASMX bao gồm một yếu tố trong thông báo SOAP kết quả, vì vậy tôi phải tạo ra nó.

Mã minh họa:

Public Sub SomeWebMethod()
    Try
        ...
    Catch ex As Exception
        Dim soapex As SoapException = Me.GenerateSoapFault(ex)
        Throw soapex
    End Try
End Sub

Private Function GenerateSoapFault(ex As Exception) As SoapException
    Dim document As XmlDocument = New XmlDocument()
    Dim faultDetails As XmlNode = document.CreateNode(XmlNodeType.Element, SoapException.DetailElementName.Name, SoapException.DetailElementName.Namespace)
    faultDetails.InnerText = ex.Message
    Dim exceptionType As String = ex.GetType().ToString()
    Dim soapex As SoapException = New SoapException("SoapException", SoapException.ClientFaultCode, Context.Request.Url.ToString, faultDetails)
    Return soapex
End Function

1

Trong hầu hết các ngôn ngữ (afaik), ngoại lệ đi kèm với một số tính năng bổ sung. Hầu hết, một ảnh chụp nhanh của dấu vết ngăn xếp hiện tại được lưu trữ trong đối tượng. Điều này là tốt để gỡ lỗi, nhưng cũng có thể có ý nghĩa bộ nhớ.

Như @ThinkingMedia đã nói, bạn thực sự đang sử dụng ngoại lệ làm đối tượng lỗi.

Trong ví dụ mã của bạn, có vẻ như bạn chủ yếu làm điều này để tái sử dụng mã và để tránh thành phần. Cá nhân tôi không nghĩ rằng đây là một lý do tốt để làm điều này.

Một lý do khác có thể là lừa ngôn ngữ để cung cấp cho bạn một đối tượng lỗi với dấu vết ngăn xếp. Điều này cung cấp thêm thông tin theo ngữ cảnh cho mã xử lý lỗi của bạn.

Mặt khác, chúng ta có thể giả định rằng việc giữ dấu vết ngăn xếp xung quanh có chi phí bộ nhớ. Ví dụ, nếu bạn bắt đầu thu thập những vật thể này ở đâu đó, bạn có thể thấy một tác động bộ nhớ khó chịu. Tất nhiên điều này phần lớn phụ thuộc vào cách thực hiện dấu vết ngăn xếp ngoại lệ trong ngôn ngữ / công cụ tương ứng.

Vì vậy, nó là một ý tưởng tốt?

Cho đến nay tôi đã không thấy nó được sử dụng hoặc khuyến nghị. Nhưng điều này không có nhiều ý nghĩa.

Câu hỏi là: Theo dõi ngăn xếp có thực sự hữu ích cho việc xử lý lỗi của bạn không? Hay nói chung hơn, bạn muốn cung cấp thông tin nào cho nhà phát triển hoặc quản trị viên?

Kiểu ném / thử / bắt điển hình làm cho nó cần thiết để có dấu vết ngăn xếp, bởi vì nếu không thì bạn không biết nó đến từ đâu. Đối với một đối tượng được trả về, nó luôn xuất phát từ hàm được gọi. Theo dõi ngăn xếp có thể chứa tất cả thông tin mà nhà phát triển cần, nhưng có lẽ thứ gì đó ít nặng nề và cụ thể hơn sẽ đủ.


-4

Câu trả lời là có. Xem http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Exceptions và mở mũi tên cho hướng dẫn kiểu C ++ của Google về các ngoại lệ. Bạn có thể thấy ở đó những lý lẽ mà họ đã từng quyết định chống lại, nhưng nói rằng nếu họ phải làm lại từ đầu, họ có thể đã quyết định.

Tuy nhiên, điều đáng chú ý là ngôn ngữ Go cũng không sử dụng ngoại lệ một cách thành ngữ, vì những lý do tương tự.


1
Xin lỗi, nhưng tôi không hiểu làm thế nào những hướng dẫn này giúp trả lời câu hỏi: Họ chỉ đơn giản khuyên bạn nên tránh hoàn toàn việc xử lý ngoại lệ. Đây không phải là những gì tôi đang hỏi về; Câu hỏi của tôi là liệu có hợp pháp không khi sử dụng loại ngoại lệ để mô tả tình trạng lỗi trong tình huống đối tượng ngoại lệ kết quả sẽ không bị ném. Dường như không có gì về điều đó trong các hướng dẫn này.
stakx
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.