C# in 키워드 정리

C#에서 메서드를 정의할 때 in 키워드를 사용하면 call by value 가 아닌 call by reference로 동작하게 만들어줍니다. 그래서 주로 구조체(value type)를 복사 없이 참조로 넘길 때 유용하지요.

참조 타입의 매개변수에 in 키워드를 쓰는 건 어떤 효과가 있을까요? generic parameter를 정의할 때도 in 키워드가 붙는 경우도 있던데 어떤 의미일까요?
이참에 한 번 정리해 보겠습니다.

위쪽에 스샷으로 붙인 learn.microsoft.com의 문서에는 다섯 가지 쓰임새를 소개합니다.

  1. 제네릭 타입 매개변수에서 아용
  2. 메서드 매개변수에서 값 대신 참조를 전달하도록 지정
  3. foreach 문에서 사용
  4. linq의 from 절에서 사용
  5. linq의 in 절에서 사용

이 중에서 3, 4, 5는 이번 포스팅의 관심사가 아닙니다. 2번을 먼저 정리하고, 그 다음 1번도 살펴보겠습니다.

구조체를 인자로 넘길 때 in 사용

in 키워드를 사용하여 전달받은 매개변수는 수정이 불가한 읽기 전용 참조입니다. 사이즈가 큰 구조체를 readonly로 전달할 때 사용하면 성능상의 이득을 볼 수 있습니다.

1
2
3
4
5
6
7
8
void Print(in Point point)
{
// point.X = 10; // 컴파일 에러. 값을 수정할 수 없습니다.
Console.WriteLine(point.X);
}

Point point = new Point(10, 20);
Print(point);

value type 모두에 해당하는 이야기 이지만, 8byte를 넘지 않는 premitive type이나 작은 구조체인 경우에는 성능상의 이득은 없습니다.

참조 타입에 in을 붙였을 때 차이점

매개변수가 참조타입일 때 in 키워드를 사용하는 것은 어떤 효과가 있을까요?

1. 참조 변수가 readonly 입니다.

1
2
3
4
5
6
7
void Print(in List<int> list)
{
list.Add(10); // 컨테이너에 값을 넣을 수는 있습니다.
list = new List<int>(); // CS8331: 변수에 새로운 객체를 할당할 수는 없습니다.

Console.WriteLine(list.Count);
}

위 코드를 C++의 std::vector<int>로 비유할 때, listconst std::vector<int>&와 같은 의미일 거라고 생각하는 것이 흔히 하는 실수입니다.
하지만 std::vector<int> const *와 같이 동작합니다. list컨테이너에 값을 넣을 수는 있지만, list 변수 자체를 다른 객체로 바꿀 수는 없습니다.

그러니 C#과 꼭 어울리는 표현은 아니지만, 참조형 매개변수에 in 키워드를 쓰는 것은 포인터 변수를 const로 만들어주는 효과가 있다고 말할 수 있습니다.
실전에선 이게 그렇게 의미있게 쓰이는 일이 많지는 않을 듯 합니다.

2. 호출하는 곳에서 암시적 변환이 발생하지 않도록 합니다.

1
2
3
4
5
6
7
8
9
void Print(in IList<int> list)
{
Console.WriteLine(list.Count);
}

List<int> list = new List<int>();
Print(list); // ok
Print(in list); // error CS1503: 1 인수: 'in System.Collections.Generic.List<int>'에서 'in System.Collections.Generic.IList<int>'(으)로 변환할 수 없습니다.
Print(in list as IList<int>); // error CS8156: 식은 참조로 전달되거나 반환될 수 없으므로 이 컨텍스트에서 사용할 수 없습니다.

호출하는 곳에서 in을 적지 않으면 문제가 없는데, in을 적으면 컴파일 에러가 발생합니다.
이는 List<int>IList<int>로 암시적 변환이 가능하지만, in 키워드를 사용하면 암시적 변환이 일어나지 않기 때문입니다.
이걸 암시적 변환(implicit casting)이라고 봐야 할지 모르겠습니다. List<int>IList<int> 인터페이스를 구현했기 때문에, upcasting인 셈이지만, in 키워드를 사용하면 이것도 불가능해집니다.
as 키워드를 써서 명시적으로 변환해 주어도 에러를 피할 수 없습니다. 이 땐 ref, out 키워드에 expression을 참조로 전달할 수 없는 것과 같은 이유로 컴파일 에러가 발생합니다.
아예 다른 타입으로 변환하는 ‘찐’ 암시적 변환을 막아주는 예제는 아래와 같습니다. 아래 것은 좀 더 그럴싸 하지요.

1
2
3
4
5
6
7
8
void Print(in int value)
{
Console.WriteLine(value);
}

short value = 10;
Print(value); // ok
Print(in value); // error CS1503: 1 인수: 'in short'에서 'in int'(으)로 변환할 수 없습니다.

제네릭 타입 매개변수에서 사용

약간은 벗어나는 이야기 일 수도 있지만, in 키워드에 대해 정리하는 김에 같이 적어봅니다.

C#에서 제네릭 타입 매개변수에 in 키워드를 사용하는 것은 공변성(covariance)과 반공변성(contravariance)의 개념과 관련이 있습니다. in 키워드는 반공변성을 나타내며, 이는 특정 타입 매개변수가 제네릭 타입의 입력으로만 사용될 수 있음을 의미합니다.

공변성(covariance) vs 반공변성(contravariance)

  • 공변성: 제네릭 타입에서 반환되는 값의 타입을 더 구체적인 하위 타입으로 사용할 수 있게 하는 것. out 키워드로 표현됩니다.
  • 반공변성: 제네릭 타입에서 파라미터로 전달하는 값을 더 일반적인 상위 타입으로 사용할 수 있게 하는 것. in 키워드로 표현됩니다.

in 키워드의 효과

in 키워드를 사용하면 제네릭 타입 매개변수가 반공변성을 가지게 됩니다. 즉, 더 일반적인 타입의 객체를 사용할 수 있습니다. 이때 해당 타입 매개변수는 입력으로만 사용되며, 반환값으로는 사용할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface IComparer<in T>
{
int Compare(T x, T y);
}

public class Animal { }
public class Dog : Animal { }

public class AnimalComparer : IComparer<Animal>
{
public int Compare(Animal x, Animal y)
{
return 0; // 단순 비교
}
}

IComparer<Dog> dogComparer = new AnimalComparer(); // 허용됨 (반공변성)

위 코드에서 IComparer<in T> 인터페이스는 T 타입 매개변수를 입력으로만 사용하므로, IComparer<Animal>IComparer<Dog>에 할당할 수 있게 됩니다.
간단하게 여기까지만. 공변성과 반공변성에 대한 이야기는 다음에 기회가 되면 다른 포스팅에서 정리해보죠.

결론

  • in 키워드는 주로 크기가 큰 값 타입의 매개변수를 읽기 전용 참조로 전달할 때 사용합니다.
  • 참조 타입의 매개변수에 in 키워드를 사용하면 변수의 값을 readonly로 만들어줍니다.
  • 호출하는 곳에서 in 키워드를 사용하면 암시적 변환이 발생하지 않습니다.
  • 제네릭 타입 매개변수에 in 키워드를 사용하면 반공변성을 가지게 됩니다.

참고