자주 묻는 질문들

이 문서는 Rust 프로그래밍 언어에 대한 흔한 질문들을 답하기 위해 존재합니다. 이 문서는 언어에 대한 완전한 안내서도 아니고, 언어를 가르치는 도구도 아닙니다. 이 문서는 Rust 커뮤니티에서 사람들이 되풀이하여 맞닥뜨리는 질문들을 답하고, Rust의 일부 설계가 왜 그렇게 결정되었는지를 밝히기 위한 참조서입니다.

여기에서 답하지 않았지만 흔하거나 중요한 질문이 누락되어 있다고 생각하신다면 저희가 고칠 수 있도록 부담 없이 도와주세요.

Rust 프로젝트

이 프로젝트의 목표는 무엇입니까?

안전하고, 동시적이며, 실용적인 시스템 언어를 설계하고 구현하기 위함입니다.

Rust는 이 수준의 추상화와 효율을 추구하는 다른 언어들이 만족스럽지 못 하기에 존재합니다. 특히:

  1. 안전성이 너무 덜 주목되어 있습니다.
  2. 동시성 지원이 부족합니다.
  3. 실용적으로 쓰기가 힘듭니다.
  4. 자원에 대한 제어가 제한적입니다.

Rust는 효율적인 코드와 편안한 수준의 추상화를 제공하며, 동시에 위 4가지를 모두 개선하는 대안으로 만들어졌습니다.

이 프로젝트를 Mozilla가 제어하나요?

아니오. Rust는 2006년에 그레이던 호어(Graydon Hoare)가 시간을 쪼개서 하던 사이드 프로젝트로 시작하여 3년간 개발되었습니다. 2009년에 언어가 기본 테스트를 실행하고 핵심 개념들을 시연할 수 있을 정도로 성숙하자 Mozilla가 관여하기 시작했습니다. Mozilla는 여전히 Rust를 지원하고 있습니다만, Rust는 전 세계의 많은 장소에 퍼져 있는 열정적인 사람들로 이루어진 커뮤니티가 개발하고 있습니다. Rust 팀은 Mozilla 직원들과 아닌 사람들 둘 다를 포함하고, GitHub의 rust 단체에는 1,900명 이상의 서로 다른 기여자가 참여해 왔습니다.

프로젝트 거버넌스를 따라, Rust는 프로젝트의 비전과 우선 순위를 설정하는 코어 팀에 의해 관리되며 이 코어 팀이 전체적인 관점에서 프로젝트를 인도합니다. 또한 개별 관심 분야의 개발을 인도하고 장려하기 위한 서브팀들이 있으며, 핵심 언어, 컴파일러, Rust 라이브러리, Rust 도구, 그리고 공식 Rust 커뮤니티의 중재 등이 여기에 포함됩니다. 이들 각 분야 안에서의 설계는 RFC 과정을 통해 심화됩니다. RFC를 필요로 하지 않는 수정은 rustc 저장소의 풀 요청(pull request)을 통해 결정이 내려집니다.

Rust의 목표가 아닌 것은 무엇이 있나요?

  1. 우리는 특별히 최신의 기술을 도입하지 않습니다. 오래되고 자리 잡힌 기술이 더 좋습니다.
  2. 우리는 표현력, 최소주의 또는 우아함을 다른 목표에 우선하지 않습니다. 이들은 바람직하긴 하지만 부수적인 목표입니다.
  3. 우리는 C++나 기타 다른 언어의 모든 기능 집합을 커버하려 하지 않습니다. Rust는 자주 쓰이는 기능들을 제공할 것입니다.
  4. 우리는 100% 정적이거나, 100% 안전하거나, 100% 반영적(reflective)이거나, 기타 어떤 의미에서도 너무 교조적이려 하지 않습니다. 트레이드 오프는 존재합니다.
  5. 우리는 Rust가 “가능한 모든 플랫폼”에서 동작할 걸 요구하지 않습니다. 언젠가 Rust는 널리 쓰이는 하드웨어와 소프트웨어 플랫폼에서 불필요한 타협 없이 동작할 것입니다.

Mozilla에서 Rust를 사용하는 프로젝트가 무엇인가요?

주된 프로젝트는 Mozilla가 만들고 있는 실험적인 브라우저 엔진인 Servo가 있습니다. 또한 파이어폭스에 Rust 컴포넌트를 통합하는 작업도 진행 중입니다.

대규모 Rust 프로젝트의 예제가 있나요?

현재 가장 큰 오픈소스 Rust 프로젝트로는 ServoRust 컴파일러 자신이 있습니다.

Rust를 쓰는 다른 곳이 있나요?

계속 늘어나고 있습니다!

Rust를 쉽게 시도해 보려면 어떻게 해야 하나요?

Rust를 시도해 보는 가장 쉬운 방법은 Rust 코드를 작성하고 실행할 수 있는 온라인 앱인 플레이펜을 통하는 것입니다. 여러분의 시스템에서 Rust를 시도해 보고 싶다면, 설치 후 《Rust 프로그래밍 언어》의 숫자 맞추기 게임 지도서를 따라가세요.

Rust 문제들에 도움을 받으려면 어떻게 해야 하나요?

여러 방법이 있습니다:

Rust가 그동안 왜 그렇게 많이 바뀌었나요?

Rust는 안전하지만 쓸만한 시스템 프로그래밍 언어를 만들기 위한 목표로 출발했습니다. 이 목표를 달성하기 위해 수많은 아이디어들을 탐색했고, 그 중 일부는 채택되었지만(수명[lifetime], 트레이트) 다른 것들은 기각되었습니다(타입스테이트[type state] 시스템, 그린 스레딩). 또한 버전이 1.0에 근접하면서, Rust의 기능을 잘 사용하면서 질 좋고 일관된 크로스 플랫폼 API를 제공하기 위하여 표준 라이브러리의 많은 부분이 초기 설계로부터 재작성되었습니다. 이제 Rust가 1.0이 되면서 언어는 “안정화”되었다고 보장할 수 있고, 언어는 여전히 진화하겠지만 현재 Rust에서 작동하는 코드는 앞으로의 버전에서도 계속 작동할 것입니다.

Rust 언어의 버전은 어떻게 작동하나요?

Rust의 언어 버전은 유의적 버전(semantic versioning)을 따릅니다. 부(部) 버전에서 안정화된 API의 호환성을 깨뜨리는 수정은, 그 수정이 컴파일러 버그를 고치거나, 안전성 구멍을 메꾸거나, 추가적인 타입 정보를 요구하는 방향으로 디스패치 및 타입 추론을 고칠 경우에만 허용됩니다. 부 버전 변경에 대한 더 자세한 가이드라인은 언어표준 라이브러리에 대한 승낙된 RFC들에서 확인할 수 있습니다.

Rust는 세 개의 “릴리스 채널”, 즉 안정, 베타 및 나이틀리를 관리합니다. 안정 및 베타 버전은 매 6주마다 갱신되며, 기존의 나이틀리는 새 베타가 되고 기존의 베타는 새 안정 버전이 됩니다. 불안정하다고 표시되어 있거나 기능 게이트로 숨겨져 있는 언어 및 표준 라이브러리 기능은 나이틀리 버전에서만 사용할 수 있습니다. 새 기능은 불안정 상태로 도입되고, 코어 팀 및 유관한 서브팀이 재가하면 게이트가 “풀립니다”. 이런 접근을 통해 안정 버전들 사이에서는 강한 하위 호환성을 제공하면서 실험을 할 수 있습니다.

더 자세한 설명은 Rust 블로그의 “Stability as a Deliverable” 글을 읽어 보세요.

불안정한 기능을 베타나 안정 버전에서 사용할 수 있나요?

아뇨, 사용할 수 없습니다. Rust는 베타 및 안정 버전에 있는 기능들의 안정성에 강한 보장을 하려 애씁니다. 무언가가 불안정하다는 말은 우리가 아직 그 기능을 보장할 수 없다는 뜻이며, 사람들이 그게 똑같이 유지된다고 생각하며 의존하도록 하고 싶지 않다는 얘깁니다. 이렇게 함으로써 나이틀리 버전에서 야생의 수정을 시도할 기회를 얻는 동시에, 안정성을 원하는 사람들에게는 강한 보장이 여전히 제공됩니다.

매 버전마다 여러 기능들이 안정화되며, 베타 및 안정 버전은 매 6주마다 갱신됩니다. 종종 베타에는 수정 사항들이 반영되기도 합니다. 원하는 기능이 나이틀리 없이 사용 가능해지는 걸 기다리고 있다면, 이슈 트래커의 B-unstable 태그를 확인해서 해당 기능을 추적하는 이슈를 찾아 볼 수 있습니다.

"기능 게이트"가 무엇인가요?

“기능 게이트”는 Rust가 컴파일러, 언어 및 표준 라이브러리 기능을 안정화하는 데 사용하는 기작입니다. 게이트에 “막혀 있는” 기능은 나이틀리 버전으로만 접근할 수 있고, 더불어 #[feature] 속성이나 -Z unstable-options 명령줄 인자로 명시적으로 켜져야만 합니다. 기능이 안정화되면 안정 버전에서 사용 가능해지며 명시적으로 켤 필요가 없습니다. 이 시점에서 기능은 게이트가 “풀렸다”고 봅니다. 기능 게이트를 쓰면 안정화된 언어에서 사용 가능해지기 전에 개발 도중의 실험적인 기능을 실험해 볼 수 있습니다.

MIT/ASL2 이중 라이선스를 쓰는 이유는 무엇인가요?

아파치 라이선스(ASL)는 특허권의 침해를 방지하는 중요한 장치가 있지만 GPL 버전 2와 호환되지 않습니다. GPL2와 Rust를 함께 쓰는 데 문제가 없게 하기 위해 MIT로도 함께 라이선스되어 있습니다.

MPL이나 삼중 라이선스가 아니라 BSD 계열의 관대한(permissive) 라이선스를 쓰는 이유가 있나요?

어느 정도는 원 개발자(그레이던)가 그걸 좋아해서였고, 또 어느 정도는 언어들은 더 넓은 대중을 가지며 웹 브라우저 같은 제품과는 달리 더 다양한 곳에 포함되고 사용될 수 있기 때문이기도 합니다. 우리는 최대한 많은 잠재 기여자들에게 어필하려고 합니다.

성능

Rust는 얼마나 빠른가요?

빠릅니다! Rust는 이미 여러 벤치마크(이를테면 Benchmarks Game이나 기타 이것 저것)에서 관용적인 C 및 C++ 코드에 경쟁력이 있습니다.

C++와 동일하게 Rust는 비용 없는 추상화를 주요 원칙으로 삼습니다. Rust에는 전역으로 성능을 떨어뜨리는 추상화가 존재하지 않으며, 런타임 시스템에서 부하가 발생하지도 않습니다.

Rust가 LLVM에 기반해 있고 LLVM이 보기에 Clang과 비슷하게 보이려 한다는 걸 생각해 보면, LLVM에서 성능 개선이 일어난다면 Rust도 도움을 받게 됩니다. 장기적으로는 Rust 타입 시스템의 더 풍부한 정보로 C/C++ 코드에서는 어렵거나 불가능한 최적화도 가능해질 것입니다.

Rust는 쓰레기 수거(garbage collection, GC)를 하나요?

아니요. Rust의 중요 혁신 중 하나는 쓰레기 수거 없이 메모리 안전성을 보장한다는 것입니다(즉, 세그폴트가 나지 않습니다).

Rust는 GC를 피한 덕에 여러 장점을 제공할 수 있었습니다. 자원들을 예측 가능하게 해제할 수 있고, 메모리 관리 오버헤드가 낮으며, 사실상 런타임 시스템이 없습니다. 이 모든 특징들 때문에 Rust는 아무 맥락에나 깔끔하게 포함(embed)하기 쉬우며, 이미 GC를 가지고 있는 언어에 Rust 코드를 통합하기에도 훨씬 쉽습니다.

Rust의 소유권 및 빌림(borrowing) 시스템은 GC를 쓸 필요를 없애지만, 같은 시스템으로 다른 문제들, 이를테면 일반적인 자원 관리동시성 같은 것들에서도 도움을 받을 수 있습니다.

단일 소유권만으로 부족한 경우, Rust 프로그램은 GC 대신 표준적인 참조 카운팅 스마트 포인터 타입인 Rc와 이 타입의 스레드 안전한 버전인 Arc에 의존합니다.

하지만 우리는 추후 확장으로 선택 가능한 쓰레기 수거를 조사하고 있습니다. 목표는 SpidermonkeyV8 자바스크립트 엔진 같은 곳에서 제공하는 쓰레기 수거 런타임에 매끄럽게 통합할 수 있게 하는 것입니다. 또한 몇몇 사람들은 컴파일러 지원 없이 순수 Rust만으로 쓰레기 수거기를 만드는 시도를 하기도 했습니다.

제 프로그램이 왜 느린 거죠?

Rust 컴파일러는 요청이 없다면 최적화 없이 컴파일을 하는데, 이는 최적화를 하면 컴파일이 느려지고 개발 과정에서는 보통 바람직하지 않기 때문입니다.

cargo로 컴파일을 한다면 --release 플래그를 쓰세요. rustc를 직접 써서 컴파일을 한다면 -O 플래그를 쓰세요. 어느 쪽이나 최적화를 켜는 역할을 합니다.

Rust 컴파일이 느린 것 같습니다. 왜 그런 건가요?

코드를 기계어로 번역하고 최적화를 하기 때문입니다. Rust는 효율적인 기계어로 컴파일되는 고수준 추상화를 제공하고, 이 번역 과정은 특히 최적화를 할 경우 시간이 걸리게 마련입니다.

그러나 Rust의 컴파일 시간은 생각보다는 나쁜 편은 아니며, 앞으로 더 개선될 거라고 믿을 이유가 있습니다. C++와 Rust로 비슷한 크기의 프로젝트를 비교해 보면 전체 프로젝트를 컴파일하는 시간은 일반적으로 비슷하다고 봅니다. Rust 컴파일이 느리다고 느끼는 주된 원인은 C++와 Rust가 컴파일 모델이 다르다는 점, 즉 C++의 컴파일 단위는 한 파일이지만 Rust는 여러 파일로 이루어진 크레이트라는 것 때문입니다. 따라서 개발 도중에 C++ 파일 하나를 고치면 Rust에 비해 컴파일 시간이 훨씬 줄어들 수 있습니다. 현재 Rust 컴파일러를 리팩토링해서 증분 컴파일을 가능하게 하려는 대형 작업이 진행 중이며, 완료되면 Rust에서도 C++ 모델과 같이 컴파일 시간이 개선될 것입니다.

컴파일 모델과는 별개로, Rust의 언어 설계에는 컴파일 시간에 영향을 미치는 요소가 여럿 있습니다.

먼저 Rust는 비교적 복잡한 타입 시스템을 가지고 있고, 실행 시간에 Rust를 안전하게 만들기 위한 제약 사항을 강제하는 데 무시할 수 없는 컴파일 시간을 사용해야 합니다.

두번째로 Rust 컴파일러에는 오래된 기술 부채가 있으며, 특히 생성되는 LLVM IR의 품질이 좋지 못하기 때문에 LLVM이 시간을 들여 이를 “고쳐야” 합니다. 미래에는 MIR 기반 최적화 및 번역 단계가 Rust 컴파일러가 LLVM에 가하는 부하를 줄여 줄지도 모릅니다.

세번째로 Rust가 코드 생성에 LLVM을 쓰는 것은 양날의 검이라는 점입니다. LLVM 덕분에 Rust는 세계구급 런타임 성능을 보여 주지만, LLVM은 컴파일 시간에 촛점을 맞추지 않은 거대한 프레임워크이며 특히 품질이 낮은 입력에 취약합니다.

마지막으로 Rust가 일반화(제너릭) 타입을 C++와 비슷하게 단형화(monomorphise)하는 전략은 빠른 코드를 생성하지만, 다른 번역 전략에 비해 상당히 많은 코드를 생성해야 한다는 문제가 있습니다. 이 코드 팽창은 트레이트 객체를 써서 동적 디스패치와 장단을 교환할 수 있습니다.

Rust의 HashMap은 왜 느린가요?

Rust의 HashMap은 기본적으로 SipHash 해시 알고리즘을 사용합니다. 이 알고리즘은 해시 테이블 충돌 공격을 막으면서 여러 종류의 입력에 대해 적절한 성능을 내도록 설계되었습니다.

SipHash가 많은 경우 경쟁력 있는 성능을 보여 주긴 하지만, SipHash는 정수 같이 키가 짧을 경우 다른 해시 알고리즘에 비해 현저히 느립니다. 이 때문에 종종 HashMap의 성능이 낮은 걸 볼 수 있습니다. 이런 경우에는 보통 FNV 해시를 추천하지만, 이 알고리즘이 충돌 공격에서 SipHash와 다른 특성을 보인다는 점은 염두에 두어야 합니다.

왜 통합된 성능 측정 인프라가 없는 건가요?

있긴 한데요, 나이틀리 버전에만 있습니다. 궁극적으로는 착탈 가능한 통합 성능 측정 시스템을 만들 예정입니다만, 일단 현재 시스템은 불안정하다고 여겨집니다.

Rust는 꼬리 재귀(tail-call) 최적화를 하나요?

일반적으로는 아닙니다. 제한적으로 꼬리 재귀 최적화를 하긴 하지만 보장되지는 않습니다. 이 기능은 언제나 요청되어 왔기 때문에 Rust에는 이를 위해 예약어(become)가 예약되어 있습니다만, 이 기능이 기술적으로 가능한지, 그리고 가능하다면 구현이 될 것인지는 아직 불투명합니다. 특정한 맥락에서 꼬리 재귀 최적화를 하는 확장이 제안되었지만 현재 보류된 상태입니다.

Rust에는 런타임이 있나요?

Java 같은 언어들에서 말하는 그런 통상의 런타임은 없습니다만, Rust 표준 라이브러리의 일부분은 힙(heap), 스택 추적(backtrace), 되감기(unwinding) 및 보호(guard)를 제공하는 “런타임”이라고 볼 수 있습니다. 사용자의 main 함수가 실행되기 전에는 소량의 초기화 코드가 실행됩니다. 또한 Rust 표준 라이브러리는 C 표준 라이브러리를 링크하는데 여기에서도 비슷한 런타임 초기화가 일어납니다. Rust 코드는 표준 라이브러리 없이 컴파일될 수 있으며 이 경우 런타임은 대략 C와 비슷해집니다.

문법

왜 중괄호인가요? Rust의 문법이 하스켈이나 파이썬 같지 않은 이유가 있나요?

중괄호를 블록에 사용하는 건 여러 프로그래밍 언어에서 흔히 쓰이는 설계이며, 이 스타일에 이미 익숙한 사람들한테는 Rust가 이를 따르는 쪽이 편리합니다.

또한 중괄호는 프로그래머 입장에서는 더 유연한 문법을 제공하고 컴파일러 입장에서는 더 간단한 파서를 가능하게 합니다.

if 조건에서 소괄호를 생략할 수 있는데, 그럼 한 줄짜리 블럭에는 왜 중괄호를 넣어야 하나요? C 같은 문법이 안 되는 이유가 있나요?

C에서는 if 조건문에서 괄호가 필수이고 중괄호가 선택이지만, Rust에서는 반대로 합니다. 이렇게 해서 조건문 몸체와 조건을 명확하게 구분할 수 있고, 중괄호가 선택이라서 벌어지는 위험도 막을 수 있는데, 이는 Apple의 goto fail 버그와 같이 리팩토링 과정에서 흔히 생기고 잡기 어려운 오류들을 유발할 수 있습니다.

연관 배열의 리터럴 문법이 없는 이유는 무엇인가요?

Rust의 전반적인 설계는 언어의 크기를 제한하되 강력한 라이브러리를 만들 수 있게 하는 쪽을 선호합니다. Rust는 배열과 문자열 리터럴을 초기화하는 문법을 가지고 있지만 언어에 내장된 컬렉션 타입은 이걸로 전부입니다. 매우 널리 쓰이는 Vec 컬렉션 타입 같이, 라이브러리에서 정의하는 다른 타입들은 vec! 같은 매크로를 사용하여 초기화를 합니다.

나중에는 Rust가 매크로를 써서 컬렉션을 초기화하는 설계가 다른 타입에도 일반적으로 사용할 수 있도록 확장될 수 있고, 그렇게 되면 HashMap이나 Vec 같은 것 뿐만이 아니라 BTreeMap 같은 다른 타입들도 간단하게 초기화할 수 있게 될 것입니다. 그 전에 컬렉션을 더 간단한 문법으로 초기화하고 싶다면 직접 매크로를 만들 수 있습니다.

언제 암묵적인 반환을 써야 하나요?

Rust는 매우 수식 지향적인 언어이며 “암묵적인 반환”은 이 설계의 한 부분입니다. if, match나 일반 블록들은 Rust에서는 다 수식입니다. 예를 들어 다음 코드는 i64가 홀수인지 확인하고 결과를 단순히 값으로 내서 결과를 반환합니다:

fn is_odd(x: i64) -> bool {
    if x % 2 != 0 { true } else { false }
}

물론 더 간단하게는 이렇게 쓰겠지만요:

fn is_odd(x: i64) -> bool {
    x % 2 != 0
}

두 예제에서 함수의 마지막 줄은 그 함수의 반환값입니다. 중요한 것은 함수가 세미콜론으로 끝난다면 그 반환값은 ()이고, 이는 반환값이 없다는 뜻이라는 점입니다. 암묵적으로 반환하려면 세미콜론이 없어야 합니다.

명시적인 반환은 함수 몸체의 맨 끄트머리보다 이전에 반환을 해야 해서 암묵적인 반환이 불가능할 때만 쓰입니다. 물론 위 함수들도 return 예약어와 세미콜론을 쓸 수는 있지만 불필요하게 번잡하고 Rust 코드의 규약에 어긋날 것입니다.

왜 함수의 타입 서명(signature)들은 추론되지 않는 거죠?

Rust에서 선언은 타입을 명시적으로 쓰는 편이며 실제 코드는 타입을 추론하는 편입니다. 이 설계에는 몇 가지 이유가 있습니다:

match에는 모든 조건들이 들어 있어야 하나요?

리팩토링을 돕고 코드를 명료하게 하기 위함입니다.

먼저, match가 모든 가능성을 커버하고 있다면 enum에 새 변종(variant)을 넣을 때 실행 시간에 오류가 나는 게 아니라 컴파일이 실패하게 됩니다. Rust에서 이런 종류의 컴파일러 도움은 두려움 없이 리팩토링을 가능하게 합니다.

두 번째로, 이러한 체크는 기본 선택지를 명시적으로 만듭니다. 일반적으로 모든 가능성을 커버하지 않는 match를 안전하게 만드는 방법은 아무 선택지도 선택되지 않았을 때 스레드를 패닉하게 만드는 것 뿐입니다. Rust의 옛 버전에서는 match가 모든 가능성을 커버하지 않아도 되게 했는데 수많은 버그의 온상이 되었습니다.

기술되지 않은 선택지는 _ 와일드 카드로 간단하게 무시할 수 있습니다:

match val.do_something() {
    Cat(a) => { /* ... */ }
    _      => { /* ... */ }
}

숫자

부동 소숫점 계산을 할 때 f32f64 중 어느 쪽을 선호해야 하나요?

프로그램의 목적에 따라 어느 쪽을 쓸지가 달라집니다.

만약 부동 소숫점 숫자가 최대한 정밀해야 한다면 f64를 우선시하세요. 만약 크기를 작게 유지하거나 최대한의 성능을 얻고 싶고, 그에 따라 줄어드는 정밀도를 신경쓰지 않겠다면 f32가 낫습니다. 64비트 하드웨어에서도 f32가 보통 더 빠릅니다. 예를 들어 그래픽 프로그래밍에서는 높은 성능이 필요하고 화면 상의 픽셀을 표현하는 데는 32비트 부동 소숫점으로 충분하기 때문에 보통 f32를 씁니다.

잘 모르겠으면 정밀도를 우선시해서 f64를 선택하세요.

실수들을 비교하거나 HashMapBTreeMap의 키로 쓸 수 없는 이유는 뭔가요?

실수들은 ==, !=, <, <=, >>= 연산자나 partial_cmp() 함수로 비교할 수 있습니다. ==!=PartialEq 트레이트의 일부이고, <, <=, >, >=partial_cmp()PartialOrd 트레이트의 일부입니다.

실수들은 Ord 트레이트에 있는 cmp() 함수로는 비교할 수 없는데 실수에는 전순서(total order)가 없기 때문입니다. 덧붙여 실수에는 전등치(total equality) 관계도 없으므로 Eq 트레이트도 구현되어 있지 않습니다.

실수에 전순서나 전등치가 없는 이유는 부동 소숫점 실수에 있는 NaN 값은 다른 어떤 값이나 자기 자신보다 작지도, 크지도, 같지도 않기 때문입니다.

실수가 EqOrd를 구현하지 않으므로 이들 트레이트를 요구하는 타입에서는 사용 불가능하는데, 여기에는 BTreeMap이나 HashMap도 들어갑니다. 이들 타입은 그 키가 전순서나 전등치를 가지고 있다고 가정하고, 그렇지 않다면 오동작할 것이므로 이 조건은 중요합니다.

다만, f32f64를 감싸서 OrdEq 구현을 제공하는 크레이트는 존재하며 몇몇 상황에서 유용할 수 있습니다.

수치형들 사이에 변환을 하려면 어떻게 하나요?

두 가지 방법이 있는데, 하나는 as 예약어로 원시 타입 사이에서 간단한 변환을 하는 것이고, 다른 하나는 IntoFrom 트레이트를 써서 임의의 타입 변환을 하는 것입니다(트레이트를 직접 구현해서 변환을 추가할 수도 있습니다). IntoFrom 트레이트는 변환에서 손실이 일어나지 않을 때만 구현되어 있으며, 이를테면 f64::from(0f32)는 컴파일이 되지만 f32::from(0f64)는 아닙니다. 한편 as는 원시 타입들 사이에서는 모두 변환이 가능하며 필요하다면 값을 잘라냅니다.

왜 Rust에는 증감 연산자가 없나요?

전위 및 후위 증감 연산자는 편하긴 하지만 꽤 복잡합니다. 이들 연산자를 쓰려면 연산 순서를 알아야 하고, C나 C++에서 이로 인한 미묘한 버그나 정의되지 않은 동작이 흔히 발생하지요. x = x + 1이나 x += 1은 살짝 더 길 뿐이지만 모호하지 않습니다.

문자열

String이나 Vec<T>를 슬라이스(&str&[T])로 어떻게 바꾸나요?

보통 슬라이스를 예상하는 곳에서는 String이나 Vec<T>의 참조를 넘길 수 있습니다. StringVec&& mut 참조로 넘겨질 때는 deref 변환(coercion)을 통해 각자 대응되는 슬라이스로 자동으로 변환됩니다.

&str&[T]에 구현된 메소드들은 StringVec<T>에서 바로 접근할 수 있습니다. 예를 들어 trim&str의 메소드이고 some_stringString이라 하더라도 some_string.trim()은 동작할 것입니다.

일반화된 코드 같이 몇몇 상황에서는 수동으로 변환해야 할 필요가 있습니다. 수동 변환은 슬라이스 연산자를 써서 &my_vec[..]과 같이 할 수 있습니다.

&strString로 바꾸거나 반대로 하려면 어떻게 하나요?

to_string() 메소드는 &strString로 변환하고, String에서 참조를 빌리면 &str로 자동으로 변환됩니다. 아래 예제는 두 가지 방향을 모두 시연합니다:

fn main() {
    let s = "Jane Doe".to_string();
    say_hello(&s);
}

fn say_hello(name: &str) {
    println!("Hello {}!", name);
}

두 개의 다른 문자열 타입에 어떤 차이가 있나요?

String은 힙에 할당된 UTF-8 바이트를 소유하는 버퍼입니다. 변경 가능한 String은 수정할 수 있고 필요에 따라 그 용량(capacity)을 늘릴 수 있습니다. &str은 다른 데 (보통 힙에) 할당되어 있는 String으로부터 참조된 슬라이스나, 문자열 리터럴의 경우 정적 메모리를 가리키는, 용량이 고정된 “창”입니다.

&str은 Rust 언어가 구현하는 원시 타입이지만 String은 표준 라이브러리에 구현되어 있습니다.

String의 각 문자를 O(1), 즉 상수 시간에 접근하려면 어떻게 해야 하나요?

불가능합니다. 적어도 “문자”가 무슨 의미인지 제대로 이해하고 있지 않거나, 원하는 문자의 인덱스를 찾으려 문자열을 전처리하지 않는다면 말이지요.

Rust 문자열은 UTF-8로 인코딩되어 있습니다. 보기에 하나의 문자는 UTF-8에서는 ASCII 문자열는 달리 꼭 한 바이트인 건 아닙니다. 각 바이트는 “코드 단위”라고 불립니다(UTF-16에서는 코드 단위가 2바이트이고 UTF-32에서는 4바이트이지요). “코드포인트”는 하나 이상의 코드 단위로 구성되어 있고, 문자를 가장 가까이 근사한다고 할 수 있는 “자소(grapheme) 클러스터”는 여러 개의 코드포인트로 구성되어 있습니다.

따라서 UTF-8 문자열에서 바이트를 인덱싱할 수 있다 하더라도 상수 시간에 i번째 코드포인트나 자소 클러스터를 얻어낼 수는 없습니다. 하지만 원하는 코드포인트나 자소 클러스터가 어느 바이트에서 시작하는지 안다면 그건 상수 시간에 접근할 수 있습니다. str::find()나 정규식 검색 결과는 바이트 인덱스를 반환하므로 이 방법으로 접근하는 게 가능합니다.

왜 문자열이 기본적으로 UTF-8인가요?

str 타입이 UTF-8인 것은 현실에서, 특히 엔디안이 정해져 있지 않은 네트워크 전송에서 이 인코딩이 널리 쓰이기 때문이고, I/O를 할 때 어느 방향에서도 코드포인트를 다시 변환할 필요가 없는 것이 최선이라고 생각하기 때문입니다.

물론 이는 문자열 안의 특정 유니코드 코드포인트의 위치를 찾는데 O(n) 연산이 필요하다는 뜻이긴 합니다. 이미 시작하는 바이트 인덱스를 알고 있을 경우에는 예상대로 O(1) 시간이 걸리겠지만요. 어떻게 보면 바람직하지 않을 수도 있지만, 어떻게 보면 이 문제 자체가 트레이드오프로 가득 차 있기에 다음 중요한 점들을 지적할 필요가 있겠습니다:

str에서 ASCII 영역의 코드포인트를 훑는 건 바이트 단위로 안전하게 할 수 있습니다. .as_bytes()를 쓸 경우 u8을 얻는 건 O(1) 연산이며 이 값은 ASCII 범위의 char로 변환하거나 비교할 수 있습니다. 그러니까 이를테면 '\n'로 줄 바꿈을 찾는다면 바이트 단위로 검색해도 됩니다. UTF-8은 원래부터 이렇게 설계되었거든요.

대부분의 “문자 기반” 텍스트 연산들은 “ASCII 범위의 코드포인트 한정” 같이 매우 제약된 언어 가정이 있어야만 동작합니다. ASCII 범위를 벗어나면 언어학적인 단위들(글리프, 낱말, 문단)의 경계를 찾기 위해 (상수 시간이 아닌) 복잡한 알고리즘을 써야 하기 마련입니다. 저희는 “솔직한”, 언어학적으로 올바르며 유니코드에서 인증한 알고리즘을 권장합니다.

char 타입은 UTF-32입니다. 한 번에 한 코드포인트를 들여다 보는 알고리즘이 정말로 필요하다고 생각한다면 type wstr = [char]을 정의하여 str로부터 한번에 읽어들인 뒤 wstr에서 연산을 하면 됩니다. 다르게 말하면, 언어가 “기본적으로 UTF-32로 디코딩하지 않는다”고 해서 UTF-32로 디코딩하거나 다시 인코딩하는 것 자체가 불가능한 건 아니라는 말입니다.

왜 UTF-8이 UTF-16이나 UTF-32보다 보통 더 선호되는지 자세한 설명을 원한다면 UTF-8 Everywhere manifesto를 읽어 보시길 바랍니다.

어떤 문자열 타입을 써야 하죠?

Rust는 네 쌍의 문자열 타입이 있고 각각 다른 역할을 합니다. 각 쌍마다 “소유된” 문자열 타입과 “슬라이스” 문자열 타입이 따로 있고, 다음과 같이 구성되어 있습니다:

  “슬라이스” 타입 “소유된” 타입
UTF-8 str String
OS 호환용 OsStr OsString
C 호환용 CStr CString
시스템 경로 Path PathBuf

Rust의 서로 다른 문자열 타입은 각자 다른 목적을 가집니다. Stringstr은 UTF-8로 인코딩된 일반 목적의 문자열입니다. OsStringOsStr은 현재 플랫폼에 맞춰 인코딩되어 있고 운영체제와 상호작용할 때 쓰입니다. CStringCStr은 C 문자열의 Rust 버전으로 FFI 코드에 사용되고, PathBufPathOsStringOsStr에 편의를 위해 경로 조작을 위한 메소드들을 추가한 것입니다.

&strString을 동시에 받는 함수를 어떻게 짤 수 있나요?

함수의 요구 사항에 따라 여러 선택이 있습니다:

Into<String>의 사용

이 예제에서 함수는 소유된 문자열과 문자열 슬라이스를 둘 다 받으며, 어느 쪽인지에 따라 함수 몸체 안에서 아무 일도 하지 않거나 입력을 소유된 문자열로 변환합니다. 참고로 변환은 명시적으로 해야 하며 안 그러면 변환되지 않을 것입니다.

fn accepts_both<S: Into<String>>(s: S) {
    let s = s.into();   // s를 `String`으로 변환합니다.
    // ... 함수의 나머지 내용
}

AsRef<str>의 사용

이 예제에서 함수는 소유된 문자열과 문자열 슬라이스를 둘 다 받으며, 어느 쪽인지에 따라 아무 일도 하지 않거나 입력을 문자열 슬라이스로 변환합니다. 이는 입력을 참조로 받아서 다음과 같이 자동으로 일어나게 할 수 있습니다:

fn accepts_both<S: AsRef<str>>(s: &S) {
    // ... 함수의 몸체
}

Cow<str>의 사용

이 예제에서 함수는 Cow<str>을 받는데, 이는 일반화된 타입이 아니라 컨테이너로서 필요에 따라 소유된 문자열이나 문자열 슬라이스를 담을 수 있습니다.

fn accepts_cow(s: Cow<str>) {
    // ... 함수의 몸체
}

컬렉션

Rust로 벡터나 연결 리스트 같은 자료 구조를 효율적으로 구현할 수 있나요?

만약 이들 자료 구조를 구현하려는 이유가 다른 프로그램에서 그걸 사용하려는 거라면 표준 라이브러리에 효율적인 구현들이 존재하므로 그럴 필요는 없습니다.

하지만, 만약 단순히 공부를 위해서라면 안전하지 않은 코드에 발을 들일 필요가 있을 겁니다. 이들 자료 구조들이 안전한 Rust만으로도 구현할 수 있지만, 안전하지 않은 코드를 사용하는 것보다 성능이 떨어질 가능성이 높습니다. 간단하게 이유를 말하자면 벡터나 연결 리스트 같은 자료 구조들은 안전한 Rust에서 허용되지 않는 포인터와 메모리 연산에 의존하기 때문입니다.

예를 들어 양방향 연결 리스트에서는 각 노드마다 두 개의 변경 가능한 참조를 가져야 하는데 이는 Rust에서 변경 가능한 참조가 별명(alias)을 가질 수 없다는 규칙을 위배합니다. Weak<T>로 이 문제를 해결할 수 있지만 보통 원하는 것보다 성능이 떨어질 것입니다. 안전하지 않은 코드를 쓰면 이 별명 규칙을 우회할 수 있지만, 메모리 안전성을 위배하는 코드가 없다는 걸 수동으로 검증해야 합니다.

컬렉션을 움직이거나 소모(consume)하지 않고 각 원소에 대해 반복하려면 어떻게 하나요?

가장 쉬운 방법은 컬렉션의 IntoIterator 구현을 사용하는 겁니다. 다음은 &Vec을 쓰는 예제입니다:

let v = vec![1,2,3,4,5];
for item in &v {
    print!("{} ", item);
}
println!("\nLength: {}", v.len());

Rust의 for 반복문은 반복하고자 하는 대상에 대해 (IntoIterator 트레이트에 정의된) into_iter()를 호출합니다. IntoIterator 트레이트를 구현하는 아무 값이나 for 반복문에서 사용할 수 있습니다. IntoIterator&Vec&mut Vec에 구현되어 있으며, 이 경우 into_iter()가 컬렉션을 옮기거나 소모하는 것이 아니라 그 내용물을 빌리도록 합니다. 다른 표준 컬렉션에 대해서도 똑같은 관계가 성립합니다.

만약 옮기거나 소모하는 반복자가 필요하다면 for 반복문에서 반복할 때 &&mut 없이 쓰세요.

빌리는 반복자를 직접 접근하고 싶다면 보통 iter() 메소드를 써서 얻을 수 있습니다.

배열 선언에 배열 크기를 넣어야 하는 이유가 무엇인가요?

꼭 그럴 필요는 없습니다. 배열을 직접 선언한다면 원소의 갯수로부터 크기가 추론됩니다. 하지만 고정된 크기의 배열을 받는 함수를 선언한다면 컴파일러가 배열이 얼마나 클 지를 알아야 합니다.

하나 짚고 넘어가야 하는 게 있는데, Rust는 현재 서로 다른 크기의 배열에 대해 일반화를 지원하지 않습니다. 만약 갯수가 바뀔 수 있는 값들의 연속된 컨테이너를 받고자 한다면 (소유권이 필요하냐 마냐에 따라) Vec이나 슬라이스를 사용하세요.

소유권

그래프 같이 사이클을 포함하는 자료 구조를 구현하려면 어떻게 해야 하나요?

적어도 네 가지 선택이 있습니다(Too Many Linked Lists에서 더 길게 설명합니다):

자기 자신의 필드를 가리키는 참조를 가진 구조체는 어떻게 선언해야 하나요?

가능하지만 의미가 없습니다. 구조체는 자기 자신을 영구히 빌리게 되며 따라서 더 이상 움직일 수 없게 됩니다. 다음 코드가 이런 상황을 보여 줍니다:

use std::cell::Cell;

#[derive(Debug)]
struct Unmovable<'a> {
    x: u32,
    y: Cell<Option<&'a u32>>,
}


fn main() {
    let test = Unmovable { x: 42, y: Cell::new(None) };
    test.y.set(Some(&test.x));

    println!("{:?}", test);
}

값으로 호출하기, 소모(consume)하기, 움직이기, 그리고 소유권을 넘기기에 서로 무슨 차이가 있나요?

다 같은 뜻입니다. 네 가지 경우에서 모두, 값이 새 소유자에게 옮겨가고, 원 소유자가 소유를 잃어버려 더 이상 쓸 수 없게 됩니다. 만약 타입이 Copy 트레이트를 구현한다면 원 소유자의 값은 무효화되지 않아 계속 쓸 수 있습니다.

왜 어떤 타입은 함수에 넘긴 뒤에도 재사용할 수 있지만 다른 타입은 그렇지 않나요?

타입이 Copy 트레이트를 구현하면 함수에 전달될 때 복사됩니다. Rust의 모든 수치형은 Copy를 구현하지만, 구조체는 기본적으로 Copy를 구현하지 않기 때문에 대신 이동이 일어납니다. 즉 구조체는 함수에서 다시 반환되거나 하지 않는 한 더 이상 다른 데서 사용할 수 없게 됩니다.

"움직인 값을 사용"(use of moved value)했다는 오류를 어떻게 다뤄야 하나요?

이 오류는 사용하려는 값이 새 소유권자에게 옮겨갔다는 뜻입니다. 먼저 문제의 옮김이 정말로 필요한 것이었는가를 확인해 보세요. 값이 함수 안으로 옮겨갔다면 함수가 대신 참조를 쓰도록 재작성해야 할 수도 있습니다. 그게 아니고, 만약 옮겨가는 타입이 Clone을 구현할 경우, 옮겨가기 전에 clone()을 호출하면 값의 복사본이 옮겨가고 원래 값은 계속 쓸 수 있게 됩니다. 다만 값을 복제하는 건 일반적으로 최후의 수단이어야 하는데, 복제가 추가로 할당을 일으켜 비쌀 수 있기 때문입니다.

옮겨가는 값의 타입이 직접 만든 것이라면, (옮겨가는 대신 암묵적으로 복사를 할 경우) Copy나 (명시적으로 복사할 경우) Clone을 구현하는 걸 생각해 보세요. Copy은 대부분 #[derive(Copy, Clone)]로 구현되고(CopyClone를 필요로 합니다), Clone#[derive(Clone)]로 구현합니다.

어느 쪽도 가능하지 않다면 함수를 고쳐서, 소유권을 얻은 함수에서 나갈 때 그 값의 소유권을 반환하도록 해야 할 수 있습니다.

메소드 선언에서 언제 self, &self, 또는 &mut self를 쓰는지 규칙이 있나요?

빌림 체커(borrow checker)를 이해하는 방법은 무엇인가요?

빌림 체커는 Rust 코드를 평가하는 과정에서 오로지 몇 가지 규칙만 적용하는데, 《Rust 프로그래밍 언어》의 빌림(borrowing) 장에서 확인할 수 있습니다. 이 규칙은 다음과 같습니다:

첫째, 모든 빌림은 소유권자의 그것보다 작거나 같은 범위(scope)동안 지속되어야 합니다. 둘째, 다음 두 종류의 빌림 중 하나를 가질 수 있지만 둘을 동시에 가질 수는 없습니다:

  • 자원에 대한 하나 이상의 참조 (&T)
  • 정확히 하나의 변경 가능한 참조 (&mut T)

규칙들 자체는 간단하지만 이를 일관되게 지키는 건, 특히 수명과 소유권에 대해 생각하는 습관이 들지 않았을 경우 간단하지 않습니다.

빌림 체커를 이해하는 첫 단계는 산출된 오류를 읽는 것입니다. 빌림 체커를 인식된 문제를 해결하는 데 양질의 도움을 제공하게 하려 많은 노력이 투자되었습니다. 빌림 체커 문제를 만났을 때는 먼저 보고된 오류를 느리고 주의 깊게 읽고, 설명된 오류를 이해한 뒤에야 코드를 접근할 수 있습니다.

두번째 단계는 Cell, RefCell, 그리고 Cow 같이 Rust 표준 라이브러리가 제공하는, 소유권 및 변경 가능성에 관련된 컨테이너 타입들에 친숙해지는 것입니다. 이들은 특정한 소유권 및 변경 가능성 상황을 표현하는 데 유용하고 필요한 도구로, 최소한의 성능 비용만 지불하도록 작성되었습니다.

빌림 체커를 이해하는 데 가장 중요한 부분은 연습입니다. Rust의 강력한 정적 분석 보장은 많은 프로그래머가 이전에 겪어 본 것에 비해 엄격하고 꽤 다릅니다. 모든 것에 완전히 익숙해지려면 얼마간의 시간이 필요할 것입니다.

만약 빌림 체커와 다투고 있거나 인내가 바닥이 났다면, 언제든 Rust 커뮤니티에 도움을 청해 보세요.

Rc가 유용한 때는 언제인가요?

Rc는 Rust의 원자적이지 않은 참조 카운팅되는 포인터 타입으로, 이 질문은 공식 문서에서 커버하고 있습니다. 요약하자면 Rc 및 스레드 안전한 버전인 Arc는 공유된 소유권을 표현하는 데 유용하고, 아무도 접근하지 않을 때 연관된 메모리를 시스템이 자동으로 해제하도록 합니다.

함수에서 어떻게 클로저를 반환하나요?

함수에서 클로저를 반환하려면 그 클로저는 “이동 클로저”로, 클로저가 move 예약어로 선언되었어야만 합니다. 《Rust 프로그래밍 언어》에서 설명하듯, 이 예약어는 클로저가 갈무리된 변수들을 부모 스택 프레임과 무관한 사본으로 가지게 합니다. 안 그랬다가는, 클로저를 반환하면 더 이상 올바르지 않은 변수에 접근할 수 있게 될테니 안전하지 않게 됩니다. 다르게 말하면 잠재적으로 잘못된 메모리를 읽을 수 있게 된단 얘기죠. 클로저는 또한 Box로 감싸져 있어서 힙에 할당되어야만 합니다. 《Rust 프로그래밍 언어》에서 자세한 내용을 읽어 보세요.

Deref 변환(coercion)이 무엇이고 어떻게 동작하나요?

Deref 변환은 포인터에 대한 참조(예를 들어 &Rc<T>&Box<T>)를 자동으로 그 내용물의 참조(예를 들어 &T)로 변환하는 편리한 변환입니다. Deref 변환은 Rust를 더 사용하기 편리하게 하려 존재하며, Deref 트레이트를 통해 구현됩니다.

Deref 구현은 구현하는 타입이 deref 메소드를 호출하여 대상 타입으로 변환될 수 있다는 걸 나타냅니다. 이 메소드는 호출되는 타입의 변경 불가능한 참조를 받아서 (같은 수명을 가지는) 대상 타입의 참조를 반환합니다. * 전위 연산자는 deref 메소드의 축약입니다.

이들이 “변환”이라 불리는 건 《Rust 프로그래밍 언어》에서 언급하듯 다음 규칙 때문입니다:

만약 타입 UDeref<Target=T>를 구현하면, &U 값은 자동으로 &T로 변환됩니다.

예를 들어 &Rc<String>이 있으면 이 규칙에 따라 &String으로 변환되며, 이는 다시 같은 방법으로 &str로 변환됩니다. 따라서 함수가 &str 인자를 받는다면 &Rc<String>을 그대로 넘겨 주고 모든 변환 과정이 Deref 트레이트로 자동으로 처리되도록 할 수 있습니다.

가장 흔한 deref 변환의 종류로는 이런 게 있습니다:

수명

수명(lifetime)을 왜 쓰나요?

수명은 메모리 안전성에 대한 Rust의 해답입니다. Rust는 수명을 사용해서 쓰레기 수거(garbage collection)의 성능 비용 없이 메모리 안전성을 보장합니다. 수명 개념은 다양한 학술 연구에 기반해 있으며, 《Rust 프로그래밍 언어》에서 참조를 확인할 수 있습니다.

왜 수명 문법이 지금과 같은가요?

'a 문법은 ML 계열의 프로그래밍 언어에서 따 왔는데, 여기서 'a 문법은 일반화된 타입 인자를 나타내는 데 사용됩니다. Rust의 경우 수명 문법은 모호하지 않고, 눈에 띄어야 했으며 타입 선언에서 트레이트와 참조와 함께 쓰기 좋아야 했습니다. 다른 문법도 의논되었으나 이보다 확실히 더 좋은 문법이 제시되진 않았습니다.

함수 안에서 만든 무언가를 빌려서 반환하려면 어떻게 하나요?

빌린 물건이 함수보다 더 오래 살아 남는다는 걸 보장해야 합니다. 이는 다음과 같이 출력 수명을 입력 수명에 매어 놓아서 가능합니다:

type Pool = TypedArena<Thing>;

// (아래 수명 표시는 설명을 위해서 명시적으로 쓰인 것뿐이며,
// 뒤의 질문에서 설명하는 탈락(eilsion) 규칙에 따라
// 생략할 수 있습니다)
fn create_borrowed<'a>(pool: &'a Pool,
                       x: i32,
                       y: i32) -> &'a Thing {
    pool.alloc(Thing { x: x, y: y })
}

또는 String 같이 소유하는 타입들을 반환해서 참조를 아예 없애는 대안도 있습니다:

fn happy_birthday(name: &str, age: i64) -> String {
    format!("Hello {}! You're {} years old!", name, age)
}

이 접근은 더 간단하지만 종종 불필요한 할당이 일어납니다.

왜 어떤 참조에는 &'a T같이 수명이 있고 다른 참조에는 &T같이 없는 건가요?

사실 모든 참조 타입에는 수명이 있지만, 대부분의 경우 직접 쓸 필요가 없습니다. 규칙은 다음과 같습니다:

  1. 함수 몸체에서는 수명을 명시적으로 쓸 필요가 전혀 없으며 항상 올바른 값이 추론될 것입니다.
  2. 함수 서명 (예를 들어 인자 타입이나 반환 타입) 안에서는 수명을 명시적으로 써야 할 수 있습니다. 여기에서는 “수명 탈락(elision)”이라는 간단한 기본값이 적용되는데 이는 다음 세 규칙으로 구성되어 있습니다:
    • 인자에서 탈락된 각 수명은 서로 다른 인자가 됩니다.
    • 입력 수명이 하나 뿐이면, 그게 탈락되었든 아니든 그 함수의 반환 값에 있는 모든 탈락된 수명에 할당됩니다.
    • 입력 수명이 여럿 있지만 그 중 하나가 &self거나 &mut self라면, self의 수명이 모든 탈락된 출력 수명에 할당됩니다.
  3. 마지막으로 structenum 정의에서는 모든 수명이 명시적으로 선언되어야 합니다.

만약 이 규칙이 컴파일 오류를 일으킨다면, Rust 컴파일러는 일어난 오류를 가리키는 오류 메시지를 제공하면서 그 오류가 일어난 추론 단계에 따라 가능한 수정을 제시할 것입니다.

Rust는 어떻게 "널 포인터가 없다"는 것과 "유령 포인터(dangling pointer)가 없다"는 것을 보장하나요?

&Foo&mut Foo 타입의 값을 만드는 유일한 방법은 이미 존재하는 Foo 타입의 값을 참조가 가리키는 값으로 명시하는 것 뿐입니다. 참조는 주어진 코드 영역(즉, 참조의 수명) 안에서 원래 값을 “빌리며”, 참조가 값을 빌린 동안에는 원래 값을 옮기거나 소멸시킬 수 없습니다.

null 없이 값이 없다는 걸 어떻게 표현하나요?

Option 타입을 씁니다. 이 타입은 Some(T)이거나 None일 수 있는데, Some(T)T 타입의 값이 들어 있다는 걸 나타내고, None은 값이 없다는 걸 나타냅니다.

일반화 (제너릭)

"단형화"(monomorphisation)가 무엇인가요?

단형화는 일반화된 함수(나 구조체)의 각 사용을, 함수를 호출하는 데 쓰인 인자 타입(이나 구조체의 용례)에 따라 각 인스턴스로 특수화합니다.

단형화 과정에서는 함수가 인스턴스화된 타입의 집합 하나 하나마다 일반화된 함수의 새 사본이 번역됩니다. 이는 C++가 사용하는 전략과 같습니다. 모든 함수 호출에 대해 특수화되고 정적으로 디스패치되는 빠른 코드가 만들어지지만, 서로 다른 타입에 대해 인스턴스된 함수는 “코드 팽창”을 일으켜 다른 번역 전략에 비해 더 큰 바이너리를 생성해낼 수 있다는 트레이드오프가 있습니다.

타입 인자 대신 트레이트 객체를 받는 함수들은 단형화를 거치지 않습니다. 대신 트레이트 객체의 함수들은 실행 시간에 동적으로 디스패치됩니다.

함수와 아무 변수도 갈무리하지 않는 클로저 사이에 어떤 차이가 있나요?

함수와 클로저는 동작상으로는 동일하지만, 구현이 다르기 때문에 실행 시간에는 다른 표현을 가집니다.

함수는 언어에 내장된 원시 타입이며, 클로저는 근본적으로 Fn, FnMut, 그리고 FnOnce 세 트레이트에 대한 문법 설탕입니다. 클로저를 만들 때 Rust 컴파일러는 자동으로 이들 세 트레이트 중 적절한 것들을 구현하고, 갈무리된 환경의 변수들을 멤버로 가지는 구조체를 만들어 함수로 불릴 수 있도록 합니다. 벌거벗은(bare) 함수는 환경을 갈무리할 수 없습니다.

이들 트레이트 사이의 큰 차이는 self 인자를 받는 방법에 있습니다. Fn&self를, FnMut&mut self를, 그리고 FnOnceself를 받습니다.

클로저가 환경에서 아무 변수도 갈무리하지 않는다 하여도, 다른 클로저와 마찬가지로 실행 시간에는 두 개의 포인터로 표현됩니다.

상류(higher-kinded) 타입이 무엇인가요? 그게 어째서 필요하다는 건가요? Rust에 상류 타입이 없는 이유는 무엇인가요?

상류 타입은 인자가 채워져 있지 않은 타입입니다. Vec, ResultHashMap 같은 타입 생성자는 모두 상류 타입의 예로, 각각 특정한 타입을 가리키려면 Vec<u32>와 같이 추가로 타입 인자가 필요합니다. 상류 타입을 지원한다는 얘기는 “완전한” 타입들이 쓰일 수 있는 곳 어디에나, 이를테면 일반화된 함수 같은 곳에서 “불완전한” 타입도 쓸 수 있다는 뜻입니다.

i32, bool이나 char 같은 완전한 타입은 종류(kind)가 *입니다(이 표기법은 타입 이론에서 유래합니다). Vec<T> 같이 인자가 하나인 타입은 종류가 * -> *이며, 이는 이를테면 Vec<T>i32 같은 완전한 타입을 받아서 Vec<i32> 같은 완전한 타입을 반환한다는 뜻입니다. HashMap<K, V, S> 같이 인자가 세 개인 타입은 종류가 * -> * -> * -> *이며, 세 개의 완전한 타입(i32, String, RandomState 같이)을 받아서 새로운 완전한 타입 HashMap<i32, String, RandomState>를 만들어 냅니다.

위 예제에 덧붙여 타입 생성자는 수명 인자도 받을 수 있는데 여기서는 Lt라고 표기하겠습니다. 예를 들어 slice::Iter는 종류가 Lt -> * -> *인데, Iter<'a, u32> 처럼 인스턴스화되어야 하기 때문입니다.

상류 타입 지원이 없으면 특정한 종류의 일반화된 코드를 짜기 어렵습니다. 특히 흔히 수명 등으로 종종 매개변수화되는 반복자 같은 개념을 추상화하는 데 문제가 생깁니다. 이것 때문에 Rust의 컬렉션을 추상화하는 트레이트는 그간 만들 수 없었습니다.

또 다른 흔한 예제로는 함수자(functor)나 모나드(monad) 같은 개념으로, 둘 다 하나의 타입이 아닌 타입 생성자입니다.

Rust는 현재 상류 타입을 지원하지 않는데 저희가 하고자 하는 다른 개선점보다 우선순위가 낮았기 때문입니다. 이 설계는 대규모로 많은 곳들에 영향을 미치기 때문에 주의깊게 접근하고 싶은 것도 있습니다. 하지만 현재 상류 타입을 지원하지 않는 데는 다른 특별한 이유가 있는 건 아닙니다.

일반화 타입에 <T=Foo> 같은 이름이 붙은 타입 인자는 무엇인가요?

이들은 연관 타입(associated type)이라 하며, where 절로 표현할 수 없는 트레이트 제약을 가능케 합니다. 예를 들어 일반화된 제약 X: Bar<T=Foo>는 “XBar 트레이트를 구현해야 하고, 그 Bar의 구현에서 XBar의 연관 타입 TFoo를 선택해야 한다”는 뜻입니다. where 절만으로 표현할 수 없는 제약의 예제로는 Box<Bar<T=Foo>> 같은 트레이트 객체가 있습니다.

연관 타입은, 일반화 과정에서 타입의 무리가 존재해 한 타입 인자가 무리의 모든 타입을 결정하는 경우가 종종 있기 때문에 존재합니다. 예를 들어 그래프 트레이트는 그래프 자신을 가리키는 Self 타입을 가질 수 있고, 정점과 간선을 위한 연관 타입을 가질 수 있습니다. 각 그래프 타입은 연관 타입들을 유일하게 결정합니다. 연관 타입은 이러한 타입의 무리를 다루는 걸 훨씬 간명하게 만들고, 많은 경우 타입 추론에도 도움이 됩니다.

연산자를 오버로드할 수 있나요? 어떤 게 가능하고 어떻게 하나요?

대응되는 트레이트를 구현해서 여러 연산자들에 원하는 구현을 제공할 수 있습니다. +라면 Add, *라면 Mul 등등이 있습니다. 이런 식으로 씁니다:

use std::ops::Add;

struct Foo;

impl Add for Foo {
    type Output = Foo;
    fn add(self, rhs: Foo) -> Self::Output {
        println!("Adding!");
        self
    }
}

다음 연산자들을 오버로드할 수 있습니다:

연산자 트레이트
+ Add
+= AddAssign
이항 - Sub
-= SubAssign
* Mul
*= MulAssign
/ Div
/= DivAssign
단항 - Neg
% Rem
%= RemAssign
& BitAnd
| BitOr
|= BitOrAssign
^ BitXor
^= BitXorAssign
! Not
<< Shl
<<= ShlAssign
>> Shr
>>= ShrAssign
* Deref
mut * DerefMut
[] Index
mut [] IndexMut

Eq/PartialEqOrd/PartialOrd가 나뉜 이유는 무엇인가요?

Rust의 몇몇 타입들은 그 값들이 부분적으로만 순서가 있거나, 부분적으로만 등치 관계입니다. 부분 순서(partial ordering)란 주어진 타입의 어떤 값들이 서로 작지도 크지도 않을 수 있다는 뜻입니다. 부분 등치(partial equality)란 주어진 타입의 어떤 값들이 자기 자신과 같지 않을 수 있다는 뜻입니다.

부동 소숫점 타입들(f32f64)이 좋은 예제입니다. 모든 부동 소숫점 타입에는 NaN(“not a number”, 즉 “숫자가 아님”을 뜻함) 값이 있습니다. NaN은 자기 자신과 같지도 않고(NaN == NaN은 거짓입니다), 다른 부동 소숫점 값보다 작지도 크지도 않습니다. 따라서 f32f64PartialOrdPartialEq를 구현하지만 OrdEq는 구현하지 않습니다.

부동 소숫점에 대한 이전 질문에서 설명되었듯, 이러한 구분은 몇몇 컬렉션이 올바른 결과를 내는데 전순서/전등치에 의존하기 때문에 중요합니다.

입출력

파일을 String으로 읽으려면 어떻게 하나요?

std::ioRead 트레이트에 있는 read_to_string() 메소드를 씁니다.

use std::io::Read;
use std::fs::File;

fn read_file(path: &str) -> Result<String, std::io::Error> {
    let mut f = try!(File::open(path));
    let mut s = String::new();
    try!(f.read_to_string(&mut s));  // `s`는 "foo.txt"의 내용을 담습니다
    Ok(s)
}

fn main() {
    match read_file("foo.txt") {
        Ok(_) => println!("Got file contents!"),
        Err(err) => println!("Getting file contents failed with error: {}", err)
    };
}

파일을 효율적으로 읽으려면 어떻게 하나요?

File 타입은 Read 트레이트를 구현하며, 여기에는 데이터를 읽고 쓰는 다양한 함수들이 존재하는데 read(), read_to_end(), bytes(), chars(), 그리고 take() 등이 포함됩니다. 각 함수들은 주어진 파일로부터 일정한 만큼의 입력을 읽어 들입니다. read()는 기반 시스템이 한 번의 호출에서 제공하는 한 최대한 많은 입력을 읽어 들입니다. read_to_end()는 전체 버퍼를 벡터로 읽어 들이고, 필요에 따라 공간을 할당합니다. bytes()chars()는 파일 안의 바이트와 문자에 대해 반복을 수행합니다. 마지막으로 take()로 파일로부터 최대 지정한 만큼의 바이트를 읽을 수 있습니다. 이들 함수들로 필요한 어떤 데이터라도 효율적으로 읽어 들일 수 있습니다.

버퍼를 통해서 읽고 싶다면 BufReader 구조체를 사용하세요. 이 구조체는 읽는 과정에서 시스템 호출의 숫자를 줄이는 데 도움이 됩니다.

Rust에서 어떻게 비동기 입출력을 하나요?

Rust에서 비동기 입출력을 제공하는 라이브러리로는 mio, tokio, mioco, coio-rs, 그리고 rotor 등이 있습니다.

Rust에서 명령행 인자를 받는 방법은 무엇인가요?

가장 쉬운 방법은 Args를 써서 입력 인자들에 대한 반복자를 받는 것입니다.

더 강력한 무언가를 찾고 있다면 crates.io에 여러 선택이 있습니다.

오류 처리

Rust에는 왜 예외(exception)가 없나요?

예외는 제어 흐름을 이해하기 복잡하게 만들고, 타입 시스템을 넘어서는 유효성/무효성을 표현하며, (Rust의 주요 촛점인) 멀티스레딩된 코드와 잘 상호작용하지 않습니다.

Rust는 오류 처리에 타입 기반의 접근을 선호하며, 《Rust 프로그래밍 언어》에서 길게 다루고 있습니다. 이는 Rust의 제어 흐름, 동시성 및 여타 다른 것들에 더 잘 맞아 들어 갑니다.

여기 저기 보이는 unwrap()를 어떻게 할 수 없나요?

unwrap()Option이나 Result 안에 있는 값을 뽑아 내고 아무 값도 없으면 패닉을 일으키는 함수입니다.

unwrap()이 잘못된 사용자 입력 같이 예상할 수 있는 오류들을 기본으로 다루는 방법이 되어서는 안 됩니다. 현업 코드에서 이는 값이 비어 있지 않으며 만에 하나 비어 있다면 프로그램이 깨지는 단언(assertion)처럼 취급되어야 합니다.

또한 unwrap()은 아직 오류를 처리하고 싶지 않은 빠른 프로토타입이나, 오류 처리가 주요 논점을 흐릴 수 있는 블로그 글에서도 유용합니다.

try! 매크로를 쓰는 예제 코드를 실행하려고 할 때 오류가 나는 이유는 뭔가요?

아마도 함수의 반환 타입에 문제가 있을 겁니다. try! 매크로는 Result에서 값을 뽑아 내거나, Result가 들고 있는 오류를 먼저 반환합니다. 즉 tryResult를 반환하는 함수에서만 동작하며, 이 때 Err로 만들어지는 타입은 From::from(err)을 구현해야 합니다. 특히 이는 main 함수에서는 try! 매크로를 쓸 수 없다는 뜻이기도 합니다.

모든 곳에 Result를 쓰는 것 말고 더 쉽게 오류를 처리할 방법이 없나요?

다른 사람의 코드에 있는 Result를 처리하지 않는 방법을 원한다면 항상 unwrap()를 쓸 수 있지만, 아마도 원하는 게 아닐 겁니다. Result는 어떤 계산이 성공적으로 끝나거나 끝나지 않을 수 있다는 표시입니다. 이러한 실패를 처리하도록 요구하는 건 Rust가 튼튼한 코드를 권장하는 방법 중 하나입니다. Rust는 실패를 더 편리하게 처리할 수 있도록 try! 매크로 같은 도구를 제공합니다.

정말로 오류를 처리하고 싶지 않다면 unwrap()를 쓰세요. 하지만 이렇게 하면 실패시 코드가 패닉을 일으키고, 보통 이는 프로세스를 종료시킨다는 점을 유의하시길 바랍니다.

동시성

unsafe 블록 없이 여러 스레드에서 정적인 값을 사용할 수는 없을까요?

변경은 동기화가 된다면 안전합니다. (lazy-static 크레이트로 지연되어 초기화된) 정적인 Mutex를 변경하는 데는 unsafe 블록이 필요 없으며, 정적인 AtomicUsize를 변경하는 데도 필요 없습니다(이건 lazy_static 없이 초기화할 수 있습니다).

좀 더 일반적으로, 타입이 Sync를 구현하고 Drop을 구현하지 않는다면 static에서 사용할 수 있습니다.

매크로

식별자를 생성하는 매크로를 짤 수 있나요?

현재는 안 됩니다. Rust 매크로는 “위생적인(hygienic) 매크로”로, 예상치 못 하게 다른 식별와 겹치는 식별자를 갈무리하거나 만드는 걸 피합니다. 이런 매크로의 능력은 C 전처리기에서 흔히 쓰이는 스타일의 매크로와는 상당히 다릅니다. 매크로 호출은 명시적으로 지원되는 곳, 즉 아이템, 메소드 선언, 문장, 수식 및 패턴에서만 나타날 수 있습니다. 여기서 “메소드 선언”이란 메소드를 집어 넣을 수 있는 빈 공간을 말합니다. 이들은 부분적인 메소드 선언을 채우는 데 쓰일 수는 없습니다. 같은 논리로, 이들은 부분적인 변수 선언을 채우는 데 쓰일 수 없습니다.

디버깅 및 도구

Rust 프로그램은 어떻게 디버깅하나요?

Rust 프로그램은 C나 C++와 같이 gdblldb로 디버깅할 수 있습니다. 사실은 모든 Rust 설치에는 (플랫폼 지원에 따라) rust-gdb나 rust-lldb 둘 중 하나가 함께 들어 있습니다. 이들은 gdb와 lldb에 Rust 값을 보기 좋게 출력해 주도록 감싼 것입니다.

rustc가 표준 라이브러리 코드에서 패닉(panic)이 일어났다고 하는데, 제 코드의 실수를 어떻게 찾을 수 있을까요?

이 오류는 보통 사용자 코드에서 None이나 Errunwrap()해서 일어납니다. RUST_BACKTRACE=1 환경 변수를 설정해서 스택 추적(backtrace)을 켜는 게 더 많은 정보를 얻는데 도움이 됩니다. 디버그 모드로 컴파일하거나(cargo build의 기본값), 함께 들어 있는 rust-gdbrust-lldb 같은 디버거를 쓰는 것도 도움이 됩니다.

무슨 IDE를 써야 하나요?

Rust 개발 환경에는 여러 선택이 있으며 자세한 사항은 공식 IDE 지원 페이지에 설명되어 있습니다.

gofmt은 멋져요. rustfmt 같은 건 없나요?

rustfmt여기 있고, Rust 코드를 가능한한 읽기 쉽고 예측 가능하게 만들도록 활발히 개발되고 있습니다.

저수준

memcpy같이 바이트를 복사하려면 어떻게 하나요?

이미 존재하는 슬라이스를 안전하게 복제하려면 clone_from_slice를 쓸 수 있습니다.

서로 겹칠 수 있는 바이트들을 복사하려면 copy를 쓰세요. 서로 겹칠 수 없는 바이트들을 복사하려면 copy_nonoverlapping을 쓰세요. 이들 함수들은 언어의 안전성 보장을 깨뜨리는 데 쓰일 있기 때문에 unsafe입니다. 사용에 주의를 기울이세요.

표준 라이브러리 없이 Rust를 사용하는 건 할 만한가요?

물론입니다. Rust 프로그램은 #![no_std] 속성으로 표준 라이브러리를 불러 들이지 않도록 설정할 수 있습니다. 이 속성이 설정되어도 플랫폼 독립적인 원시 타입만 제공되는 Rust 코어 라이브러리는 여전히 사용할 수 있습니다. 따라서 여기에는 I/O, 동시성, 힙(heap) 할당 같은 건 포함되지 않습니다.

Rust로 운영체제를 작성할 수 있나요?

네! 사실 정확히 그걸 하는 프로젝트가 여럿 있습니다.

i32f64 같은 수치형을 빅 엔디안이나 리틀 엔디안 형식으로 파일 및 다른 바이트 스트림에 읽고 쓰려면 어떻게 하나요?

byteorder 크레이트가 정확히 그걸 하는 유틸리티이니 살펴 보세요.

Rust가 메모리 상에 값이 어떻게 배치될 지가 고정되어 있나요?

기본적으로는 아닙니다. 일반적으로 enumstruct의 배치는 정의되지 않습니다. 따라서 컴파일러가 패딩을 구분값(discriminant)을 넣는데 재사용하거나, 중첩된 enum들의 변종(variant)들을 압축하거나, 패딩을 없애기 위해 필드를 재배치하는 등의 잠재적인 최적화를 할 수 있게 됩니다. 데이터를 들고 있지 않은 (“C와 비슷한”) enum은 정의된 표현을 가지도록 할 수 있습니다. 이러한 enum은 데이터를 들고 있지 않은 이름들만의 단순 목록이므로 쉽게 구분할 수 있습니다:

enum CLike {
    A,
    B = 32,
    C = 34,
    D
}

이러한 enum#[repr(C)] 속성을 적용하면 대응되는 C 코드가 가질 표현과 같은 표현이 되도록 할 수 있습니다. 따라서 FFI 코드에서 C enum이 쓰일 대부분의 상황에서 Rust enum을 쓸 수 있습니다. 마찬가지로 struct에도 이 속성을 적용하면 C struct가 가질 배치와 같은 배치가 되도록 할 수 있습니다.

다중 플랫폼

Rust에서 플랫폼 의존적인 동작을 표현하는 일반적인 방법은 무엇인가요?

플랫폼 의존적인 동작은 target_ostarget_family, target_endian 같은 조건부 컴파일 속성으로 표현할 수 있습니다.

Rust를 안드로이드 및 iOS 프로그래밍에 쓸 수 있나요?

네 할 수 있습니다! 이미 Rust를 안드로이드iOS에서 사용하는 예제가 있습니다. 설정에 조금 시간이 들긴 하지만 Rust는 두 플랫폼에서 모두 잘 동작합니다.

제 Rust 프로그램을 웹 브라우저에서 실행할 수 있나요?

아마도요. Rust는 asm.jsWebAssembly 모두를 실험적으로 지원합니다.

Rust에서 크로스 컴파일은 어떻게 하나요?

Rust에서는 크로스 컴파일을 할 수 있지만 설치 과정이 좀 필요합니다. 모든 Rust 컴파일러는 크로스 컴파일러지만 라이브러리는 해당 플랫폼 용으로 크로스 컴파일될 필요가 있습니다.

Rust는 지원되는 플랫폼에 대해서 표준 라이브러리의 사본을 배포하고 있으며, 배포판 페이지의 각 빌드 디렉토리에 있는 rust-std-* 파일들로 들어 있습니다만, 아직 이걸 자동으로 설치하는 방법은 없습니다.

모듈 및 크레이트

모듈과 크레이트 사이에 어떤 관계가 있나요?

Rust 컴파일러가 제가 use한 라이브러리를 찾지 못 하는 이유는 뭔가요?

여러 가능한 답이 있습니다만, 흔한 실수로는 use 속성이 크레이트 최상단에 상대적이라는 걸 깨닫지 못 하는 게 있습니다. 선언을 재작성해서 경로가 프로젝트의 최상단 파일에 선언되었을 때랑 똑같은 경로가 되도록 한 뒤 문제가 해결되는지를 확인해 보세요.

또한 selfsuper를 써서 use 경로를 각각 현재 모듈이나 상위 모듈에 상대적으로 만들 수 있습니다.

라이브러리를 use하는 데 대한 완전한 정보에 대해선 《Rust 프로그래밍 언어》의 “크레이트와 모듈” 장을 읽으세요.

왜 모듈 파일을 정의하기 위해 크레이트 최상위에 mod를 넣어야 하나요? 그냥 use로 지정하면 안 되나요?

Rust에서 모듈은 제자리에 선언하거나 다른 파일에서 선언할 수 있습니다. 각각의 예제는 다음과 같습니다:

// main.rs에서
mod hello {
    pub fn f() {
        println!("hello!");
    }
}

fn main() {
    hello::f();
}
// main.rs에서
mod hello;

fn main() {
    hello::f();
}

// hello.rs에서
pub fn f() {
    println!("hello!");
}

첫 예제에서 모듈은 모듈이 사용되는 곳과 같은 파일에 정의되어 있습니다. 둘째 예제에서 메인 파일의 모듈 선언은 컴파일러에게 hello.rshello/mod.rs를 찾아 보고 그 파일을 읽으라고 말해 줍니다.

moduse의 차이를 주목하세요. mod는 모듈이 존재한다고 선언하지만, use는 다른 곳에 선언된 모듈을 참조하여 그 내용물을 현재 모듈의 범위 안에 가져 옵니다.

Cargo가 프록시를 사용하도록 설정하려면 어떻게 하나요?

Cargo 환경설정 문서에 설명되어 있듯, 환경설정 파일의 [http] 아래에 “proxy” 변수를 설정해서 Cargo가 프록시를 쓰도록 할 수 있습니다.

이미 크레이트를 use했는데도 왜 컴파일러가 메소드 구현을 찾지 못 하는 걸까요?

트레이트에 선언된 메소드라면 명시적으로 트레이트 선언을 들여 와야 합니다. 즉, 트레이트를 구현하는 구조체가 있는 모듈만 들이는 것으로 충분하지 않고, 트레이트 자신도 들여 와야 합니다.

왜 컴파일러가 use 선언을 자동으로 추론하지 못 하나요?

가능할 수도 있겠지만 별로 원하는 게 아닐 겁니다. 많은 경우 컴파일러가 단순히 주어진 식별자가 선언된 곳을 찾아 보면서 올바른 모듈을 찾아 들여 내는 것이 가능하겠지만, 일반적으로는 아닐 수 있습니다. 서로 겨루는 선택지가 여럿 있다면 rustc에서 어떤 식으로 결정을 하든 일부 경우에 놀라움과 혼란을 가져올 것이고, Rust는 이름이 어디에서 오는지를 명시적으로 쓰길 선호합니다.

예를 들어 컴파일러가 서로 겨루는 식별자 선언들 중 먼저 들여 온 모듈에서 나온 선언을 선택한다고 칩시다. 그러니까 만약 모듈 foo와 모듈 bar가 둘 다 식별자 baz를 정의하고, foo가 먼저 등록된 모듈이라면, 컴파일러는 use foo::baz;를 삽입하게 됩니다.

mod foo;
mod bar;

// use foo::baz  // 이걸 컴파일러가 삽입합니다.

fn main() {
  baz();
}

이게 일어난다는 걸 알고 있다면 아마도 몇 글자 절약이 되기는 하겠지만, baz()가 사실 bar::baz()이 되길 원했다면 예기치 않은 오류 메시지가 나올 가능성이 크게 올라가며, 함수 호출의 의미가 모듈 선언에 의존하므로 코드의 가독성이 떨어집니다. 이는 우리가 원하는 트레이드오프가 아닙니다.

다만, 미래에는 IDE가 선언들을 다루는 걸 도와줄 수 있으며 그럼 두 접근의 장점을 모두 얻게 될 겁니다. 기계가 이름을 가져 오는 걸 돕지만 그 이름이 어디서 오는지는 명시적인 선언을 쓰는 거죠.

Rust 라이브러리를 어떻게 동적으로 읽어 들이나요?

여러 플랫폼에서 동적 링크를 제공하는 libloading으로 동적 라이브러리를 불러 들이세요.

왜 crates.io에는 이름 공간이 없나요?

https://crates.io의 설계에 대한 공식 설명을 전재하자면:

crates.io의 첫 한 달동안 여러 분들께서 저희들에게 패키지 이름 공간을 도입할 가능성에 대해 질문을 해 왔습니다.

패키지 이름 공간은 여러 저자들이 하나의 일반적인 이름을 쓸 수 있게 하지만, 패키지가 Rust 코드에서 참조되는 방법과 패키지에 대한 사람들 사이의 소통에 복잡도를 더합니다. 얼핏 보면 여러 저자들이 http 같은 이름을 쓸 수 있을 것 같지만, 실은 이는 사람들이 이 패키지들을 wycats의 httpreem의 http 같은 식으로 가리켜야 한다는 뜻일 뿐입니다. wycats-httpreem-http 같은 패키지 이름에 비해 별달리 장점이 없지요.

이름 공간이 없는 패키지 생태계들을 살펴 본 결과 사람들이 (“tenderlove의 libxml2” 대신 nokogiri 같이) 더 창의적인 이름들을 쓰는 편이라는 걸 알게 되었습니다. 아무런 계층도 담고 있지 않는다는 것도 있고 해서, 이런 창의적인 이름들은 짧고 기억하기 쉬운 편입니다. 이들은 패키지에 대해 간결하고 모호함 없이 소통하기 쉽게 만들고, 신나는 브랜드를 만들지요. 또한 우리는 NPM이나 RubyGems 같이 수만개의 패키지가 있는 성공적인 생태계에서 커뮤니티가 하나의 이름 공간만으로 번창하는 것 또한 보아 왔습니다.

요약하자면 우리는 Piston이 단순히 piston 대신 bvssvni/game-engine 같은 이름을 선택했다고 해서 (그래서 다른 사용자가 wycats/game-engine을 고를 수 있게 된다 해서) Cargo 생태계가 더 좋아질 거라고 생각하지 않습니다.

이름 공간은 그 자체로 여러 방면에서 복잡하고, 나중에 필요해진다면 호환되는 방법으로 추가할 수 있다는 점에서, 우리는 하나의 공유된 이름 공간을 계속 쓰려고 합니다.

라이브러리

HTTP 요청을 어떻게 보내나요?

표준 라이브러리에는 HTTP 구현이 포함되어 있지 않으므로 외부 크레이트를 사용해야 합니다. 가장 간단하게는 reqwest가 있습니다. hyper를 써서 Rust로 만들어져 있고, 다른 라이브러리들도 여럿 있습니다. curl 크레이트는 curl 라이브러리의 바인딩을 제공하는 널리 쓰이는 라이브러리입니다.

Rust로 GUI 애플리케이션을 작성하려면 어떻게 해야 하나요?

Rust로 GUI 애플리케이션을 만드는 방법은 여럿 있습니다. GUI 프레임워크들의 목록을 참고하세요.

JSON/XML을 파싱하는 방법은 무엇인가요?

Serde는 Rust 데이터를 다른 여러 포맷으로 직렬화(serialize)하고 역직렬화(deserialize)하는데 추천하는 라이브러리입니다.

표준 2차원/3차원/... 벡터 및 도형 크레이트가 있나요?

아직요! 만들어 보실래요?

Rust로 OpenGL 앱을 작성하려면 어떻게 해야 하죠?

Glium는 Rust에서 OpenGL 프로그래밍에 쓰이는 주요 라이브러리입니다. GLFW 또한 괜찮은 대안입니다.

Rust로 비디오 게임을 만들 수 있나요?

가능합니다! Rust로 만들어진 주요 게임 프로그래밍 라이브러리는 Piston이 있으며, Rust 게임 프로그래밍을 위한 서브레딧과 IRC 채널(Mozilla IRC#rust-gamedev 채널)도 있습니다.

디자인 패턴

Rust는 객체 지향적(object-oriented)인가요?

Rust는 여러 패러다임을 지원합니다. 객체 지향 언어에서 할 수 있는 많은 것들은 Rust에서도 할 수 있지만, 전부 가능한 건 아니고, 여러분에게 익숙한 추상화를 사용하지 않을 수도 있습니다.

객체 지향적인 개념을 Rust에 어떻게 대응시키죠?

상황에 따라 다릅니다. 다중 상속과 같은 객체 지향 개념을 Rust로 옮기는 방법은 여럿 있습니다만, Rust는 객체 지향이 아니기에 객체 지향 언어들과는 상당히 다르게 보일 수 있습니다.

선택 인자가 있는 구조체를 설정하는 인터페이스를 어떻게 만들어야 할까요?

가장 쉬운 방법은 구조체 인스턴스를 생성하는 어떤 함수에든 (보통 new()에) Option 타입을 쓰는 겁니다. 또 다른 방법은 빌더(builder) 패턴을 써서, 타입을 생성하기 전에 멤버 변수를 인스턴스화하는 특정 함수들을 호출해야 하도록 하는 것입니다.

Rust에서 전역 변수를 쓰려면 어떻게 하죠?

Rust에서 전역 변수는 컴파일 시간에 계산된 전역 상수라면 const 선언을 쓸 수 있고, 변경 가능한 전역 변수는 static을 쓸 수 있습니다. 다만 static mut 변수를 변경하려면 unsafe가 필요한데, 이는 안전한 Rust에서는 발생하지 않는다고 보장하는 데이터 레이스(data race)가 일어날 수 있기 때문입니다. conststatic 값의 중요한 차이는 static에서는 참조를 얻을 수 있지만 const는 지정된 메모리 위치를 가지지 않기 때문에 불가능하다는 점입니다. conststatic에 대해 더 자세한 정보에 대해서는 《Rust 프로그래밍 언어》를 읽으세요.

절차적으로 정의되는 컴파일 시간 상수는 어떻게 설정하나요?

Rust는 현재 컴파일 시간 상수를 제한적으로 지원합니다. 원시 값을 const 선언으로 정의할 수 있고(static과 비슷하지만, 변경할 수 없고 메모리에서 지정된 위치를 가지지 않습니다), const 함수나 선천적인 메소드도 정의할 수 있습니다.

이 기작으로 선언할 수 없는 명령적인 상수를 선언하려면 lazy-static 크레이트를 사용하세요. 이 크레이트는 컴파일 시간 평가를 상수가 처음 사용될 때 자동으로 평가하는 걸로 흉내냅니다.

`main` 이전에 실행되는 초기화 코드를 만들 수 있나요?

Rust에는 “main 이전의 삶”이라는 개념이 없습니다. lazy-static 크레이트가 가장 가까운 것일텐데, 이 크레이트는 “main보다 이전”이라는 시간을 정적 변수를 처음 사용할 때 지연하여 초기화하는 걸로 흉내냅니다.

Rust에서 상수 수식이 아닌 값을 전역에 넣을 수 있나요?

아니요. 전역 변수는 상수 수식이 아닌 생성자를 가질 수 없고 소멸자를 아예 가질 수 없습니다. 정적 생성자는 정적 초기화 순서를 이식성 있는 방법으로 보장하는 게 어려워서 바람직하지 않습니다. main 이전의 삶은 종종 잘못된 기능으로 꼽히므로, Rust에서는 허용되지 않습니다.

C++ FQA에서 “정적 초기화 순서 사기” 부분과, 이 기능을 가지고 있는 C#에서의 도전을 다루는 Eric Lippert의 블로그도 보세요.

상수 수식이 아닌 전역 변수는 lazy-static 크레이트로 근사할 수 있습니다.

다른 언어들

C의 struct X { static int X; }; 같은 코드를 Rust에서는 어떻게 만드나요?

Rust는 위의 코드 조각에 쓰여진 식의 static 필드가 없습니다. 대신 주어진 모듈에서만 볼 수 있는 static 변수를 선언할 수 있습니다.

C 스타일의 열거형을 정수로 바꾸거나 반대로 하려면 어떻게 하나요?

C 스타일의 열거형은 (e가 열거형일 때) e as i64 같은 식으로 as 수식으로 정수로 바꿀 수 있습니다.

반대로 바꾸려면 match 문장을 써서, 서로 다른 숫자 값들을 열거형의 서로 다른 가능한 값들로 대응시킬 수 있습니다.

왜 Rust 프로그램의 바이너리 크기가 C 프로그램보다 큰 거죠?

Rust 프로그램이 동작이 같은 C 프로그램보다 기본값으로 더 큰 바이너리 크기를 가지는 데 영향을 미치는 여러 요소가 있습니다. 일반적으로 Rust는 작은 프로그램의 크기보다는 현실 프로그램의 성능을 최적화하는 걸 선호합니다.

단형화

Rust는 일반화된 코드를 단형화하는데, 이는 일반화된 함수나 타입이 프로그램에서 쓰인 구체적인 타입마다 새 버전으로 생성된다는 뜻입니다. 이는 C++에서 템플릿이 동작하는 방법과 비슷합니다. 예를 들어 다음 프로그램에서는:

fn foo<T>(t: T) {
    // ... 뭔가를 함
}

fn main() {
    foo(10);       // i32
    foo("hello");  // &str
}

foo의 서로 다른, 하나는 i32 입력으로 특수화되고 다른 하나는 &str 입력으로 특수화된, 두 개의 버전이 최종 바이너리에 들어가게 됩니다. 이는 일반화된 함수의 정적 디스패치를 효율적으로 만들지만 바이너리 크기의 비용을 치루어야 합니다.

디버그 기호

Rust 프로그램은 릴리스 모드일 때도 일부 디버그 기호가 유지된 채로 컴파일됩니다. 이는 패닉시 스택 추적(backtrace)을 제공하는 데 쓰이고, strip이나 다른 기호 제거 도구로 지울 수 있습니다. Cargo에서 릴리스 모드로 컴파일할 경우 rustc에서 최적화 레벨 3을 설정하는 거랑 같다는 것도 지적해야 겠네요. 대안 최적화 레벨(s 또는 z라고 부릅니다)이 최근에 들어 왔으며 이걸로 성능 대신 크기를 최적화해 달라고 컴파일러한테 말할 수 있습니다.

Jemalloc

Rust는 기본 할당자(allocator)로 jemalloc을 쓰기 때문에 컴파일된 Rust 바이너리에 얼마간의 크기가 추가됩니다. Jemalloc은 흔히 쓰이는 시스템에서 제공하는 할당자에 비해 성능 특징이 더 나은 일관되고 질 좋은 할당자라 선택되었습니다. 사용자 정의 할당자를 더 쉽게 쓸 수 있게 하는 작업이 진행 중입니다만 아직 완료되진 않았습니다.

링크 시간 최적화

Rust는 기본값으로 링크 시간 최적화(link-time optimization)를 하지 않지만 이를 하도록 지정할 수 있습니다. 이 최적화는 Rust 컴파일러가 잠재적으로 할 수 있는 최적화의 양을 늘리며, 바이너리 크기에도 작은 영향을 줄 수 있습니다. 앞에서 언급한 크기 최적화 모드와 함께 쓰면 더 큰 효과가 있을 것입니다.

표준 라이브러리

Rust 표준 라이브러리에는 libbacktrace와 libunwind가 들어 가는데 일부 프로그램에서는 바람직하지 않을 수 있습니다. 따라서 #![no_std]를 쓰면 작은 바이너리가 나올 수 있지만, 보통 작성 중인 Rust 코드에 작지 않은 변화가 필요하게 됩니다. Rust를 표준 라이브러리 없이 사용하는 게 종종 동일한 C 코드와 기능적으로 유사하다는 점도 지적해 둡니다.

예를 들어 다음 C 프로그램은 이름을 읽어서 그 이름을 가진 사람한테 “hello”라고 말합니다.

#include <stdio.h>

int main(void) {
    printf("What's your name?\n");
    char input[100] = {0};
    scanf("%s", input);
    printf("Hello %s!\n", input);
    return 0;
}

Rust로 이걸 재작성하면 대략 다음과 같은 게 되는데요:

use std::io;

fn main() {
    println!("What's your name?");
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap();
    println!("Hello {}!", input);
}

이 프로그램을 컴파일해서 C 프로그램과 비교하면 바이너리가 더 크고 더 많은 메모리를 쓸 겁니다. 하지만 이 프로그램은 위 C 코드와 완전히 동일하지 않습니다. 동일한 Rust 코드는 대신 다음과 비슷하게 생겼을 겁니다:

#![feature(lang_items)]
#![feature(libc)]
#![feature(no_std)]
#![feature(start)]
#![no_std]

extern crate libc;

extern "C" {
    fn printf(fmt: *const u8, ...) -> i32;
    fn scanf(fmt: *const u8, ...) -> i32;
}

#[start]
fn start(_argc: isize, _argv: *const *const u8) -> isize {
    unsafe {
        printf(b"What's your name?\n\0".as_ptr());
        let mut input = [0u8; 100];
        scanf(b"%s\0".as_ptr(), &mut input);
        printf(b"Hello %s!\n\0".as_ptr(), &input);
        0
    }
}

#[lang="eh_personality"] extern fn eh_personality() {}
#[lang="panic_fmt"] fn panic_fmt() -> ! { loop {} }
#[lang="stack_exhausted"] extern fn stack_exhausted() {}

실제로 이 코드는 C와 대비해서 메모리 사용량이 비슷하겠지만, 대신 프로그래머에게 더 많은 복잡도를 지우고, Rust가 보통 제공하는 정적인 보장들 또한 없습니다(여기서는 unsafe를 써서 보장을 제거했습니다).

왜 Rust는 C 같이 안정화된 ABI가 없는 건가요? 그리고 왜 `extern`을 달아야 하는 거죠?

ABI에 노력을 투자하는 건 앞으로 가능한, 어쩌면 득이 될 수도 있는 언어 변경을 제한할 수 있는 큰 결정입니다. Rust가 2015년 5월에야 1.0이 되었다는 걸 볼 때 안정된 ABI 같은 큰 투자를 하기에는 아직 너무 이릅니다. 하지만 미래에도 일어나지 않을 거라는 얘기는 아닙니다. (C++가 오랫동안 안정된 ABI를 명시하지 않은 채 유지되긴 했지만요.)

extern 예약어를 쓰면 Rust가 잘 정의된 C ABI 같이 특정한 ABI를 써서 다른 언어와 상호작용하도록 할 수 있습니다.

Rust 코드가 C 코드를 호출할 수 있나요?

네. C 코드를 Rust에서 부르는 것은 C++에서 C 코드를 부르는 것만큼 효율적이도록 설계되었습니다.

C 코드가 Rust 코드를 호출할 수 있나요?

네. Rust 코드가 extern 선언으로 노출되어 C의 ABI와 호환되도록 만들어야 합니다. 이러한 함수는 C 코드에 함수 포인터로 전달되거나, #[no_mangle] 속성으로 기호 꾸미기(symbol mangling)를 껐을 경우, C 코드에서 바로 호출될 수 있습니다.

전 이미 C++를 완벽하게 짤 수 있는데, Rust가 어떤 잇점이 있나요?

현대적인 C++는 안전하고 올바른 코드를 짜기 더 수월하도록 하는 많은 기능들을 가지고 있습니다만, 완벽한 건 아니고 여전히 위험을 불러오기가 쉽습니다. C++ 코어 개발자들은 이 문제를 해결하려 노력하고 있지만, C++는 그들이 지금 구현하려 하는 많은 아이디어에 앞서는 오랜 역사로 제약을 받습니다.

Rust는 첫 날부터 안전한 시스템 프로그래밍 언어로 설계되었으며, 이는 C++를 제대로 안전하게 만들기를 매우 복잡하게 만드는 역사적인 설계 결정들에 제한을 받지 않는다는 뜻입니다. C++에서 안전성은 주의깊은 개인적인 규율로 달성되고 틀리기 매우 쉽습니다. Rust에서 안전성은 기본값입니다. Rust는 여러분보다 덜 완전한 사람들을 포함하는 팀에서, 안전성 버그를 피하려 그들의 코드를 재확인하는 데 시간을 쓸 필요가 없이 함께 일할 수 있도록 합니다.

C++의 템플릿 특수화 같은 걸 Rust에서는 어떻게 할 수 있을까요?

Rust는 현재 템플릿 특수화와 완전히 같은 기능을 가지고 있지 않지만, 현재 작업이 진행 중이며 아마 곧 추가될 것입니다. 다만 연관 타입으로 비슷한 결과를 얻을 수도 있습니다.

Rust의 소유권 시스템이 C++의 "이동" 의미론과 어떻게 연관되나요?

기반 개념은 비슷하지만 실제로는 두 시스템은 굉장히 다르게 동작합니다. 두 시스템 모두에서 값을 “옮기는” 건 기반하는 자원의 소유권을 이전하는 방법입니다. 예를 들어 문자열을 옮긴다면 문자열의 버퍼를 복사하는 대신 이전하기만 할 겁니다.

Rust에서 소유권 이전은 기본 동작입니다. 예를 들어 String을 인자로 받는 함수를 만들었다면, 이 함수는 호출하는 쪽에서 지급한 String 값의 소유권을 가져 갑니다:

fn process(s: String) { }

fn caller() {
    let s = String::from("Hello, world!");
    process(s); // `s`의 소유권을 `process`로 넘김
    process(s); // 오류! 소유권이 이미 이전됨.
}

위 조각에서 볼 수 있듯 caller 함수에서 process의 첫 호출은 변수 s의 소유권을 이전합니다. 컴파일러는 소유권을 추적하고, 따라서 process의 두번째 호출에서는 같은 값의 소유권을 두 번 주는 게 불법이기에 오류가 납니다. Rust는 또한 값에 현재 진행형인 참조가 존재할 경우 값을 옮길 수 없게 할 것입니다.

C++는 다른 접근을 취합니다. C++에서 기본값은 값을 복사(좀 더 정확히는 복사 생성자를 호출)하는 것입니다. 하지만 호출되는 함수가 그 인자를 string&& 같이 “rvalue 참조”로 선언할 수 있으며, 이는 그들이 그 인자가 소유한 일부 자원(이 경우 문자열의 내부 버퍼)의 소유권을 넘겨 받을 거라는 걸 나타냅니다. 이 때 호출하는 함수는 임시 수식을 넘기거나 std::move로 명시적으로 옮겨야 합니다. 따라서 위의 process 함수와 대략적으로 같은 코드는 다음과 같을 것입니다:

void process(string&& s) { }

void caller() {
    string s("Hello, world!");
    process(std::move(s));
    process(std::move(s));
}

C++ 컴파일러는 이동을 추적할 의무가 없습니다. 예를 들어 위 코드는, 적어도 clang의 기본 설정에서는, 경고나 오류를 내지 않고 컴파일됩니다. 게다가 C++에서 (내장 버퍼 말고) s 자신의 소유권은 caller에 남기 때문에, caller가 반환될 때 s가 분명 이동했음에도 소멸자가 불리게 됩니다(반대로 Rust에서 이동된 값은 새 소유권자에 의해서만 소멸됩니다).

C++에서 Rust와 상호작용하거나, Rust에서 C++와 상호작용하려면 어떻게 하나요?

Rust와 C++ 둘 다 C와 상호작용할 수 있습니다. Rust와 C++ 모두 C와 외부 함수 인터페이스를 제공하며, 이를 각자와 소통하기 위해 쓸 수 있습니다. C 바인딩을 만드는 게 너무 지루하다면, 언제나 rust-bindgen을 써서 자동으로 동작하는 C 바인딩을 만드는 데 도움을 받을 수 있습니다.

Rust에는 C++ 같은 생성자가 있나요?

아뇨. 추가적인 언어 복잡도 없이 함수가 생성자와 같은 역할을 수행합니다. Rust에서 생성자에 대응되는 함수의 일반적인 이름은 new()로, 이는 언어 규칙이 아니라 단순한 규약일 따름입니다. new() 함수는 다른 함수랑 다를 바가 없고, 이런 식으로 씁니다:

struct Foo {
    a: i32,
    b: f64,
    c: bool,
}

impl Foo {
    fn new() -> Foo {
        Foo {
            a: 0,
            b: 0.0,
            c: false,
        }
    }
}

Rust에는 복사 생성자가 있나요?

정확히는 아닙니다. Copy를 구현하는 타입은 C랑 비슷하게, 추가 작업 없이 표준적인 “얕은(shallow) 복사”를 하게 됩니다(이는 C++에서 자명하게 복사할 수 있는 타입들과 비슷합니다). 사용자 정의된 복사 동작이 필요한 Copy 타입을 구현하는 건 불가능합니다. 대신 Rust에서 “복사 생성자”는 Clone 트레이트를 구현하여 명시적으로 clone 메소드를 호출하는 걸로 만들어집니다. 사용자 정의된 복사 연산자를 명시적으로 만드는 건 그 아래의 복잡도를 보여 주며, 개발자가 잠재적으로 비싼 연산을 파악하기 더 쉽게 만듭니다.

Rust에는 이동 생성자가 있나요?

아뇨. 모든 타입의 값들은 memcpy로 옮겨집니다. 덕분에 일반적인 안전하지 않은 코드를 짜기 훨씬 간단해지는데, 대입이나 인자를 넘기고 반환하는 과정에서 되감기(unwinding) 같은 부수 효과가 일어날 수 없다는 걸 보장할 수 있기 때문입니다.

Go와 Rust가 비슷한 점은 무엇이고 다른 점은 무엇인가요?

Rust와 Go는 상당히 다른 설계 목표를 가집니다. 전부는 아니지만(다 나열하기에는 많습니다), 다음 차이들이 가장 중요하다고 볼 수 있습니다:

Rust 트레이트를 하스켈 타입 클래스와 비교하면 어떤가요?

Rust 트레이트는 하스켈 타입 클래스와 비슷하지만, Rust가 상류(higher-kinded) 타입을 표현할 수 없기 때문에 덜 강력합니다. Rust의 연관 타입은 하스켈의 타입 무리(type family)와 동일합니다.

하스켈 타입 클래스와 Rust 트레이트 사이에 구체적인 차이로는 이런 게 있습니다:

문서

왜 Stack Overflow의 Rust 답변 중에는 틀린 게 많은가요?

Rust 언어는 여러 해 동안 개발되어 왔으며, 2015년 5월에서야 1.0 버전에 도달했습니다. 그 이전에는 언어가 상당히 많이 바뀌었고 Stack Overflow의 답변 중 많은 것들은 언어가 옛날 버전일 때 작성된 것입니다.

시간이 지날수록 더 많은 답변이 현재 버전을 기준으로 작성되고, 따라서 오래된 답변의 비율이 줄어들 것이므로 이 문제는 자연히 개선될 것입니다.

Rust 문서에 문제를 보고하려면 어디에 하나요?

Rust 문서의 문제는 Rust 컴파일러의 이슈 트래커에 보고할 수 있습니다. 보고에 앞서 먼저 기여 가이드라인을 읽어 주세요.

제 프로젝트가 의존하는 라이브러리의 rustdoc 문서를 어떻게 볼 수 있나요?

cargo doc으로 프로젝트의 문서를 생성할 때는 활성화되어 있는 의존하는 버전들의 문서도 함께 생성됩니다. 이들은 프로젝트의 target/doc 디렉토리에 저장됩니다. cargo doc --open으로 문서가 생성된 뒤에 문서를 열어 보거나, 아니면 직접 target/doc/index.html을 열어 보세요.