함수
함수의 매개변수와 반환 타입을 선언하고 구문과 표현식을 구분해 블록의 마지막 값으로 결과를 반환합니다.
러스트 코드는 함수로 가득합니다. 이미 많은 프로그램의 시작점인 main 함수와, 새 함수를 선언하는 fn 키워드를 보았습니다. 이 글에서는 함수를 정의하고 부르는 방법, 매개변수로 값을 넘기는 방법, 그리고 러스트에서 함수가 값을 돌려주는 방식을 이해하는 데 필요한 구문과 표현식의 차이를 살펴봅니다.
함수 정의하고 부르기
러스트에서는 함수와 변수의 이름을 모두 소문자로 쓰고 단어 사이를 밑줄로 잇는 스네이크 케이스(snake case)로 짓는 것이 관례입니다. cargo new functions로 새 프로젝트를 만들고 src/main.rs에 다음 코드를 넣습니다.
fn main() {
println!("Hello, world!");
another_function();
}
fn another_function() {
println!("Another function.");
}함수는 fn 뒤에 이름과 괄호를 적어 정의하며, 중괄호가 본문의 시작과 끝을 알려 줍니다. 정의한 함수는 이름 뒤에 괄호를 붙여 부릅니다. another_function은 main보다 뒤에 정의되어 있지만, 러스트는 함수가 소스의 어디에 있는지 따지지 않고 부르는 쪽에서 볼 수 있는 범위 어딘가에 정의되어 있기만 하면 됩니다. 실행하면 main의 문장이 차례로 실행되어, 먼저 Hello, world!를 출력한 뒤 another_function을 불러 그 안의 메시지를 출력합니다.
Hello, world!
Another function.매개변수
함수에는 매개변수(parameter)를 둘 수 있습니다. 매개변수는 함수 시그니처(signature)의 일부인 특별한 변수로, 함수를 부를 때 넘긴 구체적인 값을 받습니다. 엄밀히는 이 구체적인 값을 인수(argument)라고 하지만, 일상적으로는 함수 정의의 변수와 부를 때 넘기는 값을 구분하지 않고 둘 다 매개변수나 인수라고 부르기도 합니다.
fn main() {
another_function(5);
}
fn another_function(x: i32) {
println!("The value of x is: {x}");
}The value of x is: 5another_function은 i32 타입의 매개변수 x 하나를 받습니다. 5를 넘겨 부르면 x가 5가 되고, println!이 그 값을 자리표시자 자리에 넣어 출력합니다. 함수 시그니처에서는 매개변수마다 타입을 반드시 적어야 하는데, 이는 러스트를 설계할 때 일부러 정한 규칙입니다. 함수 정의에 타입이 적혀 있으면 컴파일러가 그 함수를 부르는 코드를 보고 의도한 타입을 짐작할 필요가 없고, 기대한 타입을 알고 있으므로 더 도움이 되는 오류 메시지를 낼 수 있습니다.
매개변수가 여러 개라면 쉼표로 나눕니다.
fn main() {
print_labeled_measurement(5, 'h');
}
fn print_labeled_measurement(value: i32, unit_label: char) {
println!("The measurement is: {value}{unit_label}");
}The measurement is: 5h첫 매개변수 value는 i32, 둘째 매개변수 unit_label은 char이며, 5와 'h'를 넘겼으므로 두 값이 이어진 5h가 출력됩니다.
구문과 표현식
함수의 본문은 구문(statement)이 이어지다가, 마지막에 표현식(expression)이 하나 올 수도 있습니다. 러스트는 표현식 기반의 언어이므로 이 둘의 차이를 아는 것이 중요하며, 다른 언어에는 이런 구분이 없거나 다를 수 있습니다. 구문은 어떤 동작을 하지만 값을 돌려주지 않는 명령이고, 표현식은 계산되어 값이 됩니다.
let으로 변수를 만들고 값을 넣는 것은 구문입니다. 다음 main의 let y = 6;도 구문이며, 함수 정의 자체도 구문입니다.
fn main() {
let y = 6;
}구문은 값을 돌려주지 않으므로, let 구문을 다른 변수에 넣을 수 없습니다. 다음 코드는 컴파일되지 않습니다.
fn main() {
let x = (let y = 6);
}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표현식이 와야 할 자리에 let 구문이 왔다는 오류이며, 괄호가 필요 없다는 경고도 함께 나옵니다. let은 if와 while의 조건에서만 직접 쓸 수 있다는 안내가 붙는데, 그 쓰임은 if let을 사용한 간결한 제어 흐름과 패턴이 사용될 수 있는 모든 곳에서 다룹니다. C나 Ruby처럼 대입이 대입한 값을 돌려주는 언어에서는 x = y = 6으로 두 변수에 6을 넣을 수 있지만, 러스트의 대입은 값 대신 유닛 ()이 됩니다. 그래서 x = y = 6은 y에 6을 넣은 뒤 x에는 ()을 넣으려 하므로, x가 정수 변수라면 타입이 맞지 않아 컴파일되지 않습니다.
러스트 코드는 대부분 표현식입니다. 5 + 6 같은 계산은 11이라는 값이 되는 표현식이고, let y = 6;의 6도 6이 되는 표현식입니다. 함수 호출과 매크로 호출도 표현식이며, 중괄호로 만든 새 범위의 블록도 표현식입니다.
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {y}");
}The value of y is: 4y에 넣은 중괄호 블록은 4가 되는 표현식입니다. 블록 안에서 x에 3을 넣은 뒤 마지막 줄의 x + 1이 블록의 값이 되고, 그 값이 let 구문에 따라 y에 묶입니다. 마지막 줄 x + 1에 세미콜론이 없다는 점에 주의합니다. 표현식 끝에 세미콜론을 붙이면 그 값을 버리는 구문이 되어 블록의 마지막 값이 없어지고, 그러면 블록은 유닛 ()이 됩니다. 이 규칙은 함수가 값을 돌려주는 방식으로 그대로 이어집니다.
반환 값을 갖는 함수
함수는 부른 쪽에 값을 돌려줄 수 있습니다. 돌려주는 값에 이름을 붙이지는 않지만, 그 타입은 화살표 -> 뒤에 적어야 합니다. 함수의 반환 값은 본문 블록의 마지막 표현식의 값이며, return 키워드로 값을 지정해 중간에 돌아갈 수도 있지만 대부분의 함수는 마지막 표현식으로 값을 돌려줍니다.
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {x}");
}The value of x is: 5five 함수의 본문에는 함수 호출도, let 구문도 없이 숫자 5 하나만 있지만, 러스트에서는 올바른 함수입니다. 본문의 마지막 표현식 5가 반환 값이고, 그래서 반환 타입을 -> i32로 적었습니다. let x = five();는 함수가 돌려준 값으로 변수를 초기화하며, five가 5를 돌려주므로 let x = 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: 6main의 x와 plus_one의 매개변수 x는 이름만 같을 뿐 서로 다른 변수입니다. 매개변수 x가 인수 5를 받고, 본문의 x + 1이 6을 돌려주며, main은 그 6을 자기 변수 x에 묶습니다. 그런데 x + 1 끝에 세미콜론을 붙여 표현식을 구문으로 바꾸면 컴파일되지 않습니다.
fn main() {
let x = plus_one(5);
println!("The value of x is: {x}");
}
fn plus_one(x: i32) -> i32 {
x + 1;
}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핵심은 타입이 맞지 않는다(mismatched types)는 것입니다. plus_one은 i32를 돌려준다고 선언했지만, x + 1;은 계산한 값을 버리는 구문이어서 본문에 마지막 표현식이 남지 않습니다. 그래서 본문 블록은 유닛 ()이 되고, 선언한 반환 타입 i32와 맞지 않습니다. 컴파일러는 이 세미콜론을 지우면 값을 돌려줄 수 있다는 도움말까지 보여 줍니다.
아래 그림은 같은 i32 반환 시그니처에서 세미콜론 하나가 본문 블록의 타입을 어떻게 바꾸는지 비교합니다. x + 1은 i32 값을 남겨 선언과 맞고, x + 1;은 값을 버려 블록이 ()이 되므로 E0308 오류가 납니다. 두 경로는 함께 실행되는 순서가 아니라 서로 다른 두 사본입니다.
RUST · RETURN CONTRACT
세미콜론은 블록의 값과 반환 타입을 바꾼다
x + 1은 i32 값을 남기고, x + 1;은 계산 결과를 버립니다. 정상적으로 끝까지 실행되는 함수 본문은 선언한 출력 타입을 만족해야 합니다.
공통 출력 계약 · 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;
}
-
1 · 결과 버림
x + 1;은 표현식 구문입니다. 계산은 하지만 세미콜론 때문에 그 결과를 사용하지 않습니다. -
2 · 블록 타입
끝 피연산자가 없고 실행이 끝까지 도달하므로 본문 블록은 단위 타입
()입니다. -
FAIL · E0308
본문 () ≠ 출력 i32이므로 컴파일러가 기대한i32와 발견한()의 계약 불일치를 알립니다.
return value;는 도달한 자리에서 일찍 반환하며, 그 값도 선언한 출력 타입과 맞아야 합니다. 끝까지 도달하지 않는 발산 함수는 별도 경우입니다.정리
- 함수는
fn으로 정의하고, 이름은 스네이크 케이스로 지으며, 부르는 쪽에서 볼 수 있는 범위에 있기만 하면 정의한 위치는 상관없습니다. - 매개변수는 함수 시그니처에서 타입을 반드시 적고, 여러 개라면 쉼표로 나눕니다.
- 구문은 동작을 하지만 값을 돌려주지 않으며,
let은 구문이라 다른 변수에 넣을 수 없습니다. - 표현식은 값이 되며, 블록은 마지막 표현식의 값이 됩니다.
- 표현식 끝에 세미콜론을 붙이면 값을 버리는 구문이 되고, 마지막 표현식이 없는 블록은
()이 됩니다. - 함수의 반환 타입은
->뒤에 적고, 본문의 마지막 표현식이나return으로 값을 돌려줍니다.
연습 문제
1. 호출 전후의 값
다음 프로그램은 무엇을 출력하나요? plus_one이 source 자체를 바꾸는지, 매개변수 x가 source와 어떤 관계인지를 생각해 보세요.
fn plus_one(x: i32) -> i32 {
x + 1
}
fn main() {
let source = 8;
let result = plus_one(source);
println!("{source} {result}");
}해설 보기
8 9를 출력합니다. plus_one의 매개변수 x는 source의 값 8을 받은 별개의 변수이며, x + 1인 9를 돌려주어 result에 묶입니다. i32 같은 정수는 넘길 때 값이 복사되므로 source는 그대로 8입니다. 모든 타입이 이렇게 복사되는 것은 아니며, String처럼 복사되지 않는 타입은 소유권이 뭔가요?에서 다룹니다.
2. 세미콜론과 return
plus_one의 본문을 각각 x + 1;과 return x + 1;로 바꾼 두 사본이 있습니다. 둘 다 컴파일되나요?
해설 보기
x + 1;만 있는 사본은 계산한 값을 버리고 본문 끝에서 ()이 되므로, i32를 돌려준다는 선언과 맞지 않아 E0308 오류로 컴파일되지 않습니다. return x + 1;은 세미콜론이 붙어 있어도 return이 그 값을 함수 밖으로 돌려주므로 컴파일되며, x + 1과 같은 결과를 냅니다. 마지막 줄의 세미콜론만 볼 것이 아니라, 함수 밖으로 값을 보내는 길이 있는지를 봐야 합니다.
3. 블록의 값
다음 코드에서 y는 무엇이 되나요? x * 2 뒤에 세미콜론을 붙이면 어떻게 되나요?
let y = {
let x = 10;
x * 2
};해설 보기
블록의 마지막 표현식 x * 2가 20이므로 y는 20입니다. 세미콜론을 붙이면 x * 2;는 값을 버리는 구문이 되어 블록의 값이 ()이 되고, y도 ()이 됩니다. 이 코드 자체는 컴파일되지만, 그 뒤에 y를 정수로 쓰려 하면 타입이 맞지 않아 오류가 납니다.
다음 글 주석에서는 코드에 설명을 남기는 일반 주석과, 문서를 만드는 문서화 주석을 살펴봅니다.