← 문제로

46. params 가변 인자: 오버로드 해석과 호출당 배열 할당

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

해설 — params 가변 인자: 오버로드 해석과 호출당 배열 할당

난이도: 중

요약

params 는 문법 설탕이다. 컴파일러는 흩어진 인자들을 호출 지점에서 새 배열로 묶어(expanded form) 전달한다. 일반(고정 인자) 오버로드가 적용되면 params 오버로드보다 항상 우선한다. 인자가 여러 개면 호출마다 배열이 힙에 할당되지만, 인자 0개는 컴파일러가 Array.Empty<T>() 로 처리해 할당이 없다.

출력

exact(int,int)
params(len=3)
params(len=0)
params(len=2)
hit / argc=2

핵심 개념 / 각 호출 분석

  • (1) Which(1,2) → exact(int,int): Which(int,int) 는 일반 형식(normal form) 으로, Which(params int[]) 는 확장 형식(expanded form) 으로만 적용된다. 오버로드 해석은 일반 형식으로 적용되는 후보를 확장 형식으로만 적용되는 후보보다 더 낫다 고 판정한다 → 고정 오버로드 선택. 배열 할당 없음.
  • (2) Which(1,2,3) → params(len=3): 고정 오버로드가 없어 params 확장 형식만 적용. 컴파일러가 new int[]{1,2,3} 를 호출마다 새로 할당해 넘긴다. → int[3] 힙 할당 1회.
  • (3) Which() → params(len=0): 확장 형식, 원소 0개. Roslyn 은 빈 배열에 대해 Array.Empty<int>() (프로세스 전역 캐시된 싱글턴)를 넘기므로 할당 없음.
  • (4) Which(new int[]{1,2}) → params(len=2): int[] 를 직접 넘기면 params 매개변수의 일반 형식(배열 하나를 그대로 받는 형태)으로 바인딩된다. 컴파일러는 확장하지 않고 넘긴 배열을 그대로 사용한다. 여기서 배열 할당은 new int[]{1,2} 리터럴 자체이지, params 확장 때문이 아니다.
  • (5) Log("hit", 42, "orc") → hit / argc=2: params object[] 이므로 object[]{ (object)42, "orc" } 가 할당된다. 값 형식 42 는 object 로 담기며 박싱된다(힙에 박스 객체 1개 + object[2] 배열 1개).

단계별 내부 동작

  1. 컴파일러가 호출 지점에서 각 인자의 형식 변환 가능성으로 후보 집합을 만든다.
  2. 각 후보를 "일반 형식으로 적용 가능한가 / 확장 형식으로만 적용 가능한가" 로 분류한다.
  3. 더 나은 함수 멤버(better function member) 규칙: 일반 형식 후보 > 확장 형식 후보. 그래서 (1)에서 고정 오버로드가 이긴다.
  4. 확장 형식이 선택되면, 컴파일러가 newarr + 원소 대입 IL 을 삽입해 배열을 만든다. 인자 0개는 Array.Empty<T>() 로 최적화한다.

흔한 오해·함정

  • "params 는 공짜" — 아니다. 인자 1개 이상이면 호출마다 배열 할당이 일어난다. 핫 패스(틱 루프·로깅) 에서 params object[] 는 배열 + 박싱으로 GC 압박을 만든다.
  • 핫 경로 회피 패턴: 인자 수별 고정 오버로드(Log(string), Log(string, object), ...)를 제공하고 마지막에 params 폴백을 두면, 흔한 소량 인자 호출에서 할당·박싱을 없앤다(BCL 의 여러 API 가 이 방식).
  • 명시적 배열 vs 확장: 배열을 직접 넘기면 재사용되지만, 그 배열을 메서드가 보관·수정하면 호출자 배열이 오염될 수 있다.
  • null 전달: Which(null) 은 int[] 오버로드엔 안 맞지만 params object[] 류에선 args == null (빈 배열 아님)이 되어 NullReferenceException 을 유발할 수 있다.

면접 포인트

  • params 의 일반 형식 vs 확장 형식과 오버로드 "betterness"(고정 오버로드 우선).
  • 호출당 배열 할당·Array.Empty<T>() 최적화·params object[] 의 박싱 비용과 핫 패스 회피 패턴.
  • 명시적 배열 전달 시 확장이 일어나지 않는다는 점과 그 부작용(배열 공유).