Share via


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

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

제약 조건 설명
where T : struct 형식 인수는 형식을 포함하는 record struct nullable이 아닌 값 형식이어야 합니다. 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 컨텍스트에서 nullable이 아닌 참조 형식인 T 경우 U nullable이 아닌 참조 형식이어야 합니다. nullable 참조 형식 T 인 경우 U null을 허용하거나 null을 허용하지 않을 수 있습니다.
where T : default 이 제약 조건은 메서드를 재정의하거나 명시적 인터페이스 구현을 제공할 때 비제한 형식 매개 변수를 지정해야 할 경우 모호성을 해결합니다. default 제약 조건은 class 또는 struct 제약 조건이 없는 기본 메서드를 의미합니다. 자세한 내용은 default 제약 조건 사양 제안을 참조하세요.

일부 제약 조건은 상호 배타적이고 일부 제약 조건은 지정된 순서로 되어 있어야 합니다.

  • , , notnullclass?unmanaged 제약 조건 중 structclass하나만 적용할 수 있습니다. 이러한 제약 조건을 제공하는 경우 해당 형식 매개 변수에 대해 지정된 첫 번째 제약 조건이어야 합니다.
  • 기본 클래스 제약 조건(where T : Base또는 where T : Base?)을 제약 조건, class또는 class?notnullunmanaged제약 조건struct과 결합할 수 없습니다.
  • 두 가지 형식으로 최대 하나의 기본 클래스 제약 조건을 적용할 수 있습니다. nullable 기본 형식을 지원하려면 .를 사용합니다 Base?.
  • 인터페이스의 nullable이 아닌 형식과 nullable 형식의 이름을 모두 제약 조건으로 지정할 수는 없습니다.
  • new() 제약 조건은 struct 또는 unmanaged 제약 조건과 결합할 수 없습니다. 제약 조건을 new() 지정하는 경우 해당 형식 매개 변수에 대한 마지막 제약 조건이어야 합니다.
  • 제약 조건은 재정의 default 또는 명시적 인터페이스 구현에만 적용할 수 있습니다. 또는 제약 조건과 structclass 결합할 수 없습니다.

제약 조건을 사용하는 이유

제약 조건은 형식 매개 변수의 기능 및 기대치를 지정합니다. 해당 제약 조건을 선언하면 제약 형식의 작업 및 메서드 호출을 사용할 수 있습니다. 제네릭 클래스 또는 메서드가 단순 할당 이외의 제네릭 멤버에 대한 작업을 사용하는 경우 형식 매개 변수에 제약 조건을 적용합니다. 여기에는 지원되지 않는 메서드 호출이 포함됩니다 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 : Employee, System.Collections.Generic.IList<T>, IDisposable, 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 : IComparable<T> 제약 조건을 적용 where T : IEquatable<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 제약 조건

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?로 해석됩니다. 그러나 TNullable<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);

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

열거형 제약 조건

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}");

형식 인수는 선언된 인터페이스를 구현함

일부 시나리오에서는 형식 매개 변수에 제공된 인수가 해당 인터페이스를 구현해야 합니다. 예시:

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static T operator +(T left, T right);
    public abstract static T operator -(T left, T right);
}

이 패턴을 사용하면 C# 컴파일러가 오버로드된 연산자나 static virtual 또는 static abstract 메서드에 대한 포함 형식을 결정할 수 있습니다. 포함 형식에 더하기 및 빼기 연산자를 정의할 수 있도록 구문을 제공합니다. 이 제약 조건이 없으면 형식 매개 변수가 아닌 매개 변수와 인수를 인터페이스로 선언해야 합니다.

public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
    public abstract static IAdditionSubtraction<T> operator +(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);

    public abstract static IAdditionSubtraction<T> operator -(
        IAdditionSubtraction<T> left,
        IAdditionSubtraction<T> right);
}

앞의 구문에서는 구현자가 해당 메서드에 대해 명시적 인터페이스 구현을 사용해야 합니다. 추가 제약 조건사용 약관을 제공하면 인터페이스가 형식 매개 변수 측면에서 연산자를 정의할 수 있습니다. 인터페이스를 구현하는 형식은 인터페이스 메서드를 암시적으로 구현할 수 있습니다.

참고 항목