C#

[C#]제네릭 형식 제약 조건(where)

DevStory 2021. 9. 23.

제네릭(Generic)은 특정 데이터 타입(Data Type)에 국한되지 않고 모든 타입을 허용하는 제네릭 메서드와 제네릭 클래스를 구현할 수 있지만, 특정 조건에만 대응되는 데이터 타입이 필요한 경우가 있습니다.

 

이러한 경우 where 키워드를 사용하여 제약 조건을 추가할 수 있으며, 제약 조건을 만족하지 않는 경우 컴파일 에러가 발생하도록 할 수 있습니다.

 

이번 포스팅에서는 제네릭 제약 조건이 무엇인지 정리하였습니다.

 


제네릭 제약 조건 추가

다음은 모든 타입을 허용하는 제네릭 클래스입니다.

class GenericClass <T>
{
    public T objMember { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        GenericClass<int> genericObj1 = new GenericClass<int>();
        GenericClass<string> genericObj2 = new GenericClass<string>();
        GenericClass<ArrayList> genericObj3 = new GenericClass<ArrayList>();
    }
}

제네릭은 모든 타입을 허용하는 기법이므로 GenericClass 클래스의 objMember 멤버 변수는 값 형식인 int형이 될 수도 있고 참조 형식인 string형과 ArrayList 타입도 가능합니다.

 

"GenericClass 클래스의 objMember 멤버 변수는 값 형식만 가능하도록 구현"해야 한다는 제약 조건이 주어질 경우 참조 형식인 string형과 ArrayList타입은 objMember 멤버 변수의 타입으로 허용되지 않습니다.

 

이러한 제약 조건을 구현하기 위해 GenericClass 클래스 선언문에 where 조건을 추가합니다.

class GenericClass <T> where T : struct
{
    public T objMember { get; set; }
}

여기서 struct는 구조체가 아니라 제네릭에서 값 형식만 허용한다는 제약 조건입니다.

 

즉, GenericClass 클래스 멤버 변수 objMember는 값 형식만 타입으로 허용합니다.

 

struct 제약 조건을 추가하고 다음 코드를 실행하면 컴파일 에러가 발생합니다.

class GenericClass <T> where T : struct
{
    public T objMember { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        GenericClass<int> genericObj1 = new GenericClass<int>();
        GenericClass<string> genericObj2 = new GenericClass<string>();
        GenericClass<ArrayList> genericObj3 = new GenericClass<ArrayList>();
    }
}

string 타입과 ArrayList는 참조 형식이므로 제약 조건에 위반되어 컴파일 에러가 발생합니다.


제네릭 제약 조건 종류

제약 조건 설명
where T : struct T는 null을 허용하지 않는 값 형식이어야 합니다.
where T : class T는 참조 형식이어야 합니다.
where T : new() T는 매개 변수가 없는 public 생성자가 있어야 합니다. 다른 제약 조건과 함께 사용되는 경우 new() 제약 조건을 마지막에 지정해야 합니다.
where T : notnull T는 null이 아닌 형식이어야 합니다.
where T : unmanaged T는 null이 아닌 비관리형 형식이어야 합니다.
where T : <base class name> T는 지정된 기본 클래스이거나 이 클래스에서 파생된 클래스여야 합니다.
where T : <interface name> T는 지정된 기본 인터페이스이거나 이 인터페이스에서 파생된 유형이여야 합니다.
where T : U U는 인터페이스, 추상 클래스 또는 일반 클래스가 될 수 있으며, T는 U에서 상속되어야 합니다.

struct 제약 조건은 위에서 설명했으며, class 제약 조건은 struct 제약 조건과 유사하므로 설명은 생략하도록 하겠습니다.

 

new 제약 조건부터 순서대로 정리하였으며, default 제약 조건도 존재하지만 C# 9.0부터 지원하며 자세한 자료가 없어서 정리하지 못했습니다.


new 제약 조건

new 제약 조건은 인스턴스를 생성하기 위해 사용되는 new 연산자와 동일하며, 데이터 타입에 기본 생성자가 존재하는 타입만 허용합니다.

 

다음 코드는 new 연산자를 사용하여 인스턴스를 생성하는 코드입니다.

int intVar = new int();

// string 타입은 기본 생성자 없으므로 컴파일 에러
string strVar1 = new string();

// 문자열을 전달해서 값을 초기화하는 경우는 정상
string strVar2 = new string("ABC");

ArrayList arrList = new ArrayList();

int 형과 ArrayList는 기본 생성자가 존재하지만, string 타입은 기본 생성자가 존재하지 않기 때문에 컴파일 에러가 발생합니다.

 

다음 코드는 제네릭 제약 조건에 new 제약 조건을 추가한 코드입니다.

class GenericClass <T> where T : new()
{
    public T objMember { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        GenericClass<int> genericObj1 = new GenericClass<int>();
        
        // 컴파일 에러
        GenericClass<string> genericObj2 = new GenericClass<string>();

        GenericClass<ArrayList> genericObj3 = new GenericClass<ArrayList>();
    }
}

제네릭에서도 string 타입은 기본 생성자가 존재하지 않기 때문에 new 제약 조건이 있는 경우 컴파일 에러가 발생합니다.

 

new 제약 조건은 다음 코드처럼 다른 제약 조건과 함께 사용할 수 있습니다.

class GenericClass <T> where T : class, new()
{
    public T objMember { get; set; }
}

단, struct 제약 조건과는 사용할 수 없으며 new 제약 조건은 마지막에 위치해야 합니다.


notnull 제약 조건

notnull 제약 조건은 C# 8.0부터 도입되었으며, null이 아닌 값 형식 또는 참조 형식으로 타입을 지정해야 합니다.

 

다른 제약 조건과는 다르게 제약 조건을 위반하면, 컴파일 에러가 아니라 경고를 발생합니다.


unmanaged 제약 조건

unmanaged 제약 조건은 관리가 되지 않는 타입들을 허용하는 제약 조건입니다.

 

아래는 비관리형 타입입니다.

  • sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal, bool
  • 열거형
  • 포인터

 

다음 코드는 unmanaged 제약 조건을 추가한 코드입니다.

public struct TestStruct
{
    public int X;
    public ArrayList Y;
}

class GenericClass <T> where T : unmanaged
{
    public T objMember { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        // 정상
        GenericClass<int> genericObj1 = new GenericClass<int>();

        // 컴파일 에러
        GenericClass<TestStruct> genericObj2 = new GenericClass<TestStruct>();
    }
}

TestStruct 구조체에서 비관리형 타입이 아닌 ArrayList를 타입으로 정의하여 컴파일 에러가 발생합니다.


기반 클래스 이름 제약 조건

기반 클래스 이름 제약 조건은 제네릭 유형을 특정 클래스로 제한하며, 특정 클래스 또는 특정 클래스에서 파생된 클래스만 허용합니다.

 

다음 코드는 SuperPerson 클래스를 제네릭 제약 조건으로 추가한 코드입니다.

public class GenericClass<T> where T : SuperPerson
{
}

public class SuperPerson
{
}

public class Person : SuperPerson
{
}

class Program
{
    static void Main(string[] args)
    {
        GenericClass<SuperPerson> superPersonObj = new GenericClass<SuperPerson>();

        GenericClass<Person> personObj = new GenericClass<Person>();
    }
}

제네릭 유형은 제약 조건인 SuperPerson 클래스 또는 SuperPerson 클래스에 파생된 Person 클래스로 설정할 수 있습니다.


인터페이스 이름 제약 조건

인터페이스 이름 제약 조건은 기반 클래스 이름 제약 조건과 유사합니다.

 

인터페이스를 제네릭 유형으로 제한하며, 해당 인터페이스 또는 파생된 유형만 허용합니다.

public class GenericClass<T> where T : IPerson
{
}

public interface IPerson
{
}

public class Person : IPerson
{
}

class Program
{
    static void Main(string[] args)
    {
        GenericClass<IPerson> superPersonObj = new GenericClass<IPerson>();

        GenericClass<Person> personObj = new GenericClass<Person>();
    }
}

U 제약 조건

다음 코드는 U 제약 조건을 추가한 코드입니다. GenerClass 클래스의 제네릭 유형인 T는 U에 상속됩니다.

public class GenericClass<T, U> where T : U
{
    public void DoWork(T subClass, U baseClass)
    {

    }
}

public interface IPerson
{
}

public class Person : IPerson
{
}

class Program
{
    static void Main(string[] args)
    {
        GenericClass<Person, IPerson> genericObj = new GenericClass<Person, IPerson>();
    }
}

멀티 제약 조건

멀티 제약 조건은 제네릭 클래스에 제네릭 유형이 2개 이상이며, 제네릭 유형마다 제약 조건이 존재하는 경우입니다.

 

다음 코드는 제네릭 유형이 2개이며, 제네릭 유형마다 제약 조건이 다른 경우입니다.

public class GenericClass<T, X> where T : struct where X : class
{
}

제네릭 유형 T는 struct 제약 조건을 추가하였으며, X는 class 제약 조건을 추가하였습니다.

반응형

댓글