함수
함수의 매개변수와 반환 타입을 선언하고 구문과 표현식을 구분해 블록의 마지막 값으로 결과를 반환합니다.
러스트 코드는 온통 함수로 가득합니다.
여러분은 이미 이 언어에서 가장 중요한 함수 중 하나를 보셨습니다.
바로 많은 프로그램의 시작점인 main 함수를 말이죠.
또한 새로운 함수를 선언하도록 해주는 fn 키워드도 보셨습니다.
러스트 코드는 함수나 변수 이름을 위한 관례로 스네이크 케이스(snake case) 방식을 이용하는데, 이는 모든 글자를 소문자로 쓰고 밑줄(underscore)로 단어를 구분하는 방식을 말합니다.
다음은 예시로 함수를 정의한 프로그램입니다.
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
러스트에서는 fn 뒤에 함수 이름과 괄호를 붙여서 함수를 정의합니다.
중괄호는 함수 본문의 시작과 끝을 컴파일러에게 알려줍니다.
함수의 이름 뒤에 괄호 묶음을 쓰면 우리가 정의해 둔 어떤 함수든 호출할 수 있습니다.
another_function이 프로그램 내에 정의되어 있으므로, main 함수에서 해당 함수를 호출할 수 있습니다.
소스 코드 내에서 another_function이 main 함수 이후에 정의되어 있다는 점을 주목하세요.
이 함수를 main 함수 앞에서 정의할 수도 있습니다.
러스트는 여러분의 함수 위치를 고려하지 않으며, 호출하는 쪽에서 볼 수 있는 스코프 어딘가에 정의만 되어있으면 됩니다.
함수에 대해 좀 더 알아보기 위해서 functions라는 이름의 새 바이너리 프로젝트를 시작해봅시다.
another_function 예제를 src/main.rs 에 넣고 실행해 보면 다음과 같은 결과를 보게 될 것입니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
main 함수 안의 내용이 순서대로 수행됩니다.
먼저 Hello, world! 메시지가 출력된 다음, another_function이 호출되고 그 안의 메시지가 출력됩니다.
매개변수
함수는 매개변수(parameter) 를 갖도록 정의될 수 있으며, 이는 함수 시그니처(function signature)의 일부인 특별한 변수입니다.
함수가 매개변수를 갖고 있으면 이 매개변수에 대한 구체적인 값을 전달할 수 있습니다.
엄밀하게는 이러한 구체적인 값을 인수(argument) 라고 부르지만, 일상적인 대화에서는 보통 함수 정의부 내의 변수나 함수를 호출할 때 전달되는 구체적인 값에 대해 매개변수와 인수라는 용어를 혼용하는 경향이 있습니다.
아래의 another_function에는 매개변수가 추가되었습니다.
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}
이 프로그램을 실행하면 다음과 같은 결과를 볼 수 있습니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
another_function 선언을 보면 x라는 이름의 매개변수를 하나 가지고 있습니다.
x의 타입은 i32로 명시되어 있습니다.
5가 another_function으로 전달되면, println! 매크로는 포맷 문자열 내 중괄호 쌍의 위치에 5를 집어넣습니다.
함수 시그니처에서는 각 매개변수의 타입을 반드시 선언해야 합니다.
이는 러스트를 설계하면서 신중하게 내린 결정 사항입니다.
함수의 정의에 타입 명시를 강제하면 이 함수를 다른 코드에서 사용할 때 여러분이 의도한 타입을 컴파일러가 추측하지 않아도 되게 됩니다.
컴파일러는 또한 함수가 기대한 타입이 무엇인지 알고 있으면 더욱 유용한 에러 메시지를 제공할 수 있습니다.
여러 매개변수를 정의하려면 아래처럼 쉼표 기호로 매개변수 정의를 구분하세요.
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}
이 예제에서는 두 개의 매개변수를 갖는 함수 print_labeled_measurement를 생성합니다.
첫 번째 매개변수 이름은 value이고 i32 타입입니다.
두 번째는 unit_label이라는 이름이고 char 타입입니다.
이 함수는 value와 unit_label을 담고 있는 텍스트를 출력합니다.
한번 코드를 실행해봅시다.
여러분의 function 프로젝트의 src/main.rs 내용을 위의 예제로 변경한 뒤에, cargo run으로 실행시키면 됩니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
value에는 5를, unit_label에는 h를 넣어 함수를 호출했으므로,
이 값들이 프로그램의 출력에 들어갔습니다.
구문과 표현식
함수 본문 블록은 0개 이상의 구문(statement) 뒤에 선택적으로 마지막 표현식(expression)을 둘 수 있습니다.
지금까지 우리가 다룬 함수들은 표현식으로 끝나지 않았지만, 표현식이 구문의 일부분으로 쓰인 건 보셨습니다.
러스트는 표현식 기반의 언어이므로, 구문과 표현식의 구분은 러스트를 이해하는데 중요합니다.
다른 언어들은 이런 구분이 없으므로, 구문과 표현식이 무엇이며
둘 간의 차이가 함수의 본문에 어떤 영향을 주는지 살펴보겠습니다.
- 구문은 블록 안에서 선언이나 표현식 평가를 순서대로 배치합니다. 표현식 구문은 표현식을 평가한 뒤 그 결과를 사용하지 않습니다.
- 표현식은 값으로 평가되며 다른 표현식 안에 중첩될 수 있습니다. 몇 가지 예제를 살펴봅시다.
우리는 실제로는 이미 구문과 표현식을 사용해 봤습니다.
let 키워드로 변수를 만들고 값을 할당하는 것은 구문입니다.
예제 2-1의 let y = 6;은 구문입니다.
예제 2-1: 구문 하나로 되어있는 main 함수
fn main() {
let y = 6;
}
함수 정의는 아이템(item)입니다. 아이템을 블록 안에 선언하면 아이템 선언 구문이 되지만, 위의 최상위 main 정의 자체는 크레이트의 아이템입니다.
따라서 아래와 같이 let 구문을 다른 변수에 할당하려고 하면 에러가 납니다.
fn main() {
let x = (let y = 6);
}
이 프로그램을 실행하면 다음과 같은 에러를 보게 됩니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
--> src/main.rs:2:14
|
2 | let x = (let y = 6);
| ^^^
|
= note: only supported directly in conditions of `if` and `while` expressions
warning: unnecessary parentheses around assigned value
--> src/main.rs:2:13
|
2 | let x = (let y = 6);
| ^ ^
|
= note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
|
2 - let x = (let y = 6);
2 + let x = let y = 6;
|
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` (bin "functions") due to 1 previous error; 1 warning emitted
let y = 6은 이 초기화 위치에서 일반 값 표현식이 아니므로 x에 결괏값을 바인딩할 수 없습니다. let 패턴은 if와 while의 조건에서는 직접 사용할 수 있지만, 그때도 값 자체를 만들어 내는 일반 표현식과는 역할이 다릅니다.
또한 러스트의 할당 표현식 y = 6은 할당한 값 6이 아니라 단위 값 ()로 평가됩니다. =는 오른쪽부터 묶이므로 x = y = 6은 x = (y = 6)으로 해석됩니다. 이때 y에는 6이 저장되지만 내부 대입의 결과인 ()가 x에 전달되므로, C처럼 같은 값 6을 두 변수에 전달하는 다중 대입은 되지 않습니다. x의 타입이 ()라면 이 구문 형태 자체는 컴파일될 수 있습니다.
여러분이 작성하는 러스트 코드의 대부분은 표현식이며, 이는 어떤 값을 평가합니다.
5 + 6과 같은 간단한 수학 연산을 살펴봅시다.
이 수식은 11이라는 값을 평가하는 표현식입니다.
표현식은 구문의 일부일 수 있습니다.
예제 2-1의 let y = 6;이라는 구문에서 6은 6이라는 값을 평가하는 표현식입니다.
함수 호출은 표현식입니다. 매크로 호출도 이 예제의 println!처럼 표현식 위치에서 사용될 수 있습니다.
아래 예제처럼 중괄호로 만들어진 새로운 스코프 블록도 표현식입니다.
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}
아래의 표현식.
{
let x = 3;
x + 1
}같은 경우에는 4를 평가하는 코드 블록입니다.
이 값이 let 구문의 일부로서 y에 바인딩됩니다.
여러분이 지금까지 봐온 것과 다르게 x + 1 줄의 마지막이 세미콜론으로 끝나지 않은 점을 주목하세요.
블록의 마지막 표현식을 결과로 사용하려면 뒤에 세미콜론을 붙이지 않습니다.
여기에 세미콜론을 추가하면 표현식 구문이 되어 계산 결과를 버립니다. 그러면 끝까지 정상적으로 실행되는 이 블록에는 마지막 피연산자가 없으므로 블록의 값과 타입은 단위 값 ()와 단위 타입 ()이 됩니다.
이 점을 상기하면서 이후부터 함수의 반환 값과 표현식을 살펴보길 바랍니다.
반환 값을 갖는 함수
함수는 호출한 코드에 값을 반환할 수 있습니다.
반환되는 값을 명명해야 할 필요는 없지만, 그 값의 타입은 화살표(->) 뒤에 선언되어야 합니다.
함수가 본문 끝까지 정상적으로 실행되면 반환 값은 함수 본문 블록의 마지막 표현식 값과 같습니다.
return 키워드와 값을 지정하여 함수로부터 일찍 값을 반환할 수 있지만, 대부분의 함수들은 암묵적으로 마지막 표현식 값을 반환합니다.
값을 반환하는 함수의 예를 보겠습니다.
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}
five 함수에는 함수 호출, 매크로, 심지어 let 구문도 없이 그저 5란 숫자 하나가 있습니다.
러스트에서는 이게 완벽하게 유효한 함수입니다.
함수 반환 값의 타입도 -> i32로 명시되어 있다는 점을 주목하세요.
결과는 아래와 같이 나와야 합니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
5는 five 함수의 반환 값이며, 이 때문에 반환 타입을 i32으로 한 것이지요.
좀 더 자세히 보자면, 중요한 지점이 두 곳 있습니다.
첫째로, let x = five(); 라인은 함수의 반환 값을 변수의 초깃값으로 사용하는 것을 보여줍니다.
five의 반환 값이 5이기 때문에, 해당 라인은 다음과 동일합니다.
let x = 5;두 번째로, five 함수는 매개변수 없이 반환 타입만 정의되어 있지만,
본문에는 5만이 세미콜론 없이 외롭게 있는데, 그 이유는 이 값이
반환하려는 값에 대한 표현식이기 때문입니다.
다른 예제도 살펴봅시다.
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1
}
이 코드를 실행하면 The value of x is: 6이 출력됩니다.
만일 x + 1 끝에 세미콜론이 추가되어 표현식이 구문으로 변경되면 에러가 발생합니다.
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}
이 코드를 컴파일하면 다음과 같은 에러가 나타납니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
error[E0308]: mismatched types
--> src/main.rs:7:24
|
7 | fn plus_one(x: i32) -> i32 {
| -------- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
8 | x + 1;
| - help: remove this semicolon to return this value
For more information about this error, try `rustc --explain E0308`.
error: could not compile `functions` due to previous error
주 에러 메시지 mismatched types는 이 코드의 핵심 문제를 보여줍니다.
plus_one 함수의 정의에는 i32 값을 반환한다고 되어 있지만, x + 1;은 계산 결과를 버리는 표현식 구문입니다. 본문 블록에는 마지막 표현식이 남지 않고 실행이 끝까지 도달하므로 블록의 타입은 유닛 타입 ()입니다.
따라서 본문 블록의 ()와 선언된 반환 타입 i32가 서로 달라 에러가 발생합니다.
위의 출력 결과에서 러스트는 이 문제를 바로잡는데 도움이 될 수 있는 메시지를 제공합니다.
바로 세미콜론을 제거하면 에러가 수정될 것이란 제안이지요.
아래 다이어그램은 같은 함수 시그니처에서 세미콜론 하나가 본문 블록의 타입과 반환 계약을 어떻게 바꾸는지 비교합니다.
RETURN CONTRACT
함수가 본문 끝까지 정상적으로 실행되면, 본문 블록의 타입이 시그니처의 출력 타입과 맞아야 합니다. 아래 두 경로는 세미콜론에서 처음 갈립니다.
공통 출력 계약 · i32
fn plus_one은 i32 인수를 받고, 함수가 정상적으로 반환하면 호출자에게 i32 값을 내보내겠다고 선언합니다.
끝 피연산자가 있는 경로
fn plus_one(x: i32) -> i32 {
x + 1
}
-
1 · 끝 피연산자
x + 1이 블록의 마지막 표현식으로 평가되어i32값을 남깁니다. -
2 · 블록 타입
끝 피연산자의 타입을 따라 본문 블록도
i32입니다. -
PASS · 타입 일치
본문 i32 = 출력 i32이므로 호출자에게 계산 결과를 반환할 수 있습니다.
세미콜론으로 결과를 버린 경로
fn plus_one(x: i32) -> i32 {
x + 1;
}
-
FIRST DIVERGENCE
x + 1;은 표현식 구문입니다. 계산은 하지만 세미콜론 때문에 그 결과를 사용하지 않습니다. -
2 · 블록 타입
끝 피연산자가 없고 실행이 끝까지 도달하므로 본문 블록은 단위 타입
()입니다. -
FAIL · E0308
본문 () ≠ 출력 i32이므로 컴파일러가 기대한i32와 발견한()의 계약 불일치를 알립니다.
return value;는 도달한 자리에서 일찍 반환하며, 그 값도 선언한 출력 타입과 맞아야 합니다. 끝까지 도달하지 않는 발산 함수는 별도 경우입니다.