얼마나 많은 null 검사로 충분합니까?
null을 확인할 필요 가 없는 경우에 대한 지침은 무엇입니까 ?
내가 늦게 작업해온 상속 된 코드의 대부분은 null 검사 광고 구역을 가지고 있습니다. 사소한 함수에 대한 널 검사, 널이 아닌 반환을 나타내는 API 호출에 대한 널 검사 등. 어떤 경우에는 널 검사가 합리적이지만 많은 곳에서 널이 합당한 기대치가 아닙니다.
나는 "다른 코드를 믿을 수 없다", "항상 방어 적으로 프로그램", "언어가 널이 아닌 값을 보장 할 때까지, 나는 항상 확인하겠다"에 이르기까지 다양한 주장을 들었다. 나는 이러한 원칙 중 상당 부분에 확실히 동의하지만 과도한 null 검사로 인해 일반적으로 이러한 원칙을 위반하는 다른 문제가 발생한다는 것을 발견했습니다. 끈질긴 null 검사가 정말 그만한 가치가 있습니까?
종종, 과도한 null 검사를 사용하는 코드가 실제로 품질이 더 좋지 않고 품질이 떨어지는 것을 관찰했습니다. 많은 코드가 null 검사에 너무 집중되어있어 개발자가 가독성, 정확성 또는 예외 처리와 같은 다른 중요한 특성을 잃어버린 것 같습니다. 특히 std :: bad_alloc 예외를 무시하는 많은 코드가 있지만 new
.
C ++에서는 널 포인터를 역 참조하는 예측할 수없는 동작으로 인해 어느 정도 이해합니다. null 역 참조는 Java, C #, Python 등에서 더 우아하게 처리됩니다. 방금 경계 한 null 검사의 잘못된 예를 보셨나요? 아니면 실제로 뭔가가 있나요?
이 질문은 주로 C ++, Java 및 C #에 관심이 있지만 언어에 구애받지 않는 질문입니다.
과도한 것으로 보이는 null 검사의 몇 가지 예 는 다음과 같습니다.
이 예제는 C ++ 사양에서 실패한 새 예외가 예외를 throw한다고 말하므로 비표준 컴파일러를 설명하는 것 같습니다. 비준수 컴파일러를 명시 적으로 지원하지 않는 한 이것이 의미가 있습니까? 이 되나요 어떤 Java 또는 C 번호 (또는 C ++ / CLR)과 같은 관리 언어의 의미?
try {
MyObject* obj = new MyObject();
if(obj!=NULL) {
//do something
} else {
//??? most code I see has log-it and move on
//or it repeats what's in the exception handler
}
} catch(std::bad_alloc) {
//Do something? normally--this code is wrong as it allocates
//more memory and will likely fail, such as writing to a log file.
}
또 다른 예는 내부 코드에서 작업 할 때입니다. 특히 자신의 개발 관행을 정의 할 수있는 소규모 팀이라면 불필요 해 보인다. 일부 프로젝트 또는 레거시 코드에서는 문서를 신뢰하는 것이 합리적이지 않을 수 있습니다.하지만 귀하 또는 귀하의 팀이 제어하는 새 코드의 경우 이것이 정말로 필요합니까?
당신이 볼 수 있고 업데이트 할 수있는 (또는 책임있는 개발자에게 소리를 지르는) 메서드가 계약을 가지고 있다면, 여전히 null을 확인해야합니까?
//X is non-negative.
//Returns an object or throws exception.
MyObject* create(int x) {
if(x<0) throw;
return new MyObject();
}
try {
MyObject* x = create(unknownVar);
if(x!=null) {
//is this null check really necessary?
}
} catch {
//do something
}
private 또는 기타 내부 함수를 개발할 때 계약에서 null이 아닌 값만 호출 할 때 명시 적으로 null을 처리해야합니까? 왜 null 검사가 assert보다 더 나은가요?
(분명히 공개 API에서는 API를 잘못 사용하여 사용자에게 소리를 지르는 것이 무례한 것으로 간주되므로 null 검사가 중요합니다.)
//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value, or -1 if failed
int ParseType(String input) {
if(input==null) return -1;
//do something magic
return value;
}
비교 :
//Internal use only--non-public, not part of public API
//input must be non-null.
//returns non-negative value
int ParseType(String input) {
assert(input!=null : "Input must be non-null.");
//do something magic
return value;
}
먼저 이것은 계약 확인의 특별한 경우입니다. 런타임시 문서화 된 계약이 충족되는지 확인하는 것 외에는 아무것도하지 않는 코드를 작성하고 있습니다. 실패는 어딘가에있는 일부 코드에 결함이 있음을 의미합니다.
더 일반적으로 유용한 개념의 특수한 경우를 구현하는 것에 대해 항상 약간 모호합니다. 계약 검사는 API 경계를 처음으로 넘을 때 프로그래밍 오류를 포착하므로 유용합니다. 널이 당신이 확인하고 싶은 계약의 유일한 부분이라는 것을 의미하는 널의 특별한 점은 무엇입니까? 아직도,
입력 유효성 검사 주제 :
null은 Java에서 특별합니다. 많은 Java API가 작성되어 null이 주어진 메서드 호출로 전달할 수있는 유일한 유효하지 않은 값입니다. 이러한 경우 null 검사는 입력을 "완전히 검증"하므로 계약 검사에 찬성하는 전체 인수가 적용됩니다.
반면 C ++에서 NULL은 거의 모든 주소가 올바른 유형의 개체가 아니기 때문에 포인터 매개 변수가 취할 수있는 거의 2 ^ 32 (최신 아키텍처에서는 2 ^ 64) 잘못된 값 중 하나 일뿐입니다. 해당 유형의 모든 객체 목록이 없으면 입력을 "완전히 검증"할 수 없습니다.
그런 다음 질문은 NULL이 얻지 못하는 특별한 처리를 받기에 충분히 일반적인 잘못된 입력 (foo *)(-1)
입니까?
Java와 달리 필드는 NULL로 자동 초기화되지 않으므로 초기화되지 않은 가비지 값은 NULL만큼 그럴듯합니다. 그러나 때때로 C ++ 객체는 "아직 가지고 있지 않습니다"를 의미하는 명시 적으로 NULL로 초기화 된 포인터 멤버를 가지고 있습니다. 호출자가이 작업을 수행하면 NULL 검사로 진단 할 수있는 상당한 종류의 프로그래밍 오류가 있습니다. 예외는 소스가없는 라이브러리의 페이지 오류보다 디버깅하기가 더 쉬울 수 있습니다. 따라서 코드 부풀림에 신경 쓰지 않는다면 도움이 될 수 있습니다. 그러나 당신이 생각해야 할 것은 당신의 호출자이며 당신 자신이 아닙니다. 이것은 방어적인 코딩이 아닙니다. 왜냐하면 그것은 (foo *) (-1)이 아니라 NULL에 대해서만 '방어'하기 때문입니다.
NULL이 유효한 입력이 아닌 경우 포인터가 아닌 참조로 매개 변수를 취하는 것을 고려할 수 있지만, 많은 코딩 스타일이 상수가 아닌 참조 매개 변수를 승인하지 않습니다. 그리고 만약 호출자가 당신에게 * fooptr를 전달한다면, fooptr이 NULL 인 경우, 어쨌든 그것은 아무 것도 좋은 일을하지 않은 것입니다. 당신이하려는 것은 당신의 호출자가 "흠, fooptr가 여기서 null이 될 수 있는가?"라고 생각할 가능성이 더 높기를 바라면서 함수 서명에 더 많은 문서를 짜는 것입니다. 그들이 당신에게 포인터로 전달하는 것보다 명시 적으로 역 참조해야 할 때. 그것은 지금까지만 진행되지만, 그것이 도움이 될 수 있습니다.
나는 C #을 모르지만 참조가 유효한 값 (적어도 안전한 코드에서)을 보장한다는 점에서 Java와 비슷하지만 모든 유형에 NULL 값이있는 것은 아니라는 점에서 Java와 다르다는 것을 이해합니다. 따라서 null 검사는 거의 가치가 없다고 생각합니다. 안전한 코드에있는 경우 null이 유효한 입력이 아니면 nullable 형식을 사용하지 말고 안전하지 않은 코드에있는 경우 다음과 같은 추론이 적용됩니다. C ++에서.
출력 유효성 검사 주제 :
비슷한 문제가 발생합니다. Java에서 출력 유형을 알고 값이 null이 아니라는 것을 "완전히 검증"할 수 있습니다. C ++에서는 NULL 검사로 출력을 "완전히 유효성 검사"할 수 없습니다. 함수가 방금 풀린 자체 스택의 개체에 대한 포인터를 반환했음을 알고 있기 때문입니다. 그러나 NULL이 호출 수신자 코드의 작성자가 일반적으로 사용하는 구조로 인해 일반적인 잘못된 반환 인 경우이를 확인하면 도움이됩니다.
모든 상황에서:
가능한 경우 "실제 코드"대신 어설 션을 사용하여 계약을 확인하십시오. 일단 앱이 작동하면 모든 피 호출자가 모든 입력을 확인하고 모든 호출자가 반환 값을 확인하는 것을 원하지 않을 것입니다.
비표준 C ++ 구현으로 이식 가능한 코드를 작성하는 경우 null을 확인하고 예외를 포착하는 질문의 코드 대신 다음과 같은 함수가있을 것입니다.
template<typename T>
static inline void nullcheck(T *ptr) {
#if PLATFORM_TRAITS_NEW_RETURNS_NULL
if (ptr == NULL) throw std::bad_alloc();
#endif
}
그런 다음 새 시스템으로 이식 할 때 수행하는 작업 목록 중 하나로 PLATFORM_TRAITS_NEW_RETURNS_NULL (및 다른 PLATFORM_TRAITS)을 올바르게 정의합니다. 분명히 알고있는 모든 컴파일러에 대해이 작업을 수행하는 헤더를 작성할 수 있습니다. 누군가가 여러분의 코드를 가져와 여러분이 아무것도 모르는 비표준 C ++ 구현에서 컴파일한다면, 그들은 근본적으로 이것보다 더 큰 이유 때문에 스스로해야 할 것입니다.
오늘 작성하는 코드가 소규모 팀이고 좋은 문서를 가질 수 있지만 다른 사람이 유지 관리해야하는 레거시 코드로 바뀔 수 있다는 점을 기억해야합니다. 다음 규칙을 사용합니다.
다른 사람에게 노출 될 공용 API를 작성하는 경우 모든 참조 매개 변수에 대해 null 검사를 수행합니다.
내 응용 프로그램에 내부 구성 요소를 작성하는 경우 null이있을 때 특별한 작업을 수행해야하거나이를 매우 명확하게 만들고 싶을 때 null 검사를 작성합니다. 그렇지 않으면 무슨 일이 일어나고 있는지 상당히 분명하기 때문에 null 참조 예외가 발생해도 괜찮습니다.
다른 사람들의 프레임 워크에서 반환 데이터로 작업 할 때 null이 반환되는 것이 가능하고 유효한 경우에만 null을 확인합니다. 그들의 계약이 null을 반환하지 않는다고 말하면 나는 확인하지 않을 것입니다.
코드와 계약을 작성하는 경우 계약과 관련하여 코드를 사용하고 계약이 올바른지 확인할 책임이 있습니다. "null이 아닌 x를 반환"이라고 말하면 호출자가 null을 확인하지 않아야합니다. 해당 참조 / 포인터에서 널 포인터 예외가 발생하면 계약이 잘못된 것입니다.
Null 검사는 신뢰할 수 없거나 적절한 계약이없는 라이브러리를 사용할 때만 극도로 가야합니다. 개발 팀의 코드 인 경우 계약이 깨져서는 안된다는 점을 강조하고 버그 발생시 계약을 잘못 사용하는 사람을 추적하십시오.
예를 들어 프로젝트 내에서만 사용할 수있는 메서드 인 경우 코드가 사용되는 방식에 따라 일부가 달라집니다. API 오류 검사에는 어설 션보다 더 강력한 것이 필요합니다.
그래서 이것은 단위 테스트와 같은 것들로 지원되는 프로젝트 내에서는 괜찮습니다.
internal void DoThis(Something thing)
{
Debug.Assert(thing != null, "Arg [thing] cannot be null.");
//...
}
누가 호출하는지 제어 할 수없는 메서드에서는 다음과 같은 것이 더 좋을 수 있습니다.
public void DoThis(Something thing)
{
if (thing == null)
{
throw new ArgumentException("Arg [thing] cannot be null.");
}
//...
}
상황에 따라 다릅니다. 내 대답의 나머지 부분은 C ++를 가정합니다.
- 내가 사용하는 모든 구현이 실패시 bad_alloc을 던지기 때문에 new의 반환 값을 테스트하지 않습니다. 작업중인 코드에서 새로운 null 반환에 대한 레거시 테스트가 표시되면 잘라 내고 아무것도 바꾸지 않습니다.
- 사소한 코딩 표준이 금지하지 않는 한 문서화 된 전제 조건을 주장합니다. 공개 된 계약을 위반하는 손상된 코드는 즉시 극적으로 실패해야합니다.
- 손상된 코드로 인한 것이 아닌 런타임 오류로 인해 null이 발생하면 던집니다. fopen 실패 및 malloc 실패 (C ++에서 거의 사용하지 않지만)는이 범주에 속합니다.
- 할당 실패에서 복구하려고하지 않습니다. Bad_alloc은 main ()에서 잡 힙니다.
- null 테스트가 내 클래스의 공동 작업자 인 개체에 대한 것이라면 코드를 다시 작성하여 참조로 가져갑니다.
- 공동 작업자가 실제로 존재하지 않는 경우 Null 개체 디자인 패턴을 사용하여 잘 정의 된 방식으로 실패 할 자리 표시자를 만듭니다.
일반적으로 NULL 검사는 코드 테스트 가능성에 작은 음수 토큰을 추가하므로 악의적입니다. 모든 곳에서 NULL 검사를 사용하면 "pass null"기술을 사용할 수 없으며 단위 테스트를 수행 할 때 문제가 발생합니다. null 검사보다 메서드에 대한 단위 테스트를하는 것이 좋습니다.
Check out decent presentation on that issue and unit testing in general by Misko Hevery at http://www.youtube.com/watch?v=wEhu57pih5w&feature=channel
Older versions of Microsoft C++ (and probably others) did not throw an exception for failed allocations via new, but returned NULL. Code that had to run in both standard-conforming and older versions would have the redundant checking that you point out in your first example.
It would be cleaner to make all failed allocations follow the same code path:
if(obj==NULL)
throw std::bad_alloc();
It's widely known that there are procedure-oriented people (focus on doing things the right way) and results-oriented people (get the right answer). Most of us lie somewhere in the middle. Looks like you've found an outlier for procedure-oriented. These people would say "anything's possible unless you understand things perfectly; so prepare for anything." For them, what you see is done properly. For them if you change it, they'll worry because the ducks aren't all lined up.
When working on someone else's code, I try to make sure I know two things.
1. What the programmer intended
2. Why they wrote the code the way they did
For following up on Type A programmers, maybe this helps.
So "How much is enough" ends up being a social question as much as a technical question - there's no agreed-upon way to measure it.
(It drives me nuts too.)
Personally I think null testing is unnnecessary in the great majority of cases. If new fails or malloc fails you have bigger issues and the chance of recovering is just about nil in cases where you're not writing a memory checker! Also null testing hides bugs a lot in the development phases since the "null" clauses are frequently just empty and do nothing.
When you can specify which compiler is being used, for system functions such as "new" checking for null is a bug in the code. It means that you will be duplicating the error handling code. Duplicate code is often a source of bugs because often one gets changed and the other doesn't. If you can not specify the compiler or compiler versions, you should be more defensive.
As for internal functions, you should specify the contract and make sure that contract is enforce via unit tests. We had a problem in our code a while back where we either threw an exception or returned null in case of a missing object from our database. This just made things confusing for the caller of the api so we went through and made it consistant throughout the entire code base and removed the duplicate checks.
The important thing (IMHO) is to not have duplicate error logic where one branch will never be invoked. If you can never invoke code, then you can't test it, and you will never know if it is broken or not.
I'd say it depends a little on your language, but I use Resharper with C# and it basically goes out of it's way to tell me "this reference could be null" in which case I add a check, if it tells me "this will always be true" for "if (null != oMyThing && ....)" then I listen to it an don't test for null.
Whether to check for null or not greatly depends on the circumstances.
For example in our shop we check parameters to methods we create for null inside the method. The simple reason is that as the original programmer I have a good idea of exactly what the method should do. I understand the context even if the documentation and requirements are incomplete or less than satisfactory. A later programmer tasked with maintenance may not understand the context and may assume, wrongly, that passing null is harmless. If I know null would be harmful and I can anticipate that someone may pass null, I should take the simple step of making sure that the method reacts in a graceful way.
public MyObject MyMethod(object foo)
{
if (foo == null)
{
throw new ArgumentNullException("foo");
}
// do whatever if foo was non-null
}
I only check for NULL when I know what to do when I see NULL. "Know what to do" here means "know how to avoid a crash" or "know what to tell the user besides the location of the crash". For example, if malloc() returns NULL, I usually have no option but to abort the program. On the other hand, if fopen() returns NULL, I can let the user know the file name that could not be open and may be errno. And if find() returns end(), I usually know how to continue without crashing.
Lower level code should check use from higher level code. Usually this means checking arguments, but it can mean checking return values from upcalls. Upcall arguments need not be checked.
The aim is to catch bugs in immediate and obvious ways, as well as documenting the contract in code that does not lie.
I don't think it's bad code. A fair amount of Windows/Linux API calls return NULL on failure of some sort. So, of course, I check for failure in the manner the API specifies. Usually I wind up passing control flow to an error module of some fashion instead of duplicating error-handling code.
If I receive a pointer that is not guaranteed by language to be not null, and am going to de-reference it in a way that null will break me, or pass out put my function where I said I wouldn't produce NULLs, I check for NULL.
It is not just about NULLs, a function should check pre- and post-conditions if possible.
It doesn't matter at all if a contract of the function that gave me the pointer says it'll never produce nulls. We all make bugs. There's a good rule that a program shall fail early and often, so instead of passing the bug to another module and have it fail, I'll fail in place. Makes things so much easier to debug when testing. Also in critical systems makes it easier to keep the system sane.
Also, if an exception escapes main, stack may not be rolled up, preventing destructors from running at all (see C++ standard on terminate()). Which may be serious. So leaving bad_alloc unchecked can be more dangerous than it seems.
Fail with assert vs. fail with a run time error is quite a different topic.
Checking for NULL after new() if standard new() behavior has not been altered to return NULL instead of throwing seems obsolete.
There's another problem, which is that even if malloc returned a valid pointer, it doesn't yet mean you have allocated memory and can use it. But that is another story.
My first problem with this, is that it leads to code which is littered with null checks and the likes. It hurts readability, and i’d even go as far as to say that it hurts maintainability because it really is easy to forget a null check if you’re writing a piece of code where a certain reference really should never be null. And you just know that the null checks will be missing in some places. Which actually makes debugging harder than it needs to be. Had the original exception not been caught and replaced with a faulty return value, then we would’ve gotten a valuable exception object with an informative stacktrace. What does a missing null check give you? A NullReferenceException in a piece of code that makes you go: wtf? this reference should never be null!
So then you need to start figuring out how the code was called, and why the reference could possibly be null. This can take a lot of time and really hurts the efficiency of your debugging efforts. Eventually you’ll figure out the real problem, but odds are that it was hidden pretty deeply and you spent a lot more time searching for it than you should have.
Another problem with null checks all over the place is that some developers don’t really take the time to properly think about the real problem when they get a NullReferenceException. I’ve actually seen quite a few developers just add a null check above the code where the NullReferenceException occurred. Great, the exception no longer occurs! Hurray! We can go home now! Umm… how bout ‘no you can’t and you deserve an elbow to the face’? The real bug might not cause an exception anymore, but now you probably have missing or faulty behavior… and no exception! Which is even more painful and takes even more time to debug.
At first, this seemed like a strange question: null
checks are great and a valuable tool. Checking that new
returns null
is definitely silly. I'm just going to ignore the fact that there are languages that allow that. I'm sure there are valid reasons, but I really don't think I can handle living in that reality :) All kidding aside, it seems like you should at least have to specify that the new
should return null
when there isn't enough memory.
Anyway, checking for null
where appropriate leads to cleaner code. I'd go so far as to say that never assigning function parameters default values is the next logical step. To go even further, returning empty arrays, etc. where appropriate leads to even cleaner code. It is nice to not have to worry about getting null
s except where they are logically meaningful. Nulls as error values are better avoided.
Using asserts is a really great idea. Especially if it gives you the option of turning them off at runtime. Plus, it is a more explicitly contractual style :)
참고URL : https://stackoverflow.com/questions/302736/how-much-null-checking-is-enough
'code' 카테고리의 다른 글
어떤 cpan 설치 프로그램이 적합한가요? (0) | 2020.11.15 |
---|---|
4xx / 5xx에서 예외를 throw하지 않고 Powershell 웹 요청 (0) | 2020.11.15 |
Rails 작업이 있습니다. 스크립트 / 러너 또는 레이크를 사용해야합니까? (0) | 2020.11.15 |
XPath를 사용하여 텍스트 내용과 속성 값을 기반으로 노드를 어떻게 선택합니까? (0) | 2020.11.15 |
문자열의 첫 번째 (또는 마지막) n 개 문자 추출 (0) | 2020.11.14 |