where
where (제네릭 형식 제약 조건)
제네릭 정의에서는 where 절을 써서 제네릭 형식·메서드·대리자·로컬 함수의 형식 매개변수에 넘길 수 있는 형식에 제약을 걸어요. 인터페이스나 기본 클래스를 지정할 수도 있고, 특정 형식이 참조 형식·값 형식·관리되지 않는 형식(unmanaged type)이어야 한다고 요구할 수도 있죠. 이런 제약은 '형식 인수(type argument)가 반드시 갖춰야 하는 능력'을 선언하는 거예요. where 절은 선언된 기본 클래스나 구현한 인터페이스 뒤에 둡니다.
예를 들어, 형식 매개변수 T가 IComparable<T> 인터페이스를 구현하도록 제약를 건 제네릭 클래스 AGenericClass를 이렇게 선언할 수 있어요.
public class AGenericClass<T> where T : IComparable<T> { }
[!NOTE] 쿼리 식에서의 where 절이 궁금하다면 where 절을 참고하세요.
본문
where 절에는 기본 클래스 제약 조건(base class constraint)도 넣을 수 있어요. 기본 클래스 제약은 '그 제네릭 형식의 형식 인수로 쓸 형식이 지정된 클래스를 기본 클래스로 갖거나, 바로 그 기본 클래스 자신이어야 한다'는 뜻이에요. 기본 클래스 제약을 쓰면 다른 제약보다 앞에 와야 해요. 그런데 System.Object, System.Array, System.ValueType는 기본 클래스 제약으로 쓸 수 없어요. 다음 예시가 기본 클래스로 지정할 수 있는 형식들을 보여줍니다.
public class UsingEnum<T> where T : System.Enum { }
public class UsingDelegate<T> where T : System.Delegate { }
public class Multicaster<T> where T : System.MulticastDelegate { }
nullable 컨텍스트에서는 컴파일러가 기본 클래스 형식의 null 허용 여부까지 강제해요. 기본 클래스가 null을 허용하지 않으면(예: Base) 형식 인수도 null을 허용하지 않아야 하고, 기본 클래스가 nullable이면(예: Base?) 형식 인수는 nullable·non-nullable 참조 형식 어느 쪽이든 허용돼요. 기본 클래스가 non-nullable인데 형식 인수가 nullable 참조 형식이면 컴파일러가 경고를 냅니다.
where 절은 형식이 class인지 struct인지도 지정할 수 있어요. struct 제약을 쓰면 System.ValueType 기본 클래스 제약을 굳이 따로 지정하지 않아도 돼요. System.ValueType는 애초에 기본 클래스 제약으로 쓸 수 없고요. 다음 예시가 class 제약과 struct 제약을 모두 보여줍니다.
class MyClass<T, U>
where T : class
where U : struct
{ }
nullable 컨텍스트에서 class 제약은 형식이 non-nullable 참조 형식이어야 한다고 요구해요. nullable 참조 형식까지 허용하려면 class? 제약을 쓰면 되는데, 그러면 nullable·non-nullable 참조 형식 모두 통과시킵니다.
where 절에는 notnull 제약도 포함할 수 있어요. notnull 제약은 형식 매개변수를 null이 아닌 형식으로 제한해요. 그 형식은 값 형식이거나 non-nullable 참조 형식이면 돼요. notnull 제약은 nullable enable 컨텍스트에서 컴파일된 코드에만 쓸 수 있어요. 다른 제약과 달리, 형식 인수가 notnull 제약을 어기면 컴파일러는 오류 대신 경고만 냅니다. 그리고 경고도 nullable enable 컨텍스트에서만 생성돼요.
#nullable enable
class NotNullContainer<T>
where T : notnull
{
}
#nullable restore
nullable 참조 형식이 도입되면서 제네릭 메서드에서 T?의 의미에 모호함이 생길 수 있어요. T가 struct라면 T?는 System.Nullable<T>와 같지만, T가 참조 형식이라면 T?는 null이 유효한 값이라는 뜻이니까요. 문제는 재정의(override) 메서드는 제약을 못 붙인다는 데서 옵니다. 여기서 새로 나온 default 제약이 이 모호함을 풀어줘요. 기본 클래스나 인터페이스가 메서드를 두 가지로 오버로드한다면 — 하나는 struct 제약을 명시하고, 다른 하나는 struct도 class도 적용하지 않은 경우 — default 제약을 붙이면 됩니다.
public abstract class B
{
public void M<T>(T? item) where T : struct { }
public abstract void M<T>(T? item);
}
default 제약은 파생 클래스나 명시적 인터페이스 구현에서 '제약이 없는 그 메서드'를 재정의하겠다고 지정할 때 써요. 이 제약은 기본 메서드를 재정의하거나 명시적 인터페이스를 구현하는 메서드에서만 유효합니다.
public class D : B
{
// Without the "default" constraint, the compiler tries to override the first method in B
public override void M<T>(T? item) where T : default { }
}
[!IMPORTANT]
notnull제약이 포함된 제네릭 선언은 nullable oblivious 컨텍스트에서 쓸 수는 있지만, 컴파일러가 그 제약을 강제하지는 않아요.
where 절에는 unmanaged 제약도 쓸 수 있어요. unmanaged 제약은 형식 매개변수를 관리되지 않는 형식으로 알려진 형식으로 한정하지요. 이 제약 덕분에 C#에서 저수준 interop 코드를 훨씬 쉽게 짤 수 있어요. 모든 관리되지 않는 형식에 걸쳐 재사용 가능한 루틴을 만들 수 있거든요. unmanaged 제약은 class나 struct 제약과 함께 쓸 수 없어요. 그리고 형식이 반드시 struct여야 한다는 것까지 강제합니다.
class UnManagedWrapper<T>
where T : unmanaged
{ }
where 절에는 생성자 제약 조건 new()도 포함할 수 있어요. 이 제약이 있으면 new 연산자로 형식 매개변수의 인스턴스를 만들 수 있습니다. new() 제약은 컴파일러에게 '어떤 형식 인수가 와도 접근 가능한 매개변수 없는 생성자를 갖고 있어야 한다'는 걸 알려주죠. 예를 들어 이런 식이에요.
public class MyGenericClass<T> where T : IComparable<T>, new()
{
// The following line is not possible without new() constraint:
T item = new T();
}
new() 제약은 where 절에서 맨 마지막에 와요. 단, 뒤에 allows ref struct 안티-제약(anti-constraint)이 붙는 경우는 예외예요. new() 제약은 struct나 unmanaged 제약과는 함께 쓸 수 없어요. 그 제약들을 만족하는 형식은 모두 접근 가능한 매개변수 없는 생성자를 갖고 있기 때문에 new() 제약이 중복되거든요.
이 안티-제약은 T의 형식 인수가 ref struct 형식일 수 있다고 선언해요. 예를 들어 이런 모양이지요.
public class GenericRefStruct<T> where T : allows ref struct
{
// Scoped is allowed because T might be a ref struct
public void M(scoped T parm)
{
}
}
T가 ref struct일 수 있기 때문에, 제네릭 형식이나 메서드는 T의 어떤 인스턴스에 대해서도 ref 안전성 규칙을 지켜야 해요. allows ref struct 절은 class나 class? 제약과 함께 쓸 수 없어요. 그리고 이 안티-제약은 반드시 해당 형식 인수의 모든 제약 뒤에 와야 합니다.
형식 매개변수가 여러 개라면 각 매개변수마다 where 절을 하나씩 써요. 예를 들면 이렇습니다.
public interface IMyInterface { }
namespace CodeExample
{
class Dictionary<TKey, TVal>
where TKey : IComparable<TKey>
where TVal : IMyInterface
{
public void Add(TKey key, TVal val) { }
}
}
제네릭 메서드의 형식 매개변수에도 제약을 붙일 수 있어요. 다음 예시를 보면 금방 감이 오죠.
public class Container
{
public void MyMethod<T>(T t) where T : IMyInterface { }
}
대리자(delegate)의 형식 매개변수에 제약을 다는 문법도 메서드와 똑같아요.
delegate T MyDelegate<T>() where T : new();
제네릭 대리자에 대한 자세한 내용은 제네릭 대리자를 참고하세요.
제약 조건의 문법과 사용법을 더 자세히 알고 싶다면 형식 매개변수에 대한 제약 조건 문서를 보시면 됩니다.