오류 처리 고려 사항


31

문제 :

오랜 시간이 지난 지금, 나는 그 exceptions메커니즘이 실제로 해결되어야한다고 생각하지 않기 때문에 메커니즘 에 대해 걱정하고 있습니다.

CLAIM :이 주제와 관련하여 오랫동안 논란이 있었으며 대부분은 exceptions오류 코드 를 비교하고 반환하는 데 어려움을 겪고 있습니다. 이것은 분명히 여기의 주제가 아닙니다.

오류를 정의하려고하면 Bjarne Stroustrup & Herb Sutter의 CppCoreGuidelines에 동의합니다.

오류는 기능이 광고 목적을 달성 할 수 없음을 의미합니다

CLAIM :이 exception메커니즘은 오류 처리를위한 언어 의미입니다.

CLAIM : 과제를 달성하지 못한 기능에 대해서는 "변명 없음"이 있습니다 : 사전 / 사후 조건을 잘못 정의하여 기능이 결과를 보장 할 수 없거나 일부 예외적 인 경우가 개발에 시간을 소비하기에 충분히 중요한 것으로 간주되지 않음 해결책. IMO를 고려할 때 일반 코드와 오류 코드 처리의 차이점은 (구현 전) 매우 주관적인 선입니다.

CLAIM : 사전 또는 사후 조건이 유지되지 않는 경우를 표시하기 위해 예외를 사용하는 것이 exception메커니즘 의 또 다른 목적 , 주로 디버깅 목적입니다. 나는이 사용법을 목표로하지 않습니다 exceptions.

많은 서적, 자습서 및 기타 소스에서 오류 처리는 상당히 객관적인 과학으로 표시되는 경향이 있으며, 이는 해결 된 exceptions것이며 catch모든 상황에서 복구 할 수있는 강력한 소프트웨어 를 제공 하기 위해 필요 합니다. 그러나 개발자로서 몇 년 동안 다른 접근 방식으로 문제를 볼 수 있습니다.

  • 프로그래머는 특정 사례를 신중하게 구현하기에는 너무 드물게 보일 때 예외를 던져서 작업을 단순화하는 경향이 있습니다. 일반적인 경우는 메모리 부족 문제, 디스크 가득 참 문제, 손상된 파일 문제 등입니다. 충분할 수도 있지만 항상 아키텍처 수준에서 결정되는 것은 아닙니다.
  • 프로그래머는 라이브러리의 예외에 대한 문서를주의 깊게 읽지 않으며 일반적으로 함수가 언제 언제 발생하는지 알지 못합니다. 또한, 그들이 알더라도 실제로 관리하지 않습니다.
  • 프로그래머는 조기에 예외를 포착하지 않는 경향이 있으며, 그렇게 할 때는 대부분 로그를 작성하고 추가로 처리해야합니다. (첫 번째 포인트 참조).

두 가지 결과가 있습니다.

  1. 자주 발생하는 오류는 개발 초기에 감지되어 디버깅됩니다 (좋은).
  2. 드문 예외는 관리되지 않으며 사용자 홈에서 시스템이 멋진 로그 메시지와 함께 충돌하게합니다. 때때로 오류가보고되거나보고되지 않습니다.

이를 고려할 때 IMO 오류 메커니즘의 주요 목적은 다음과 같아야합니다.

  1. 특정 사례가 관리되지 않는 코드에 표시하십시오.
  2. 이 상황이 발생하면 이슈 런타임을 관련 코드 (적어도 발신자)에게 알리십시오.
  3. 복구 메커니즘을 제공합니다

exception오류 처리 메커니즘으로서의 의미론 의 주요 결함 은 IMO입니다. throw소스 코드 에서 a 가 어디에 있는지 쉽게 알 수 있지만 선언을 보면 특정 함수가 발생할 수 있는지 알 수 없습니다. 이것은 위에서 소개 한 모든 문제를 가져옵니다.

언어는 언어의 다른 측면 (예 : 강력한 변수 유형)에 대해 오류 코드를 엄격하게 시행하고 확인하지 않습니다.

해결책을위한 시도

이를 개선하기 위해 매우 간단한 오류 처리 시스템을 개발했습니다.이 오류 처리 시스템은 오류 처리를 일반 코드와 동일한 수준으로 중요하게 만듭니다.

아이디어는 다음과 같습니다.

  • 각각의 (관련된) 기능은 success매우 가벼운 물체에 대한 참조를 수신하고 경우에 따라 오류 상태로 설정할 수 있습니다. 텍스트 오류가 저장 될 때까지 개체가 매우 밝습니다.
  • 제공된 객체에 이미 오류가있는 경우 함수는 해당 작업을 건너 뛰는 것이 좋습니다.
  • 오류를 무시해서는 안됩니다.

전체 디자인은 각 측면 (약 10 페이지)과 OOP에 적용하는 방법을 철저히 고려합니다.

Success수업의 예 :

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;
};

용법:

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;

나는 그것을 내 (자신의) 많은 코드에서 사용했고 프로그래머 (me)가 예외적 인 경우와 그 해결 방법 (좋은)에 대해 더 생각하도록 강요했습니다. 그러나 학습 곡선이 있으며이를 사용하는 코드와 잘 통합되지 않습니다.

질문

프로젝트에서 그러한 패러다임을 사용하는 의미를 더 잘 이해하고 싶습니다.

  • 문제의 전제가 정확합니까? 또는 관련된 것을 놓쳤습니까?
  • 솔루션이 좋은 건축 아이디어입니까? 아니면 가격이 너무 높습니까?

편집하다:

방법 비교 :

//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
"이 질문은 연구 노력을 보여줍니다. 유용하고 명확합니다."라는 의견에 동의하지 않았습니다. (세부 사항은 답변에 따를 수 있습니다.)
Martin Ba

2
물론, 나는 그것을 이해하고 동의합니다! 이 질문의 목적은 비판을받는 것입니다. 그리고 OP의 옳은 것이 아니라 좋은 / 나쁜 질문을 나타내는 질문의 점수.
Adrian Maire

2
내가 올바르게 이해하면 예외에 대한 주된 어려움은 사람들이 예외를 처리하는 대신 (C ++로) 무시할 수 있다는 것입니다. 그러나 성공 구성에는 의도적으로 동일한 결함이 있습니다. 예외와 마찬가지로 무시해도됩니다. 더 나쁜 것은 더 장황하고 계단식 리턴으로 이어지며 업스트림에 "잡을"수조차 없습니다.
dagnelies

3
왜 모나드와 같은 것을 사용하지 않습니까? 그들은 오류를 암시 적으로 만들지 만 실행 중에는 침묵하지 않습니다. 실제로 코드를 볼 때 가장 먼저 생각한 것은 "monads, nice"였습니다. 그들을보세요.
bash0r

2
내가 예외를 좋아하는 주된 이유는 주어진 코드 블록에서 예기치 않은 모든 오류를 포착하고 일관되게 처리 할 수 ​​있기 때문입니다. 그렇습니다. 코드가 작업을 수행하지 않아야 할 이유가 없습니다. "버그가있었습니다"는 나쁜 이유 이지만 여전히 발생합니다 . 그리고 발생하는 경우 원인을 기록하고 메시지를 표시하거나 다시 시도하려고합니다. (원격 시스템과 복잡하고 재시작 가능한 상호 작용을 수행하는 코드가 있습니다. 원격 시스템이 다운되면 로그를 기록하고 처음부터 다시 시도하고 싶습니다)
user253751

답변:


32

오류 처리 는 아마도 프로그램에서 가장 어려운 부분 일 것입니다.

일반적으로 오류 조건이 있다는 것을 쉽게 알 수 있습니다. 그러나 우회 할 수없는 방식으로 신호를 보내고 적절하게 처리하는 것은 매우 어려운 일입니다 ( 아브라함의 예외 안전 수준 참조 ).

C에서 신호 오류는 솔루션에 동형 인 리턴 코드에 의해 수행됩니다.

C ++은 단점으로 인해 예외를 도입했습니다. 은 이러한 접근 방식 . 즉, 호출자가 오류가 발생했는지 여부를 확인하고 끔찍하게 다르게 실패하는 경우를 기억하는 경우에만 작동합니다. "매번 괜찮다면 ..."이라고 말하는 것을 발견 할 때마다 문제가 있습니다. 인간은 돌보아도 세심한주의를 기울이지 않습니다.

그러나 문제는 예외에 자체 문제가 있다는 것입니다. 즉, 보이지 않는 / 숨겨진 제어 흐름. 이것은 오류 처리 상용구에 의해 코드의 논리가 난독 화되지 않도록 오류 사례를 숨기는 것입니다. 오류 경로를 근소하게 만드는 대신 "행복한 경로"를 훨씬 더 명확하고 빠릅니다!


다른 언어가이 문제에 어떻게 접근하는지 보는 것이 흥미 롭습니다.

  • Java는 예외 및 검사되지 않은 예외를 확인했습니다.
  • Go는 오류 코드 / 패닉을 사용합니다.
  • 녹은 합계 유형 / 패닉을 사용합니다).
  • 일반적으로 FP 언어.

C ++은 확인 된 예외 형식을 사용하는 데 사용되었지만 noexcept(<bool>)대신 기본으로 향하여 더 이상 사용되지 않고 단순화되었음을 알 수 있습니다 . 확인 된 예외는 확장 성이 부족하여 어색한 매핑 / 중첩을 유발할 수 있다는 점에서 다소 문제가 있습니다. 복잡한 예외 계층 (가상 상속의 주요 사용 사례 중 하나는 예외입니다 ...)

반대로 Go and Rust는 다음과 같은 접근 방식을 취합니다.

  • 오류는 대역에서 신호를 보내야합니다.
  • 예외적 인 상황에서는 예외를 사용해야합니다.

후자는 (1) 예외의 이름을 공황으로 지정 하고 (2) 여기에 유형 계층 / 복합 절이 없다는 점에서 분명 합니다. 이 언어는 "공황"의 내용을 검사 할 수있는 기능을 제공하지 않습니다. 유형 계층 구조, 사용자 정의 내용, 단지 "죄송합니다. 복구가 불가능합니다."

이를 통해 사용자는 적절한 오류 처리를 효과적으로 권장하면서도 예외적 인 상황에서 쉽게 구제 할 수있는 방법을 남길 수 있습니다 (예 : "아직 구현하지 않았습니다!").

물론 Go 접근 방식은 불행히도 오류 확인을 쉽게 잊을 수 있다는 점에서 귀하의 접근 방식과 매우 유사합니다 ...

... 그러나 Rust 방식은 주로 두 가지 유형을 중심으로합니다.

  • Option유사한 인 std::optional,
  • Result이는 Ok와 Err의 두 가지 변형입니다.

성공 여부를 확인하지 않고 실수로 결과를 사용할 수있는 기회가 없기 때문에 훨씬 더 깔끔합니다.


FP 언어는 세 가지 계층으로 나눌 수있는 구문에서 오류 처리를 구성합니다.-Functor-적용 / 대체-Monads / 대체

Haskell의 Functortypeclass를 살펴 보자 .

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

우선, 타입 클래스는 다소 비슷하지만 인터페이스와 동일하지 않습니다. Haskell의 함수 시그니처는 처음에는 약간 무섭게 보입니다. 그러나 그것들을 해독합시다. 이 함수 fmap는 함수를 다소 비슷한 첫 번째 매개 변수로 사용합니다 std::function<a,b>. 다음은입니다 m a. 당신이 상상할 수있는 m뭔가로 std::vectorm a같은 등 std::vector<a>. 그러나 차이점은 m a명시 적으로해야한다고 말하는 것은 아닙니다 std:vector. 그래서 그것도 될 수 있습니다 std::option. 언어에 or Functor와 같은 특정 유형의 유형 클래스에 대한 인스턴스가 있다고 말하면 해당 유형 의 함수 를 사용할 수 있습니다 . 동일은 typeclasses에 대해이 작업을 수행해야합니다 , 그리고std::vectorstd::optionfmapApplicativeAlternativeMonad상태 저장 가능하고 실패한 계산을 수행 할 수 있습니다. Alternativetypeclass 구현 오류 복구 추상화. a <|> b이를 통해 term a또는 term 이라는 의미 와 같은 것을 말할 수 있습니다 b. 두 계산 모두 성공하지 못하면 여전히 오류입니다.

Haskell의 Maybe유형을 살펴 보겠습니다 .

data Maybe a
  = Nothing
  | Just a

이것은 당신이 기대하는 곳에 또는 Maybe a을 얻는다는 것을 의미 합니다 . 볼 때 위, 구현처럼 볼 수 있었다NothingJust afmap

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

case ... of표현을 패턴 일치라고하며 OOP 세계에서로 알려진 것과 유사합니다 visitor pattern. 라인 case m ofm.apply(...)점 이라고 가정하고 점은 디스패치 함수를 구현하는 클래스의 인스턴스화입니다. case ... of표현식 아래의 행 은 클래스의 필드를 범위별로 이름별로 직접 가져 오는 각각의 디스패치 함수입니다. 에 Nothing지점 우리는 생성 Nothing과에 Just a지점 우리는 우리의 유일한 값의 이름을 a다른를 만들 Just ...변환 기능 f에 적용 a. 다음과 같이 읽으십시오 : new Just(f(a)).

실제 오류 확인을 추상화하면서 잘못된 계산을 처리 할 수 ​​있습니다. 이러한 종류의 계산을 매우 강력하게 만드는 다른 인터페이스에 대한 구현이 있습니다. 실제로 MaybeRust 's에 대한 영감입니다 Option.


대신 Success수업 을 다시 진행하도록 권장합니다 Result. Alexandrescu는 실제로 expected<T>표준 제안 이 만들어 지는 이라는 매우 가까운 것을 제안 했습니다 .

나는 Rust 네이밍과 API를 고집 할 것입니다. 왜냐하면 그것은 문서화되고 작동하기 때문입니다. 물론 Rust에는 ?코드를 훨씬 더 달콤하게 만드는 멋진 접미사 연산자가 있습니다. C ++에서는 TRY매크로와 GCC의 명령문 표현식 을 사용하여 에뮬레이션합니다.

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); })

참고 : 이것은 Result자리 표시 자입니다. 적절한 구현은 캡슐화와 a를 사용합니다 union. 그러나 요점을 이해하는 것으로 충분합니다.

어느 쓰기 날 수 있습니다 ( 동작에서 볼 ) :

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);
}

내가 정말 깔끔하게 찾으십시오.

  • 오류 코드 (또는 Success클래스)를 사용하는 것과 달리 오류 를 확인하지 않으면 임의의 동작이 아닌 런타임 오류 1 이 발생합니다 .
  • 예외를 사용하는 것과는 달리, 콜 사이트 에서 기능이 실패 할 수 있으므로 명백 합니다 .
  • C ++-2X 표준 concepts을 사용하면 표준에 도달 할 수 있습니다. 이것은 우리가 오류 종류에 대한 선택을 남길 수 있기 때문에 이런 종류의 프로그래밍을 훨씬 더 즐겁게 만듭니다. 예를 std::vector들어 결과를 구현 하면 가능한 모든 솔루션을 한 번에 계산할 수 있습니다. 또는 제안한대로 오류 처리 기능을 향상시킬 수 있습니다.

1 올바르게 캡슐화 된 Result구현;)


참고 : 예외와 달리이 경량 Result에는 역 추적이 없으므로 로깅 효율성이 떨어집니다. 최소한 오류 메시지가 생성되는 파일 / 줄 번호를 기록하고 일반적으로 풍부한 오류 메시지를 작성하는 것이 유용 할 수 있습니다. TRY매크로를 사용할 때마다 파일 / 라인을 캡처하여 본질적으로 백 트레이스를 수동으로 생성하거나 플랫폼 별 코드 및 라이브러리 libbacktrace를 사용하여 콜 스택의 심볼을 나열하는 등의 방법으로 복합화 할 수 있습니다.


그러나 하나의 큰 경고가 있습니다. 기존 C ++ 라이브러리 및 심지어 std예외도 기반입니다. 타사 라이브러리의 API를 어댑터로 감싸 야 하므로이 스타일을 사용하는 것은 힘든 일이 될 것입니다 ...


3
그 매크로는 매우 잘못 보입니다. 나는 ({...})gcc 확장 이라고 가정 하지만 그렇게해서는 안되는가 if (!result.ok) return result;? 상태가 거꾸로 나타나고 불필요한 오류 사본을 만듭니다.
Mooing Duck

@MooingDuck 정답 ({...})은 gcc의 statement 표현 입니다.
jamesdlin


1
C ++ 17을 사용 std::variant하는 Result경우 구현에 사용 하는 것이 좋습니다 . 또한 오류를 무시하면 경고가 표시됩니다.[[nodiscard]]
Justin

2
@Justin : 사용 여부 std::variant는 예외 처리와 관련된 트레이드 오프를 고려할 때 약간의 맛 문제입니다. [[nodiscard]]실제로 순수한 승리입니다.
Matthieu M.

46

CLAIM : 예외 메커니즘은 오류 처리를위한 언어 의미입니다.

제어 흐름 메커니즘은 예외입니다. 이 제어 흐름 메커니즘의 동기는 오류 처리가 오류가 아닌 코드와 분리되는 것이 었 습니다. 일반적인 경우에 오류 처리는 매우 반복적이고 논리의 주요 부분과는 거의 관련이 없습니다.

CLAIM : 과제를 달성하지 못한 기능에 대해서는 "변명 없음"이 있습니다 : 사전 / 사후 조건을 잘못 정의하여 기능이 결과를 보장 할 수 없거나 일부 예외적 인 경우가 개발에 시간을 소비하기에 충분히 중요한 것으로 간주되지 않음 해결책

고려 : 파일을 만들려고합니다. 저장 장치가 가득 찼습니다.

이제, 이것은 내 전제 조건을 정의하는 데 실패한 것이 아닙니다. 공유 스토리지는이를 충족시킬 수없는 경쟁 조건에 종속되기 때문에 일반적으로 "충분한 스토리지가 있어야합니다"를 전제 조건으로 사용할 수 없습니다.

그래서 내 프로그램이 어떻게 든 여유 공간을 확보 한 다음 성공적으로 진행해야합니까? 그렇지 않으면 "솔루션 개발"에 너무 게으른가요? 솔직히 말도 안되는 것처럼 보입니다. 공유 스토리지를 관리하기위한 "해결책은" 내 프로그램의 범위를 벗어난 , 사용자가 하나 일부 공간을 발표, 또는 더 많은 스토리지를 추가되면 내 프로그램 다시 실행을 정상적으로 실패 할 수있다 훌륭한 .


성공 클래스의 역할은 오류 처리를 프로그램 논리와 매우 명시 적으로 인터리브하는 것입니다. 모든 단일 기능은 실행하기 전에 어떤 오류가 이미 발생했는지 확인해야하므로 아무 것도하지 않아야합니다. 모든 라이브러리 함수는 다른 함수로 래핑되어야합니다. 하나의 인수 (그리고 완벽하게 전달하기)는 정확히 같은 일을합니다.

또한 함수는 실패했거나 이전 함수가 실패한 경우 에도mySqrt 값을 반환해야합니다 . 따라서 마법 값 (예 :)을 반환 하거나 불확실한 값을 프로그램에 삽입하고 실행을 통해 스레드 성공 상태를 확인하지 않고는 아무것도 사용하지 않기를 바랍니다.NaN

정확성과 성능을 위해 진행할 수없는 경우 제어를 범위 밖으로 다시 전달하는 것이 훨씬 좋습니다. 예외와 C 스타일 명시 적 오류 검사는 조기 반환으로 이를 수행합니다.


비교를 위해 실제로 작동하는 아이디어의 예는 Haskell 의 Error monad 입니다. 시스템에 비해 장점은 대부분의 로직을 정상적으로 작성한 다음 한 단계가 실패하면 평가 중지를 처리하는 모나드로 래핑하는 것입니다. 이렇게하면 오류 처리 시스템에 직접 닿는 유일한 코드는 실패 할 수있는 코드 (오류가 발생)와 실패에 대처해야하는 코드 (예외를 포착)입니다.

그래도 모나드 스타일과 게으른 평가가 C ++로 잘 번역되는지 확실하지 않습니다.


1
귀하의 답변 덕분에 주제에 빛을 더합니다. and allowing my program to fail gracefully, and be re-run방금 2 시간 작업을 잃었을 때 사용자가 동의하지 않는 것 같습니다.
Adrian Maire

14
솔루션은 파일을 작성할 수있는 모든 장소에서 상황을 수정하고 재 시도하도록 사용자에게 프롬프트해야합니다. 그런 다음 잘못 될 수있는 다른 모든 것, 어떻게 든 로컬로 수정해야합니다. 예외적으로, std::exception더 높은 수준의 논리 연산을 포착 하고 사용자에게 "ex.what ()"으로 인해 X 실패했음을 알리고 준비가 완료되면 언제든 전체 작업을 다시 시도하도록 제안하십시오.
쓸모없는

13
@AdrianMaire : "정상적으로 실패하고 다시 실행하도록 허용"도로 구현할 수 있습니다 showing the Save dialog again along with an error message and allowing the user to specify an alternative location to try. 이는 첫 번째 저장 위치가 가득 찼음을 감지하는 코드로는 일반적으로 수행 할 수없는 문제를 정상적으로 처리하는 것입니다.
Bart van Ingen Schenau

3
@Useless Lazy 평가는 Rust, OCaml 및 F #와 같은 엄격한 평가 언어에서 모두 사용하는 것으로 입증 된 바와 같이 Error 모나드의 사용과 관련이 없습니다.
비트 트리

1
@Useless IMO 품질의 소프트웨어를 위해, 그것은 하지 "당신이 파일을 만들 수 있습니다 모든 장소, 당신은 상황과 재 시도를 해결하기 위해 사용자에게 메시지를 표시 할 필요가"확인 감각. 초기 프로그래머는 종종 오류 복구를 위해 놀랄만 한 길이를 보였습니다. 최소한 Knuth의 TeX 프로그램에는 오류가 가득합니다. 또한 "리터 블 프로그래밍"프레임 워크를 사용하여 다른 섹션에서 오류 처리를 유지할 수있는 방법을 찾았으므로 코드를 계속 읽을 수 있고 오류 복구는 더주의해서 작성됩니다 (오류 복구 섹션을 작성할 때 그것은 요점이며 프로그래머는 더 나은 일을하는 경향이 있습니다).
ShreevatsaR

15

프로젝트에서 그러한 패러다임을 사용하는 의미를 더 잘 이해하고 싶습니다.

  • 문제의 전제가 정확합니까? 또는 관련된 것을 놓쳤습니까?
  • 솔루션이 좋은 건축 아이디어입니까? 아니면 가격이 너무 높습니까?

접근 방식은 소스 코드에 몇 가지 큰 문제를 가져옵니다.

  • 항상의 값을 확인하는 것을 기억하는 클라이언트 코드에 의존합니다 s. 이는 오류 처리 방식에 대한 리턴 코드 사용 및 예외가 언어에 도입 된 이유 중 하나 와 공통입니다. 예외가 있으면 실패하더라도 자동으로 실패하지 않습니다.

  • 이 접근 방식으로 더 많은 코드를 작성할수록 오류 처리 (코드가 더 이상 최소가 아님)를 위해 더 많은 오류 상용구 코드를 추가해야하며 유지 관리 노력이 증가합니다.

그러나 개발자로서 몇 년 동안 다른 접근 방식으로 문제를 보았습니다.

이러한 문제에 대한 솔루션은 기술 리드 수준 또는 팀 수준에서 접근해야합니다.

프로그래머는 특정 사례를 신중하게 구현하기에 너무 드물게 보일 때 예외를 던져서 작업을 단순화하는 경향이 있습니다. 일반적인 경우는 메모리 부족 문제, 디스크 가득 참 문제, 손상된 파일 문제 등입니다. 충분할 수도 있지만 항상 아키텍처 수준에서 결정되는 것은 아닙니다.

항상 발생할 수있는 모든 유형의 예외를 처리하는 것을 발견하면 디자인이 좋지 않습니다. 처리되는 오류는 개발자의 구현 느낌이 아니라 프로젝트 사양에 따라 결정되어야합니다.

자동화 된 테스트를 설정하고 단위 테스트와 구현의 사양을 분리하여 해결합니다 (두 사람이이 작업을 수행함).

프로그래머는주의 깊게 문서를 읽지 않는 경향이 있습니다. [...] 또한, 그들이 알고 있더라도 실제로 관리하지는 않습니다.

더 많은 코드를 작성하여이 문제를 해결하지 않습니다. 최선의 방법은 세 심하게 적용되는 코드 검토라고 생각합니다.

프로그래머는 조기에 예외를 포착하지 않는 경향이 있으며, 그렇게 할 때는 대부분 로그를 작성하고 추가로 처리해야합니다. (첫 번째 포인트 참조).

적절한 오류 처리는 어렵지만 반환 값보다 실제로 예외가 덜 지루합니다 (실제로 반환되거나 i / o 인수로 전달되는지 여부).

오류 처리에서 가장 까다로운 부분은 오류를받는 방법이 아니라 오류가있을 때 응용 프로그램이 일관성있는 상태를 유지하도록하는 방법입니다.

이를 해결하기 위해 오류 조건을 식별하고 실행하는 데 더 많은주의를 기울여야합니다 (더 많은 테스트, 더 많은 단위 / 통합 테스트 등).


12
인스턴스를 인수로받을 때마다 각각을 확인 해야하는 경우 오류 후 모든 코드를 건너 뜁니다 . 이것이 바로 "이 접근 방식으로 작성한 코드가 많을수록 더 많은 상용구 코드도 추가해야합니다"라는 의미입니다. 성공 인스턴스의 경우 if로 코드를 수수께끼로 묶어야하며 잊을 때마다 버그입니다. 확인을 잊어 버린 두 번째 문제 : 다시 확인 할 때까지 실행되는 코드는 전혀 실행되지 않아야합니다 (확인을 잊어 버린 경우 계속하면 데이터가 손상됨).
utnapistim

11
아니오, 예외 처리 (또는 오류 코드 리턴)는 오류 / 예외가 논리적으로 치명적이거나 처리하지 않기로 선택하지 않는 한 충돌이 아닙니다. 모든 단계에서 이전에 오류가 발생했는지 여부를 명시 적으로 확인할 필요없이 여전히 오류 사례를 처리 수 있습니다.
쓸모없는

11
@AdrianMaire 내가 작업하는 거의 모든 응용 프로그램에서 나는 조용히 계속되는 것보다 충돌을 선호합니다. 나는 출력이 나쁘고 계속 작동하면 많은 돈을 잃을 수있는 비즈니스 크리티컬 소프트웨어를 사용합니다. 정확성이 중요하고 충돌이 허용되는 경우 예외는 매우 큰 이점이 있습니다.
Chris Hayes

1
@AdrianMaire-if 문을 잊어 버리는 방법을 예외를 처리하는 것을 잊어 버리는 것이 훨씬 어렵다고 생각합니다 ... 게다가-예외의 주요 이점은 어떤 레이어가 예외를 처리하는지입니다. 응용 프로그램 레벨 오류 메시지를 표시하기 위해 시스템 예외를 추가로 버블 링하고 더 낮은 레벨에서 알고있는 상황을 처리 할 수 ​​있습니다. 타사 라이브러리 또는 다른 개발자 코드를 사용하는 경우 이것이 유일한 선택입니다.
Milney

5
@Adrian 실수는 없습니다, 당신은 내가 쓴 내용을 읽거나 그 후반부를 놓친 것 같습니다. 필자의 요점은 테스트 / 개발 중에 예외가 발생하고 개발자가 예외를 처리해야한다는 것을 깨닫게된다는 것이 아닙니다. 요점은 생산에서 완전히 처리되지 않은 예외의 결과가 검사되지 않은 오류 코드의 결과보다 선호된다는 것입니다. 오류 코드를 놓치면 잘못된 결과를 얻고 계속 사용합니다. 당신은 예외 응용 프로그램이 충돌을 그리워하고 계속 실행하지 않으면 얻을 수없는 결과가 없습니다 잘못된 결과를 . (계속)
Mr.Mindor
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.