함수
함수는 러스트 코드 곳곳에서 매우 흔하게 등장합니다. 여러분은 이미 이 언어에서 가장
중요한 함수 중 하나인 main 함수를 보았습니다. main 은 많은 프로그램의
진입점입니다. 또한 새 함수를 선언할 수 있게 해 주는 fn 키워드도 보았습니다.
러스트 코드는 함수와 변수 이름에 snake case 를 관례적으로 사용합니다. 즉 모든 문자를 소문자로 쓰고, 단어 사이는 밑줄로 구분합니다. 다음은 함수 정의 예제를 담은 프로그램입니다.
파일명: src/main.rs
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}
러스트에서 함수는 fn 뒤에 함수 이름과 괄호를 써서 정의합니다. 중괄호는 함수
본문이 어디서 시작하고 끝나는지를 컴파일러에게 알려 줍니다.
우리가 정의한 함수는 이름 뒤에 괄호를 붙여 호출할 수 있습니다. another_function
이 프로그램 안에 정의되어 있으므로, main 함수 안에서도 호출할 수 있습니다.
소스 코드에서는 another_function 을 main 함수 뒤에 정의했지만, 앞에 정의해도
상관없습니다. 러스트는 함수를 어디에 정의했는지는 신경 쓰지 않고, 호출자가 볼 수
있는 스코프 어딘가에 정의되어 있기만 하면 됩니다.
함수를 좀 더 살펴보기 위해 functions 라는 새 바이너리 프로젝트를 시작해 봅시다.
another_function 예제를 src/main.rs 에 넣고 실행해 보세요. 다음과 같은 출력이
나올 것입니다.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.28s
Running `target/debug/functions`
Hello, world!
Another function.
줄들은 main 함수에 나타난 순서대로 실행됩니다. 먼저 “Hello, world!” 메시지가
출력되고, 그다음 another_function 이 호출되어 해당 메시지가 출력됩니다.
매개변수
함수에는 매개변수(parameters) 를 정의할 수 있습니다. 매개변수는 함수 시그니처의 일부인 특별한 변수입니다. 함수에 매개변수가 있으면, 그 매개변수들에 실제 값을 전달할 수 있습니다. 엄밀히 말하면 실제 전달되는 값은 인수(arguments) 라고 부르지만, 일상적인 대화에서는 함수 정의 안의 변수와 함수를 호출할 때 넘기는 실제 값 둘 모두를 가리켜 parameter 와 argument 를 섞어 쓰는 경우가 많습니다.
이번 버전의 another_function 에서는 매개변수를 하나 추가합니다.
파일명: src/main.rs
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` profile [unoptimized + debuginfo] target(s) in 1.21s
Running `target/debug/functions`
The value of x is: 5
another_function 선언에는 x 라는 이름의 매개변수 하나가 있습니다. x 의 타입은
i32 입니다. 우리가 another_function 에 5 를 넘기면, println! 매크로는
포맷 문자열 안에서 x 가 들어 있던 중괄호 자리에 5 를 넣습니다.
함수 시그니처에서는 각 매개변수의 타입을 반드시 선언해야 합니다. 이것은 러스트 설계에서 의도적으로 선택한 점입니다. 함수 정의에 타입 주석을 요구하면, 컴파일러는 코드의 다른 위치에서 여러분이 의미한 타입을 추론하기 위해 추가 정보를 거의 요구하지 않게 됩니다. 또한 함수가 어떤 타입을 기대하는지 알고 있으므로 더 유용한 오류 메시지를 제공할 수 있습니다.
매개변수가 여러 개라면 다음처럼 쉼표로 구분합니다.
파일명: src/main.rs
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 을 모두 포함한
문자열을 출력합니다.
이 코드를 실행해 봅시다. 현재 functions 프로젝트의 src/main.rs 에 들어 있는
프로그램을 위 예제로 바꾸고, cargo run 으로 실행하세요.
$ cargo run
Compiling functions v0.1.0 (file:///projects/functions)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/functions`
The measurement is: 5h
value 에는 5, unit_label 에는 'h' 를 넘겨 함수 호출을 했으므로, 프로그램
출력에도 그 값들이 포함됩니다.
문장과 식
함수 본문은 여러 문장들로 이루어지며, 선택적으로 마지막에 식 하나로 끝날 수 있습니다. 지금까지 우리가 본 함수들은 마지막 식을 포함하지 않았지만, 여러분은 이미 문장 안의 일부로 식을 본 적이 있습니다. 러스트는 식 중심 언어이기 때문에 이 차이를 이해하는 것이 중요합니다. 다른 언어에서는 이런 구분이 같지 않은 경우도 있으니, 문장과 식이 무엇이고 그 차이가 함수 본문에 어떤 영향을 주는지 살펴봅시다.
- 문장(statements) 은 어떤 동작을 수행하지만 값을 반환하지 않는 명령입니다.
- 식(expressions) 은 어떤 결과값으로 평가됩니다.
몇 가지 예를 살펴봅시다.
사실 우리는 이미 문장과 식을 사용해 왔습니다. let 키워드로 변수를 만들고 값을
대입하는 것은 문장입니다. 목록 3-1에서 let y = 6; 은 문장입니다.
fn main() {
let y = 6;
}
main 함수 선언함수 정의 자체도 문장입니다. 앞의 예제 전체가 그 자체로 하나의 문장입니다. (곧 보겠지만, 함수 호출 은 문장이 아닙니다.)
문장은 값을 반환하지 않습니다. 따라서 다음 코드처럼 let 문을 다른 변수에 대입할
수 없으며, 오류가 발생합니다.
파일명: src/main.rs
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 가 바인딩할 대상이 없습니다. 이것은
C나 Ruby처럼 대입문이 대입된 값을 반환하는 언어와 다른 점입니다. 그런 언어에서는
x = y = 6 이라고 써서 x 와 y 모두 6 을 갖게 할 수 있지만, 러스트에서는
그렇게 되지 않습니다.
식은 값으로 평가되며, 여러분이 러스트에서 작성하게 될 코드의 대부분을 이룹니다.
예를 들어 5 + 6 같은 수학 연산은 11 이라는 값으로 평가되는 식입니다. 식은
문장의 일부가 될 수도 있습니다. 목록 3-1에서 let y = 6; 의 6 은 값 6 으로
평가되는 식입니다. 함수 호출도 식입니다. 매크로 호출도 식입니다. 중괄호로 만든 새
스코프 블록도 식입니다. 예를 들면 다음과 같습니다.
파일명: src/main.rs
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 키워드와 값을
사용해 함수에서 일찍 빠져나올 수도 있지만, 대부분의 함수는 마지막 식을 암묵적으로
반환합니다. 다음은 값을 반환하는 함수의 예입니다.
파일명: src/main.rs
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` profile [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/functions`
The value of x is: 5
five 안의 5 는 함수의 반환값이며, 그래서 반환 타입이 i32 인 것입니다. 이것을
조금 더 자세히 봅시다. 중요한 점이 두 가지 있습니다. 첫째, let x = five(); 라는
줄은 함수 반환값을 사용해 변수를 초기화하고 있음을 보여 줍니다. 함수 five 는 5
를 반환하므로, 이 줄은 사실 다음과 같습니다.
#![allow(unused)]
fn main() {
let x = 5;
}
둘째, five 함수는 매개변수가 없고 반환값의 타입을 정의하지만, 함수 본문은
세미콜론이 없는 5 하나뿐입니다. 그 이유는 우리가 그 식의 값을 그대로 반환하고
싶기 때문입니다.
다른 예를 하나 더 봅시다.
파일명: src/main.rs
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 이 들어
있는 줄 끝에 세미콜론을 붙여 식을 문장으로 바꾸면 어떻게 될까요?
파일명: src/main.rs
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` (bin "functions") due to 1 previous error
핵심 오류 메시지인 mismatched types 는 이 코드의 본질적인 문제를 드러냅니다.
함수 plus_one 의 정의는 i32 를 반환하겠다고 말하지만, 문장은 값으로 평가되지
않으며, 이는 unit 타입 () 로 표현됩니다. 따라서 아무 값도 반환되지 않고, 이것이
함수 정의와 모순되어 오류가 발생하는 것입니다. 이 출력에서 러스트는 문제를 고칠 수
있도록 도움말도 제공합니다. 세미콜론을 제거하라고 제안하는데, 그렇게 하면 오류가
해결됩니다.