원문 캡처 · strawberry9.github.io
원문 캡처 · strawberry9.github.io

부록 C: C나 Rust를 아는 독자를 위한 안내

언어 안내서에는 C와 Rust를 각각 다루는 장이 있다. 이 부록은 두 언어에서 익힐 습관 중 유지할 것과 버릴 것을 간단히 정리한다.

C에서 넘어오는 경우

유지할 습관은 타입을 먼저 쓰는 선언, 괄호와 중괄호를 쓰는 if·while·for, 서식 문자열을 사용하는 printf, 지정 초기화자를 쓰는 구조체, //와 /* */ 주석, 메모리가 어디에 있는지 파악하는 감각이다.

컴파일러가 허용하지 않으므로 다음 습관은 버려야 한다.

  • 암묵적 변환. 여기서는 int와 long을 섞을 수 없고 i32와 i64도 마찬가지다. 모든 변환은 widen, narrow, to_float, to_int처럼 이름이 있는 호출로 한다. int는 없으며 모든 정수형에는 비트 폭이 명시된다.
  • 오버플로를 감싸서 처리하는 연산. a + b의 결과가 표현 범위를 벗어나면 값이 순환하는 대신 오류가 발생한다. 해시 함수처럼 순환 연산이 필요한 경우에는 이름으로 요청한다.
  • 널(null). 안전한 코드에는 널 포인터가 없다. 값이 없을 수 있다면 Option<T>로 표현하고 컴파일러가 확인을 요구한다.
  • 모든 곳에 포인터 사용하기. 호출된 함수가 읽기만 할 수 있는 포인터는 ref<T, shared>, 값을 바꿀 수 있는 포인터는 ref<T, exclusive>다. 소유권을 넘기는 경우에는 값을 그대로 전달한다. 주소만 필요한 일부 상황에는 unsafe 안에서 rawptr<T>를 쓸 수 있다.
  • free. 메모리를 직접 해제하지 않는다. 메모리를 소유한 값은 자원이며, 소유자의 범위가 끝날 때 해제된다. 누수와 이중 해제는 둘 다 작성할 방법이 없으므로 발생하지 않는다.
  • 오류 코드. 실패할 수 있는 함수는 Result<T, E>를 반환하고 호출자는 이를 반드시 확인해야 한다. 사용하지 않은 채 버려진 Result는 컴파일 오류다.
  • switch. 정수나 텍스트를 대상으로 하는 match가 그 역할을 하며 _ 분기를 둘 수 있다. 열거형을 대상으로 하는 match는 모든 변형을 다뤄야 한다.
  • 초기화하지 않은 변수. 변수는 읽기 전에 모든 경로에서 값을 할당해야 하며, 이 조건은 정적으로 검사한다.
  • x & 1 == 0. CobaltC에서는 의도한 대로 (x & 1) == 0으로 해석된다.

컴파일러가 예전에는 머릿속으로 기억해야 했던 사실을 관리한다는 점이 얻는 이점이다. cobc --keep-c를 실행하면 프로그램이 변환된 C 코드가 모든 검사를 명시적인 줄로 담아 보여준다.

Rust에서 넘어오는 경우

유지할 습관은 소유권과 이동, 두 가지 방식의 빌림, 페이로드가 있는 열거형, match, ?를 사용하는 Option과 Result, 제약 조건을 가진 제네릭, 캡처가 있는 클로저, 경계를 표시하는 unsafe, 데이터를 이동하거나 빌려 쓰는 스레드, 컴파일러가 안전성을 증명해야 한다는 전반적인 관점이다.

차이는 주로 검사를 컴파일 시점에 어디까지 수행하는지와 문법 표기에 있다.

  • 빌림 검사는 가능한 경우 정적으로, 불가능한 경우 동적으로 이뤄진다. Rust는 안전하다고 증명할 수 없는 프로그램을 모두 거부한다. CobaltC는 코드에 충돌이 드러나는 프로그램을 거부하고 나머지는 실행 중 검사해 충돌하는 첫 접근에서 오류를 낸다. 허용되는 프로그램의 범위는 검사기의 정교함이 아니라 언어가 정한다. 수명 주석은 없으며, 참조를 반환하는 함수에는 참조 매개변수가 정확히 하나 있어야 한다.
  • 트레이트는 없다. 내장 제약 조건은 여섯 가지이며 새 제약 조건을 선언할 수 없다. 사용자 타입에 대한 연산이 필요한 제네릭 코드는 그 연산을 함수 매개변수로 받는다.
  • 메서드 호출 문법은 없다. v.push(x)가 아니라 Vec::push(&mut v, x)를 쓴다. 빌림의 모드는 빌리는 위치에 표시한다.
  • 바인딩에 mut를 붙이지 않는다. 모든 지역 변수에 다시 값을 할당할 수 있다. 제어 대상은 값이 바뀔 수 있는지가 아니라 누가 그 값에 접근할 수 있는지다.
  • 반복자는 없다. foreach는 언어가 아는 컬렉션을 순회하며 position, retain, sort_by 같은 라이브러리 함수에는 함수 값을 전달한다. 구현할 반복자 프로토콜은 없다.
  • 텍스트 타입은 세 가지이며 표기도 서로 다르다. str은 빌릴 수 있는 슬라이스가 아니라 복사할 수 있는 텍스트 상수다. String은 소유하는 텍스트이고 StringView는 Rust의 &str처럼 String을 빌려 보는 뷰다.
  • 선언은 타입을 먼저 쓴다. i32 x = 5;와 auto y = x;처럼 쓴다. 블록, if, match는 Rust처럼 표현식이지만 조건에는 괄호를 쓰고 match 분기는 패턴 : 표현식, 형식이다.
  • 오버플로는 항상 검사한다. Rust에서는 빌드 프로필에 따라 달라지지만 CobaltC에서는 모든 빌드에서 오류가 발생한다. 다른 동작을 원하면 순환 연산 계열이나 검사 연산 계열만 사용할 수 있다.
  • 클로저의 캡처 목록은 직접 적고 검사한다. [a, b](i32 x) { ... }에서는 본문이 사용하는 바깥 변수를 정확히 모두 이름으로 지정해야 한다. 각 변수를 어떻게 캡처할지는 본문에 따라 결정되며 move는 값을 캡처한다.
  • 스레드에서 발생한 오류는 해당 스레드 안에 머문다. 오류는 스레드를 끝내고 뮤텍스를 손상된 상태로 만들며, try_join에서는 값으로 전달된다. 일반 join은 오류를 다시 발생시킨다. 패닉과 비슷하지만 정책은 라이브러리가 아니라 언어가 정한다.
  • 내부 가변성은 뮤텍스나 원자 연산만으로 처리한다. Cell이나 RefCell은 없다. Rc<T>는 공유 읽기를 제공하며, 그 안의 뮤텍스를 사용하면 값을 변경할 수 있다.

Rust 프로그래머는 CobaltC를 읽기는 쉽고 쓰기는 잠시 불편한 언어로 느끼는 경향이 있다. 한정된 함수 호출과 명시적으로 적는 빌림에 익숙해지면 그 불편함은 줄어든다. 안내서의 Rust 장에서는 각 차이와 그 이유를 자세히 설명한다.