[RUST] 러스트 프로그래밍 공식 가이드(제2판) 19장 요약
고급 기능
안전하지 않은 러스트
안전하지 않은 슈퍼파워
- 안전하지 않은 러스트로 전환하려면 unsafe 키워드를 사용해 다음 새 블록을 시작하여 안전하지 않은 코드를 집어넣음
- 안전하지 않은 슈퍼파워라고 불리는 5가지 작업
- 원시 포인터 역참조하기
- 안전하지 않은 함수 혹은 메서드 호출하기
- 가변 정적 변수에 접근하기 및 수정하기
- 안전하지 않은 트레이트 구현하기
- union의 필드 접근하기
- unsafe 키워드는 대여 검사기를 끄거나 러스트의 다른 안정성 검사를 비활성화 하는 것이 아니라 컴파일러가 메모리 안정성을 검사하지 않는 5가지 기능 허용만 제공
원시 포인터 역참조하기
- 원시포인터란, 참조자와 마찬가지로 불변 또는 가변이며 각각
*const T와*mut T로 작성됨 (*는 역참조 연산자가 아니라 타입 이름의 일부) - 원시 포인터의 맥락에서 불변이란 포인터가 역참조된 후에 직접 할당할 수 없음을 의미
- 원시 포인터의 특징
- 원시 포인터는 대여 규칙을 무시할 수 있으며, 같은 위치에 대해 불변과 가변 포인터를 동시에 가질 수 있거나 여러 개의 가변 포인터를 가질 수 있음
- 원시 포인터는 유효한 메모리를 가리키는 것을 보장받지 못함
- 원시 포인터는 널(null)이 될 수 있음
- 원시 포인터는 자동 메모리 정리를 구현하지 않음
- 이러한 보증을 적용하지 않도록 선택하면, 안정성을 포기하는 대신 더 많은 기능이나 성능을 얻을 수 있음
- 원시 포인터 사용 사례
- C 언어 코드와 상호작용할 때
- 대여 검사기가 이해하지 못하는 안전한 추상화를 구축할 때
안전하지 않은 함수 또는 메서드 호출하기
- 안전하지 않은 함수와 메서드는 정의 앞에 unsafe 키워드가 추가되며, 함수를 호출할 때 지켜야 할 요구 사항이 있음을 나타냄
- unsafe 블록 내에서 안전하지 않은 함수를 호출한다는 것은 해당 함수의 설명서를 읽었고, 함수를 올바르게 사용하는 방법을 이해하며, 함수의 계약서를 이행하고 있음을 확인했다고 러스트에게 단언하는 것과 같음
extern 함수를 사용하여 외부 코드 호출하기
- 러스트에서 다른 언어로 작성된 코드와 상호작용해야 할 경우 FFI(Foreign function interface)의 생성과 사용을 용이하게 하는 키워드 extern 을 사용함
- FFI는 프로그래밍 언어가 함수를 정의하고 다른 외래 프로그래밍 언어가 해당 함수를 호출할 수 있도록 하는 방법
- 사용 예시
1 2 3 4
extern "C" { // 외부 함수가 사용하는 ABI(application binary interface)를 정의 // 블록 내부에는 외부함수의 이름과 시그니처 나열 fn abs(input: i32) -> i32; }
- external 블록 내에 선언된 함수는 러스트 코드에서 호출되므로 안전하지않음
- 다른 언어는 러스트의 규칙과 보증을 강제하지 않으니 러스트가 이를 확인할 수 없고, 안전을 보장할 책임을 모두 프로그래머가 져야 하기 때문
가변 정적 변수의 접근 혹은 수정하기
- 러스트에서 전역 변수는 정적 변수(static variable)라고 불리며, 정적 변수의 이름은 관례적으로 SCREAMING_SNAKE_CASE를 사용
- 정적 변수는
'static라이프타임을 가진 참조자만 저장할 수 있으며, 이는 러스트 컴파일러가 라이프타임을 알아낼 수 있으므로 명시할 필요가 없음을 의미함 - 정적 변수는 가변일 수 있으며, 가변 정적 변수에 접근하고 수정하는 것은 안전하지 않음 (여러 스레트에서 가변 정적 변수에 수정할 수 있기 떄문에 안전하지 않은 것으로 간주)
안전하지 않은 트레이트 구현하기
- unsafe를 사용하여 안전하지 않은 트레이트를 구현할 수 있으며, 메서드 중 하나 이상에 컴파일러가 확인할 수 없는 불변성이 있는 경우 그 트레이트는 안전하지 않음
- trait 앞에 unsafe 키워드를 추가하고 그 트레이트의 구현체도 unsafe로 표시함으로써 트레이트가 안전하지 않다고 선언
- unsafe impl 을 사용하면 컴파일러가 확인할 수 없는 불변성은 우리가 지키겠다고 약속을 하는 것
유니언 필드에 접근하기
- union은 struct와 유사하지만, 특정 인스턴스에서 한 번에 하나의 선언된 필드만 사용됨
- 유니언은 주로 C 코드의 유니언과 상호작용하는 데 사용됨
- 러스트는 현재 유니언 인스턴스에 저장된 데이터 타입을 보장할 수 없기 때문에, 유니언 필드에 접근하는 것은 안전하지 않음
고급 트레이트
연관 타입으로 트레이트 정의에서 자리표시자 타입 지정하기
- 연관타입은 타입 자리표시자와 트레이트를 연결하여 트레이트 메서드 정의를 할 때 이러한 자리표시자 타입을 시그니처에서 사용할 수 있도록 함
- 트레이트의 구현자는 특정 구현을 위해서 자리표시자 타입 대신 사용할 구체적인 타입을 지정
- 연관타입을 사용하면 트레이트가 구현될 때까지 해당 타입이 무엇인지 정확히 알 필요 없이 임의의 타입을 사용하는 트레이트를 정의할 수 있음
- 연관 타입과 제네릭의 차이점
- 제네릭: 트레잇의 타입 파라미터를 사용자(호출자) 가 결정
- 연관타입: 트레잇을 구현하는 쪽(implementor) 이 그 트레잇이 사용하는 내부 타입을 결정
- 연관 타입을 사용하면 타입에 트레이트를 여러 번 구현할 수 없기 때문에 타입 명시를 할 필요도 사라짐
기본 제네릭 타입 매개변수와 연산자 오버로딩
- 트레이트에 제네릭 타입 매개변수를 사용하면 제네릭 타입에 대한 기본 구체적 타입을 지정할 수 있음
- 이렇게 하면 기본 타입이 작동하는 경우 트레이트의 구현자가 구체적 타입을 지정할 필요가 없음
- 제네릭 타입을 선언할 때 <PlaceholderType=ConcreteType> 문법을 사용하여 기본 타입을 지정
- 기본 제네릭 타입 매개변수가 유용한 경우 중 좋은 예로는 특정상황에서 연산자의 동작을 커스터마이징 하는 연산자 오버로딩과 함께 쓰이는 경우
- 러스트에서는 자체 연산자를 만들거나 임의의 연산자를 오버로딩 할 수 없고, std::ops에 나열된 연산자와 연관된 트레이트를 구현하여 연산자 및 해당 트레이트를 오버로딩할 수 있음
모호성 방지를 위한 완전 정규화 문법: 같은 이름의 메서드 호출하기
- 러스트에서는 어떤 트레이트에 다른 투르에티의 메서드와 같은 이름의 메서드가 있는 것을 막지 않으며, 한 타입에서 두 트레이트를 모두 구현하는 것도 막지 않음 (트레이트의 메서드와 이름이 같은 메서드를 타입에 직접 구현하는 것도 가능)
- 같은 이름의 메서드를 호출할 때는 메서드 이름 앞에 트레이트 이름을 지정하여 어떤 메서드를 사용할지 러스트에 알려줘야 함
- 어떠한 같은 이름의 메서드가 self 매개변수를 취하고 하나의 트레이트를 구현하는 여러개의 타입이 있다면 러스트는 self의 타입에 따라 어떤 트레이트의 구현체를 사용하려는 것인지 알아낼 수 있음
- 연관 함수는 self 매개변수가 없어 동일한 함수명을 가진 함수가 정의된 여러 타입 또는 트레이트가 있는 경우, 완전 정규화 문법(fully qualified syntax)을 사용하지 않는 한 러스트는 어떤 타입을 의미하는지 항상 알 수 없음
- 일반적으로 완전 정규화 문법은
<Type as Trait>::function(receiver_if_method, next_arg, ...);과 같이 사용되며 연괌함수의 경우 receiver가 없고 Type 타입을 Trait 로 취급하고 싶다고 알려줌으로써 특정 타입에 구현된 트레이트의 메서드를 호출하고 싶음을 나타냄 (완전 정규화 문법에서 프로그램의 다른 정보로부터 러스트가 알아낼 수 있는 부분은 생략할 수 있음)
슈퍼트레이트를 사용하여 한 트레이트에서 다른 트레이트의 기능을 요구하기
- 어떤 타입이 첫 번째 트레이트를 구현하려면 해당 타입이 두 번째 트레이트도 구현하도록 요구할 수 있는데 이렇게 하면 트레이트 정의가 두 번째 트레이트의 연관 아이템을 활용할 수 있음
- 트레이트의 정의가 의존하고 있는 트레이트를 트레이트의 슈퍼트레이트(supertrait)라고 함
- 예를들어 Display를 구현하고 OutlinePrint가 요구하는 기능을 제공하는 타입에 대해서만 OutlinePrint 트레이트가 작동할 것임을 명시하려면 트레이트 정의에서 OutlinePrint: Display를 지정해야함 (
trait OutlinePrint: fmt::Display {...})
뉴타입 패턴을 사용하여 외부 타입에 외부 트레이트 구현하기
- 러스트에는 트레이트나 타입이 우리 크레이트의 것인 경우에만 타입에 트레이트를 구현할 수 있다는 고아 규칙이 있었는데, 이는 튜플 구조체로 새로운 타입을 생성하는 뉴타입 패턴을 사용하면 이 제한을 우회할 수 있음
- 뉴타입 패턴에서는 튜플 구조체는 하나의 필드를 가지며 트레이트를 구현하고자 하는 타입을 얇게 감싸는 래퍼가 되는데 래퍼 타입은 우리 크레이트 내에 있게 되어 래퍼에 대한 트레이트를 구현할 수 있음 (예를 들어 Vec
에 대해 Display를 구현하고 싶다면 Vec 의 인스턴스를 보유하는 Wrapper 구조체를 만들 수 있음)
고급 타입
타입 안정성과 추상화를 위한 뉴타입 패턴 사용하기
- 뉴타입 패턴은 어떤 타입의 구현 세부 사항을 추상화하는 데에도 사용 가능하며 비공개 내부 타입의 API와 다른 공개 API를 노출할 수 있음
- 뉴타입 패턴은 구현 세부 사항을 숨기는 캡슐화를 달성하는 가벼운 방법
타입 별칭으로 타입의 동의어 만들기
- 러스트는 타입 별칭을 선언하여 기존 타입에 다른 이름을 부여하는 기능을 제공하며,
type 별칭 = 기존 타입과 같이 생성함 - 별칭은 기존타입의 동의어이고, 동일한 타입으로 처리됨
- 타입 동의어의 주요 사용 사례는 반복을 줄이는 것
절대 반환하지 않는 부정 타입
- 러스트에는 !라는 특수한 타입이 있는데, 이 타입은 값이 없기 때문에 타입 이론 용어로는 빈 타입(empty type)이라고 알려져 있으며 함수가 절대 반환하지 않을 때 반환 타입을 대신하기 때문에 부정 타입(never type)이라고 부르는 쪽이 선호됨
- 러스트에서 절대로 반환하지 않는 함수에 대해 발산 함수(diverging functions)라고 함
동적 크기 타입과 Sized 트레이트
- 동적 크기 타입(Dynamically Sized Type)은 DST 또는 크기가 지정되지 않은 타입(unsized type) 이라고도 하며 이러한 타입을 사용하면 런타임에만 크기를 알 수 있는 값을 사용하여 코드를 작성할 수 있음
- 모든 트레이트는 그 트레이트의 이름을 사용하여 참조할 수 있는 동적 크기 타입
- DST로 작업하기 위해 러스트에서는 컴파일 시점에 타입의 크기를 알 수 있는지 여부를 결정하는 Sized 트레이트를 제공
- Sized 트레이트는 컴파일 시 크기가 알려진 모든 것에 대해 자동으로 구현됨
- 기본적으로 제네릭 함수는 컴파일 시점에 크기가 알려진 타입에 대해서만 작동하기 때문에 러스트는 암묵적으로 모든 제네릭 함수에 Sized 바운드를 추가함
고급 함수와 클로저
함수 포인터
- 함수는 fn 타입으로 강제되며, fn 타입을 함수 포인터라고 함
- 클로저와 달리 fn은 트레이트가 아닌 타입이므로, Fn 트레이트 중 하나를 트레이트 바운드로 사용한 제네릭 타입 매개변수를 선언하는 대신에 fn을 매개변수 타입으로 직접 지정함
- 함수 포인터는 세 가지 클로저 트레이트(Fn, FnMut, FnOnce)를 모두 구현하므로, 클로저를 기대하는 함수에 대한 인수로 함수 포인터를 언제나 전달할 수 있음
- 제네릭 타입과 클로저 트레이트 중 하나를 사용하는 함수를 작성하여 함수나 클로저 중 하나를 받아들일 수 있도록 하는 것이 가장 좋음
클로저 반환하기
- 클로저는 트레이트로(Fn, FnMut, FnOnce) 표현되므로 크기가 고정되어 있지 않음
- 함수는 크기를 알 수 없는 타입을 직접 반환할 수 없기 때문에 컴파일 에러가 발생
- 트레이트 객체를 사용하면 크기가 고정되지 않은 클로저를 포인터 형태로 감싸 반환할 수 있음 (클로저의 크기를 알 수 없기 때문에, 이를 Box로 힙에 저장하고 고정 크기의 포인터(
Box<dyn Fn(...)>)로 반환한다는 의미)
매크로
매크로와 함수의 차이
| 구분 | 함수 (Function) | 매크로 (Macro) | | —————– | —————————————– | ———————————————— | | 정의 목적 | 특정 동작을 수행하는 코드를 재사용하기 위해 사용 | 다른 코드를 생성하는 코드를 작성하기 위해 사용 (메타프로그래밍) | | 확장 시점 | 런타임(run time) — 프로그램이 실행될 때 호출됨 | 컴파일 타임(compile time) — 컴파일러가 코드를 해석하기 전에 확장됨 | | 매개변수 처리 | 매개변수의 개수와 타입이 명확히 선언되어야 함 | 가변적인 인수 개수를 받을 수 있음 (예: println!, vec!) | | 트레이트 구현 가능 여부 | 함수는 트레이트를 구현할 수 없음 | 매크로는 트레이트 구현 코드도 생성 가능 | | 작성 난이도 | 상대적으로 간단하고 읽기 쉬움 | 러스트 코드를 생성하는 러스트 코드이므로 복잡하고 유지보수 어려움 | | 스코프 규칙 | 어디서나 정의 및 호출 가능 | 사용하기 전에 반드시 스코프로 가져와야 함 (use 필요) | | 대표 예시 | fn add(a: i32, b: i32) -> i32 { a + b } | println!(), vec![], derive 매크로 등 |
일반적인 메타프로그래밍을 위한 macro_rules!를 사용한 선언적 매크로
- 선언적 매크로는 ‘macro_rules! 매크로’ 또는 그냥 ‘매크로’ 라고도 불리며, 선언적 매크로의 핵심은 러스트 match 표현식과 비슷한 무언가를 작성할 수 있다는 것
- 선언적 매크로는 match 표현식과 유사하게 특정 코드와 연관된 패턴과 값을 비교함
- 값은 매크로에 전달된 리터럴 러스트 소스 코드이고, 패턴은 해당 소스코드의 구조와 비교되고, 매칭되면 매크로에 전달된 코드는 해당 패턴과 연관된 코드로 대체됨 (이 모든 과정은 컴파일 중에 이루어짐)
속성에서 코드를 생성하기 위한 절차적 매크로
- 절차적 매크로는 어떤 코드를 입력으로 받아서 해당 코드에 대해 작업을 수행한 다음 어떤 코드를 출력으로 생성함
- 절차적 매크로는 커스텀 derive(custom derive), 속성형(attribute-like), 함수형(function-like) 세 종류가 있으며, 모두 비슷한 방식으로 작동함
- 절차적 매크로를 만들 떄, 그 정의는 특별한 크레이트 타입을 가진 자신만의 크레이트에 있어야 함
- 절차적 매크로 함수는 입력과 출력으로 TokenStream 타입을 사용하며, 이는 소스 코드의 토큰 시퀀스를 나타냄
- 매크로 함수에는 어떤 종류의 절차적 매크로인지 지정하는 속성이 붙음
- 같은 크레이트에는 여러 종류의 절차적 매크로를 넣을 수 있음
커스텀 파생 매크로 작성 방법
- 커스텀 파생 매크로는 사용자가 타입에 #[derive(TraitName)]를 붙이면 해당 트레이트 구현 코드를 자동으로 생성해 주는 절차적 매크로
- 트레이트의 기본 구현을 생성함으로써, 사용자가 직접 트레이트를 구현하지 않아도 됨
- 매크로는 컴파일 타임에 타입 이름 등 정보를 활용하여 코드를 생성하며, 리플렉션 없이도 타입별 동작을 제공할 수 있음
- 커스텀 파생 매크로는 일반적으로 별도의 크레이트(예: hello_macro_derive)로 정의하며, proc_macro 속성을 사용
- 매크로 구현 시 TokenStream을 입력으로 받아, syn 크레이트로 파싱하고 quote 크레이트로 코드를 생성하여 TokenStream으로 반환
- quote! 매크로를 사용하면 변수를 코드 내에 삽입할 수 있고, stringify!로 타입 이름 등을 문자열로 변환할 수 있음
속성형 매크로
- 속성형 매크로는 커스텀 파생 매크로와 비슷하지만, derive 속성에 대한 코드를 생성하는 대신 새 속성을 생성할 수 있음
- 속성형 매크로는 구조체/열거형뿐 아니라 함수 등 다른 아이템에도 적용 가능한 절차적 매크로
- 매크로 함수는 두 개의 TokenStream 매개변수를 받으며, 하나는 속성의 내용, 다른 하나는 속성이 붙은 아이템 본문
- 속성형 매크로도 커스텀 파생 매크로처럼 코드를 조작하여 새로운 코드를 생성할 수 있음
함수형 매크로
- 함수형 매크로는 함수처럼 괄호로 호출하며, 임의 개수의 인수를 받아 처리할 수 있는 절차적 매크로
- 매크로는 입력 TokenStream을 받아 원하는 코드를 생성하여 반환하며, macro_rules!보다 더 유연한 처리가 가능
- 함수형 매크로의 예로는 다음과 같이 호출할 수도 있는 sql! 매크로가 있음 (
let sql = sql!(SELECT * FROM posts WHERE id=1);)- sql! 매크로는 내부에 있는 SQL 문을 파싱하고 문법적으로 올바른지 확인함
This post is licensed under CC BY 4.0 by the author.