← 문제로

16. readonly struct · in 매개변수 · 방어적 복사(defensive copy)

난이도 상
내 리뷰 · C#
해설 · C#

해설 — readonly struct · in 매개변수 · 방어적 복사(defensive copy)

난이도: 상

요약

가변 구조체의 비 readonly 메서드를 readonly 컨텍스트(readonly 필드, in 매개변수, ref readonly)에서 호출하면, 컴파일러가 "이 메서드가 상태를 바꿀 수 있다"고 가정해 호출 전에 구조체를 복사하고 그 복사본에서 메서드를 실행한다. 그래서 변경이 원본에 반영되지 않고(정확성), 매 호출마다 복사가 생긴다(성능).

예측 출력

a=11 b=11 field=10
n1=101 n2=101 local=100
d1=1 d2=2 direct=2

핵심 개념 · 단계별 내부 동작

  • (1) readonly 필드: s_counterstatic readonly. Next()Value++ 를 하는 비 readonly 메서드라 readonly 위치에서 직접 호출하면 readonly 위반이 된다. 컴파일러는 이를 막는 대신 s_counter 를 임시 복사한 뒤 복사본에서 Next() 를 호출한다.
    • a: 복사본 10→11, 반환 11. 원본은 그대로 10.
    • b: 다시 원본(여전히 10) 에서 복사 → 10→11, 반환 11.
    • field: s_counter.Value = 10(한 번도 안 바뀜). → a=11 b=11 field=10.
  • (2) in 매개변수: in Counter c 는 읽기 전용 참조다. c.Next() 호출 시 같은 이유로 매 호출 방어적 복사. local.Value=100 은 그대로, n1=n2=101.
  • (3) 직접 호출: direct 는 평범한 가변 지역 변수 → 복사 없이 제자리 변경. d1=1, d2=2, direct=2.

흔한 오해·함정

  • "in 으로 넘기면 빨라진다"는 절반만 맞다: 큰 구조체를 복사 없이 참조로 넘기려고 in 을 쓰지만, 그 안에서 비 readonly 멤버(메서드·프로퍼티 getter 포함) 를 호출하면 오히려 매번 방어적 복사가 생겨 더 느려질 수 있다.
  • 프로퍼티 getter 도 메서드다: readonly 가 아닌 getter 는 컴파일러가 변경 가능으로 보아 복사를 유발한다.
  • 변경이 조용히 사라짐: 가변 구조체를 readonly 필드/in 으로 다루면 메서드의 상태 변경이 원본에 안 남아 디버깅이 어렵다(csharp_internals/problem3 가변 구조체 함정과 한 묶음).

수정안(복사 제거)

  • 구조체를 readonly struct: 모든 멤버가 불변임을 컴파일러가 알면 방어적 복사가 사라진다.
  • 변경 없는 멤버에 readonly 한정자: public readonly int Peek() => Value; 처럼 메서드/프로퍼티에 readonly 를 붙이면 그 호출엔 복사가 안 생긴다.
  • 가변 의미가 필요하면 ref: 진짜로 제자리 변경을 원하면 in 이 아니라 ref Counter 로 넘긴다.
  • 큰 데이터는 class 고려: 값 의미가 꼭 필요 없으면 참조 타입이 복사 문제를 원천 제거.

면접 포인트

  • 방어적 복사가 끼어드는 정확한 조건(readonly 필드/in/ref readonly 위에서 비 readonly 멤버 호출)과 그 비용.
  • readonly struct/readonly 멤버가 어떻게 복사를 제거하는가, 프로퍼티 getter 도 복사 유발 대상인 점.
  • in 매개변수 최적화의 함정 — 큰 구조체를 넘겨도 비 readonly 멤버 접근이 있으면 이득이 사라진다.