형식 매개 변수에 대한 제약 조건(C# 프로그래밍 가이드)
제약 조건은 형식 인수에서 갖추고 있어야 하는 기능을 컴파일러에 알립니다. 제약 조건이 없으면 형식 인수가 어떤 형식이든 될 수 있습니다. 컴파일러는 모든 .NET 형식의 궁극적인 기본 클래스인 System.Object의 멤버만 가정할 수 있습니다. 자세한 내용은 제약 조건을 사용하는 이유를 참조하세요. 클라이언트 코드가 제약 조건을 충족하지 않는 형식을 사용하는 경우 컴파일러는 오류를 발생시킵니다. 제약 조건은 where
상황별 키워드를 사용하여 지정됩니다. 다음 표에는 다양한 형식의 제약 조건이 나열되어 있습니다.
제약 조건 | 설명 |
---|---|
where T : struct |
형식 인수는 null을 허용하지 않는 record struct (을)를 포함하는 값 형식이어야 합니다. Null 허용 값 형식에 대한 자세한 내용은 Null 허용 값 형식을 참조하세요. 모든 값 형식에는 매개 변수가 없는 액세스 가능한 생성자가 선언되거나 암시적이므로 struct 제약 조건은 new() 제약 조건을 의미하며 new() 제약 조건과 결합할 수 없습니다. struct 제약 조건을 unmanaged 제약 조건과 결합할 수 없습니다. |
where T : class |
형식 인수는 참조 형식이어야 합니다. 이 제약 조건은 모든 클래스, 인터페이스, 대리자 또는 배열 형식에도 적용됩니다. null 허용 컨텍스트에서 T 는 null을 허용하지 않는 참조 형식이어야 합니다. |
where T : class? |
형식 인수는 null을 허용하거나 null을 허용하지 않는 참조 형식이어야 합니다. 이 제약 조건은 레코드를 포함하여 모든 클래스, 인터페이스, 대리자 또는 배열 형식에도 적용됩니다. |
where T : notnull |
형식 인수는 nullable이 아닌 형식이어야 합니다. 인수는 null을 허용하지 않는 참조 형식이거나 null을 허용하지 않는 값 형식일 수 있습니다. |
where T : unmanaged |
형식 인수는 nullable이 아닌 비관리형 형식이어야 합니다. unmanaged 제약 조건은 struct 제약 조건을 나타내며 struct 또는 new() 제약 조건과 결합할 수 없습니다. |
where T : new() |
형식 인수에 매개 변수가 없는 public 생성자가 있어야 합니다. 다른 제약 조건과 함께 사용할 경우 new() 제약 조건을 마지막에 지정해야 합니다. new() 제약 조건은 struct 또는 unmanaged 제약 조건과 결합할 수 없습니다. |
where T : <기본 클래스 이름> |
형식 인수가 지정된 기본 클래스이거나 지정된 기본 클래스에서 파생되어야 합니다. null 허용 컨텍스트에서 T 는 지정된 기본 클래스에서 파생된 Null을 허용하지 않는 참조 형식이어야 합니다. |
where T : <기본 클래스 이름>? |
형식 인수가 지정된 기본 클래스이거나 지정된 기본 클래스에서 파생되어야 합니다. nullable 컨텍스트에서 T (은)는 지정된 기본 클래스에서 파생된 nullable 또는 nullable이 아닌 형식일 수 있습니다. |
where T : <인터페이스 이름> |
형식 인수가 지정된 인터페이스이거나 지정된 인터페이스를 구현해야 합니다. 여러 인터페이스 제약 조건을 지정할 수 있습니다. 제약 인터페이스가 제네릭일 수도 있습니다. null 허용 컨텍스트에서 T 는 지정된 인터페이스를 구현하는 null을 허용하지 않는 형식이어야 합니다. |
where T : <인터페이스 이름>? |
형식 인수가 지정된 인터페이스이거나 지정된 인터페이스를 구현해야 합니다. 여러 인터페이스 제약 조건을 지정할 수 있습니다. 제약 인터페이스가 제네릭일 수도 있습니다. nullable 컨텍스트에서 T (은)는 nullable 참조 형식, nullable이 아닌 참조 형식 또는 값 형식일 수 있습니다. T (은)는 nullable 값 형식일 수 없습니다. |
where T : U |
T 에 대해 제공되는 형식 인수는 U 에 대해 제공되는 인수이거나 이 인수에서 파생되어야 합니다. nullable 컨텍스트에서 만약 U (이)가 nullable이 아닌 참조 형식인 경우 T (은)는 nullable이 아닌 참조 형식이어야 합니다. U (이)가 nullable 참조 형식인 경우 T (은)는 null 허용 또는 null을 허용하지 않을 수 있습니다. |
where T : default |
이 제약 조건은 메서드를 재정의하거나 명시적 인터페이스 구현을 제공할 때 비제한 형식 매개 변수를 지정해야 할 경우 모호성을 해결합니다. default 제약 조건은 class 또는 struct 제약 조건이 없는 기본 메서드를 의미합니다. 자세한 내용은 default 제약 조건 사양 제안을 참조하세요. |
where T : allows ref struct |
이 안티 제약 조건은 T 에 대한 형식 인수가 ref struct 형식이 될 수 있음을 선언합니다. 제네릭 형식이나 메서드는 ref struct 일 수 있기 때문에 T 의 모든 인스턴스에 대해 참조 안전 규칙을 따라야 합니다. |
일부 제약 조건은 상호 배타적이고 일부 제약 조건은 지정된 순서로 되어 있어야 합니다.
struct
,class
,class?
,notnull
및unmanaged
제약 조건 중 하나만 적용할 수 있습니다. 이러한 제약 조건을 제공하는 경우 해당 형식 매개 변수에 대해 지정된 첫 번째 제약 조건이어야 합니다.- 기본 클래스 제약 조건(
where T : Base
또는where T : Base?
)은struct
,class
,class?
,notnull
또는unmanaged
제약 조건과 결합할 수 없습니다. - 두 가지 형식으로 최대 하나의 기본 클래스 제약 조건을 적용할 수 있습니다. nullable 기본 형식을 지원하려면
Base?
(을)를 사용합니다. - 인터페이스의 nullable이 아닌 형식과 nullable 형식의 이름을 모두 제약 조건으로 지정할 수는 없습니다.
new()
제약 조건은struct
또는unmanaged
제약 조건과 결합할 수 없습니다.new()
제약 조건을 지정하는 경우 해당 형식 매개 변수에 대한 마지막 제약 조건이어야 합니다. 해당되는 경우 안티 제약 조건은new()
제약 조건을 따를 수 있습니다.default
제약 조건은 재정의 또는 명시적 인터페이스 구현에만 적용할 수 있습니다.struct
또는class
제약 조건과 결합할 수 없습니다.allows ref struct
안티 제약 조건은class
또는class?
제약 조건과 결합될 수 없습니다.allows ref struct
안티 제약 조건은 해당 형식 매개 변수에 대한 모든 제약 조건을 따라야 합니다.
제약 조건을 사용하는 이유
제약 조건은 형식 매개 변수의 기능 및 기대치를 지정합니다. 해당 제약 조건을 선언하면 제약 형식의 작업 및 메서드 호출을 사용할 수 있습니다. 제네릭 클래스 또는 메서드가 단순 할당 이외의 제네릭 멤버에 대한 작업을 사용하는 경우 형식 매개 변수에 제약 조건을 적용합니다. 여기에는 System.Object에 의해 지원되지 않는 메서드 호출이 포함됩니다. 예를 들어 기본 클래스 제약 조건은 이 형식의 개체 또는 이 형식에서 파생된 개체만 해당 형식 인수를 대체할 수 있음을 컴파일러에 알려줍니다. 컴파일러에 이 보장이 있으면 해당 형식의 메서드가 제네릭 클래스에서 호출되도록 허용할 수 있습니다. 다음 코드 예제에서는 기본 클래스 제약 조건을 적용하여 GenericList<T>
클래스(제네릭 소개에 있음)에 추가할 수 있는 기능을 보여 줍니다.
public class Employee
{
public Employee(string name, int id) => (Name, ID) = (name, id);
public string Name { get; set; }
public int ID { get; set; }
}
public class GenericList<T> where T : Employee
{
private class Node
{
public Node(T t) => (Next, Data) = (null, t);
public Node? Next { get; set; }
public T Data { get; set; }
}
private Node? head;
public void AddHead(T t)
{
Node n = new Node(t) { Next = head };
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node? current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
public T? FindFirstOccurrence(string s)
{
Node? current = head;
T? t = null;
while (current != null)
{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}
이 제약 조건을 통해 제네릭 클래스에서 Employee.Name
속성을 사용할 수 있습니다. 제약 조건은 T
형식의 모든 항목을 Employee
개체 또는 Employee
에서 상속하는 개체 중 하나로 보장하도록 지정합니다.
동일한 형식 매개 변수에 여러 개의 제약 조건을 적용할 수 있으며, 제약 조건 자체가 다음과 같이 제네릭 형식일 수 있습니다.
class EmployeeList<T> where T : notnull, Employee, IComparable<T>, new()
{
// ...
public void AddDefault()
{
T t = new T();
// ...
}
}
where T : class
제약 조건을 적용하는 경우 이러한 연산자는 값 같음이 아니라 참조 ID에 대해서만 테스트하므로 형식 매개 변수에서 ==
및 !=
연산자를 사용하지 않습니다. 이러한 연산자가 인수로 사용되는 형식에서 오버로드되는 경우에도 이 동작이 발생합니다. 다음 코드는 이 내용을 보여 줍니다. String 클래스가 ==
연산자를 오버로드하지만 출력이 false입니다.
public static void OpEqualsTest<T>(T s, T t) where T : class
{
System.Console.WriteLine(s == t);
}
private static void TestStringEquality()
{
string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();
OpEqualsTest<string>(s1, s2);
}
컴파일러에서 컴파일 시간에 T
가 참조 형식이고 모든 참조 형식에 유효한 기본 연산자를 사용해야 한다는 것만 인식합니다. 값 같음을 테스트해야 하는 경우 where T : IEquatable<T>
또는 where T : IComparable<T>
제약 조건을 적용하고 제네릭 클래스를 생성하는 데 사용되는 모든 클래스에서 인터페이스를 구현합니다.
여러 매개 변수 제한
다음 예제와 같이 여러 매개 변수에 제약 조건을 적용하고, 단일 매개 변수에 여러 제약 조건을 적용할 수 있습니다.
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }
바인딩되지 않은 형식 매개 변수
공용 클래스 SampleClass<T>{}
의 T와 같이 제약 조건이 없는 형식 매개 변수를 바인딩되지 않은 형식 매개 변수라고 합니다. 바인딩되지 않은 형식 매개 변수에는 다음 규칙이 있습니다.
- 구체적인 형식 인수가 이러한 연산자를 지원한다는 보장은 없으므로
!=
및==
연산자를 사용할 수 없습니다. System.Object
로/에서 변환하거나 임의의 인터페이스 형식으로 명시적으로 변환할 수 있습니다.- null과 비교할 수 있습니다. 바인딩되지 않은 매개 변수를
null
(와)과 비교하는 경우 형식 인수가 값 형식인 경우 비교는 항상 false를 반환합니다.
제약 조건으로 형식 매개 변수 사용
다음 예제와 같이 고유한 형식 매개 변수가 있는 멤버 함수가 해당 매개 변수를 포함 형식의 형식 매개 변수로 제약해야 하는 경우 제네릭 형식 매개 변수를 제약 조건으로 사용하면 유용합니다.
public class List<T>
{
public void Add<U>(List<U> items) where U : T {/*...*/}
}
앞의 예제에서 T
는 Add
메서드 컨텍스트에서는 형식 제약 조건이고, List
클래스 컨텍스트에서는 바인딩되지 않은 형식 매개 변수입니다.
제네릭 클래스 정의에서 형식 매개 변수를 제약 조건으로 사용할 수도 있습니다. 형식 매개 변수는 다른 형식 매개 변수와 함께 꺾쇠괄호 안에 선언해야 합니다.
//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }
컴파일러에서 형식 매개 변수가 System.Object
에서 파생된다는 점을 제외하고는 형식 매개 변수에 대해 아무 것도 가정할 수 없기 때문에, 제네릭 클래스에서 형식 매개 변수를 제약 조건으로 사용하는 경우는 제한됩니다. 두 형식 매개 변수 사이의 상속 관계를 적용하려는 시나리오에서 제네릭 클래스에 형식 매개 변수를 제약 조건으로 사용합니다.
notnull
제약 조건
notnull
제약 조건을 사용하여 형식 인수가 Null을 허용하지 않는 값 형식 또는 Null을 허용하지 않는 참조 형식이어야 함을 지정할 수 있습니다. 대부분 다른 제약 조건과 달리 형식 인수가 notnull
제약 조건을 위반하면 컴파일러는 오류 대신 경고를 생성합니다.
notnull
제약 조건은 null 허용 컨텍스트에서 사용되는 경우에만 영향을 미칩니다. null 허용 인식 불가능한 컨텍스트에서 notnull
제약 조건을 추가하면 컴파일러는 제약 조건 위반에 대한 경고 또는 오류를 생성하지 않습니다.
class
제약 조건
null 허용 컨텍스트의 class
제약 조건은 형식 인수가 null을 허용하지 않는 참조 형식이어야 함을 지정합니다. null 허용 컨텍스트에서 형식 인수가 null 허용 참조 형식이면 컴파일러는 경고를 생성합니다.
default
제약 조건
nullable 참조 형식 추가로 제네릭 형식 또는 메서드에서 T?
사용이 복잡해집니다. T?
는 struct
또는 class
제약 조건과 함께 사용할 수 있지만 둘 중 하나가 있어야 합니다. class
제약 조건을 사용한 경우 T?
는 T
의 nullable 참조 형식을 나타냈습니다. 제약 조건이 적용되지 않을 때 T?
를 사용할 수 있습니다. 이 경우 값 형식 및 참조 형식에 대해 T?
는 T?
로 해석됩니다. 그러나 T
가 Nullable<T>의 인스턴스인 경우 T?
는 T
와 동일합니다. 즉, T??
가 되지 않습니다.
이제 T?
를 class
또는 struct
제약 조건 없이 사용할 수 있으므로 재정의 또는 명시적 인터페이스 구현에서 모호성이 발생할 수 있습니다. 두 경우 모두에서 재정의는 제약 조건을 포함하지 않지만 기본 클래스에서 상속합니다. 기본 클래스가 class
또는 struct
제약 조건을 적용하지 않는 경우 파생 클래스는 둘 중 어떤 제약 조건도 없이 기본 메서드에 재정의가 적용되도록 지정해야 합니다. 파생 메서드는 default
제약 조건을 적용합니다. default
제약 조건은 class
또는 struct
제약 조건을 ‘모두’ 명확하게 지정하지 않습니다.
관리되지 않는 제약 조건
unmanaged
제약 조건을 사용하여 형식 매개 변수가 null을 허용하지 않는 관리되지 않는 형식이어야 함을 지정할 수 있습니다. unmanaged
제약 조건을 사용하면 다음 예제와 같이 메모리 블록으로 조작할 수 있는 형식을 사용하도록 재사용 가능한 루틴을 작성할 수 있습니다.
unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
var size = sizeof(T);
var result = new Byte[size];
Byte* p = (byte*)&argument;
for (var i = 0; i < size; i++)
result[i] = *p++;
return result;
}
앞의 메서드는 기본 제공 형식으로 알려지지 않은 형식에서 sizeof
연산자를 사용하므로 unsafe
컨텍스트에서 컴파일해야 합니다. unmanaged
제약 조건이 없으면 sizeof
연산자를 사용할 수 없습니다.
unmanaged
제약 조건은 struct
제약 조건을 나타내며 함께 사용할 수 없습니다. struct
제약 조건은 new()
제약 조건을 나타내며 unmanaged
제약 조건은 new()
제약 조건과 결합할 수 없습니다.
대리자 제약 조건
기본 클래스 제약 조건으로 System.Delegate 또는 System.MulticastDelegate를 사용할 수 있습니다. CLR에서는 항상 이 제약 조건을 허용했지만, C# 언어에서는 이 제약 조건을 허용하지 않았습니다. System.Delegate
제약 조건을 사용하면 형식이 안전한 방식으로 대리자에서 작동하는 코드를 작성할 수 있습니다. 다음 코드는 두 대리자가 동일한 형식인 경우 이를 결합하는 확장 메서드를 정의합니다.
public static TDelegate? TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
where TDelegate : System.Delegate
=> Delegate.Combine(source, target) as TDelegate;
동일한 형식의 대리자를 결합하려면 이전 방법을 사용할 수 있습니다.
Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");
var combined = first.TypeSafeCombine(second);
combined!();
Func<bool> test = () => true;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);
마지막 줄의 주석 처리를 제거하면 컴파일되지 않습니다. first
및 test
는 모두 대리자 형식이지만 서로 다른 대리자 형식입니다.
열거형 제약 조건
System.Enum 형식을 기본 클래스 제약 조건으로 지정할 수도 있습니다. CLR에서는 항상 이 제약 조건을 허용했지만, C# 언어에서는 이 제약 조건을 허용하지 않았습니다. System.Enum
을 사용하는 제네릭은 System.Enum
의 정적 메서드를 사용하여 결과를 캐시하기 위해 형식이 안전한 프로그래밍을 제공합니다. 다음 샘플에서는 열거형 형식에 유효한 값을 모두 찾은 다음, 해당 값을 문자열 표현에 매핑하는 사전을 작성합니다.
public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
var result = new Dictionary<int, string>();
var values = Enum.GetValues(typeof(T));
foreach (int item in values)
result.Add(item, Enum.GetName(typeof(T), item)!);
return result;
}
Enum.GetValues
및 Enum.GetName
은 성능에 영향을 미치는 리플렉션을 사용합니다. 리플렉션이 필요한 호출을 반복하는 대신, EnumNamedValues
를 호출하여 캐시되고 다시 사용되는 컬렉션을 작성할 수 있습니다.
다음 샘플과 같이 이 메서드는 열거형을 만들고 해당 값과 이름의 사전을 작성하는 데 사용할 수 있습니다.
enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}
var map = EnumNamedValues<Rainbow>();
foreach (var pair in map)
Console.WriteLine($"{pair.Key}:\t{pair.Value}");
형식 인수는 선언된 인터페이스를 구현함
일부 시나리오에서는 형식 매개 변수에 제공된 인수가 해당 인터페이스를 구현해야 합니다. 예시:
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
static abstract T operator +(T left, T right);
static abstract T operator -(T left, T right);
}
이 패턴을 사용하면 C# 컴파일러가 오버로드된 연산자나 static virtual
또는 static abstract
메서드에 대한 포함 형식을 결정할 수 있습니다. 포함 형식에 더하기 및 빼기 연산자를 정의할 수 있도록 구문을 제공합니다. 이 제약 조건이 없으면 형식 매개 변수가 아닌 매개 변수와 인수를 인터페이스로 선언해야 합니다.
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
static abstract IAdditionSubtraction<T> operator +(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
static abstract IAdditionSubtraction<T> operator -(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
}
앞의 구문에서는 구현자가 해당 메서드에 대해 명시적 인터페이스 구현을 사용해야 합니다. 추가 제약 조건사용 약관을 제공하면 인터페이스가 형식 매개 변수 측면에서 연산자를 정의할 수 있습니다. 인터페이스를 구현하는 형식은 인터페이스 메서드를 암시적으로 구현할 수 있습니다.
ref struct 허용
allows ref struct
안티 제약 조건은 해당 형식 인수가 ref struct
형식이 될 수 있음을 선언합니다. 해당 형식 매개 변수의 인스턴스는 다음 규칙을 따라야 합니다.
- 상자에 넣을 수 없습니다.
- ref safety 규칙에 참여합니다.
ref struct
형식이 허용되지 않는 경우(예:static
필드) 인스턴스를 사용할 수 없습니다.- 인스턴스는
scoped
한정자로 표시될 수 있습니다.
allows ref struct
절은 상속되지 않습니다. 다음 코드에서:
class SomeClass<T, S>
where T : allows ref struct
where S : T
{
// etc
}
S
에 대한 인수는 ref struct
가 될 수 없습니다. S
에 allows ref struct
절이 없기 때문입니다.
allows ref struct
절이 있는 형식 매개 변수는 해당 형식 매개 변수에도 allows ref struct
절이 없는 한 형식 인수로 사용할 수 없습니다. 다음 예에서는 이 규칙을 보여 줍니다.
public class Allow<T> where T : allows ref struct
{
}
public class Disallow<T>
{
}
public class Example<T> where T : allows ref struct
{
private Allow<T> fieldOne; // Allowed. T is allowed to be a ref struct
private Disallow<T> fieldTwo; // Error. T is not allowed to be a ref struct
}
이전 샘플은 ref struct
형식일 수 있는 형식 인수가 ref struct
형식이 될 수 없는 형식 매개 변수로 대체될 수 없음을 보여 줍니다.
참고 항목
.NET