C#은 기본적으로 모든 인수를 값으로 전달합니다. 메서드는 복사본을 받고, 매개변수에 대입해도 호출하는 쪽에는 영향이 없습니다. ref, out, in 한정자는 대신 호출하는 쪽의 변수 자체를 넘깁니다.
출력:
by value: 50
by ref: 60
키워드는 두 번 나옵니다. 매개변수 목록(ref int score)과 호출 지점(ref score)입니다. 호출에 쓰는 것은 필수이며 의도된 것입니다. AddPoints(ref score)를 읽는 사람은 누구나 score가 바뀔 수 있음을 압니다. 빠뜨리면 컴파일러가 CS1620 Argument 1 must be passed with the 'ref' keyword를 보고합니다.
ref: 호출하는 쪽의 변수를 읽고 쓰기
ref 매개변수는 호출하는 쪽 변수의 별칭입니다. 메서드는 현재 값을 읽고 교체할 수 있습니다. 전형적인 예는 일반 매개변수로는 불가능한 값 교환입니다:
출력:
Linus Ada
volume 100
ref의 규칙:
- 인수는 리터럴이나 식이 아니라 변수(지역 변수, 매개변수, 필드, 배열 요소)여야 합니다. 속성도 해당하지 않습니다.
Clamp(ref player.Volume, 0, 100)은 오류 CS0206A non ref-returning property or indexer may not be used as an out or ref value입니다. 속성을 지역 변수에 복사해서 넘기고, 다시 대입하세요. - 변수는 호출 전에 확정 할당되어 있어야 합니다.
int n; Clamp(ref n, 0, 10);은 CS0165Use of unassigned local variable 'n'입니다.
out: 추가 결과
out은 메서드가 만들어 내는 값을 위한 것입니다. 호출하는 쪽은 변수를 초기화할 필요가 없고, 메서드는 반환하기 전에 모든 경로에서 반드시 대입해야 합니다:
static void Split(string fullName, out string first, out string last)
{
// error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
first = fullName.Split(' ')[0];
}
메서드 안에서 out 매개변수는 쓰기 전까지 할당되지 않은 것으로 취급되므로 호출하는 쪽의 이전 값을 읽을 수 없습니다. 이것이 ref와의 차이입니다. ref는 양방향이고, out은 메서드에서 호출하는 쪽으로 가는 단방향입니다.
TryParse 패턴
out의 가장 잘 알려진 용도는 기본 라이브러리의 Try 패턴입니다. 성공 여부를 bool로 반환하고 결과를 out 매개변수에 넣어서, 잘못된 입력이 절대 예외를 던지지 않게 합니다:
출력:
"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized
out var quantity(C# 7.0)는 호출 안에서 바로 변수를 선언하며, 타입은 매개변수에서 옵니다. 실패하면 TryParse는 이를 0으로 설정하고 false를 반환합니다. 파싱 성공 여부만 궁금하다면 out _로 값을 버리세요: bool isNumber = int.TryParse(input, out _);.
TryGetDiscount가 두 경로 모두에서 rate를 대입하는 방식에 주목하세요. 한 번은 명시적으로, 한 번은 그 자체가 out 호출인 Dictionary.TryGetValue에 넘겨서 대입합니다.
out과 튜플 반환
C# 7 이전에는 여러 값을 반환하는 주된 방법이 out이었습니다. 이제는 튜플이 이를 더 직접적으로 합니다:
// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);
// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);
단순히 여러 결과를 돌려줄 때는 튜플(이나 작은 클래스)을 쓰세요. out은 bool 반환 덕분에 호출을 if 조건 안에 둘 수 있는 Try 패턴에 남겨 두세요.
참조 형식: 객체를 바꾸기와 교체하기
클래스 인스턴스를 값으로 넘겨도 메서드는 이미 객체를 바꿀 수 있습니다. 복사된 값이 같은 객체에 대한 참조이기 때문입니다. ref는 한 가지 능력을 더합니다. 호출하는 쪽의 변수가 가리키는 객체를 교체하는 것입니다.
출력:
after AddItem: 2 items
after ResetByValue: 2 items
after ResetByRef: 0 items
ResetByValue는 자기 참조 복사본이 새 리스트를 가리키게 했을 뿐이라 호출하는 쪽에서는 보이지 않았습니다. ResetByRef는 호출하는 쪽의 변수 자체를 바꿨습니다. 일상적인 코드에서 클래스 매개변수에 ref가 필요한 일은 드뭅니다. 대신 새 객체를 반환하세요.
구조체에서는 차이가 더 큽니다. 구조체 인수는 필드 단위로 복사되기 때문입니다. 호출하는 쪽의 구조체(Point, 큰 설정 구조체)를 수정해야 하는 메서드에는 ref가 필요합니다.
in: 참조로 전달하는 읽기 전용
C# 7.2는 참조로 전달하되 대입을 금지하는 in을 추가했습니다. 목적은 성능입니다. 큰 구조체가 호출마다 복사되지 않고, 호출하는 쪽은 값이 바뀌지 않음을 보장받습니다.
// C# 7.2 and later
struct Matrix4
{
public double M11, M12, M13, M14, M21, M22, M23, M24,
M31, M32, M33, M34, M41, M42, M43, M44;
}
static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;
static void Reset(in Matrix4 m)
{
m.M11 = 0;
// error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}
호출 지점에서 in은 선택 사항입니다. Trace(matrix)와 Trace(in matrix) 둘 다 동작합니다. int, double, DateTime 같은 작은 타입에는 in이 이득이 없고 조금 느릴 수도 있으니, 자주 넘기는 여러 필드짜리 구조체에 쓰세요.
제한 사항
ref와out매개변수는 선택적일 수 없습니다(CS1741).in매개변수는 가능합니다:static void Connect(in int retries = 3)은 올바른 코드입니다.- 런타임 수준에서는 같기 때문에 오버로드가
ref와out(또는in)으로만 달라질 수는 없습니다.F(int x)와F(ref int x)는 함께 존재할 수 있습니다. async메서드와 반복기(yield return을 쓰는 메서드)는ref,out,in매개변수를 가질 수 없습니다. 이런 메서드에서는 튜플이나 결과 객체를 반환하세요.- 람다나 로컬 함수는 둘러싼 메서드의
ref,out,in매개변수를 캡처할 수 없습니다. 먼저 지역 변수에 복사하세요.
ref, out, in 비교
| 호출하는 쪽이 먼저 대입해야 함 | 메서드가 대입해야 함 | 메서드가 읽을 수 있음 | 호출 지점 키워드 | |
|---|---|---|---|---|
| (없음) | 예 | 아니요 | 예(복사본) | 없음 |
ref | 예 | 아니요 | 예 | 필수 |
out | 아니요 | 예, 모든 경로에서 | 대입한 뒤에만 | 필수 |
in | 예 | 허용 안 됨 | 예 | 선택 |
자주 묻는 질문
C#에서 ref와 out의 차이는 무엇인가요?
둘 다 복사본이 아니라 호출하는 쪽의 변수 자체를 넘깁니다. ref는 호출 전에 변수에 값이 대입되어 있어야 하고, 메서드가 읽고 바꿀 수 있습니다. out은 미리 대입할 필요가 없고, 메서드는 쓰기 전에 읽을 수 없으며, 반환하기 전에 반드시 대입해야 합니다(그렇지 않으면 CS0177). out은 "추가 결과"라는 뜻입니다.
C#에서 out var는 무슨 뜻인가요?
C# 7.0부터 호출 안에서 out 변수를 선언할 수 있습니다: if (int.TryParse(text, out var number)). 변수의 타입은 매개변수로부터 추론되며, 문장이 끝난 뒤에도 범위에 있습니다. out _는 필요 없는 결과를 버립니다.
C#에서 객체는 참조로 전달되나요?
아니요. ref, out, in을 쓰지 않는 한 모든 인수는 값으로 전달됩니다. 클래스의 경우 복사되는 값이 참조이므로, 메서드가 객체의 필드를 바꾸면 호출하는 쪽에서도 보입니다. ref 없이 할 수 없는 것은 호출하는 쪽의 변수가 다른 객체를 가리키게 만드는 것입니다.
C#의 in 매개변수 한정자는 무엇인가요?
in(C# 7.2 이상)은 인수를 참조로 넘기되 읽기 전용으로 넘깁니다. 메서드는 여기에 대입할 수 없습니다. 호출할 때마다 큰 구조체를 복사하지 않기 위해 존재합니다. int나 DateTime 같은 작은 타입에는 이점이 없습니다.
"Argument 1 must be passed with the 'ref' keyword" 오류가 나는 이유는 무엇인가요?
메서드가 ref 매개변수를 선언했는데 호출에서 키워드를 빠뜨렸기 때문입니다. C#은 읽는 사람이 변수가 바뀔 수 있음을 알 수 있도록 호출 지점에도 ref(와 out)를 요구합니다: Increment(count);가 아니라 Increment(ref count);입니다.