제네릭 (Generic)_일반화 프로그래밍

개발조하·2023년 10월 25일
0

C#

목록 보기
3/11
post-thumbnail

1. '제네릭'의 탄생 동기 (feat.제네릭 메서드)

int타입의 배열을 출력하는 메소드 IntPrint를 만들었다.
근데 float타입의 배열과 string타입의 배열도 출력해야하는 일이 발생했다.
그렇다면?
우리는 같은 형식의 출력하는 함수를 타입별로 중복해서 만들어줘야 한다.
아래 예시 코드가 바로 그러한 상황이다.
IntPrintFloatPrint, StringPrint가 같은 형식으로 중복된다;;; (매우 비효율적)

namespace GenericTest
{
    internal class Program
    {
        static void Main(string[] args)
        {

            int[] Num1 = { 1, 2, 3, 4 };
            float[] Num2 = { 0.1f, 0.2f, 0.3f, 0.4f };
            string[] Num3 = { "일", "이", "삼", "사" };

            IntPrint(Num1);
            FloatPrint(Num2);
            StringPrint(Num3);
        }
        
        static void IntPrint(int[] Num)
        {
            foreach(int num in Num)
            {
                Console.Write($"{num} / ");
            }
            Console.WriteLine();
        }

        static void FloatPrint(float[] Num)
        {
            foreach (float num in Num)
            {
                Console.Write($"{num} / ");
            }
            Console.WriteLine();
        }

        static void StringPrint(string[] Num)
        {
            foreach (string num in Num)
            {
                Console.Write($"{num} / ");
            }
            Console.WriteLine();
        }
    }
}

중복 코드를 발생시키지 않기 위해 object 타입을 떠올릴 수 있다.
object 타입으로 코드 중복을 해결해보면??

namespace GenericTest
{
    internal class Program
    {
        static void Main(string[] args)
        {

            int[] Num1 = { 1, 2, 3, 4 };
            float[] Num2 = { 0.1f, 0.2f, 0.3f, 0.4f };
            string[] Num3 = { "일", "이", "삼", "사" };

            ArrayPrint(Num1.Select(x => (object)x).ToArray());
            ArrayPrint(Num2.Select(x => (object)x).ToArray());
            ArrayPrint(Num3.Select(x => (object)x).ToArray());
        }
        
        static void ArrayPrint(object[] Num)
        {
            foreach(object num in Num)
            {
                Console.Write($"{num} / ");
            }
            Console.WriteLine();
        }
    }
}

ㄴ 중복 코드는 사라졌지만, LINQ의 Select를 사용하여 각 배열의 요소를 object로 변환하고, 그 후 .ToArray()를 사용하여 변환한 데이터를 배열로 만들어 ArrayPrint() 메서드에 전달해야하는 오히려 머리 아픈 현상이 일어난다..
ㄴ 뿐만 아니라, object 타입 변수에 값 타입 인스턴스를 넣고 캐스팅한다면 박싱/언박싱을 해야하기 떄문에 성능에 지장을 준다..
❌ 그러므로, object로 서로 다른 타입을 하나로 묶는 것은 좋은 방법은 아니다 ❌

💡이럴 때 사용하는 것이 '제네릭(일반화 프로그래밍)'이다!

2. '제네릭(일반화 프로그래밍)'이란?

  • 일반화란?
    개별적인 것이나 특수한 것의 공통점을 찾아 일반적인 것으로 만드는 것이다.
  • 일반화 프로그래밍(제네릭)이란?
    데이터 형식을 일반화하는 기법으로 특정 타입에 국한되지 않고 모든 타입을 멤버 변수의 타입으로 설정할 수 있다. 즉, 변수의 데이터 타입 떄문에 여러 개의 메서드/클래스를 작성해야 하는 경우 한 개의 메서드/클래스로 구현할 수 있는 기법이다.

위에 작성했던 코드를 '제네릭 메서드'로 바꿔보자!

namespace GenericTest
{
    internal class Program
    {
        static void ArrayPrint<T>(T[] Num)
        {
            foreach (T num in Num)
            {
                Console.Write($"{num} / ");
            }
            Console.WriteLine();
        }

        static void Main(string[] args)
        {

            int[] Num1 = { 1, 2, 3, 4 };
            float[] Num2 = { 0.1f, 0.2f, 0.3f, 0.4f };
            string[] Num3 = { "일", "이", "삼", "사" };

            ArrayPrint<int>(Num1);
            ArrayPrint<float>(Num2);
            ArrayPrint<string>(Num3);
        }
    }
}

중복 코드가 사라지고 확실히 코드가 깔끔해졌다! 하나씩 살펴보자.

2.1 <T>의 의미

<T>는 제네릭 형식 매개변수로, 이 메서드가 어떤 형식의 배열에 대해서도 작동할 수 있음을 나타낸다. 일반적으로 Type의 약자인 T로 작성한다.
T[] Num은 T 형식의 배열을 입력으로 받는 것을 나타낸다.

foreach 루프를 사용하여 배열 Num의 모든 요소를 반복하고 각 요소를 출력한다. T는 메서드 호출 시에 실제 형식으로 대체된다.

ArrayPrint() 메서드를 호출할 때 int, float, string 형식을 각각 인수로 전달한다. 제네릭 메서드를 호출할 때 형식 인수를 제공함으로써 메서드는 해당 형식에 대한 코드를 실행한다.

3. 제네릭 클래스

3.1 제네릭 클래스 만들기

먼저, 제네릭 클래스가 아닌 중복 코드로 만든 코드이다.

namespace GenericTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            IntClass intC = new IntClass();
            DoublecClass doubleC = new DoublecClass();
            StringClass stringC = new StringClass();

            intC.Print(100);
            doubleC.Print(1.1);
            stringC.Print("백");
        }
    }

    class IntClass
    {
        public void Print(int val)
        {
            Console.WriteLine($"val = {val}");
        }
    }

   class DoublecClass
    {
        public void Print(double val)
        {
            Console.WriteLine($"val = {val}");
        }
    }

    class StringClass
    {
        public void Print(string val)
        {
            Console.WriteLine($"val = {val}");
        }
    }
}

이를 제네릭 클래스로 바꿔보면,

namespace GenericTest
{
    internal class Program
    {
        static void Main(string[] args)
        {
            GenericClass<int> intObj = new GenericClass<int>();
            GenericClass<double> doubleObj = new GenericClass<double>();
            GenericClass<string> stringObj = new GenericClass<string>();

            intObj.Print(100);
            doubleObj.Print(1.1);
            stringObj.Print("백");
        }
    }
    class GenericClass<T>
    {
        public void Print(T val)
        {
            Console.WriteLine($"val = {val}");
        }
    }
}

ㄴ 이렇게 GenericClass 하나로 여러 타입의 값을 사용할 수 있다.

3.2 where T : ~ (형식 매개변수 제약 시키기)

  • 받아들일 객체의 제한 조건을 달 때 사용한다.
    ㄴ 코드는 구체적이고 제한적으로 설계되어야 오류 발생이 줄고, 촘촘한 코드가 만들어질 수 있기 때문!
제약설명
where T : structT는 값 형식이어야 한다.
where T : classT는 참조 형식이어야 한다.
where T : new()T는 반드시 매개변수가 없는 생성자가 있어야 한다.
where T : 부모 클래스명T는 명시한 부모 클래스의 자식 클래스여야 한다.
where T : 인터페이스명T는 명시한 인터페이스를 반드시 구현해야 한다. 복수의 인터페이스를 명시해도 된다.
where T : UT는 또 다른 형식 매개변수 U로부터 상속받은 클래스여야 한다.

ㄴ int, double, string은 모두 IComparable 인터페이스를 상속받기 때문에 오류가 발생하지 않는다! 만약, IComparable을 상속받지 않는 다른 Class나 Struct가 타입으로 들어오면 오류 발생!

3.3 System.Collections.Generic (일반화 컬렉션)

List<T> Queue<T> Stack<T> Dictionary<TKey, TValue>

3.3.1 List<T>

비일반화 클래스인 ArrayList와 같은 기능을 한다. 다만 아무 형식의 객체나 마구 집어넣을 수 있었던 ArrayList와는 달리 List<T>는 형식 매개변수에 입력한 형식 외에는 입력을 허용하지 않는다.

3.3.2 Queue<T>

비일반화 클래스인 Queue와 같은 기능을 하며 사용법도 동일하다.

3.3.3 Stack<T>

비일반화 클래스인 Stack과 같은 기능과 사용법이다.

3.3.3 Dictionary<TKey, TValue>

Hashtable의 일반화 버전이다. 형식 매개변수 2개를 요구하며 TKey는 Key, TValue는 Value를 위한 형식이다.

4. '제네릭'의 장점

  1. 코드의 재사용
    : 제네릭 메서드/클래스를 다양한 변수 형식에 재사용할 수 있다.
  2. 형식 안정성
    : 컴파일러는 형식 불일치 오류를 방지하므로 안정성이 확보된다.
  3. 가독성
    : 일반적인 메서드를 작성할 때보다 코드가 간결하며 가독성이 좋다.

📄참고자료

profile
Unity 개발자 취준생의 개발로그, Slow and steady wins the race !

0개의 댓글