4.3. 반환
이 절에서는 함수 본체가 결과 값을 만들어내는 규칙을 정의합니다.
4.3.1. 핵심 규칙: return 은 필수가 아님
함수 본체는 블록 표현식 에서 정의하는 블록이며, 본체 마지막 식의 값이 그대로 함수의 반환값이 됩니다.
fn add(x: i32, y: i32) -> i32 {
x + y // 자동 반환
}
return 키워드는 조기 종료가 필요할 때만 쓰며, 정상 흐름에서 단일 값을 반환할 때는 마지막 식으로 표현하는 것이 관용적인 방식입니다.
4.3.2. 명시적 return
명시적인 반환은 다음과 같은 문법으로 표기합니다.
return_expr ::= "return" expr?
return expr;
return;
return expr; 은 expr 의 값을 반환하면서 함수를 종료시키고, return; 은 값 없이 (즉, unit 을 반환하면서) 함수를 종료시킵니다. 또한 return 식 자체는 divergent 하기 때문에, 그 자리에서 평가 흐름이 함수 밖으로 빠져나가게 됩니다.
fn early(x: i32) -> i32 {
if x < 0 { return 0; } // 조기 종료
x * 2
}
fn validate(s: str) -> bool {
if s.len() == 0 { return false; }
if s.starts_with("#") { return false; }
true
}
4.3.3. 반환 타입과 본체의 일치
함수 선언 에서 정의한 반환 타입은 본체의 모든 반환 경로에서 일치해야 합니다. 여기서 말하는 반환 경로란 본체의 마지막 식 타입과 모든 return expr; 의 expr 타입을 가리킵니다.
이들이 서로 다른 타입이라면 표현식 개요 의 분기 결과 규칙에 따라 합 타입으로 합쳐지며, 그 합 타입이 명시된 반환 타입과 호환되어야 합니다.
fn maybe_int(cond: bool) -> i32 | str {
if cond { return 42i32; }
"no" // 두 경로의 합 = i32 | str, 명시와 일치
}
4.3.4. 반환 타입 생략
함수 선언 에서 살펴본 대로, 반환 타입은 생략할 수 있으며 생략 시에는 본체에서 추론합니다.
fn double(x: i32) {
x * 2 // 추론: i32
}
추론한 타입은 모든 값 경로 (return expr 와 마지막 식) 의 타입이 같으면 그 타입, 다르면 그 합입니다. 값 경로와 값이 없는 경로가 섞이거나, 반환 타입이 자기 자신에 의존하면 컴파일 에러입니다: 재귀 함수에는 -> T 가 필요합니다. 본체의 끝이나 return 에 닿는 경로가 하나도 없으면, 추론한 타입은 diverging 타입입니다 (4.3.7).
4.3.5. 다중 반환: 튜플로 묶기
여러 값을 반환하려면 튜플을 사용하면 됩니다.
fn divmod(n: i32, d: i32) -> (i32, i32) {
let q = n / d;
let r = n % d;
(q, r)
}
let (quotient, remainder) = divmod(10i32, 3i32);
이때 튜플 반환 타입은 타입 표현식 의 튜플 타입을 따르고, 받는 쪽에서는 분해 대입 의 튜플 분해를 활용하면 됩니다.
별도의 다중 반환 키워드나 구문은 도입하지 않으며, 튜플만으로도 충분합니다.
4.3.6. 결과를 폐기하는 호출
함수 결과를 받지 않을 때는 다음과 같이 작성합니다.
compute(); // 결과 폐기 (식 + ;)
_ = compute(); // 의도적 폐기 (명시적)
두 형태 모두 결과를 폐기하지만, _ = ... 쪽은 사용자가 의도적으로 결과를 무시한다는 신호가 됩니다. 도구나 린터가 결과 무시 경고를 두는 경우 _ = ... 형태는 그 경고를 우회하는 acknowledgement 로 작동합니다.
4.3.7. divergent 함수
본체가 절대 정상 종료하지 않는 함수, 즉 항상 return, throw, 무한 루프 등으로 흐름이 빠져나가는 함수도 있습니다.
fn fail_always() throws {
throw ValueError { msg: "fatal" };
}
fn loop_forever() {
loop { }
}
이러한 함수는 반환 타입이 어떤 값도 가질 수 없다는 의미를 갖습니다. 표준 라이브러리에서는 이 자리를 위한 발산 타입 (Rust 의 ! 나 Never 에 해당) 을 제공하며, 정확한 이름은 라이브러리 사양에서 정의됩니다.
divergent 함수의 호출은 블록 표현식 의 divergent 블록과 같이 임의의 타입에 호환됩니다.
let x: i32 = if cond {
42i32
} else {
fail_always() // divergent - i32 자리에서 OK
};
4.3.8. return 의 위치
return 식은 함수 본체 안 어디에든 등장할 수 있습니다. 블록 안, 루프 안, match 가지 안 등 어디에서도 사용할 수 있습니다.
fn find(xs: List<i32>, target: i32) -> i32 | none {
for x in xs {
if x == target { return x; } // 루프 안에서 함수 자체 반환
}
none
}
루프나 match 등 안쪽에 있는 return 은 둘러싸는 함수 자체에서 빠져나가게 됩니다. 만약 루프만 빠져나가고 싶다면 루프 의 break 를 사용하면 됩니다.
4.3.9. 반환값의 평가 시점
return expr; 의 expr 은 return 식이 평가될 때 완전히 평가된 뒤에 함수에서 빠져나갑니다.
fn f() -> i32 {
return compute_a() + compute_b(); // 둘 다 평가 후 합 반환
}
따라서 부작용이 있는 식이 return 안에 있는 경우, 반환 직전에 그 부작용이 수행됩니다.
4.3.10. return 과 try/catch
try 본체 안의 return 도 마찬가지로 둘러싸는 함수에서 빠져나갑니다. 다만 그 과정에서 에러 제어 흐름 의 finally 본체가 있다면 반드시 평가됩니다.
fn cleanup_aware() -> i32 {
try {
return 42i32; // finally 가 먼저 실행됨
} finally {
cleanup();
}
}
위 함수의 경우 cleanup() 을 호출한 뒤에 42i32 를 반환합니다.
4.3.11. return 과 클로저 / 중첩 함수
클로저나 중첩 함수의 return 은 그 클로저나 함수 자체에서 빠져나가는 것이지, 둘러싸는 함수에서 빠져나가는 것이 아닙니다.
fn outer() -> i32 {
let f = |x: i32| -> i32 {
if x < 0 { return 0; } // 이 클로저에서만 빠져나감
x * 2
};
f(-5i32); // 이 호출에서 f 가 0 을 반환
100i32 // outer 자체는 100 반환
}
중첩 함수와 클로저의 정의에 대한 자세한 내용은 클로저 에서 다룹니다.