예외 처리 메커니즘없이 현대 언어를 디자인해야하는 이유는 무엇입니까?


47

많은 현대 언어 는 풍부한 예외 처리 기능을 제공 하지만 Apple의 Swift 프로그래밍 언어 는 예외 처리 메커니즘을 제공하지 않습니다 .

내가 예외를 겪었을 때, 이것이 의미하는 바에 대해 마음을 감싸는 데 어려움을 겪고 있습니다. Swift에는 어설 션이 있으며 물론 반환 값이 있습니다. 그러나 나는 예외 기반 사고 방식이 예외없이 세계에 어떻게 매핑되는지 이해하는 데 어려움을 겪고 있습니다 (그리고 그 문제에 대해 왜 그런 세계가 바람직한가 ). 예외로 할 수있는 Swift와 같은 언어로 할 수없는 일이 있습니까? 예외를 잃어 무언가를 얻습니까?

예를 들어 내가 가장 표현하는 방법 같은 것을

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

예외 처리가없는 언어 (예 : 스위프트)?


11
panic똑같지 않은 것을 무시하면 목록에 Go를 추가 할 수 있습니다 . 또한 거기에서 말한 바와 같이 예외는 GOTO분명한 이유로 아무도 그렇게 부르지 않지만 을 수행하는 정교하고 편안한 방법에 지나지 않습니다 .
JensG

3
귀하의 질문에 대한 정답은 예외를 작성하기 위해 언어 지원이 필요하다는 것입니다. 언어 지원에는 일반적으로 메모리 관리가 포함됩니다. 예외는 어디에서나 발생할 수 있고 어디에서나 잡힐 수 있으므로 제어 흐름에 의존하지 않는 객체를 처리하는 방법이 필요합니다.
Robert Harvey

1
@Robert, 나는 당신을 따르지 않습니다. C ++은 가비지 수집없이 예외를 지원하도록 관리합니다.
Karl Bielefeldt

3
@ KarlBielefeldt : 내가 이해 한 것에서 큰 비용으로. 다시 한 번, 노력과 필요한 도메인 지식에 큰 비용을 들이지 않고 C ++에서 수행되는 것이 있습니까?
Robert Harvey

2
@RobertHarvey : 좋은 지적입니다. 나는 이것들에 대해 충분히 열심히 생각하지 않는 사람들 중 하나입니다. ARC가 GC라는 생각에 빠져 들었지만 물론 그렇지 않습니다. 그래서 기본적으로 (거의 파악한다면) 예외는 객체를 처분하는 것이 제어 흐름에 의존하는 언어에서 지저분한 것입니다 (C ++에도 불구하고)?
orome 2009 년

답변:


33

임베디드 프로그래밍에서는 스택 해제의 오버 헤드가 실시간 성능을 유지하려고 할 때 허용 할 수없는 변동으로 간주 되었기 때문에 전통적으로 예외는 허용되지 않았습니다. 스마트 폰은 기술적으로 실시간 플랫폼으로 간주 될 수 있지만 임베디드 시스템의 기존 제한 사항이 더 이상 적용되지 않는 지금은 충분히 강력합니다. 나는 철저하게하기 위해 그것을 제기합니다.

함수형 프로그래밍 언어에서는 예외가 지원되는 경우가 많지만 거의 사용되지 않는 경우는 거의 없습니다. 한 가지 이유는 기본적으로 게으르지 않은 언어에서도 가끔씩 수행되는 게으른 평가입니다. 대기열에 넣은 장소와 다른 스택으로 실행되는 함수가 있으면 예외 처리기를 어디에 둘지 결정하기가 어렵습니다.

다른 이유는 일급 함수가 옵션 및 미래와 같은 구성을 허용하여 예외의 구문 적 이점을 더 유연하게 제공하기 때문입니다. 다시 말해, 나머지 언어는 예외가 당신을 사지 않을 정도로 충분히 표현 적입니다.

저는 Swift에 익숙하지 않지만 오류 처리에 대해 읽은 내용은 오류 처리가 더 기능적인 스타일 패턴을 따르도록 의도되었음을 나타냅니다. 나는 코드로 예를 본 적이 successfailure선물처럼 매우 모양 블록을.

다음 Future이 스칼라 튜토리얼 의 예제를 사용하는 것 입니다 .

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

예외를 사용하여 예제와 거의 동일한 구조를 가지고 있음을 알 수 있습니다. future블록은 같다 try. onFailure블록은 예외 처리기 같다. 스칼라에서는 대부분의 기능적 언어와 Future마찬가지로 언어 자체를 사용하여 완전히 구현됩니다. 예외처럼 특별한 구문이 필요하지 않습니다. 즉, 자신 만의 유사한 구성을 정의 할 수 있습니다. timeout예를 들어 블록 또는 로깅 기능을 추가 할 수 있습니다.

또한 미래를 전달하고 함수에서 반환하거나 데이터 구조 등에 저장할 수 있습니다. 최고의 가치입니다. 스택 바로 위에 전파되어야하는 예외처럼 제한되지 않습니다.

옵션은 약간 다른 방식으로 오류 처리 문제를 해결하므로 일부 사용 사례에 더 적합합니다. 한 가지 방법 만 고수하지는 않습니다.

이것들은 "예외를 잃음으로써 얻는 것"입니다.


1
따라서 Future본질적으로 함수에서 반환되는 값을 기다리지 않고 검사하는 방법입니다. Swift와 마찬가지로 반환 값을 기반으로하지만 Swift와 달리 반환 값에 대한 응답은 나중에 발생할 수 있습니다 (예외와 약간 유사). 권리?
orome

1
당신은 Future올바르게 이해 하지만, 당신이 Swift를 잘못 특성화하고 있다고 생각합니다. 예를 들어이 stackoverflow 답변 의 첫 번째 부분을 참조하십시오 .
Karl Bielefeldt

흠, 나는 Swift를 처음 사용하기 때문에 대답하기가 약간 어렵습니다. 그러나 내가 실수하지 않으면 : 그것은 본질적 으로 나중에 호출 할 수 있는 핸들러를 전달합니다 . 권리?
orome 2009 년

예. 기본적으로 오류가 발생하면 콜백을 생성합니다.
Karl Bielefeldt

Either더 나은 예가 될 것입니다 IMHO
Paweł Prażak

19

예외로 인해 코드를 추론하기가 더 어려워 질 수 있습니다. 그것들은 고 토스만큼 강력하지는 않지만 지역적이지 않기 때문에 동일한 문제를 많이 일으킬 수 있습니다. 예를 들어 다음과 같은 명령 코드가 있다고 가정 해 봅시다.

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

이러한 절차 중 하나라도 예외를 throw 할 수 있는지 한 눈에 알 수 없습니다. 이를 파악하려면 이러한 각 절차의 설명서를 읽어야합니다. (일부 언어에서는이 정보를 사용하여 형식 서명을 보강하여이 작업을 약간 더 쉽게 수행 할 수 있습니다.) 위의 코드는 프로 시저 발생 여부에 관계없이 잘 컴파일되므로 예외 처리를 잊어 버리기가 쉽습니다.

또한, 의도가 예외를 다시 발신자에게 전파하려는 경우에도 일관되지 않은 상태로 남아 있지 않도록 방지하기 위해 코드를 추가해야하는 경우가 있습니다 (예 : 커피 메이커가 고장 나더라도 여전히 엉망을 정리하고 반품해야 함) 찻잔!). 따라서 많은 경우 예외를 사용하는 코드는 추가 정리가 필요하지 않은 코드처럼 복잡하게 보입니다.

충분히 강력한 타입 시스템으로 예외를 모방 할 수 있습니다. 예외를 피하는 많은 언어는 반환 값을 사용하여 동일한 동작을 얻습니다. C에서 수행되는 방식과 비슷하지만 현대적인 유형의 시스템은 일반적으로 오류 조건을 처리하는 것을 잊어 버리기보다 우아하고 우아합니다. 그들은 또한 성가신 설탕을 제공하여 물건을 덜 성가 시게 만들고 때로는 예외와 같이 깨끗하게 만듭니다.

특히, 별도의 기능으로 구현하지 않고 유형 시스템에 오류 처리 기능을 내장함으로써 오류와 관련이없는 다른 부분에 "예외"를 사용할 수 있습니다. (예외 처리는 실제로 모나드의 속성이라는 것이 잘 알려져 있습니다.)


선택 사항을 포함하여 Swift가 보유한 유형 시스템이이를 달성하는 "강력한 유형 시스템"이라는 것이 맞습니까?
orome

2
예, 선택 사항이며보다 일반적으로 합계 유형 (Swift / Rust에서 "enum"이라고 함)이이를 달성 할 수 있습니다. 그러나 사용하기 쉽도록하기 위해 약간의 추가 작업이 필요합니다. Swift에서는 선택적 체인 구문을 사용하여이를 수행합니다. Haskell에서는이를 모나 딕 표기법으로 수행합니다.
Rufflewind

하지 꽤 쓸모 경우 "충분히 강력한 타입 시스템은"스택 추적을 줄 수
파블 Prażak

1
예외로 인해 제어 흐름이 흐려진다는 점을 지적하면 +1입니다. 그냥 auxilary 참고로 : 그것은 예외가 실제로보다 더 악하지 여부를 추론하기 위하여 서 goto다음은 goto아주 작은 범위를 제공하는 기능은 예외가 많은 일부 십자가와 같은 역할을, 작은 정말 하나의 함수로 제한 goto하고 come from(참조 en.wikipedia.org/wiki/INTERCAL ;-)). 두 가지 코드를 거의 연결할 수 있으므로 일부 세 번째 함수는 코드를 건너 뛸 수 있습니다. 할 수없는 유일한 일은 goto다시 돌아가는 것입니다.
cmaster

2
@ PawełPrażak 많은 고차 함수로 작업 할 때 스택 추적은 그다지 가치가 없습니다. 입력과 출력에 대한 강력한 보증과 부작용 방지는 이러한 간접적 인 지시로 인해 혼란스러운 버그가 발생하지 않습니다.
Jack

11

여기에는 훌륭한 답변이 있지만 중요한 한 가지 이유가 충분히 강조되지 않았다고 생각합니다. 예외가 발생하면 객체가 잘못된 상태로 남아있을 수 있습니다. 예외를 "잡을"수 있으면 예외 처리기 코드는 이러한 잘못된 개체에 액세스하여 작업 할 수 있습니다. 해당 객체에 대한 코드가 완벽하게 작성되지 않는 한 끔찍하게 잘못 될 것입니다. 매우 어렵습니다.

예를 들어 Vector를 구현한다고 가정 해보십시오. 누군가가 일련의 객체로 Vector를 인스턴스화하지만 초기화 중에 예외가 발생하는 경우 (아마도 새로 할당 된 메모리에 객체를 복사하는 동안) Vector 구현을 올바르게 코딩하는 것은 매우 어렵습니다. 메모리가 누출되었습니다. Stroustroup의이 짧은 논문은 Vector 예제를 다룹니다 .

그리고 그것은 빙산의 일각 일뿐입니다. 예를 들어 일부 요소를 복사했지만 모든 요소가 아닌 경우 어떻게합니까? Vector와 같은 컨테이너를 올바르게 구현하려면 거의 모든 작업을 되돌릴 수 있어야하므로 데이터베이스 트랜잭션과 같은 전체 작업이 원자 적입니다. 이것은 복잡하며 대부분의 응용 프로그램에서 잘못됩니다. 그리고 올바르게 수행 되더라도 컨테이너 구현 프로세스가 크게 복잡합니다.

따라서 일부 현대 언어는 가치가 없다고 결정했습니다. 예를 들어, Rus에는 예외가 있지만 "포착"될 수 없으므로 코드가 유효하지 않은 상태의 객체와 상호 작용할 수있는 방법이 없습니다.


1
캐치의 목적은 오류가 발생한 후 개체를 일관성있게 만들거나 불가능한 경우 종료하는 것입니다.
JoulinRouge

@JoulinRouge 알고 있습니다. 그러나 일부 언어는 그러한 기회를 제공하지 않고 전체 프로세스를 중단하기로 결정했습니다. 그러한 언어 디자이너들은 당신이하고 싶은 정리의 종류를 알고 있지만 그렇게하기에는 너무 까다 롭다는 결론을 내 렸습니다. 나는 당신이 그들의 선택에 동의하지 않을 수도 있다는 것을 알고 있습니다 ... 그러나 그들이 이러한 특별한 이유로 의식적으로 선택했다는 것을 아는 것이 가치가 있습니다.
Charlie Flowers

6

내가 처음 Rust 언어에 대해 놀랐던 것은 캐치 예외를 지원하지 않는다는 것입니다. 예외 를 던질 수는 있지만 작업 (스레드는 생각하지만 항상 별도의 OS 스레드는 아님)이 종료 될 때 런타임 만 예외를 잡을 수 있습니다. 직접 작업을 시작하면 작업이 정상적으로 종료되었는지 또는 종료되었는지를 물을 수 있습니다 fail!().

따라서 그것은 fail종종 관용적이지 않습니다 . 예를 들어, 테스트 하네스 (사용자 코드의 상태를 모르는), 컴파일러의 최상위 레벨 (대부분의 컴파일러는 대신 포크) 또는 콜백을 호출 할 때와 같은 몇 가지 경우가 있습니다. 사용자 입력에.

대신 일반적인 관용구는 템플릿 을 사용 하여Result 처리해야 할 오류를 명시 적으로 전달하는 것입니다. 이것은에 의해 상당히 쉽게 만들어 매크로 한 결과를 산출하고있다 하나, 또는 그렇지 않으면 초기 함수에서 반환하는 경우 성공적으로 암을 얻을 수있는 표현을 감싸 할 수있다.try!

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}

1
따라서 Go의 접근 방식과 마찬가지로 이것도 Swift와 비슷 assert하지만 그렇지 않은 것은 아닙니다 catch.
orome

1
스위프트에서 시도하십시오! 의미 : 예, 이것이 실패 할 수 있다는 것을 알고 있지만 실패하지 않을 것이라고 확신하므로 처리하지 않으며 코드가 실패하면 코드가 잘못됩니다.
gnasher729

6

제 생각에는 예외는 런타임에 코드 오류를 감지하는 데 필수적인 도구입니다. 테스트와 프로덕션 모두에서. 스택 추적과 함께 메시지를 자세하게 작성하여 로그에서 발생한 일을 파악할 수 있습니다.

예외는 대부분 개발 도구이며 예기치 않은 경우 생산에서 합리적인 오류 보고서를 얻는 방법입니다.

코드를 더 읽기 쉽고 유지 보수 가능하게 만드는 우려 (예상 오류 만있는 일반적인 경로와 예기치 않은 오류에 대한 일반적인 처리기에 도달 할 때까지 떨어지는 행복한 경로)를 제외하고는 가능한 모든 코드를 준비하는 것은 실제로 불가능합니다 읽을 수없는 오류를 처리하기 위해 오류 처리 코드로 부풀려도 예기치 않은 경우가 발생합니다.

그것은 실제로 "예기치 않은"의 의미입니다.

Btw., 예상되는 것과 호출되지 않은 결정은 콜 사이트에서만 가능합니다. 그렇기 때문에 Java에서 확인 된 예외가 해결되지 않은 이유는 API를 개발할 때 예상되거나 예상치 못한 것이 전혀 명확하지 않은 결정입니다.

간단한 예 : 해시 맵의 API에는 두 가지 방법이 있습니다.

Value get(Key)

Option<Value> getOption(key)

발견되지 않은 경우 첫 번째 예외를 던지고, 후자는 선택적 값을 제공합니다. 어떤 경우에는 후자가 더 의미가 있지만 다른 경우에는 코드가 주어진 키에 대한 값이 있기를 간절히 기대해야하므로 키가 없으면 기본 코드 때문에 수정할 수없는 오류입니다. 가정은 실패했다. 이 경우 실제로 호출이 실패하는 경우 코드 경로에서 벗어나 일반 처리기로 내려가는 것이 바람직한 동작입니다.

코드는 실패한 기본 가정을 처리하려고 시도해서는 안됩니다.

물론 그것들을 확인하고 잘 읽을 수있는 예외를 던지는 것을 제외하고.

예외를 던지는 것은 악한 것이 아니라 잡는 것일 수 있습니다. 예기치 않은 오류를 수정하지 마십시오. 루프 나 작업을 계속하고 로그를 기록하고 알 수없는 오류를보고하려는 몇 곳에서 예외를 잡아라.

모든 곳에서 캐치 블록은 매우 나쁜 생각입니다.

의도를 쉽게 표현할 수있는 방식으로 API를 설계하십시오 (예 : 키를 찾을 수없는 것과 같은 특정 사례를 예상할지 여부를 선언). 그런 다음 API 사용자는 예상치 못한 상황에 대해서만 통화를 선택할 수 있습니다.

사람들이 예외를 재발송하고 오류 처리 자동화 및 새로운 언어와의 우려를 더 잘 분리하기 위해이 중요한 도구를 생략하여 너무 멀리가는 이유는 나쁜 경험이라고 생각합니다.

그것은, 그리고 그들이 실제로 유익한 것에 대한 오해입니다.

모나드 바인딩을 통해 모든 것을 수행함으로써 그것들을 시뮬레이션하면 코드를 쉽게 읽을 수 있고 유지 보수 할 수 없으며 스택 추적이 없어 결국이 방법을 악화시킬 수 있습니다.

기능적 스타일 오류 처리는 예상되는 오류 사례에 적합합니다.

예외 처리가 자동으로 나머지를 모두 처리하도록하십시오.


3

스위프트는 여기서 Objective-C와 동일한 원칙을 사용합니다. Objective-C에서 예외는 프로그래밍 오류를 나타냅니다. 충돌보고 도구를 제외하고는 처리되지 않습니다. "예외 처리"는 코드를 수정하여 수행됩니다. (예를 들어 프로세스 간 통신에서 약간의 예외가 있습니다. 그러나 그것은 매우 드물고 많은 사람들이 절대로 그것에 접근하지 않습니다. 그리고 Objective-C는 실제로 시도 / catch / finally / throw를 시도하지만 거의 사용하지 않습니다). Swift는 예외를 잡을 가능성을 제거했습니다.

Swift에는 예외 처리 와 비슷 하지만 오류 처리 만하는 기능이 있습니다. 역사적으로 Objective-C는 상당히 광범위한 오류 처리 패턴을 가지고있었습니다. 메소드는 BOOL (성공의 경우 YES) 또는 객체 참조 (실패의 경우 nil이 아닌 실패의 경우 nil)를 반환하고 "NSError에 대한 포인터 *"매개 변수를 갖습니다. NSError 참조를 저장하는 데 사용됩니다. Swift는 이러한 메소드에 대한 호출을 자동으로 예외 처리와 유사한 것으로 변환합니다.

일반적으로 Swift 함수는 함수가 올바르게 작동하면 결과, 실패하면 오류와 같은 대안을 쉽게 반환 할 수 있습니다. 오류 처리가 훨씬 쉬워집니다. 그러나 원래 질문에 대한 답변 : Swift 디자이너는 언어에 예외가없는 경우 안전한 언어를 만들고 그러한 언어로 성공적인 코드를 작성하는 것이 더 쉽다고 생각했습니다.


Swift의 경우 정답입니다. IMO. Swift는 기존 Objective-C 시스템 프레임 워크와 호환 가능해야하므로 기본적으로 예외가 없습니다. 나는 ObjC 오류 처리를 위해 어떻게 작동하는지에 얼마 전에 블로그 게시물 작성 : orangejuiceliberationfront.com/...
uliwitness

2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

C에서는 위와 같은 결과가 나타납니다.

Swift에서 예외로 할 수없는 일이 있습니까?

아니요, 아무것도 없습니다. 예외 대신 결과 코드를 처리하게됩니다.
예외를 통해 오류 처리가 올바른 경로 코드와 분리되도록 코드를 재구성 할 수 있습니다.


마찬가지로 ...throw_ioerror()예외를 던지기보다는 오류 를 반환 하라는 호출이 있습니까?
orome 2009 년

1
@raxacoricofallapatorius 예외가 없다고 주장하는 경우 프로그램은 실패시 오류 코드를 반환하고 성공시 0을 반환하는 일반적인 패턴을 따르는 것으로 가정합니다.
stonemetal 2009 년

1
@stonemetal Rust 및 Haskell과 같은 일부 언어는 형식 시스템을 사용하여 예외와 같이 숨겨진 종료점을 추가하지 않고 오류 코드보다 의미있는 것을 반환합니다. 녹 기능, 예를 들어, 반환 할 수 있습니다 Result<T, E>어느 쪽이 될 수있는, 열거 Ok<T>, 또는을 Err<E>로, T어떤 경우 원하는 타입 인 및 E오류를 나타내는 유형 인. 패턴 일치 및 일부 특정 방법은 성공 및 실패 처리를 단순화합니다. 즉, 예외가 없다고 자동으로 오류 코드를 의미한다고 가정하지 마십시오.
8bittree

1

Charlie의 답변 외에도

많은 매뉴얼과 서적에서 볼 수있는 선언 된 예외 처리 예제는 아주 작은 예제에서만 매우 똑똑해 보입니다.

유효하지 않은 객체 상태에 대한 논쟁을 제쳐두고도 큰 앱을 다룰 때 항상 큰 고통을 안겨줍니다.

예를 들어, 일부 암호화를 사용하여 IO를 처리해야하는 경우 50 가지 방법에서 20 가지 예외가 발생할 수 있습니다. 필요한 예외 처리 코드의 양을 상상해보십시오. 예외 처리에는 코드 자체보다 몇 배 더 많은 코드가 필요합니다.

실제로 예외가 표시되지 않는 경우를 알고 있으므로 예외 처리를 많이 작성할 필요가 없으므로 선언 된 예외를 무시하기 위해 몇 가지 해결 방법 만 사용하면됩니다. 실제로는 신뢰할 수있는 앱을 만들기 위해 선언 된 예외의 약 5 % 만 코드에서 처리하면됩니다.


실제로 이러한 예외는 종종 한 곳에서만 처리 할 수 ​​있습니다. 예를 들어, SSL이 실패하거나 DNS를 해석 할 수 없거나 웹 서버가 404를 리턴하는 경우 "데이터 업데이트 다운로드"기능에서 문제가되지 않습니다. 맨 위를 잡고 사용자에게 오류를보고하십시오.
Zan Lynx
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.