형식 매개 변수에 대한 제약 조건(C# 프로그래밍 가이드)

제약 조건은 형식 인수에서 갖추고 있어야 하는 기능을 컴파일러에 알립니다. 제약 조건이 없으면 형식 인수가 어떤 형식이든 될 수 있습니다. 컴파일러는 모든 .NET 형식의 궁극적인 기본 클래스인 System.Object의 멤버만 가정할 수 있습니다. 자세한 내용은 제약 조건을 사용하는 이유를 참조하세요. 클라이언트 코드가 제약 조건을 충족하지 않는 형식을 사용하는 경우 컴파일러는 오류를 발생시킵니다. 제약 조건은 where 상황별 키워드를 사용하여 지정됩니다. 다음 표에는 다양한 형식의 제약 조건이 나열되어 있습니다.

제약 조건 설명
where T : struct 형식 인수는 null을 허용하지 않는 값 형식이어야 합니다. Null 허용 값 형식에 대한 자세한 내용은 Null 허용 값 형식을 참조하세요. 모든 값 형식에 액세스할 수 있는 매개 변수가 없는 생성자가 있으므로, struct 제약 조건은 new() 제약 조건을 나타내고 new() 제약 조건과 결합할 수 없습니다. struct 제약 조건을 unmanaged 제약 조건과 결합할 수 없습니다.
where T : class 형식 인수는 참조 형식이어야 합니다. 이 제약 조건은 모든 클래스, 인터페이스, 대리자 또는 배열 형식에도 적용됩니다. C# 8.0 이상의 null 허용 컨텍스트에서 T는 null을 허용하지 않는 참조 형식이어야 합니다.
where T : class? 형식 인수는 null을 허용하거나 null을 허용하지 않는 참조 형식이어야 합니다. 이 제약 조건은 모든 클래스, 인터페이스, 대리자 또는 배열 형식에도 적용됩니다.
where T : notnull 형식 인수는 nullable이 아닌 형식이어야 합니다. 인수는 C# 8.0 이상의 null을 허용하지 않는 참조 형식이거나 null을 허용하지 않는 값 형식일 수 있습니다.
where T : default 이 제약 조건은 메서드를 재정의하거나 명시적 인터페이스 구현을 제공할 때 비제한 형식 매개 변수를 지정해야 할 경우 모호성을 해결합니다. default 제약 조건은 class 또는 struct 제약 조건이 없는 기본 메서드를 의미합니다. 자세한 내용은 제약 조건 사양 제안을 참조하세요.
where T : unmanaged 형식 인수는 nullable이 아닌 비관리형 형식이어야 합니다. unmanaged 제약 조건은 struct 제약 조건을 나타내며 struct 또는 new() 제약 조건과 결합할 수 없습니다.
where T : new() 형식 인수에 매개 변수가 없는 public 생성자가 있어야 합니다. 다른 제약 조건과 함께 사용할 경우 new() 제약 조건을 마지막에 지정해야 합니다. new() 제약 조건은 struct 또는 unmanaged 제약 조건과 결합할 수 없습니다.
where T :where T : 형식 인수가 지정된 기본 클래스이거나 지정된 기본 클래스에서 파생되어야 합니다. C# 8.0 이상의 null 허용 컨텍스트에서 T는 지정된 기본 클래스에서 파생된 null을 허용하지 않는 참조 형식이어야 합니다.
where T :where T : 형식 인수가 지정된 기본 클래스이거나 지정된 기본 클래스에서 파생되어야 합니다. C# 8.0 이상의 null 허용 컨텍스트에서 T는 지정된 기본 클래스에서 파생된 null을 허용하거나 null을 허용하지 않는 형식일 수 있습니다.
where T :where T : 형식 인수가 지정된 인터페이스이거나 지정된 인터페이스를 구현해야 합니다. 여러 인터페이스 제약 조건을 지정할 수 있습니다. 제약 인터페이스가 제네릭일 수도 있습니다. C# 8.0 이상의 null 허용 컨텍스트에서 T는 지정된 인터페이스를 구현하는 null을 허용하지 않는 형식이어야 합니다.
where T :where T : 형식 인수가 지정된 인터페이스이거나 지정된 인터페이스를 구현해야 합니다. 여러 인터페이스 제약 조건을 지정할 수 있습니다. 제약 인터페이스가 제네릭일 수도 있습니다. C# 8.0 이상의 null 허용 컨텍스트에서 T는 null 허용 참조 형식, null을 허용하지 않는 참조 형식 또는 값 형식이어야 합니다. T는 null 허용 값 형식이 아닐 수 있습니다.
where T : U T에 대해 제공되는 형식 인수는 U에 대해 제공되는 인수이거나 이 인수에서 파생되어야 합니다. null 허용 컨텍스트에서 U가 null을 허용하지 않는 참조 형식인 경우 T는 null을 허용하지 않는 참조 형식이어야 합니다. U가 null 허용 참조 형식인 경우 T는 null을 허용하거나 null을 허용하지 않는 참조 형식일 수 있습니다.

제약 조건을 사용하는 이유

제약 조건은 형식 매개 변수의 기능 및 기대치를 지정합니다. 해당 제약 조건을 선언하면 제약 형식의 작업 및 메서드 호출을 사용할 수 있습니다. 제네릭 클래스 또는 메서드가 단순 할당 또는 System.Object에서 지원하지 않는 메서드 호출 이외의 작업을 제네릭 멤버에서 사용하는 경우 형식 매개 변수에 제약 조건을 적용합니다. 예를 들어 기본 클래스 제약 조건은 이 형식의 개체나 이 형식에서 파생된 개체만 형식 인수로 사용된다고 컴파일러에 알립니다. 컴파일러에 이 보장이 있으면 해당 형식의 메서드가 제네릭 클래스에서 호출되도록 허용할 수 있습니다. 다음 코드 예제에서는 기본 클래스 제약 조건을 적용하여 GenericList<T> 클래스에 추가할 수 있는 기능을 보여 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 : Employee, IEmployee, System.IComparable<T>, new()
{
    // ...
}

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 {/*...*/}
}

앞의 예제에서 TAdd 메서드 컨텍스트에서는 형식 제약 조건이고, List 클래스 컨텍스트에서는 바인딩되지 않은 형식 매개 변수입니다.

제네릭 클래스 정의에서 형식 매개 변수를 제약 조건으로 사용할 수도 있습니다. 형식 매개 변수는 다른 형식 매개 변수와 함께 꺾쇠괄호 안에 선언해야 합니다.

//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }

컴파일러에서 형식 매개 변수가 System.Object에서 파생된다는 점을 제외하고는 형식 매개 변수에 대해 아무 것도 가정할 수 없기 때문에, 제네릭 클래스에서 형식 매개 변수를 제약 조건으로 사용하는 경우는 제한됩니다. 두 형식 매개 변수 사이의 상속 관계를 적용하려는 시나리오에서 제네릭 클래스에 형식 매개 변수를 제약 조건으로 사용합니다.

notnull 제약 조건

C# 8.0부터는 notnull 제약 조건을 사용하여 형식 인수가 nullable이 아닌 값 형식 또는 nullable이 아닌 참조 형식이어야 함을 지정할 수 있습니다. 대부분 다른 제약 조건과 달리 형식 인수가 notnull 제약 조건을 위반하면 컴파일러는 오류 대신 경고를 생성합니다.

notnull 제약 조건은 null 허용 컨텍스트에서 사용되는 경우에만 영향을 미칩니다. null 허용 인식 불가능한 컨텍스트에서 notnull 제약 조건을 추가하면 컴파일러는 제약 조건 위반에 대한 경고 또는 오류를 생성하지 않습니다.

class 제약 조건

C# 8.0부터 null 허용 컨텍스트에서 class 제약 조건은 형식 인수가 null을 허용하지 않는 참조 형식이어야 하도록 지정합니다. null 허용 컨텍스트에서 형식 인수가 null 허용 참조 형식이면 컴파일러는 경고를 생성합니다.

default 제약 조건

nullable 참조 형식 추가로 제네릭 형식 또는 메서드에서 T? 사용이 복잡해집니다. C# 8 이전에는 Tstruct 제약 조건이 적용된 경우에만 T?를 사용할 수 있었습니다. 해당 컨텍스트에서 T?TNullable<T> 형식을 나타냅니다. C# 8부터 T?struct 또는 class 제약 조건과 함께 사용할 수 있지만, 둘 중 하나는 반드시 있어야 합니다. class 제약 조건을 사용한 경우 T?T의 nullable 참조 형식을 나타냈습니다. C# 9부터는 두 제약 조건이 모두 적용되지 않은 경우 T?를 사용할 수 있습니다. 이 경우 T?는 값 형식 및 참조 형식에 대해 C# 8에서와 동일하게 해석됩니다. 그러나 TNullable<T>의 인스턴스인 경우 T?T와 동일합니다. 즉, T??가 되지 않습니다.

이제 T?class 또는 struct 제약 조건 없이 사용할 수 있으므로 재정의 또는 명시적 인터페이스 구현에서 모호성이 발생할 수 있습니다. 두 경우 모두에서 재정의는 제약 조건을 포함하지 않지만 기본 클래스에서 상속합니다. 기본 클래스가 class 또는 struct 제약 조건을 적용하지 않는 경우 파생 클래스는 둘 중 어떤 제약 조건도 없이 기본 메서드에 재정의가 적용되도록 지정해야 합니다. 이때 파생 메서드는 default 제약 조건을 적용합니다. 제약 조건은 default 제약 조건 default 명확히 표시하지 class 않습니다 struct .

관리되지 않는 제약 조건

C# 7.3부터 제약 조건을 unmanaged 사용하여 형식 매개 변수가 nullable이 아닌 unmanaged이어야 한다고 지정할 수 있습니다. 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() 제약 조건과 결합할 수 없습니다.

대리자 제약 조건

C# 7.3부터 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);

마지막 줄의 주석 처리를 제거하면 컴파일되지 않습니다. firsttest는 모두 대리자 형식이지만 서로 다른 대리자 형식입니다.

열거형 제약 조건

C# 7.3부터 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.GetValuesEnum.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}");

참조