프로그래밍/C#

[C#] 제너릭 제약 (Generic Constraint)

갓똥 2020. 3. 27. 19:18
728x90
반응형

1. 문제

using System;

class Program {
    public static int Max(int a, int b) {
        return a < b ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));
        Console.WriteLine(Max("A", "B"));
    }
}

 

위는 인자를 2개 받아 둘 중 더 큰 값을 리턴하는 Max함수를 만들어 사용하는 코드이다.
인자는 int타입만을 받고 있는데 10번째 라인과 같이 string타입도 비교를 하고 싶다고 해보자.
string타입은 비교연산자를 사용할 순 없지만 CompareTo메소드를 통해 비교가 가능하다.
CompareTo메소드는 앞이 크면 1, 작다면 -1, 같으면 0이 나오므로 0보다 작은걸로 비교하면 된다.
그럼 아래와 같은 코드가 된다.
using System;

class Program {
    public static int Max(int a, int b) {
        return a < b ? b : a;
    }
    
    public static string Max(string a, string b) {
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));
        Console.WriteLine(Max("A", "B"));
        Console.WriteLine(Max(1.2, 3.4)); // error
    }
}

 

이제 문제 없이 string타입도 비교가 가능하지만 또 다른 문제가 있다.
15번째 라인과 같이 double타입은 비교가 불가능하다.
메소드를 묶어서 템플릿으로 만들기에는 안의 내용이 다르다.
일단 안의 내용을 같게 만들어보자.
string은 비교연산자를 쓸 수 없지만 int타입은 비교연산자와 CompareTo를 모두 사용가능하다.
따라서 아래와 같은 코드로 바꿀 수 있다.
using System;

class Program {
    public static int Max(int a, int b) {
        // return a < b ? b : a;
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    public static string Max(string a, string b) {
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));
        Console.WriteLine(Max("A", "B"));
        Console.WriteLine(Max(1.2, 3.4)); // error
    }
}

 

이제 안의 내용이 같아졌으니 하나로 합칠 수 있게 되었다.

2. 해결

using System;

class Program {
    public static int Max(int a, int b) {
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    public static string Max(string a, string b) {
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));
        Console.WriteLine(Max("A", "B"));
    }
}

 

위에서 맞춘 코드로 2개로 나뉘어져 있는 Max함수를 하나로 합쳐보자.
using System;

class Program {
    // 1. object
    public static object Max(object a, object b) {
        IComparable c1 = a as IComparable;
        IComparable c2 = b as IComparable;
        
        return c1.CompareTo(c2) < 0 ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));
        Console.WriteLine(Max("A", "B"));
        
        int n = (int)Max(10, 20);
    }
}

 

첫 번째로 object로 바꾸어 묶어 보았다.
C#의 모든 타입들은 object로 부터 파생되므로 인자로 무엇이 와도 받을 수 있다.
하지만 object에는 CompareTo메소드가 없기에 에러가 난다.
이것을 사용하기 위해서는 CompareTo를 제공하는 IComparable 인터페이스로 캐스팅을 해야한다.
물론 안전한 코드로 짜려면 null 값인지 조사해야하는데, 일단 넘겼다.
또 object로 사용하면 반환 시 캐스팅이 필요하다.
16번째 라인과 같이 Max의 반환값이 object이므로 int로 캐스팅을 해줘야 한다.

또 결정적인 문제로
10, 20과 같은 value타입이 object로 인자로 전달되는데
object는 레퍼런스 타입이므로 박싱현상이 일어난다.

따라서 object로 만들수는 있지만 성능상의, 사용상의 문제가 있다.

 ① Object 사용

    => CompareTo를 호출하려면 IComparable 인터페이스 타입으로 캐스팅 후 사용

    => 반환 값을 받을 때 캐스팅 필요

    => Value type 인 경우 Boxing / UnBoxing 발생

 

 

using System;

class Program {
    // 2. IComparable
    public static object Max(IComparable a, IComparable b) {
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));
        Console.WriteLine(Max("A", "B"));
        
        int n = (int)Max(10, 20);
    }
}

 

두 번째는 IComparable 인터페이스를 사용한 경우이다.
인자로 아예 인터페이스 타입으로 받게되면 IComparable을 구현한 모든 타입이 들어갈 수 있는데
int도 구현되어 있고, string도 구현되어 있으니 문제 없다.
첫 번째 방법과 비교해 캐스팅 부분이 사라져 조금 깔끔해졌다.

하지만 문제는 여전히 있다.
C#에서 모든 인터페이스는 reference type이다.
5번째 라인에 있는 a와 b는 stack에 있는 참조 변수이고
사용할 때 넘기는 값은 넘어갈 때 Heap에 복사본을 만들고 a와 b가 그걸 가리키게 된다.
즉, Boxing 현상이 발생한다.

또, 리턴값을 받을 때 캐스팅이 여전히 필요하다.

 ② IComparable 인터페이스 사용

    => Value type 인 경우 Boxing / UnBoxing 발생

    => 반환 값을 받을 때 캐스팅 필요

 

 

 

using System;

class Program {
    // 3. Generic
    public static T Max<T>(T a, T b) {
        // 기본적으로 object로 할 수 있는 연산만 가능
        // a.Equals(b); // ok - ojbect 멤버
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));
        Console.WriteLine(Max("A", "B"));
        
        int n = Max(10, 20);
    }
}

 

마지막으로 Generic을 이용한 경우이다.
이렇게 되면 int타입 string타입 두 개가 생기게 되지만
int를 int로 받고 string을 string으로 받으므로 Boxing없이 받을 수 있다.
또 반환 값 또한 캐스팅이 필요 없다.

하지만 문제는 있다.
CompareTo 메소드를 사용할 수 없다. 
왜냐하면 컴파일러는 현재 T를 임의의 타입으로 생각하고 있는데
그 임의의 타입에 CompareTo가 있다고 확신할 수 없기 때문에 에러가 난다.
반대로 Equals는 문제가 없는데 이건 object의 멤버이고, 모든 타입은 object로 파생되기 때문이다.
그래서 제너릭을 사용하면 기본적으로 object로 할 수 있는 연산만 가능하다.
하지만 우리가 쓰고 싶은건 CompareTo 메소드이다.

이럴때는 제너릭 제약이란 것을 쓰면 된다.
using System;

class Program {
    // 3. Generic
    public static T Max<T>(T a, T b) where T : IComparable {
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));
        Console.WriteLine(Max("A", "B"));
        
        int n = Max(10, 20);
    }
}

 

뒤에 where T : IComparable 이라고만 적으면 된다.
의미는 Max는 임의의타입 T를 받을 건데 그 T는 IComparable 인터페이스를 구현해야 한다.
인터페이스가 구현되어 있지 않은 타입일 경우 에러가 난다.

이렇게 표기하는 것을 제너릭 제약이라고 부르고
이 Max는 임의의타입을 받는데 IComparable을 구현한 규칙을 지킨 타입이어야 한다.
C++20에서 concept과 같다.

 ③ Generic 사용

    => Boxing / Unboxing 현상이 없다.

    => Generic Constraint 를 표기해야 한다.

 


 ① Generic Constraint

 

using System;

class Program {
    // ,로 2개를 적을 수 있다. class, struct는 앞에 와야 한다.
    public static T Max<T>(T a, T b) where T : class, IComparable {
        return a.CompareTo(b) < 0 ? b : a;
    }
    
    static void Main() {
        Console.WriteLine(Max(10, 20));   // error
        Console.WriteLine(Max("A", "B")); // ok
        
        int n = Max(10, 20);
    }
}
728x90
반응형