때로는 실제 답변이없는 것이 예외가 아닌 클래스 라이브러리에 대한 메소드 또는 속성을 작성 해야하는 경우가 있습니다. 확인할 수 없거나 사용할 수 없거나 찾을 수 없거나 현재 사용할 수 없거나 더 이상 사용할 수있는 데이터가 없습니다.
C # 4에서 실패를 나타내는 비교적 예외적이지 않은 상황에 대한 세 가지 가능한 솔루션이 있다고 생각합니다 .
- 다른 의미가없는 마술 값을 반환합니다 (예 :
null
및-1
). - 예외를 던지십시오 (예 🙂
KeyNotFoundException
; - 되돌아
false
와의 실제 반환 값을 제공하는out
매개 변수를 (예컨대Dictionary<,>.TryGetValue
).
따라서 질문은 다음 과 같습니다. 예외가 아닌 상황에서 예외를 던져야합니까? 그리고 내가 던져서는 안되는 경우 : 언제 매개 변수 로 Try*
메소드를 구현하는 것보다 마법의 가치를 반환out
합니까? 나에게 out
매개 변수가 더러워 보이며 올바르게 사용하는 것이 더 많은 작업입니다.
디자인 지침 ( Try*
메소드 에 대해 전혀 모른다 ), 유용성 (클래스 라이브러리에 대해 요청), BCL과의 일관성 및 가독성과 같은 사실적인 답변을 찾고 있습니다.
.NET Framework 기본 클래스 라이브러리에서는 세 가지 방법이 모두 사용됩니다.
- 그렇지 않으면 의미가없는 마법의 값을 반환합니다.
Collection<T>.IndexOf
-1을 반환하고StreamReader.Read
-1을 반환하고Math.Sqrt
NaN을 반환하고Hashtable.Item
null을 돌려줍니다.
- 예외를 던지십시오 :
Dictionary<,>.Item
KeyNotFoundException을 던지고,Double.Parse
FormatException을 던집니다. 또는
- 매개 변수
false
에 실제 반환 값을 반환 하고 제공하십시오out
.
로합니다 Hashtable
C #에서 더 제네릭 없었다 시점에서 만든, 그것은 사용 object
하기 때문에 반환 할 수 있습니다 null
마법의 값으로. 그러나 제네릭의 경우 예외가에서 사용 Dictionary<,>
되며 처음에는 예외가 없었습니다 TryGetValue
. 분명히 통찰력이 바뀝니다.
물론, Item
– TryGetValue
및 Parse
– TryParse
이중성은 이유가있다, 그래서 나는 비 뛰어난 실패에 대한 예외를 던지는 C # 4에 있다고 가정 하지 . 그러나 Try*
방법이 존재하더라도 항상 존재하는 것은 아닙니다 Dictionary<,>.Item
.
답변
나는 당신의 예제가 실제로 동등한 것이라고 생각하지 않습니다. 세 가지 그룹이 있으며 각 그룹은 각자의 행동에 대한 자신의 이론적 근거가 있습니다.
- 매직 값은 “완료”조건과 같은 경우
StreamReader.Read
또는 유효한 답이 될 수없는 사용하기 쉬운 값이있을 때 좋은 옵션입니다 (-1의 경우IndexOf
). - 함수의 의미론에서 호출자가 작동 할 것이라고 확신하는 경우 예외를 처리하십시오. 이 경우 존재하지 않는 키 또는 잘못된 이중 형식은 정말 예외적입니다.
- 연산이 가능한지 여부에 대한 의미가 프로브되는 경우 출력 매개 변수를 사용하고 부울을 리턴하십시오.
제공하는 예제는 사례 2와 3에 대해 완벽하게 명확합니다. 마법 값의 경우 이것이 좋은 디자인 결정인지 또는 모든 경우에 해당되지 않는지 논쟁 할 수 있습니다.
에 NaN
의해 반환되는 Math.Sqrt
것은 특별한 경우입니다-부동 소수점 표준을 따릅니다.
답변
API 사용자에게해야 할 일을 전달하려고합니다. 예외를 던지면 예외를 잡을 필요가 없으며 문서를 읽는 것만으로 모든 가능성을 알 수 있습니다. 개인적으로 특정 방법으로 발생할 수있는 모든 예외를 찾기 위해 문서를 파헤치는 것이 느리고 지루합니다.
매직 값은 여전히 문서를 읽고 const
값을 해독하기 위해 일부 테이블을 참조 해야합니다. 적어도 예외가 아닌 발생이라고 부르는 예외에 대한 오버 헤드는 없습니다.
그렇기 때문에 out
때로는 매개 변수가 찌그러 지더라도 Try...
구문 이있는 해당 방법을 선호 합니다. 표준 .NET 및 C # 구문입니다. API 사용자에게 결과를 사용하기 전에 반환 값을 확인해야한다는 내용을 전달하고 있습니다. out
유용한 오류 메시지와 함께 두 번째 매개 변수를 포함시켜 디버깅에 다시 도움을 줄 수도 있습니다. 이것이 Try...
with out
매개 변수 솔루션에 투표하는 이유 입니다.
또 다른 옵션은 특별한 “결과”객체를 반환하는 것입니다.
interface IMyResult
{
bool Success { get; }
// Only access this if Success is true
MyOtherClass Result { get; }
// Only access this if Success is false
string ErrorMessage { get; }
}
그런 다음 입력 매개 변수 만 있고 하나만 반환하기 때문에 함수 가 올바르게 보입니다 . 그것이 반환하는 것은 일종의 튜플 일뿐입니다.
사실, 당신이 그런 종류의 일에 빠져 있다면, Tuple<>
.NET 4에 도입 된 새로운 클래스를 사용할 수 있습니다 . 개인적으로 나는 각 필드의 의미가 줄 수 없기 때문에 덜 명확하다는 사실을 좋아하지 않습니다 Item1
및 Item2
유용한 이름.
답변
귀하의 예가 이미 보여 주듯이, 각각의 경우는 개별적으로 평가되어야하며, 특히 예외적 인 상황과 흐름 제어 사이에 상당한 회색 스펙트럼이 있으며, 특히 귀하의 방법을 재사용 할 수 있고 매우 다른 패턴으로 사용될 수있는 경우 원래 설계된 것보다 특히 “예외”를 사용하여이를 구현할 수있는 가능성에 대해 즉시 논의하는 경우 “비 예외적”의 의미에 동의하지 마십시오.
우리는 또한 어떤 디자인이 코드를 읽고 유지하기 가장 쉬운 지에 대해서는 동의하지 않을 수도 있지만, 라이브러리 디자이너는 그에 대한 명확한 개인적인 비전을 가지고 있으며 관련된 다른 고려 사항과 균형을 맞출 필요가 있다고 가정합니다.
짧은 답변
매우 빠른 방법을 설계하고 예기치 않은 재사용 가능성을 예상 할 때를 제외하고는 장의 감정을 따르십시오.
긴 대답
미래의 각 발신자는 양방향에서 원하는대로 오류 코드와 예외를 자유롭게 변환 할 수 있습니다. 따라서 성능, 디버거 친 화성 및 일부 제한된 상호 운용성 컨텍스트를 제외하고 두 가지 디자인 방식이 거의 동일합니다. 일반적으로 성능이 저하되므로 이에 초점을 맞추겠습니다.
-
경험상 예외를 던지는 것이 정규 수익보다 200 배 더 느릴 것으로 예상하십시오 (실제로 상당한 차이가 있습니다).
-
경험상, 예외를 던지면 프로그래머가 오류 코드를 다른 오류 코드로 변환하는 프로그래머에 의존하지 않기 때문에 대부분의 마법 값에 비해 훨씬 깨끗한 코드를 허용 할 수 있습니다. 일관성 있고 적절한 방식으로 처리하기에 충분한 컨텍스트가있는 지점. (특별한 경우 : 일부 유형의 결함이 아닌 일부 유형의 경우
null
자동으로 변환되는 경향이 있기 때문에 다른 마법 값보다 더 나은 경향이NullReferenceException
있습니다. )
교훈은 무엇입니까?
응용 프로그램 수명 동안 (응용 프로그램 초기화와 같이) 몇 번만 호출되는 함수의 경우 코드를 더 명확하고 이해하기 쉽게하는 것을 사용하십시오. 성능은 문제가되지 않습니다.
버리기 기능의 경우 코드를 더 깨끗하게하는 것을 사용하십시오. 그런 다음 프로파일 링 (필요한 경우)을 수행하고 측정 또는 전체 프로그램 구조를 기반으로 의심되는 최상위 병목 현상이 발생하면 예외를 반환하여 코드를 반환하십시오.
값 비싼 재사용 가능한 기능을 위해서는 더 깨끗한 코드를 제공하는 것을 사용하십시오. 기본적으로 항상 네트워크 왕복을 수행하거나 디스크상의 XML 파일을 구문 분석해야하는 경우 예외를 발생시키는 오버 헤드는 무시할 수 있습니다. “예외가 아닌 실패”에서 더 빨리 돌아가는 것보다 실수로 실패하지 않은 세부 정보를 잃지 않는 것이 더 중요합니다.
마른 재사용 기능에는 더 많은 생각이 필요합니다. 예외를 사용 하면 함수 본문이 매우 빠르게 실행되는 경우 (많은) 호출의 절반에서 예외를 볼 수있는 호출자에게 100 배의 속도 저하를 강요 합니다. 예외는 여전히 설계 옵션이지만이를 감당할 수없는 발신자에게는 낮은 오버 헤드 대안을 제공해야합니다. 예를 봅시다.
Dictionary<,>.Item
느슨하게 말해서 null
값을 반환하는 것에서 KeyNotFoundException
.NET 1.1과 .NET 2.0 사이의 던지기 로 바뀌는 훌륭한 예를 나열합니다 ( Hashtable.Item
실제로 제네릭이 아닌 선구자 로 간주하려는 경우에만 ). 이 “변경”의 이유는 여기에 관심이 없습니다. 값 유형의 성능 최적화 (더 이상 복싱 없음)는 원래의 마법 값 ( null
)을 옵션이 아닌 것으로 만들었습니다 . out
매개 변수는 성능 비용의 작은 부분을 다시 가져옵니다. 후자의 성능 고려 사항은 을 던지는 오버 헤드와 비교할 때 완전히 무시할 수KeyNotFoundException
있지만 예외 디자인은 여전히 우수합니다. 왜?
- ref / out 매개 변수는 “실패”사례뿐만 아니라 매번 비용이 발생합니다.
- 관심있는 사람
Contains
은 인덱서에 대한 호출 전에 호출 할 수 있으며이 패턴은 자연스럽게 읽습니다. 개발자가 호출을 잊고 싶지만Contains
성능 문제가 발생할 수는 없습니다.KeyNotFoundException
눈치 채고 고칠 정도로 시끄 럽습니다.
답변
상대적으로 예외적이지 않은 상황에서 실패를 나타내는 최선의 방법은 무엇입니까?
실패를 허용해서는 안됩니다.
나는 손이 흔들리고 이상적이지만 내 말을 듣는다. 디자인을 수행 할 때 실패 모드가없는 버전을 선호하는 경우가 많이 있습니다. LINQ는 실패한 ‘FindAll’대신에 빈 열거 형을 반환하는 where 절을 사용합니다. 사용하기 전에 초기화해야하는 객체를 갖는 대신 생성자가 객체를 초기화하도록합니다 (또는 초기화되지 않은 것이 감지되면 초기화). 핵심은 소비자 코드에서 실패 분기를 제거하는 것입니다. 이것이 문제이므로 집중하십시오.
이를위한 또 다른 전략은 KeyNotFound
시나리오입니다. 3.0 이후로 작업 한 거의 모든 코드베이스에는 다음과 같은 확장 방법이 있습니다.
public static class DictionaryExtensions {
public static V GetValue<K, V>(this IDictionary<K, V> arg, K key, Func<K,V> ifNotFound) {
if (!arg.ContainsKey(key)) {
return ifNotFound(key);
}
return arg[key];
}
}
이에 대한 실제 실패 모드는 없습니다. ConcurrentDictionary
비슷한 GetOrAdd
내장되어 있습니다.
그러나 피할 수없는 경우가 항상 있습니다. 세 사람 모두 자리가 있지만 첫 번째 옵션을 선호합니다. 널 (null)의 위험으로 만들어진 모든 것에도 불구하고, 그것은 잘 알려져 있으며 “예외가 아닌”세트를 구성하는 많은 ‘항목을 찾을 수 없음’또는 ‘결과가 적용되지 않습니다’시나리오에 적합합니다. 특히 nullable 값 형식을 만들 때 ‘이것이 실패 할 수 있습니다’의 중요성은 코드에서 매우 명시 적이며 잊어 버릴 수 없습니다.
두 번째 옵션은 사용자가 바보 같은 일을 할 때 충분합니다. 잘못된 형식의 문자열을 제공하고 날짜를 12 월 42 일로 설정하려고 시도합니다. 테스트 중에 잘못된 코드가 식별되고 수정되도록 신속하고 훌륭하게 날려 버리고 싶은 것입니다.
마지막 옵션은 점점 싫어하는 옵션입니다. 아웃 매개 변수는 어색하며 한 가지에 집중하고 부작용이없는 방법을 만들 때 모범 사례 중 일부를 위반하는 경향이 있습니다. 또한 out 매개 변수는 일반적으로 성공하는 동안에 만 의미가 있습니다. 즉, 일반적으로 동시성 문제 또는 성능 고려 사항 (예 : DB로 두 번째 여행을 원하지 않는 경우)으로 제한되는 특정 작업에 필수적입니다.
반환 값과 출력 매개 변수가 사소한 것이 아니라면 결과 개체에 대한 Scott Whitlock의 제안이 선호됩니다 (예 : Regex Match
클래스).
답변
항상 예외를 던지는 것을 선호합니다. 그것은 실패 할 수있는 모든 기능들 사이에 균일 한 인터페이스를 가지고 있으며, 가장 바람직한 특성 인 실패를 가능한 한 시끄럽게 나타냅니다.
그 주 Parse
와 TryParse
정말 고장 모드에서 떨어져 같은 것이 아니다. TryParse
값을 반환 할 수 있다는 사실은 실제로 직교합니다. 예를 들어 일부 입력의 유효성을 검사하는 상황을 고려하십시오. 값이 유효한 한 실제로 값을 신경 쓰지 않습니다. 그리고 일종의 IsValidFor(arguments)
기능 을 제공하는 데 아무런 문제가 없습니다 . 그러나 절대 기본 작동 모드가 될 수는 없습니다 .
답변
다른 사람들이 지적했듯이, 부울 리턴 값을 포함한 마술 값은 “범위 끝”마커를 제외하고는 그다지 큰 해결책이 아닙니다. 이유 : 오브젝트의 메소드를 검사하더라도 의미가 명시 적이 지 않습니다. 실제로 전체 개체에 대한 전체 설명서를 “오, 예 -42를 반환하면 bla bla bla”로 전체 문서를 읽어야합니다.
이 솔루션은 기록적인 이유로 또는 성능상의 이유로 사용될 수 있지만 피해야합니다.
이것은 두 가지 일반적인 경우를 남깁니다 : 조사 또는 예외.
여기서 경험의 규칙은 프로그램이 / 무의식적으로 / 일부 조건을 위반할 때 처리하는 것을 제외하고는 프로그램이 예외에 반응해서는 안된다는 것입니다. 이런 일이 발생하지 않도록 프로빙을 사용해야합니다. 따라서 예외는 관련 프로빙이 사전에 수행되지 않았거나 전혀 예상치 못한 일이 발생했음을 의미합니다.
예:
주어진 경로에서 파일을 작성하려고합니다.
이 경로가 파일 작성 또는 쓰기에 적합한 지 여부를 미리 평가하려면 File 객체를 사용해야합니다.
프로그램이 여전히 불법이거나 쓸 수없는 경로에 쓰려고 시도하는 경우, 탈출을해야합니다. 경쟁 조건으로 인해 발생할 수 있습니다 (일부 사용자가 디렉토리를 제거했거나 문제가 발생한 후 읽기 전용으로 만들었습니다).
예기치 않은 실패 (예외로 신호 처리)를 처리하고 사전에 작업에 적합한 조건 (프로빙)인지 확인하는 작업은 일반적으로 다르게 구성되므로 다른 메커니즘을 사용해야합니다.
답변
Try
코드가 일어난 일을 나타낼 때 패턴이 최선의 선택 이라고 생각합니다 . 나는 param을 싫어하고 nullable 객체를 좋아합니다. 나는 다음 수업을 만들었습니다.
public sealed class Bag<TValue>
{
public Bag(TValue value, bool hasValue = true)
{
HasValue = hasValue;
Value = value;
}
public static Bag<TValue> Empty
{
get { return new Bag<TValue>(default(TValue), false); }
}
public bool HasValue { get; private set; }
public TValue Value { get; private set; }
}
그래서 다음 코드를 작성할 수 있습니다
public static Bag<XElement> GetXElement(this XElement element, string elementName)
{
try
{
XElement result = element.Element(elementName);
return result == null
? Bag<XElement>.Empty
: new Bag<XElement>(result);
}
catch (Exception)
{
return Bag<XElement>.Empty;
}
}
nullable처럼 보이지만 값 유형뿐만 아니라
또 다른 예
public static Bag<string> TryParseString(this XElement element, string attributeName)
{
Bag<string> attributeResult = GetString(element, attributeName);
if (attributeResult.HasValue)
{
return new Bag<string>(attributeResult.Value);
}
return Bag<string>.Empty;
}
private static Bag<string> GetString(XElement element, string attributeName)
{
try
{
string result = element.GetAttribute(attributeName).Value;
return new Bag<string>(result);
}
catch (Exception)
{
return Bag<string>.Empty;
}
}