함수
함수
Rust에서 함수는 fn 키워드로 선언해요. 함수는 블록(본문), 이름, 매개변수 집합, 출력 타입으로 이루어져 있어요. 이름을 제외하면 이 모두가 선택적이에요.
본문
Function ->
FunctionQualifiers fn IDENTIFIER GenericParams?
( FunctionParameters? )
FunctionReturnType? WhereClause?
( BlockExpression | ; )
FunctionQualifiers -> const? async? ItemSafety? ( extern Abi? )?
ItemSafety -> safe | unsafe
Abi -> STRING_LITERAL | RAW_STRING_LITERAL
FunctionParameters ->
SelfParam ,?
| ( SelfParam , )? FunctionParam ( , FunctionParam )* ,?
SelfParam -> OuterAttribute* ( ShorthandSelf | TypedSelf )
ShorthandSelf -> ( & | & Lifetime )? mut? self
TypedSelf -> mut? self : Type
FunctionParam -> OuterAttribute* ( FunctionParamPattern | ... | Type )
FunctionParamPattern -> PatternNoTopAlt : ( Type | ... )
FunctionReturnType -> -> Type
함수는 그 이름을 값 네임스페이스에 정의해요. 즉 위치한 모듈이나 블록의 값 네임스페이스에 그 함수 이름을 등록하죠. 함수는 매개변수로 입력 변수 집합을 선언할 수 있는데, 호출자가 이를 통해 함수에 인자를 전달해요. 그리고 함수가 완료될 때 호출자에게 반환할 값의 출력 타입도 선언할 수 있어요.
출력 타입을 명시하지 않으면 unit 타입이 돼요. 함수를 참조하면 해당 함수의 zero-sized 함수 항목 타입(function item type) 의 일급 값이 되고, 이를 호출하면 함수로의 직접 호출로 평가돼요.
간단한 함수 예시를 볼게요.
#![allow(unused)]
fn main() {
fn answer_to_life_the_universe_and_everything() -> i32 {
return 42;
}
}
safe 함수 한정자는 의미상으로 extern 블록 안에서만 허용돼요.
함수 매개변수
함수 매개변수는 반박 불가능한 패턴(irrefutable pattern) 이에요. 그래서 else 없는 let 바인딩에서 유효한 어떤 패턴이든 매개변수로도 유효해요.
#![allow(unused)]
fn main() {
fn first((value, _): (i32, i32)) -> i32 { value }
}
- 첫 번째 매개변수가
SelfParam이면 그 함수는 메서드(method) 예요.self매개변수를 가진 함수는 트레잇이나 구현(impl)의 연관 함수로만 나타날 수 있어요. ...토큰을 가진 매개변수는 가변 인자 함수(variadic function) 를 나타내며, extern 블록 함수의 마지막 매개변수로만 쓸 수 있어요. 가변 매개변수는args: ...처럼 선택적 식별자를 가질 수 있어요.
함수 본문
함수의 본문 블록은 개념적으로 또 다른 블록으로 감싸져 있어요. 그 블록은 먼저 인자 패턴을 바인딩하고, 그 다음 함수 본문의 값을 반환해요. 즉 블록의 꼬리 표현식이 평가되면 호출자에게 반환되는 거죠. 물론 본문 안에 명시적 return 표현식이 있으면, 도달했을 때 그 암묵적 반환을 단락(short-cut)시켜요.
위 함수는 아래처럼 쓰인 것과 같은 동작을 해요.
// argument_0은 호출자가 넘긴 실제 첫 번째 인자예요
let (value, _) = argument_0;
return {
value
};
본문 블록이 없는 함수는 세미콜론으로 끝나요. 이 형태는 트레잇이나 extern 블록에서만 나타날 수 있어요.
제네릭 함수
제네릭 함수는 서명에 하나 이상의 매개변수화된 타입을 허용해요. 각 타입 매개변수는 함수 이름 뒤의, 꺾쇠괄호로 감싸고 쉼표로 구분한 목록에 명시적으로 선언해야 해요.
#![allow(unused)]
fn main() {
// foo는 A와 B에 대해 제네릭이에요
fn foo<A, B>(x: A, y: B) {
}
}
함수 서명과 본문 안에서 타입 매개변수의 이름을 타입 이름으로 쓸 수 있어요. 타입 매개변수에 트레잇 바운드를 지정하면 그 타입의 값에 대해 해당 트레잇의 메서드를 호출할 수 있어요. 이건 where 구문으로 지정해요.
#![allow(unused)]
fn main() {
use std::fmt::Debug;
fn foo<T>(x: T) where T: Debug {
}
}
제네릭 함수를 참조하면 그 타입은 참조된 컨텍스트에 따라 인스턴스화돼요. 아래에서 foo를 호출하면 타입 매개변수 T가 i32로 인스턴스화돼요.
#![allow(unused)]
fn main() {
use std::fmt::Debug;
fn foo<T>(x: &[T]) where T: Debug {
// 세부 사항 생략
}
foo(&[1, 2]);
}
타입 매개변수는 함수 이름 뒤의 후행 경로 구성 요소로 명시적으로 제공할 수도 있어요. 타입 매개변수를 결정할 충분한 컨텍스트가 없으면 필요할 수 있어요. 예를 들어 mem::size_of::<u32>() == 4처럼요.
extern 함수 한정자
extern 함수 한정자를 쓰면 특정 ABI로 호출할 수 있는 함수 정의를 제공할 수 있어요.
extern "ABI" fn foo() { /* ... */ }
이것은 종종 extern 블록 항목과 함께 사용되는데, extern 블록은 정의 없이 호출할 수 있는 함수 선언을 제공해요.
unsafe extern "ABI" {
unsafe fn foo(); /* 본문 없음 */
safe fn bar(); /* 본문 없음 */
}
unsafe { foo() };
bar();
함수 항목의 FunctionQualifiers에서 extern과 ABI를 생략하면 ABI "Rust"가 지정돼요.
#![allow(unused)]
fn main() {
fn foo() {}
}
위 코드는 아래와 동등해요.
#![allow(unused)]
fn main() {
extern "Rust" fn foo() {}
}
함수는 외부 코드로부터 호출될 수 있어요. Rust와 다른 ABI를 사용하면 예를 들어 C 같은 다른 프로그래밍 언어에서 호출할 수 있는 함수를 제공할 수 있어요.
#![allow(unused)]
fn main() {
// "C" ABI를 가진 함수 선언
extern "C" fn new_i32() -> i32 { 0 }
// "stdcall" ABI를 가진 함수 선언
#[cfg(any(windows, target_arch = "x86"))]
extern "stdcall" fn new_i32_stdcall() -> i32 { 0 }
}
extern 블록에서처럼, extern 키워드를 쓰고 ABI를 생략하면 기본 ABI는 "C" 라는 점도 기억해요. 즉 아래 코드는 동등해요.
#![allow(unused)]
fn main() {
extern fn new_i32() -> i32 { 0 }
let fptr: extern fn() -> i32 = new_i32;
}
#![allow(unused)]
fn main() {
extern "C" fn new_i32() -> i32 { 0 }
let fptr: extern "C" fn() -> i32 = new_i32;
}
언와인딩(Unwinding)
대부분의 ABI 문자열은 두 가지 변형이 있어요. 하나는 -unwind 접미사가 있는 것, 하나는 없는 것이에요. Rust ABI는 항상 언와인딩을 허용하므로 Rust-unwind ABI는 없어요. ABI 선택과 런타임 패닉 핸들러를 합쳐서, 함수 밖으로 언와인딩할 때의 동작이 결정돼요.
아래 표는 각 ABI 경계(해당 ABI 문자열을 쓰는 함수 선언 또는 정의)에 도달하는 언와인딩 연산의 동작을 나타내요. Rust 런타임은 다른 언어의 런타임 안에서 전적으로 일어나는 언와인딩(즉 Rust ABI 경계에 도달하지 않고 던지고 잡히는 언와인딩)에는 영향받지도, 영향을 줄 수도 없어요.
panic-unwind 열은 panic! 매크로와 유사한 표준 라이브러리 메커니즘을 통한 패닉, 그리고 배열 범위를 벗어난 인덱싱이나 정수 오버플로우 같은 Rust 연산으로 인한 패닉을 가리켜요.
"unwinding" ABI 범주는 "Rust"(extern으로 표시되지 않은 Rust 함수의 암묵적 ABI), "C-unwind", 그리고 이름에 -unwind가 있는 다른 모든 ABI를 가리켜요. "non-unwinding" ABI 범주는 "C"와 "stdcall"을 포함한 그 외 모든 ABI 문자열을 가리켜요.
네이티브 언와인딩은 타깃별로 정의돼요. C++ 예외를 던지고 잡는 것을 지원하는 타깃에서는 이 기능을 구현하는 메커니즘을 가리켜요. 일부 플랫폼은 "강제 언와인딩(forced unwinding)"이라는 형태를 구현하는데, Windows의 longjmp와 glibc의 pthread_exit가 이런 식으로 구현돼요. 강제 언와인딩은 표의 "Native unwind" 열에서 명시적으로 제외돼요.
| panic runtime | ABI | panic-unwind | Native unwind (unforced) |
|---|---|---|---|
panic=unwind |
unwinding | unwind | unwind |
panic=unwind |
non-unwinding | abort (아래 주석 참조) | undefined behavior |
panic=abort |
unwinding | panic aborts without unwinding | abort |
panic=abort |
non-unwinding | panic aborts without unwinding | undefined behavior |
panic=unwind에서, non-unwinding ABI 경계에 의해 패닉이 abort로 전환되면, 소멸자(Drop 호출)가 하나도 실행되지 않거나, ABI 경계까지의 모든 소멸자가 실행돼요. 이 두 동작 중 어느 쪽이 일어날지는 명시되어 있지 않아요.
FFI 경계를 가로지르는 언와인딩의 다른 고려 사항과 제한 사항은 Panic 문서의 관련 섹션을 참고해요.
const 함수
const 함수의 정의는 const functions 문서를 참고해요.
async 함수
함수는 async로 한정할 수 있고, 이를 unsafe 한정자와 결합할 수도 있어요.
#![allow(unused)]
fn main() {
async fn regular_example() { }
async unsafe fn unsafe_example() { }
}
async 함수는 호출했을 때 아무 작업도 하지 않아요. 대신 인자를 future로 캡처해요. 그 future가 폴링(poll)되면 함수의 본문이 실행되죠. async 함수는 대략 impl Future를 반환하고 본문이 async move 블록인 함수와 동등해요.
#![allow(unused)]
fn main() {
// 소스
async fn example(x: &str) -> usize {
x.len()
}
}
대략 아래와 동등해요.
#![allow(unused)]
fn main() {
use std::future::Future;
// 디슈가링된 버전
fn example<'a>(x: &'a str) -> impl Future<Output = usize> + 'a {
async move { x.len() }
}
}
실제 디슈가링은 더 복잡해요.
- 디슈가링된 반환 타입은 async fn 선언의 모든 라이프타임 매개변수를 캡처하는 것으로 가정돼요. 위 디슈가링 예시에서 명시적으로
'a를 아웃라이브(따라서 캡처)하는 게 보이죠. - 본문의
async move블록은 사용되지 않거나_패턴에 바인딩된 것을 포함해 모든 함수 매개변수를 캡처해요. 이렇게 하면 함수가 async가 아니었을 때와 같은 순서로 매개변수가 드롭되는데, 다만 반환된 future가 완전히 await된 시점에 드롭돼요.
자세한 내용은 async blocks 문서를 참고해요. async 함수는 Rust 2018부터 사용할 수 있어요.
async와 unsafe 결합
async이면서 unsafe인 함수를 선언하는 것은 합법이에요. 결과 함수는 호출하기에 unsafe하고, (다른 async 함수처럼) future를 반환해요. 이 future는 그냥 평범한 future라서, "await"하는 데 unsafe 컨텍스트가 필요하지 않아요.
#![allow(unused)]
fn main() {
// await됐을 때 `x`를 역참조하는 future를 반환해요.
//
// 건전성(soundness) 조건: 결과 future가 완료될 때까지
// `x`를 역참조해도 안전해야 해요.
async unsafe fn unsafe_example(x: *const i32) -> i32 {
*x
}
async fn safe_example() {
// 처음 호출하려면 `unsafe` 블록이 필요해요:
let p = 22;
let future = unsafe { unsafe_example(&p) };
// 하지만 여기서는 `unsafe` 블록이 필요 없어요. 이 코드는
// `p`의 값을 읽어요:
let q = future.await;
}
}
이 동작은 impl Future를 반환하는 함수로의 디슈가링 결과예요. 이 경우 디슈가링 결과 함수는 unsafe 함수지만 반환 값은 동일해요.
async 함수에서 unsafe는 다른 함수에서와 똑같이 쓰여요. 즉 함수가 건전성을 보장하기 위해 호출자에게 추가적인 의무를 부과한다는 뜻이에요. 위에서 unsafe_example는 포인터 x를 인자로 받고 await 시 역참조했죠. 이는 x가 future 실행을 마칠 때까지 유효해야 한다는 뜻이고, 그걸 보장할 책임은 호출자에게 있어요.
함수의 속성
함수에는 외부 속성(outer attribute)이 허용돼요. 내부 속성은 본문 블록 안의 { 바로 다음에 허용돼요. 아래 예시는 함수의 내부 속성을 보여줘요. 이 함수는 "Example"이라는 단어로만 문서화되어 있어요.
#![allow(unused)]
fn main() {
fn documented() {
#![doc = "Example"]
}
}
린트를 제외하면, 함수 항목에는 외부 속성만 쓰는 것이 관용적이에요. 함수에서 의미를 갖는 속성은 다음과 같아요: cfg_attr, cfg, cold, deprecated, doc, export_name, inline, link_section, must_use, no_mangle, 린트 검사 속성, 프로시저 매크로 속성, 테스트 속성.
함수 매개변수의 속성
함수 매개변수에는 외부 속성이 허용되며, 허용되는 내장 속성은 cfg, cfg_attr, allow, warn, deny, forbid로 제한돼요.
#![allow(unused)]
fn main() {
fn len(
#[cfg(windows)] slice: &[u16],
#[cfg(not(windows))] slice: &[u8],
) -> usize {
slice.len()
}
}
항목에 적용된 프로시저 매크로 속성이 사용하는 불활성(inert) 도우미 속성도 허용되지만, 그런 불활성 속성을 최종 TokenStream에 포함하지 않도록 주의해야 해요. 아래 코드는 어디에도 공식 정의되지 않은 some_inert_attribute라는 불활성 속성을 정의하고, some_proc_macro_attribute 프로시저 매크로가 그 존재를 감지해 출력 토큰 스트림에서 제거할 책임을 진다는 예시예요.
#[some_proc_macro_attribute]
fn foo_oof(#[some_inert_attribute] arg: u8) {
}
더 알아보기 (Learn more)
- Rust Reference - Items — 항목 일반.
- Rust Reference - Traits — 트레잇 내 연관 함수.
- Rust Reference - async blocks — async 동작 상세.