32. ref 반환·ref 지역 변수·ref readonly 와 인플레이스 수정
난이도 상내 리뷰 · C#
해설 · C#
해설 — ref 반환·ref 지역 변수·ref readonly 와 인플레이스 수정
난이도: 상
요약
출력:
A (Hp=100,Mana=50)
B pool=(Hp=100,Mana=50) copy=(Hp=-999,Mana=50)
C idx1=(Hp=100,Mana=50) idx2=(Hp=7,Mana=0)
D (Hp=100,Mana=50)
ref Entity GetRef(int i) => ref _items[i] 는 원소의 **저장 위치(별칭)**를 돌려준다. ref 지역 변수로 받으면 그 위치를 직접 가리켜 제자리 수정이 원본 배열에 반영되고(복사 없음), ref 없이 받으면 값 형식이 복사되어 원본과 분리된다. ref readonly는 복사 없이 읽되 수정을 금지한다.
핵심 개념
- ref return: 값이 아니라 "그 값이 사는 저장 위치"를 반환한다(배열 원소, 필드 등 lvalue). 큰 struct 를 복사 없이 제자리 수정/읽기 할 수 있어 ECS 류 고성능 코드에 유용.
- ref local:
ref var x = ref expr;로 저장 위치를 별칭한다. 이후x.Field = ...는 원본 위치를 바꾼다.x = ref other;로 재바인딩도 가능(C# 7.3+). - ref readonly / in: 읽기 전용 별칭. 복사를 피하면서 수정은 막는다(방어적 복사 이슈는 readonly struct 문제 참조).
단계별 내부 동작
- (A):
slot이_items[1]을 별칭.slot.Hp=100; slot.Mana=50이 배열 원소를 직접 갱신 →(Hp=100,Mana=50). - (B):
Entity copy = pool.GetRef(1);— 반환은 ref 지만 받는 쪽이 값 타입 변수라 그 시점에 복사가 일어난다.copy.Hp=-999는 복사본만 바꾼다.pool은 그대로(Hp=100,Mana=50),copy=(Hp=-999,Mana=50). - (C):
slot = ref pool.GetRef(2);—slot을_items[2]로 재바인딩(값 대입이 아니라 별칭 변경).slot.Hp=7은_items[2]를 바꿈.idx1(=_items[1])은 그대로(Hp=100,Mana=50),idx2=(Hp=7,Mana=0). - (D):
ref readonly로_items[1]을 복사 없이 읽음 →(Hp=100,Mana=50).
흔한 오해·함정
Entity copy = pool.GetRef(1);는 여전히 복사다. ref return 을 받으려면 받는 쪽도ref(또는ref readonly)여야 별칭이 유지된다.ref없이 받으면 그 자리에서 값 복사.slot = pool.GetRef(2);(ref 없음) vsslot = ref pool.GetRef(2);(ref 있음) 는 완전히 다르다: 전자는slot이 가리키는 위치(_items[1])에 _items[2]의 값을 복사 대입(원본 _items[1]을 덮어씀), 후자는slot을 _items[2]로 재바인딩. 이 구분이 ref local 의 가장 헷갈리는 부분.- 스택 이스케이프 방지: ref return 은 호출자보다 오래 사는 저장 위치만 가리킬 수 있다. 지역 변수/
stackalloc/메서드 파라미터(값)의 주소를 ref 로 반환하려 하면 컴파일러가 거부한다(ref safety,ref struct/Span<T>규칙과 동일 계열). 필드/배열 원소는 객체 수명에 묶여 안전. - ref 필드(C# 11)와
scoped는 이 안전성 규칙을 더 세분화한다.
면접 포인트
- ref return 이 "값이 아니라 저장 위치"를 돌려준다는 점과, 받는 쪽이
ref여야 별칭이 유지된다는 점. slot = expr(값 대입)과slot = ref expr(재바인딩)의 의미 차이.- ref safety(스택 이스케이프 방지)와 왜 지역/stackalloc 주소를 반환할 수 없는지, ECS·
Span<T>고성능 시나리오에서의 활용.