보다 엄격한 언어를 사용한다고해서 목표 게시물이 구현을 올바르게 수행하는 것에서 스펙을 올바르게 얻는 것으로 이동하는 것이 아닙니다. 매우 잘못되었지만 논리적으로 일관성있는 것을 만드는 것은 어렵습니다. 이것이 컴파일러가 많은 버그를 잡는 이유입니다.
형식 시스템이 실제로 의미하는 것을 의미하지 않기 때문에 일반적으로 공식화되는 포인터 산술은 소리가 나지 않습니다. 가비지 수집 언어 (추상화를위한 일반적인 접근 방식)로 작업하면이 문제를 완전히 피할 수 있습니다. 또는 어떤 종류의 포인터를 사용하는지에 대해 더 구체적으로 지정할 수 있으므로 컴파일러는 일관성이 없거나 서면으로 올바르게 증명할 수없는 것을 거부 할 수 있습니다. 이것은 Rust와 같은 일부 언어의 접근 방식입니다.
생성 된 형식은 증명과 동일하므로이를 잊어 버린 형식 시스템을 작성하면 모든 종류의 문제가 발생합니다. 우리가 타입을 선언 할 때 실제로 변수에 무엇이 있는지에 대한 진실을 주장하고 있다고 가정합니다.
- int * x; // 거짓 주장. x가 존재하고 int를 가리 키지 않습니다.
- int * y = z; // z가 int를 가리키는 것으로 입증 된 경우에만 true
- * (x + 3) = 5; // (x + 3)이 x와 동일한 배열의 int를 가리키는 경우에만 true
- int c = a / b; // b가 0이 아닌 경우에만 "nonzero int b = ...;"
- 널 입력 가능 int * z = NULL; // nullable int *는 int *와 같지 않습니다
- int d = * z; // z는 nullable이므로 거짓 주장
- if (z! = NULL) {int * e = z; } // z가 null이 아니기 때문에 Ok
- 자유 (y); int w = * y; // y는 더 이상 w에 존재하지 않으므로 거짓 주장
이 세계에서 포인터는 null 일 수 없습니다. NullPointer 역 참조는 존재하지 않으며 포인터가 널 (null) 여부를 검사 할 필요가 없습니다. 대신, "nullable int *"는 값을 null 또는 포인터로 추출 할 수있는 다른 유형입니다. 이것은 널이 아닌 가정이 시작 되는 시점에서 예외를 로그하거나 널 브랜치로 내려가는 것을 의미합니다.
이 세계에서는 범위를 벗어난 배열 오류도 존재하지 않습니다. 컴파일러가 범위 내에 있음을 증명할 수 없으면 컴파일러가이를 입증 할 수 있도록 다시 작성하십시오. 만약 그것이 불가능하다면, 그 지점에서 가정을 수동으로 넣어야합니다; 컴파일러는 나중에 모순을 찾을 수 있습니다.
또한 초기화되지 않은 포인터를 가질 수 없으면 초기화되지 않은 메모리에 대한 포인터가 없습니다. 해제 된 메모리에 대한 포인터가 있으면 컴파일러가이를 거부해야합니다. Rust에는 이러한 종류의 증명을 기대할 수 있도록 다양한 포인터 유형이 있습니다. 독점적으로 소유 한 포인터 (즉, 별칭 없음), 불변의 구조에 대한 포인터가 있습니다. 기본 스토리지 유형은 변경할 수 없습니다.
또한 입력 표면 영역을 정확히 예상되는 것으로 제한하기 위해 프로토콜 (인터페이스 멤버 포함)에 대해 잘 정의 된 문법을 적용하는 문제도 있습니다. "정확성"에 대한 것은 다음과 같습니다. 1) 정의되지 않은 모든 상태를 제거 합니다. 2) 논리적 일관성을 유지 합니다. 거기에 도달하기 어려운 것은 (정확성의 관점에서) 극도로 나쁜 툴링을 사용하는 것과 관련이 있습니다.
이것이 바로 두 가지 최악의 관행이 전역 변수와 고 토스 인 이유입니다. 이러한 것들로 인해 사후 / 불변 / 불변 조건이 발생하지 않습니다. 또한 유형이 그렇게 효과적인 이유이기도합니다. 유형이 강해짐에 따라 (실제 값을 사용하기 위해 종속 유형을 사용함) 구조적 정확성 증명 자체에 접근합니다. 일치하지 않는 프로그램이 컴파일에 실패합니다.
바보 같은 실수가 아니라는 점을 명심하십시오. 또한 영리한 침입자로부터 코드 기반을 방어하는 것에 관한 것입니다. "공식적으로 지정된 프로토콜 준수"와 같은 중요한 속성에 대한 설득력있는 기계 생성 증거없이 제출을 거부해야하는 경우가 있습니다.