ref 는 고치러, out 은 채우러 간다
앞 글에서 매개변수는 사본이라고 했습니다. 대부분은 그게 맞습니다. 그런데 두 가지 경우에 곤란합니다.
첫째, 값 두 개를 돌려줘야 할 때. return 은 하나만 돌려줍니다.
나눗셈의 몫과 나머지를 같이 돌려주고 싶으면 방법이 없습니다.
둘째, 실패할 수 있을 때. 문자열을 숫자로 바꾸는 메서드는 “바꾼 값” 과 “바꿀 수 있었는지” 둘 다 알려줘야 합니다.
앞에서 이런 코드를 여러 번 봤습니다.
if (int.TryParse(input, out int number))
{
Console.WriteLine(number);
}
out 이 그때마다 나왔는데 설명을 미뤄뒀습니다. 여기서 정리합니다.
사본 말고 자리 자체를 넘기는 방법이 둘 있습니다.
ref와out입니다.
ref 는 원본 자리를 넘긴다
void AddTen(ref int n)
{
n = n + 10;
}
int score = 50;
AddTen(ref score);
Console.WriteLine(score); // 60
앞 글의 코드에 ref 두 개를 붙였을 뿐인데 결과가 달라졌습니다.
ref 를 붙이면 값을 복사하지 않습니다.
메서드 안의 n 과 밖의 score 가 같은 자리를 가리킵니다.
이름이 두 개일 뿐 자리는 하나입니다.
쓸 때 지켜야 할 것이 두 가지입니다.
정의와 호출 양쪽에 ref 를 씁니다. 호출하는 쪽에도 적게 한 것은 일부러입니다.
AddTen(score) 라고만 적혀 있으면 읽는 사람은 밖의 값이 바뀔 거라고 생각하지 못합니다.
ref 가 눈에 보여야 “이건 바뀔 수 있다” 를 압니다.
넘기기 전에 값이 들어 있어야 합니다.
int score;
AddTen(ref score); // 컴파일 에러: score 에 아직 값이 없습니다
ref 는 이미 있는 값을 고치러 가는 것이라, 고칠 값이 없으면 말이 안 됩니다.
out 은 빈 통을 주고 채워 받는다
out 은 반대입니다. 값을 안 넣고 넘깁니다. 대신 메서드가 채워 줍니다.
void Divide(int a, int b, out int quotient, out int remainder)
{
quotient = a / b;
remainder = a % b;
}
Divide(17, 5, out int q, out int r);
Console.WriteLine(q + " ... " + r); // 3 ... 2
return 으로는 하나만 돌려주는데, out 을 두 개 붙여서 두 개를 받았습니다.
여기도 규칙이 둘입니다.
넘기기 전에 값이 없어도 됩니다. 오히려 그게 보통입니다. 받으러 가는 것이니까요.
메서드 안에서 반드시 값을 넣어야 합니다.
void Divide(int a, int b, out int quotient, out int remainder)
{
quotient = a / b;
// remainder 를 안 채우면 컴파일 에러
}
빈 통을 받았으면 채워서 보내야 합니다. 안 채우고 끝내는 길이 하나라도 있으면 컴파일이 안 됩니다. 그래서 부른 쪽은 값이 들어 있다고 믿어도 됩니다.
둘의 차이
ref |
out |
|
|---|---|---|
| 넘기기 전 | 값이 들어 있어야 한다 | 없어도 된다 |
| 메서드 안에서 | 안 바꿔도 된다 | 반드시 값을 넣어야 한다 |
| 방향 | 들고 갔다가 고쳐서 온다 | 빈 통을 주고 채워 온다 |
| 쓰는 자리 | 이미 있는 값을 바꾸는 것이 목적 | 결과를 여러 개 받아야 할 때 |
한 문장으로 하면 이렇습니다.
ref는 고치러 가고,out은 채우러 갑니다.
TryParse 가 바로 이 방식이다
이제 앞에서 계속 보이던 그 모양이 읽힙니다.
if (int.TryParse(input, out int number))
{
Console.WriteLine(number);
}
TryParse 는 두 가지를 알려줘야 합니다.
바꿀 수 있었는가와 바꾼 값입니다.
- 성공 여부는
bool로return합니다. 그래야if괄호에 바로 넣을 수 있습니다. - 바꾼 값은
out으로 채워 줍니다.
실패하면 out 에는 0 이 들어갑니다. 채우기는 채워야 하니까요.
그래서 return 값을 확인하지 않고 number 만 쓰면 안 됩니다.
실패한 0 인지 진짜 0 인지 구분이 안 됩니다.
Dictionary 의 TryGetValue, Stack 의 TryPop 도 같은 모양입니다.
실패할 수 있는 일은 성공 여부를 돌려주고 결과는 out 으로 채운다.
이 방식이 C# 전반에 반복됩니다. 앞으로 Try 로 시작하는 이름을 보면 이 모양이라고 보면 됩니다.
out 변수는 넘기는 자리에서 바로 선언할 수 있습니다.
int number;
if (int.TryParse(input, out number)) // 미리 선언해도 되고
if (int.TryParse(input, out int number)) // 그 자리에서 선언해도 됩니다
아래쪽이 짧고, 그 변수가 어디서 왔는지도 한 줄에 보입니다.
자주 쓰지는 않습니다
ref 와 out 은 알아 둬야 하지만 자주 쓰는 도구는 아닙니다.
값을 하나 돌려주면 되는 일은 그냥 return 이 낫습니다.
읽는 사람이 “이 메서드는 무엇을 돌려주는가” 를 반환 타입 한 곳에서 봅니다.
out 이 늘어나면 결과가 여기저기 흩어져서 오히려 읽기 어려워집니다.
out 이 정말 어울리는 자리는 Try 패턴 처럼
“성공 여부와 값을 같이 줘야 하는” 경우입니다.
그 외에는 값을 묶어서 하나로 돌려주는 방법이 보통 더 낫습니다.
정리
- 매개변수는 기본이 사본입니다.
ref와out은 자리 자체를 넘깁니다. ref는 밖의 변수와 같은 자리를 가리킵니다. 안에서 고치면 밖이 고쳐집니다.ref는 정의와 호출 양쪽에 적어야 하고, 넘기기 전에 값이 들어 있어야 합니다.out은 값 없이 넘기고 메서드가 채웁니다. 안 채우면 컴파일 에러입니다.ref는 고치러 가고out은 채우러 갑니다.Try로 시작하는 메서드가 이 방식입니다. 성공 여부는return, 값은out.- 둘 다 자주 쓰지는 않습니다. 하나면
return이 낫습니다.