숏컷 코드
// ref — 호출자 변수를 직접 수정
void Increment(ref int value) => value++;
int score = 10;
Increment(ref score); // score == 11
// out — 메서드에서 추가 결과 반환
bool TryDivide(int left, int right, out int result)
{
if (right == 0) { result = 0; return false; }
result = left / right;
return true;
}
if (TryDivide(20, 4, out int quotient))
Console.WriteLine(quotient); // 5
// in — 읽기 전용 참조 (복사 방지)
void PrintArea(in Rectangle rect)
=> Console.WriteLine(rect.Width * rect.Height);문법
어떤 전달 방식이 있나
매개변수 전달은 아래 네 가지를 같이 보면 헷갈림이 줄어듭니다.
void AddOne(int n) { } // 값 복사
void AddOne(ref int n) { } // 읽고 쓰는 참조
bool TryGet(out int n) { } // 출력 전용 참조
void Print(in BigStruct value) { } // 읽기 전용 참조- 기본값 전달: 복사
ref: 읽고 쓰기out: 메서드가 채워 주기in: 읽기 전용
내부 동작 — 포인터의 안전한 추상화
ref와 out은 컴파일러가 포인터를 사용해 구현합니다. 값을 복사하는 대신 호출자 변수의 메모리 주소를 메서드에 전달합니다. IL 코드에서는 ldarga / ldind 명령어가 사용됩니다.
// 작성한 코드
Increment(ref score);
// IL 수준에서 일어나는 일 (개념)
// &score (score의 주소)를 Increment에 전달
// Increment 내부에서 *ptr = *ptr + 1 수행ref vs out — 초기화 계약
ref | out | |
|---|---|---|
| 호출 전 초기화 | 필수 | 불필요 |
| 메서드 내부 할당 | 선택 | 필수 |
| 전달 전 값 읽기 | 가능 | 불가 |
| 의도 | 양방향 수정 | 출력 전용 |
out은 메서드 안에서 반드시 값을 설정해야 합니다. 컴파일러가 모든 코드 경로에서 할당을 강제합니다.
bool TryParse(string s, out int result)
{
// result = 0; 를 빠뜨리면 컴파일 오류
return int.TryParse(s, out result);
}in 매개변수 — 방어적 복사에 주의
in은 읽기 전용 참조입니다. 큰 struct를 복사하지 않고 전달해 성능을 높이는 것이 목적입니다. 그런데 in 매개변수로 받은 struct에서 non-readonly 메서드를 호출하면 컴파일러가 방어적 복사본을 만듭니다. 원본이 수정될까봐 임시 복사본을 사용하는 것입니다.
struct Mutable { public int X; public void Modify() => X++; }
void Foo(in Mutable m)
{
m.Modify(); // ⚠️ m의 복사본에서 호출됨 — 원본 변경 없음
}
// 해결: struct를 readonly struct로 선언하면 방어적 복사 없음
readonly struct Immutable { public int X { get; init; } }ref return과 ref local
메서드가 참조 자체를 반환할 수 있습니다. 배열 내 요소를 직접 수정하는 경우 유용합니다.
ref int FindFirst(int[] arr, int target)
{
for (int i = 0; i < arr.Length; i++)
if (arr[i] == target) return ref arr[i];
throw new KeyNotFoundException();
}
int[] numbers = { 1, 2, 3, 4 };
ref int slot = ref FindFirst(numbers, 3);
slot = 99; // numbers[2] == 99 — 배열 원소 직접 수정현대 C#의 대안 비교
// out을 쓰는 전통적 패턴
bool TryGetUser(int id, out string name) { ... }
// 튜플로 대체 (가독성 향상)
(bool Found, string Name) TryGetUser(int id) { ... }
// 내장 라이브러리 메서드는 여전히 out 사용
int.TryParse("42", out int n);
Dictionary<K,V>.TryGetValue(key, out var value);int x = 1;
// ❌ ref 인수는 변수여야 한다
// Increment(ref 1);
// ✅ 변수 전달
Increment(ref x);체크포인트
| 키워드 | 방향 | 호출 전 초기화 | 주요 용도 |
|---|---|---|---|
ref | 양방향 | 필수 | 호출자 변수 직접 수정 |
out | 출력 전용 | 불필요 | Try 패턴, 다중 반환 |
in | 입력 전용 | 필수 | 대형 struct 복사 방지 |
ref return | 반환 참조 | — | 컬렉션 원소 직접 노출 |
주의할 점
ref/out이 많아지면 메서드 계약이 복잡해집니다. 공개 API에서는 튜플이나 Result 타입이 더 명확합니다. in은 성능 최적화 목적으로만 사용하고, non-readonly struct와 함께 쓸 때 방어적 복사가 발생하는지 항상 확인하세요.
out int _처럼 무시 패턴을 사용하면 불필요한 결과를 버릴 수 있습니다. int.TryParse(s, out _)는 파싱 성공 여부만 확인할 때 깔끔합니다.
참고 링크
3 sources