Xem xét xử lý lỗi


31

Vấn đề:

Từ lâu, tôi lo lắng về exceptionscơ chế này, vì tôi cảm thấy nó không thực sự giải quyết được những gì cần làm.

YÊU CẦU: Có những cuộc tranh luận dài bên ngoài về chủ đề này và hầu hết trong số họ đấu tranh để so sánh exceptionsvới việc trả lại mã lỗi. Đây chắc chắn không phải là chủ đề ở đây.

Cố gắng xác định lỗi, tôi đồng ý với CppCoreGuiances, từ Bjarne Stroustrup & Herb Sutter

Một lỗi có nghĩa là chức năng không thể đạt được mục đích được quảng cáo của nó

YÊU CẦU: Cơ exceptionchế là một ngữ nghĩa ngôn ngữ để xử lý lỗi.

YÊU CẦU: Đối với tôi, "không có lý do" cho chức năng không đạt được nhiệm vụ: Hoặc chúng tôi đã xác định sai các điều kiện trước / sau để chức năng có thể đảm bảo kết quả hoặc một số trường hợp đặc biệt không được coi là đủ quan trọng để dành thời gian phát triển một giải pháp. Xem xét rằng, IMO, sự khác biệt giữa mã thông thường và xử lý mã lỗi là (trước khi thực hiện) là một dòng rất chủ quan.

YÊU CẦU: Sử dụng các ngoại lệ để chỉ ra khi không giữ một điều kiện trước hoặc sau là một mục đích khác của exceptioncơ chế, chủ yếu cho mục đích gỡ lỗi. Tôi không nhắm mục tiêu sử dụng này exceptionsở đây.

Trong nhiều sách, hướng dẫn và các nguồn khác, chúng có xu hướng hiển thị xử lý lỗi như một môn khoa học khá khách quan, đã được giải quyết exceptionsvà bạn chỉ cần catchchúng để có một phần mềm mạnh mẽ, có thể phục hồi từ mọi tình huống. Nhưng vài năm làm nhà phát triển khiến tôi thấy vấn đề từ một cách tiếp cận khác:

  • Các lập trình viên có xu hướng đơn giản hóa nhiệm vụ của họ bằng cách đưa ra các ngoại lệ khi trường hợp cụ thể dường như quá hiếm để được thực hiện cẩn thận. Các trường hợp điển hình của vấn đề này là: hết các vấn đề về bộ nhớ, các vấn đề về đĩa đầy, các vấn đề về tệp bị hỏng, v.v ... Điều này có thể là đủ, nhưng không phải lúc nào cũng được quyết định từ cấp độ kiến ​​trúc.
  • Các lập trình viên có xu hướng không đọc tài liệu cẩn thận về các trường hợp ngoại lệ trong các thư viện và thường không nhận thức được chức năng nào và khi nào chức năng ném. Hơn nữa, ngay cả khi họ biết, họ không thực sự quản lý chúng.
  • Các lập trình viên có xu hướng không nắm bắt ngoại lệ đủ sớm, và khi họ làm, chủ yếu là để đăng nhập và ném xa hơn. (tham khảo điểm đầu tiên).

Điều này có hai hậu quả:

  1. Lỗi xảy ra thường xuyên được phát hiện sớm trong quá trình phát triển và gỡ lỗi (điều này là tốt).
  2. Các trường hợp ngoại lệ hiếm hoi không được quản lý và làm cho hệ thống gặp sự cố (với một thông điệp tường trình đẹp) tại nhà của người dùng. Một số lần lỗi được báo cáo, hoặc thậm chí không.

Xem xét rằng, IMO mục đích chính của một cơ chế lỗi phải là:

  1. Hiển thị trong mã nơi một số trường hợp cụ thể không được quản lý.
  2. Truyền đạt thời gian chạy vấn đề đến mã liên quan (ít nhất là người gọi) khi tình huống này xảy ra.
  3. Cung cấp cơ chế phục hồi

Lỗ hổng chính của exceptionngữ nghĩa như một cơ chế xử lý lỗi là IMO: thật dễ dàng để biết vị trí của a throwtrong mã nguồn, nhưng hoàn toàn không rõ ràng để biết liệu một hàm cụ thể có thể ném bằng cách xem khai báo hay không. Điều này mang lại tất cả các vấn đề mà tôi đã giới thiệu ở trên.

Ngôn ngữ không thực thi và kiểm tra mã lỗi nghiêm ngặt như đối với các khía cạnh khác của ngôn ngữ (ví dụ: các loại biến mạnh)

Thử giải pháp

Với mục đích cải thiện điều này, tôi đã phát triển một hệ thống xử lý lỗi rất đơn giản, nó cố gắng đặt việc xử lý lỗi ở cùng mức độ quan trọng hơn mã thông thường.

Ý tưởng là:

  • Mỗi hàm (có liên quan) nhận được một tham chiếu đến một successđối tượng rất nhẹ và có thể đặt nó thành trạng thái lỗi trong trường hợp. Đối tượng rất nhẹ cho đến khi một lỗi với văn bản được lưu.
  • Một chức năng được khuyến khích bỏ qua nhiệm vụ của nó nếu đối tượng được cung cấp đã có lỗi.
  • Một lỗi không bao giờ được ghi đè.

Thiết kế đầy đủ rõ ràng xem xét kỹ lưỡng từng khía cạnh (khoảng 10 trang), cũng như cách áp dụng nó cho OOP.

Ví dụ về Successlớp học:

class Success
{
public:
    enum SuccessStatus
    {
        ok = 0,             // All is fine
        error = 1,          // Any error has been reached
        uninitialized = 2,  // Initialization is required
        finished = 3,       // This object already performed its task and is not useful anymore
        unimplemented = 4,  // This feature is not implemented already
    };

    Success(){}
    Success( const Success& v);
    virtual ~Success() = default;
    virtual Success& operator= (const Success& v);

    // Comparators
    virtual bool operator==( const Success& s)const { return (this->status==s.status && this->stateStr==s.stateStr);}
    virtual bool operator!=( const Success& s)const { return (this->status!=s.status || this->stateStr==s.stateStr);}

    // Retrieve if the status is not "ok"
    virtual bool operator!() const { return status!=ok;}

    // Retrieve if the status is "ok"
    operator bool() const { return status==ok;}

    // Set a new status
    virtual Success& set( SuccessStatus status, std::string msg="");
    virtual void reset();

    virtual std::string toString() const{ return stateStr;}
    virtual SuccessStatus getStatus() const { return status; }
    virtual operator SuccessStatus() const { return status; }

private:
    std::string stateStr;
    SuccessStatus status = Success::ok;
};

Sử dụng:

double mySqrt( Success& s, double v)
{
    double result = 0.0;
    if (!s) ; // do nothing
    else if (v<0.0) s.set(Error, "Square root require non-negative input.");
    else result = std::sqrt(v);
    return result;
}

Success s;
mySqrt(s, 144.0);
otherStuff(s);
saveStuff(s);
if (s) /*All is good*/;
else cout << s << endl;

Tôi đã sử dụng nó trong nhiều mã (của riêng tôi) và nó buộc người lập trình (tôi) phải suy nghĩ thêm về các trường hợp đặc biệt có thể xảy ra và cách giải quyết chúng (tốt). Tuy nhiên, nó có một đường cong học tập và không tích hợp tốt với mã hiện đang sử dụng nó.

Câu hỏi

Tôi muốn hiểu rõ hơn về ý nghĩa của việc sử dụng một mô hình như vậy trong một dự án:

  • Là tiền đề cho vấn đề chính xác? hoặc tôi đã bỏ lỡ một cái gì đó có liên quan?
  • Là giải pháp một ý tưởng kiến ​​trúc tốt? hoặc giá quá cao?

CHỈNH SỬA:

So sánh giữa các phương pháp:

//Exceptions:

    // Incorrect
    File f = open("text.txt"); // Could throw but nothing tell it! Will crash
    save(f);

    // Correct
    File f;
    try
    {
        f = open("text.txt");
        save(f);
    }
    catch( ... )
    {
        // do something 
    }

//Error code (mixed):

    // Incorrect
    File f = open("text.txt"); //Nothing tell you it may fail! Will crash
    save(f);

    // Correct
    File f = open("text.txt");
    if (f) save(f);

//Error code (pure);

    // Incorrect
    File f;
    open(f, "text.txt"); //Easy to forget the return value! will crash
    save(f);

    //Correct
    File f;
    Error er = open(f, "text.txt");
    if (!er) save(f);

//Success mechanism:

    Success s;
    File f;
    open(s, "text.txt");
    save(s, f); //s cannot be avoided, will never crash.
    if (s) ... //optional. If you created s, you probably don't forget it.

25
Được ủng hộ cho "Câu hỏi này cho thấy nỗ lực nghiên cứu; nó hữu ích và rõ ràng", không phải vì tôi đồng ý: Tôi nghĩ rằng một số suy nghĩ bị sai lệch. (Chi tiết có thể theo sau trong một câu trả lời.)
Martin Ba

2
Hoàn toàn, tôi hiểu và đồng ý về điều đó! Mục đích của câu hỏi này là bị chỉ trích. Và điểm số của câu hỏi để chỉ ra câu hỏi tốt / xấu, không phải OP đúng.
Adrian Maire

2
Nếu tôi hiểu chính xác, sự hiểu biết chính của bạn về các ngoại lệ là mọi người có thể bỏ qua nó (trong c ++) thay vì xử lý chúng. Tuy nhiên, cấu trúc Thành công của bạn có cùng một lỗ hổng theo thiết kế. Giống như ngoại lệ, họ sẽ bỏ qua nó. Thậm chí tệ hơn: nó dài dòng hơn, dẫn đến lợi nhuận xếp tầng và thậm chí bạn không thể "bắt" nó ngược dòng.
dagnelies

3
Tại sao không chỉ sử dụng một cái gì đó như monads? Họ làm cho lỗi của bạn tiềm ẩn nhưng họ sẽ không im lặng trong khi chạy. Trên thực tế, điều đầu tiên tôi nghĩ khi nhìn vào mã của bạn là "đơn nguyên, tốt đẹp". Có một cái nhìn vào họ.
bash0r

2
Lý do chính tôi thích ngoại lệ là chúng cho phép bạn bắt tất cả các lỗi không mong muốn từ một khối mã nhất định và xử lý chúng một cách nhất quán. Đúng, không có lý do chính đáng nào mà mã không nên thực hiện nhiệm vụ của mình - "có lỗi" là một lý do xấu nhưng nó vẫn xảy ra và khi nó xảy ra, bạn muốn đăng nhập nguyên nhân và hiển thị thông báo hoặc thử lại. (Tôi có một số mã thực hiện tương tác phức tạp, có thể khởi động lại với một hệ thống từ xa; nếu hệ thống từ xa bị hỏng, tôi muốn đăng nhập và thử lại từ đầu)
user253751

Câu trả lời:


32

Xử lý lỗi có lẽ là phần khó nhất của chương trình.

Nói chung, nhận ra rằng có một điều kiện lỗi là dễ dàng; tuy nhiên, việc báo hiệu nó theo cách không thể tránh được và xử lý nó một cách thích hợp (xem các mức An toàn Ngoại lệ của Áp-ra-ham ) thực sự khó khăn.

Trong C, lỗi báo hiệu được thực hiện bằng mã trả về, là đẳng cấu cho giải pháp của bạn.

C ++ đưa ra các trường hợp ngoại lệ vì cách tiếp cận ngắn như vậy; cụ thể là, nó chỉ hoạt động nếu người gọi nhớ kiểm tra xem có lỗi xảy ra hay không và thất bại cách nhau khủng khiếp. Bất cứ khi nào bạn thấy mình nói "Mọi thứ đều ổn ... miễn là bạn gặp vấn đề; con người không phải là tỉ mỉ, ngay cả khi họ quan tâm.

Tuy nhiên, vấn đề là các ngoại lệ có vấn đề riêng của họ. Cụ thể, dòng điều khiển vô hình / ẩn. Điều này đã được dự định: ẩn trường hợp lỗi để logic của mã không bị xáo trộn bởi phần mềm xử lý lỗi. Nó làm cho "đường dẫn hạnh phúc" rõ ràng hơn (và nhanh chóng!), Với chi phí làm cho các đường dẫn lỗi không thể hiểu được.


Tôi thấy thú vị khi xem cách các ngôn ngữ khác tiếp cận vấn đề:

  • Java đã kiểm tra các ngoại lệ (và những trường hợp không được kiểm tra),
  • Go sử dụng mã lỗi / hoảng loạn,
  • Rust sử dụng các loại tổng / hoảng loạn).
  • Ngôn ngữ FP nói chung.

C ++ đã từng có một số dạng ngoại lệ được kiểm tra, bạn có thể nhận thấy nó đã bị phản đối và đơn giản hóa về cơ bản noexcept(<bool>)thay vào đó: hoặc là một hàm được khai báo là có thể ném, hoặc nó được tuyên bố là không bao giờ. Các trường hợp ngoại lệ được kiểm tra có phần có vấn đề ở chỗ chúng thiếu khả năng mở rộng, điều này có thể gây ra ánh xạ / lồng nhau khó xử. Và hệ thống phân cấp ngoại lệ phức tạp (một trong những trường hợp sử dụng chính của thừa kế ảo là ngoại lệ ...).

Ngược lại, Go và Rust thực hiện cách tiếp cận:

  • lỗi nên được báo hiệu trong băng tần,
  • ngoại lệ nên được sử dụng cho các tình huống thực sự đặc biệt.

Điều thứ hai khá rõ ràng ở chỗ (1) họ đặt tên cho sự hoảng loạn ngoại lệ của họ và (2) không có mệnh đề phân cấp / mệnh đề phức tạp ở đây. Ngôn ngữ không cung cấp phương tiện để kiểm tra nội dung của "hoảng loạn": không phân cấp loại, không có nội dung do người dùng xác định, chỉ là "rất tiếc, mọi thứ đã sai đến mức không thể phục hồi".

Điều này khuyến khích người dùng sử dụng xử lý lỗi một cách hiệu quả, trong khi vẫn để lại một cách dễ dàng để bảo lãnh trong các tình huống đặc biệt (chẳng hạn như: "chờ đã, tôi chưa thực hiện điều đó!").

Tất nhiên, không may, cách tiếp cận Go rất giống với bạn ở chỗ bạn có thể dễ dàng quên kiểm tra lỗi ...

... Tuy nhiên, cách tiếp cận Rust chủ yếu tập trung vào hai loại:

  • Option, tương tự như std::optional,
  • Result, đó là một biến thể hai khả năng: Ok và Err.

việc này gọn gàng hơn nhiều vì không có cơ hội vô tình sử dụng kết quả mà không kiểm tra thành công: nếu bạn làm thế, chương trình sẽ hoảng loạn.


Các ngôn ngữ FP tạo thành xử lý lỗi của chúng trong các cấu trúc có thể được chia thành ba lớp: - Functor - Applicative / Alternative - Monads / Alternative

Chúng ta hãy xem kiểu chữ của FunctorHaskell:

class Functor m where
  fmap :: (a -> b) -> m a -> m b

Trước hết, typeclass có phần giống nhau nhưng không bằng giao diện. Chữ ký chức năng của Haskell trông hơi đáng sợ trên cái nhìn đầu tiên. Nhưng hãy giải mã chúng. Hàm fmaplấy một hàm làm tham số đầu tiên có phần giống với std::function<a,b>. Điều tiếp theo là một m a. Bạn có thể tưởng tượng mnhư một cái gì đó như std::vectorm anhư một cái gì đó như std::vector<a>. Nhưng sự khác biệt là, điều m ađó không nói lên rằng nó phải rõ ràng std:vector. Vì vậy, nó có thể là một std::option, quá. Bằng cách nói ngôn ngữ mà chúng ta có một ví dụ cho typeclass Functorcho một loại cụ thể như std::vectorhay std::option, chúng ta có thể sử dụng chức năng fmapcho loại đó. Điều tương tự phải được thực hiện cho các kiểu chữ Applicative, AlternativeMonadcho phép bạn thực hiện các tính toán thất bại, có thể. Các Alternativetypeclass thực hiện trừu tượng phục hồi lỗi. Bằng cách đó, bạn có thể nói một cái gì đó giống như a <|> bý nghĩa của nó hoặc là thuật ngữ ahoặc thuật ngữ b. Nếu cả hai tính toán đều thành công, đó vẫn là một lỗi.

Chúng ta hãy xem Maybeloại của Haskell .

data Maybe a
  = Nothing
  | Just a

Điều này có nghĩa là, nơi bạn mong đợi a Maybe a, bạn nhận được Nothinghoặc Just a. Khi nhìn fmaptừ trên cao, một triển khai có thể trông giống như

fmap f m = case m of
  Nothing -> Nothing
  Just a -> Just (f a)

Các case ... ofbiểu hiện được gọi là mô hình phù hợp và tương tự như những gì được biết đến trong thế giới OOP như visitor pattern. Hãy tưởng tượng dòng case m ofnhư m.apply(...)và các dấu chấm là instantiation của một lớp thực hiện các chức năng văn. Các dòng bên dưới case ... ofbiểu thức là các hàm điều phối tương ứng mang các trường của lớp trực tiếp theo phạm vi theo tên. Trong Nothingnhánh chúng ta tạo Nothingvà trong Just anhánh chúng ta đặt tên giá trị duy nhất của chúng ta avà tạo giá trị khác Just ...với hàm biến đổi fđược áp dụng cho a. Đọc nó như : new Just(f(a)).

Điều này bây giờ có thể xử lý các tính toán sai lầm trong khi trừu tượng hóa kiểm tra lỗi thực tế. Có các triển khai cho các giao diện khác làm cho loại tính toán này rất mạnh mẽ. Thật ra, Maybelà nguồn cảm hứng cho Rust's Option-Type.


Tôi sẽ khuyến khích bạn làm lại Successlớp của bạn Resultđể thay thế. Alexandrescu thực sự đề xuất một cái gì đó thực sự gần gũi, được gọi là expected<T>, trong đó các đề xuất tiêu chuẩn đã được thực hiện .

Tôi sẽ gắn bó với việc đặt tên Rust và API đơn giản vì ... nó được ghi lại và hoạt động. Tất nhiên, Rust có một ?toán tử hậu tố tiện lợi sẽ làm cho mã ngọt ngào hơn nhiều; trong C ++, chúng tôi sẽ sử dụng biểu thức câu lệnhTRY macro và GCC để mô phỏng nó.

template <typename E>
struct Error {
    Error(E e): error(std::move(e)) {}

    E error;
};

template <typename E>
Error<E> error(E e) { return Error<E>(std::move(e)); }

template <typename T, typename E>
struct [[nodiscard]] Result {
    template <typename U>
    Result(U u): ok(true), data(std::move(u)), error() {}

    template <typename F>
    Result(Error<F> f): ok(false), data(), error(std::move(f.error)) {}

    template <typename U, typename F>
    Result(Result<U, F> other):
        ok(other.ok), data(std::move(other.data)),  error(std::move(other.error)) {}

    bool ok = false;
    T data;
    E error;
};

#define TRY(Expr_) \
    ({ auto result = (Expr_); \
       if (!result.ok) { return result; } \
       std::move(result.data); })

Lưu ý: đây Resultlà một giữ chỗ. Một thực hiện đúng sẽ sử dụng đóng gói và a union. Đó là đủ để có được điểm trên tuy nhiên.

Cho phép tôi viết ( xem nó trong hành động ):

Result<double, std::string> sqrt(double x) {
    if (x < 0) {
        return error("sqrt does not accept negative numbers");
    }
    return x;
}

Result<double, std::string> double_sqrt(double x) {
    auto y = TRY(sqrt(x));
    return sqrt(y);
}

mà tôi thấy thực sự gọn gàng:

  • Không giống như việc sử dụng mã lỗi (hoặc Successlớp của bạn ), việc quên kiểm tra lỗi sẽ dẫn đến lỗi thời gian chạy 1 thay vì một số hành vi ngẫu nhiên,
  • Không giống như việc sử dụng các ngoại lệ, rõ ràng tại trang web cuộc gọi mà các chức năng có thể thất bại nên không có gì bất ngờ.
  • với tiêu chuẩn C ++ - 2X, chúng tôi có thể nhận được conceptstrong tiêu chuẩn. Điều này sẽ làm cho loại chương trình này trở nên dễ chịu hơn rất nhiều vì chúng ta có thể để sự lựa chọn thay vì loại lỗi. Ví dụ, với việc thực hiện std::vectorkết quả, chúng ta có thể tính toán tất cả các giải pháp có thể cùng một lúc. Hoặc chúng tôi có thể chọn cải thiện xử lý lỗi, như bạn đề xuất.

1 Với Resultviệc thực hiện được đóng gói đúng cách ;)


Lưu ý: không giống như ngoại lệ, trọng lượng nhẹ Resultnày không có backtraces, khiến việc đăng nhập kém hiệu quả hơn; bạn có thể thấy hữu ích khi ít nhất ghi nhật ký số tập tin / dòng mà thông báo lỗi được tạo và nói chung là viết một thông báo lỗi phong phú. Điều này có thể được kết hợp bằng cách chụp tệp / dòng mỗi khi TRYmacro được sử dụng, về cơ bản là tạo backtrace bằng tay hoặc sử dụng mã và thư viện dành riêng cho nền tảng như libbacktraceđể liệt kê các ký hiệu trong bảng gọi.


Mặc dù có một cảnh báo lớn: các thư viện C ++ hiện có và thậm chí std, dựa trên các ngoại lệ. Sẽ là một trận chiến khó khăn khi sử dụng phong cách này, vì API của bất kỳ thư viện bên thứ 3 nào cũng phải được bọc trong một bộ chuyển đổi ...


3
Cái vĩ mô đó có vẻ ... rất sai. Tôi sẽ giả sử ({...})là một số phần mở rộng gcc, nhưng ngay cả như vậy, không nên if (!result.ok) return result;? Tình trạng của bạn xuất hiện ngược và bạn tạo một bản sao lỗi không cần thiết.
Vịt Mooing

@MooingDuck Câu trả lời giải thích đó ({...})biểu thức phát biểu của gcc .
jamesdlin


1
Tôi khuyên bạn nên sử dụng std::variantđể triển khai Resultnếu bạn đang sử dụng C ++ 17. Ngoài ra, để nhận được cảnh báo nếu bạn bỏ qua lỗi, hãy sử dụng[[nodiscard]]
Justin

2
@Justin: Có nên sử dụng std::varianthay không là một vấn đề của hương vị khi đánh đổi xung quanh việc xử lý ngoại lệ. [[nodiscard]]thực sự là một chiến thắng thuần túy
Matthieu M.

46

YÊU CẦU: Cơ chế ngoại lệ là một ngữ nghĩa ngôn ngữ để xử lý lỗi

ngoại lệ là một cơ chế dòng điều khiển. Động lực cho cơ chế luồng điều khiển này, cụ thể là tách biệt xử lý lỗi với mã xử lý không lỗi, trong trường hợp phổ biến là xử lý lỗi rất lặp lại và ít liên quan đến phần chính của logic.

YÊU CẦU: Đối với tôi, "không có lý do" cho chức năng không đạt được nhiệm vụ: Hoặc chúng tôi đã xác định sai các điều kiện trước / sau để chức năng có thể đảm bảo kết quả hoặc một số trường hợp đặc biệt không được coi là đủ quan trọng để dành thời gian phát triển một giải pháp

Xem xét: Tôi cố gắng tạo một tập tin. Các thiết bị lưu trữ đã đầy.

Bây giờ, đây không phải là một thất bại trong việc xác định các điều kiện tiên quyết của tôi: bạn không thể sử dụng "phải có đủ dung lượng lưu trữ" làm điều kiện tiên quyết, bởi vì lưu trữ được chia sẻ phải tuân theo các điều kiện chủng tộc khiến điều này không thể đáp ứng.

Vì vậy, chương trình của tôi bằng cách nào đó có nên giải phóng không gian và sau đó tiến hành thành công, nếu không thì tôi quá lười biếng để "phát triển giải pháp"? Điều này có vẻ thẳng thắn vô nghĩa. "Giải pháp" để quản lý lưu trữ được chia sẻ nằm ngoài phạm vi chương trình của tôi và cho phép chương trình của tôi không hoạt động một cách duyên dáng và được chạy lại sau khi người dùng đã giải phóng một số dung lượng hoặc thêm một số dung lượng lưu trữ, đều ổn .


Những gì lớp thành công của bạn làm là xen kẽ xử lý lỗi rất rõ ràng với logic chương trình của bạn. Mỗi chức năng cần kiểm tra, trước khi chạy, cho dù một số lỗi đã xảy ra, điều đó có nghĩa là nó không nên làm gì cả. Mỗi hàm thư viện cần được bọc trong một hàm khác, với một đối số nữa (và hy vọng chuyển tiếp hoàn hảo), thực hiện chính xác điều tương tự.

Cũng lưu ý rằng mySqrthàm của bạn cần trả về một giá trị ngay cả khi nó bị lỗi (hoặc một hàm trước đó đã bị lỗi). Vì vậy, bạn sẽ trả về một giá trị ma thuật (như NaN) hoặc đưa một giá trị không xác định vào chương trình của bạn và hy vọng không có gì sử dụng mà không kiểm tra trạng thái thành công mà bạn đã thực hiện trong quá trình thực thi.

Đối với tính chính xác - và hiệu suất - tốt hơn hết là vượt qua sự kiểm soát ngoài phạm vi một khi bạn không thể đạt được bất kỳ tiến bộ nào. Các trường hợp ngoại lệ và kiểm tra lỗi rõ ràng theo kiểu C với trả về sớm đều hoàn thành việc này.


Để so sánh, một ví dụ về ý tưởng của bạn thực sự có hiệu quả là Lỗi đơn nguyên trong Haskell. Ưu điểm so với hệ thống của bạn là bạn viết phần lớn logic của mình một cách bình thường, và sau đó bọc nó trong đơn nguyên, đảm nhiệm việc tạm dừng đánh giá khi một bước thất bại. Bằng cách này, mã duy nhất chạm trực tiếp vào hệ thống xử lý lỗi là mã có thể bị lỗi (ném lỗi) và mã cần đối phó với lỗi (bắt ngoại lệ).

Tôi không chắc rằng phong cách đơn nguyên và đánh giá lười biếng dịch tốt sang C ++.


1
Nhờ câu trả lời của bạn, nó thêm ánh sáng cho chủ đề. Tôi đoán người dùng sẽ không đồng ý and allowing my program to fail gracefully, and be re-runkhi anh ta vừa mất 2h làm việc:
Adrian Maire

14
Giải pháp của bạn có nghĩa là mỗi nơi bạn có thể tạo một tệp, bạn cần nhắc người dùng khắc phục tình huống và thử lại. Sau đó, mọi thứ khác có thể đi sai, bạn cũng cần phải sửa lỗi cục bộ. Với các trường hợp ngoại lệ, bạn chỉ cần nắm bắt std::exceptionở mức cao hơn của hoạt động logic, nói với người dùng "X thất bại vì ex.what ()" và đề nghị thử lại toàn bộ hoạt động khi và nếu chúng sẵn sàng.
Vô dụng

13
@AdrianMaire: "Cho phép thất bại một cách duyên dáng và được chạy lại" cũng có thể được thực hiện như showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try. Đó là cách xử lý duyên dáng cho một vấn đề thường không thể thực hiện được từ mã phát hiện vị trí lưu trữ đầu tiên đã đầy.
Bart van Ingen Schenau

3
@Usless Đánh giá lười biếng không liên quan gì đến việc sử dụng đơn vị Lỗi, bằng chứng là các ngôn ngữ đánh giá nghiêm ngặt như Rust, OCaml và F # đều sử dụng rất nhiều.
8bittree

1
@ Không cần IMO cho phần mềm chất lượng, điều đó nghĩa là mỗi nơi bạn có thể tạo một tệp, bạn cần nhắc người dùng khắc phục tình huống và thử lại. Các lập trình viên ban đầu thường có chiều dài đáng kể để phục hồi lỗi, ít nhất là chương trình TeX của Knuth có đầy đủ. Và với khung lập trình chữ viết tiếng Anh của anh ấy, anh ấy đã tìm ra cách để xử lý lỗi trong phần khác, để mã vẫn có thể đọc được và việc khôi phục lỗi được viết cẩn thận hơn (vì khi bạn viết phần khôi phục lỗi, đó là điểm và lập trình viên có xu hướng làm một công việc tốt hơn).
ShreevatsaR

15

Tôi muốn hiểu rõ hơn về ý nghĩa của việc sử dụng một mô hình như vậy trong một dự án:

  • Là tiền đề cho vấn đề chính xác? hoặc tôi đã bỏ lỡ một cái gì đó có liên quan?
  • Là giải pháp một ý tưởng kiến ​​trúc tốt? hoặc giá quá cao?

Cách tiếp cận của bạn mang lại một số vấn đề lớn vào mã nguồn của bạn:

  • nó dựa vào mã máy khách luôn ghi nhớ để kiểm tra giá trị của s. Điều này là phổ biến với các mã trả về sử dụng cho phương pháp xử lý lỗi và một trong những lý do khiến các ngoại lệ được đưa vào ngôn ngữ: với các ngoại lệ, nếu bạn thất bại, bạn không thất bại trong âm thầm.

  • bạn viết càng nhiều mã theo cách tiếp cận này, bạn cũng sẽ phải thêm mã nồi hơi lỗi để xử lý lỗi (mã của bạn không còn tối giản nữa) và nỗ lực bảo trì của bạn tăng lên.

Nhưng vài năm làm nhà phát triển khiến tôi thấy vấn đề từ một cách tiếp cận khác:

Các giải pháp cho những vấn đề này nên được tiếp cận ở cấp lãnh đạo kỹ thuật hoặc cấp nhóm:

Các lập trình viên có xu hướng đơn giản hóa nhiệm vụ của họ bằng cách đưa ra các ngoại lệ khi trường hợp cụ thể dường như quá hiếm để được thực hiện cẩn thận. Các trường hợp điển hình của vấn đề này là: hết các vấn đề về bộ nhớ, các vấn đề về đĩa đầy, các vấn đề về tệp bị hỏng, v.v ... Điều này có thể là đủ, nhưng không phải lúc nào cũng được quyết định từ cấp độ kiến ​​trúc.

Nếu bạn thấy mình xử lý mọi loại ngoại lệ có thể bị ném, mọi lúc, thì thiết kế không tốt; Những lỗi nào được xử lý, nên được quyết định theo các thông số kỹ thuật cho dự án, chứ không phải theo những gì các nhà phát triển cảm thấy muốn thực hiện.

Địa chỉ bằng cách thiết lập kiểm tra tự động, phân tách đặc điểm kỹ thuật của đơn vị kiểm tra và thực hiện (có hai người khác nhau thực hiện việc này).

Các lập trình viên có xu hướng không đọc tài liệu cẩn thận [...] Hơn nữa, ngay cả khi họ biết, họ không thực sự quản lý chúng.

Bạn sẽ không giải quyết điều này bằng cách viết thêm mã. Tôi nghĩ rằng đặt cược tốt nhất của bạn là đánh giá mã được áp dụng tỉ mỉ.

Các lập trình viên có xu hướng không nắm bắt ngoại lệ đủ sớm, và khi họ làm, chủ yếu là để đăng nhập và ném xa hơn. (tham khảo điểm đầu tiên).

Xử lý lỗi đúng là khó, nhưng ít tẻ nhạt hơn với các ngoại lệ so với giá trị trả về (cho dù chúng thực sự được trả lại hoặc được chuyển dưới dạng đối số i / o).

Phần khó nhất trong xử lý lỗi không phải là cách bạn nhận lỗi, mà là làm thế nào để đảm bảo ứng dụng của bạn giữ trạng thái nhất quán khi có lỗi.

Để giải quyết vấn đề này, cần chú ý nhiều hơn đến việc xác định và chạy trong các điều kiện lỗi (kiểm tra nhiều hơn, kiểm tra đơn vị / tích hợp nhiều hơn, v.v.).


12
Tất cả các mã sau khi một lỗi được bỏ qua, nếu bạn nhớ kiểm tra từng và mỗi lần bạn nhận được một thể hiện làm đối số . Đây là những gì tôi muốn nói bởi "bạn viết càng nhiều mã theo cách tiếp cận này, bạn càng phải thêm mã soạn mã lỗi". Bạn sẽ phải đánh đố mã của mình bằng if trên ví dụ thành công và mỗi khi bạn quên, đó là một lỗi. Vấn đề thứ hai gây ra bởi việc quên kiểm tra: mã thực thi cho đến khi bạn kiểm tra lại, hoàn toàn không nên được thực thi (tiếp tục nếu bạn quên kiểm tra, làm hỏng dữ liệu của bạn).
utnapistim

11
Không, xử lý một ngoại lệ (hoặc trả lại mã lỗi) không phải là sự cố - trừ khi lỗi / ngoại lệ gây tử vong về mặt logic hoặc bạn chọn không xử lý nó. Bạn vẫn có cơ hội xử lý trường hợp lỗi mà không cần phải kiểm tra rõ ràng ở mọi bước xem có xảy ra lỗi trước đó không
Vô dụng

11
@AdrianMaire Trong hầu hết mọi ứng dụng tôi làm việc, tôi cực kỳ thích một sự cố hơn là tiếp tục âm thầm. Tôi làm việc trên phần mềm quan trọng trong kinh doanh khi lấy một số đầu ra xấu và tiếp tục vận hành trên nó có thể dẫn đến mất rất nhiều tiền. Nếu tính chính xác là rất quan trọng và sự cố được chấp nhận, thì ngoại lệ có một lợi thế rất lớn ở đây.
Chris Hayes

1
@AdrianMaire - Tôi nghĩ khó có thể quên được việc xử lý một ngoại lệ rằng phương pháp quên câu lệnh if của bạn ... Bên cạnh đó - lợi ích chính của các ngoại lệ là lớp nào xử lý chúng. Bạn có thể muốn để một ngoại lệ hệ thống nổi lên hơn nữa để hiển thị thông báo lỗi ở cấp ứng dụng nhưng xử lý các tình huống mà bạn biết ở mức thấp hơn. Nếu bạn đang sử dụng thư viện của bên thứ ba hoặc mã nhà phát triển khác thì đây thực sự là lựa chọn duy nhất ...
Milney

5
@Adrian Không có lỗi, bạn dường như đã đọc sai những gì tôi đã viết hoặc bỏ lỡ nửa sau của nó. Toàn bộ quan điểm của tôi không phải là ngoại lệ sẽ xuất hiện trong quá trình thử nghiệm / phát triển và các nhà phát triển sẽ nhận ra rằng họ cần phải xử lý chúng. Vấn đề là hậu quả của một ngoại lệ hoàn toàn chưa được xử lý trong sản xuất là thích hợp hơn với hậu quả của mã lỗi không được kiểm soát. nếu bạn bỏ lỡ mã lỗi, bạn nhận được và tiếp tục sử dụng kết quả sai. Nếu bạn bỏ lỡ ngoại lệ, ứng dụng gặp sự cố và không tiếp tục chạy, bạn sẽ không nhận được kết quả không sai . (tt)
Mr.Mindor
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.