공부/C#

이것이 C#이다 Chapter 11 일반화 프로그래밍

bokob 2024. 3. 9. 20:16

11.1 일반화 프로그래밍이란?

특수한 개념으로부터 공통된 개념을 찾아 묶는 것을 '일반화(Generalization)'라고 한다.

일반화 프로그래밍(Generic Programming)은 일반화를 이용하는 프로그래밍 기법이다.

일반화 프로그래밍이 일반화하는 대상은 '데이터 타입(Data Type)'이다.

 

예를 들어, 정수형 1차원 배열을 복사하는 메소드를 작성했다고 하자.

void CopyArray(int[] source, int[] target)
{
    for(int i=0; i<source.Length; i++)
        target[i] = source[i];
}

이번에는 문자열 배열을 복사한느 기능이 필요해졌다고 하자. 그래서 CopyArray() 메소드를 오버로딩했다.

void CopyArray(string[] source, string[] target)
{
    for(int i=0; i<source.Length; i++)
        target[i] = source[i];
}

이번에는 다른 타입의 배열을 복사해야한다. 이런 작업을 반복하다 보니 CopyArray()의 버전은 늘어갔다.

잘 보면 CopyArray() 메소드의 매개변수에 입력되는 배열의 타입만 다를 뿐, 내부 논리는 똑같다.

그렇다면 특수한 타입을 사용한느 코드를 일반화하면 CopyArray()를 오버로딩하지 않고도 모든 타입을 지원할 수 있을 것 같다는 생각이 들 것이다.

일반화 프로그래밍은 이러한 아이디어에서 나온 프로그래밍 패러다임이다.

 

11.2 일반화 메소드

일반화 메소드(Generic Method)는 이름처럼 (데이터 타입을) 일반화한 메소드이다.

일반화 메소드의 선언 문법은 일반 메소드의 선언 문법과 대부분 비슷하다. 다만 일반화할 타입이 들억갈 자리에 구체적인 타입의 이름 대신 타입 매개변수(Type Parameter)가 들어간다는 것이 다르다.

한정자 반환_타입 메소드_이름<타입_매개변수>(매개변수 목록)
{
    // ...
}

 

// int 버전
void CopyArray(int[] source, int[] target)
{
    for(int i=0; i<source.Length; i++)
        target[i] = source[i];
}

// string 버전
void CopyArray(string[] source, string[] target)
{
    for(int i=0; i<source.Length; i++)
        target[i] = source[i];
}

// 기타 타입 등등 버전
...

 

이 코드에서 데이터 타입이 사용된 부분을 T 기호로 치환해보자. T는 타입(Type)을 뜻한다.

void CopyArray(T[] source, T[] target)
{
    for(int i=0; i<source.Length; i++)
        target[i] = source[i];
}

 

T는 C#이 지원하는 타입이 아니다(적어도 기본 타입에는 포함되어 있지 않다). 따라서 이 코드를 컴파일하면 컴파일 에러가 날 것이다. 이 코드를 컴파일하려면 T가 구체적으로 어떤 타입인지 알려줘야 한다. 타입 매개변수를 입력하는 방법은 다음과 같다.

메소드 이름 뒤에 홑화살괄호 < 와 >를 넣어주고 그 사이에 T를 넣으면 T는 '타입 매개변수(Type Parameter)'가 된다.

CopyArray()를 호출할 때 <> 사이에 T 대신 타입의 이름을 입력하면 컴파일러는 메소드의 나머지 부분에 대해서도 T를 타입 매개변수 값으로 치환한다.

void CopyArray<T>(T[] source, T[] target)
{
    for(int i=0; i<source.Length; i++)
        target[i] = source[i];
}

 

일반화 메소드 CopyArray()를 호출해보자.

int[] source = {1, 2, 3, 4, 5};
int[] target = new int[source.Length];

// 타입 매개변수 T에 int를 대입한다.
CopyArray<int>(source, target);

foreach(int element in target)
    Console.WriteLine(element);

 

11.3 일반화 클래스

일반화 클래스는 (데이터 타입을) 일반화한 클래스이다. 일반화 클래스를 선언하는 문법은 다음과 같다.

일반화 클래스가 그랬던 것처럼, 일반화 클래스도 타입 매개변수가 있는 것을 제외하면 보통의 클래스와 똑같다.

class 클래스_이름 <타입 매개변수>
{
    // ...
}

 

Array_Int와 Array_Double은 기능이 같은 클래스이다. 하지만 내부적으로 사용하는 데이터 타입이 다르므로 클래스를 분리해서 구현했다.

class Array_Int
{
    private int[] array;
    // ...
    public int GetElement(int index) {return array[index];}
}

class Array_Double
{
    private double[] array;
    // ...
    public double GetElement(int index) {return array[index];}
}

 

이 두 클래스는 데이터 타입만 빼고 다른 부분이 모두 같으니 일반화할 수 있다. 다음은 타입 매개변수를 이용해서 위의 코드를 일반화 클래스로 개선한 것이다.

class Array_Generic<T>
{
    private T[] array;
    // ...
    public T GetElement(int index) {return array[index];}
}

 

다음과 같이 사용한다.

Array_Generic<int> intArr = new Array_Generic<int>();
Array_Generic<double> dblArr = new Array_Generic<double>();

Array_Generic 클래스의 타입 매개변수 T는 객체를 생성할 때 입력받은 타입으로 치환되어 다음과 같이 컴파일된다.

 

11.4 타입 매개변수 제약시키기

일반화 메소드나 일반화 클래스가 입력받는 타입 매개변수 T는 '모든' 데이터 타입을 대신할 수 있다.

이렇게 모든 타입에 대응할 수 있는 타입 매개변수가 필요할 때도 있지만, 특정 조건을 갖춘 타입에만 대응하는 타입 매개변수가 필요할 때도 있다. 이때 타입 매개변수의 조건에 제약을 줄 수 있다.

예를 들어 만들었던 MyList<T> 클래스의 타입 매개변수 T에 'MyClass로부터 상속받는 타입이어야 할 것'이라는 제약을 주려면 다음과 같이 클래스 선언문의 헤더에 where 절을 추가해준다.

class MyList<T> where T : MyClass
{
    // ...
}

 

일반화 메소드는 만들었던 CopyArray<T>()로 예를 들자면, 타입 매개변수 T에 '값 타입이어야 할 것'이라는 제약은 다음과 같이 줄 수 있다.

void CopyArray<T>(T[] source, T[] target) where T : struct
{
    for(int i=0; i<source.Length; i++)
        target[i] = source[i];
}

 

일반화 코드에서 타입을 제약하는 문법은 다음과 같으며, 타입 매개변수에 대한 일반화 클래스나 일반화 메소드 모두에 동일하게 사용된다.

where 타입_매개변수 : 제약_조건

 

제약 조건에는 여러 가지가 올 수 있는데, 다음 표에 where 절과 함께 사용할 수 있는 제약 조건이 정리되어 있다.

제약 설명
where T : struct T는 값 타입이어야 한다.
where T : class T는 참조 타입이어야 한다.
where T : new() T는 반드시 매개변수가 없는 생성자가 있어야 한다.
where T : 기반_클래스_이름 T는 명시한 기반 클래스의 파생 클래스여야 한다.
where T : 인터페이스_이름 T는 명시한 인터페이스를 반드시 구현해야 한다. 인터페이스_이름에는 여러 개의 인터페이스를 명시할 수도 있다.
where T : U T는 또 다른 타입 매개변숫 U로부터 상속받은 클래스여야 한다.

 

struct는 앞의 CopyArray<T>() 메소드에서 사용 예를 확인했고, class도 struct와 사용법이 거의 같으니 패스한다.

타입 매개변수를 특정 클래스의 파생 클래스로 한정하는 것도 했으니 패스한다.

제약 조건이 인터페이스를 구현하는 클래스인 경우도 기반 클래스의 파생 클래스인 경우와 비슷하므로 패스한다.

다뤄보지 않은 제약 조건인 new()와 타입 매개변수 U 이다.

new()의 예를 들어보자. 다음 코드의 CreateInstance<T>() 메소드는 기본 생성자를 가진 어떤 클래스의 객체라도 생성해준다. 이 메소드를 호출할 때 기본 생성자가 없는 클래스를 타입 매개변수에 넘기면 컴파일 에러가 난다.

public static T CreateInstance<T>() where T : new()
{
    return new T();
}

 

이번에는 상위 코드에서 사용되던 타입 매개변수  U로부터 상속받는 타입으로 제약 조건을 주는 예이다.

다음 코드의 CopyArray<T>()는 소속 클래스인 BaseArray<U>의 타입 매개변수 U로부터 T가 상속받아야 할 것을 강제하고 있다. -> T가 U로부터 파생된 타입이어야 한다.

class BaseArray<U> where U : Base
{
    public U[] Array{get; set;}
    public BaseArray(int size)
    {
        Array = new U[size];
    }
    
    public void CopyArray<T>(T[] Source) where T : U
    {
        Source.CopyTo(Array, 0);
    }
}

 

어려우니까 많이 연습하도록 하자.

using System;

namespace PracticeCSharp
{
    class StructArray<T> where T : struct // 값 타입으로 제약시키기
    {
        public T[] Array { get; set; }
        public StructArray(int size)
        {
            Array = new T[size];
        }
    }

    class RefArray<T> where T : class // 참조 타입으로 제약시키기
    {
        public T[] Array { get; set; }
        public RefArray(int size)
        {
            Array = new T[size];
        }
    }

    interface IFlyable
    {
        public void Fly();
    }

    class Animal : IFlyable
    {
        public string Name { get; set; }

        public Animal(string name)
        { this.Name = name; }  

        public void Fly()
        {
            Console.WriteLine("I Can Fly!");
        }
    }
    
    class FlyArray<T> where T : IFlyable // 인터페이스로 제약시키기
    {
        public T[] Array { get; set; }
        public FlyArray(int size)
        {
            Array = new T[size];
        }
    }


    class Base { }
    class Derived : Base { }
    class BaseArray<U> where U : Base // 또 다른 타입 매개변수 U로부터 상속받은 클래스로 제약시키기
    {
        public U[] Array { get; set; }
        public BaseArray(int size)
        {
            Array = new U[size];
        }
        public void CopyArray<T>(T[] Source) where T : U
        {
            Source.CopyTo(Array, 0);
        }
    }

    class Program
    {
        public static T CreateInstance<T>() where T : new()
        {
            return new T();
        }
        static void Main(string[] args)
        {
            StructArray<int> a = new StructArray<int>(3);
            a.Array[0] = 0;
            a.Array[1] = 1;
            a.Array[2] = 2;

            RefArray<StructArray<double>> b = new RefArray<StructArray<double>>(3);
            b.Array[0] = new StructArray<double>(5);
            b.Array[1] = new StructArray<double>(10);
            b.Array[2] = new StructArray<double>(1005);

            BaseArray<Base> c = new BaseArray<Base>(3);
            c.Array[0] = new Base();
            c.Array[1] = new Derived();
            c.Array[2] = CreateInstance<Base>();

            FlyArray<Animal> f = new FlyArray<Animal>(3);
            f.Array[0] = new Animal("비둘기");
            f.Array[0] = new Animal("독수리");
            f.Array[0] = new Animal("참새");

            BaseArray<Derived> d = new BaseArray<Derived>(3);
            d.Array[0] = new Derived(); // Base 타입은 여기에 할당할 수 없다.
            d.Array[1] = CreateInstance<Derived>();
            d.Array[2] = CreateInstance<Derived>();

            BaseArray<Derived> e = new BaseArray<Derived>(3);
            e.CopyArray<Derived>(d.Array);
        }
    }
}

 

11.5 일반화 컬렉션

컬렉션들은 모두 object 타입을 기반으로 했다. 어떤 타입이든 간에 object 타입을 상속하므로 object 타입으로 타입 변환이 가능하다. 컬렉션들은 바로 이 점을 이용해서 만들어진 자료구조다.

컬렉션들은 object 타입에 기반하고 있기 때문에 태생적으로 성능 문제를 안고 있다. 컬렉션의 요소에 접근할 때마다 타입 변환이 많이 일어나기 때문이다.

일반화 컬렉션은 object 타입 기반의 컬렉션이 갖고 있던 문제를 말끔히 해결한다.

일반화 컬렉션은 말 그대로 일반화에 기반해서 만들어져 있기 때문에 컴파일할 때 컬렉션에서 사용할 타입이 결정되고, 쓸데없는 타입 변환을 일으키지 않는다. 또한 잘못된 타입의 객체를 담게 될 위험도 피할 수 있다.

 

System.Collections.Generic 네임스페이스는 다양한 컬렉션 클래스를 담고 있지만, 네 가지만 알아보도록 하자.

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

List<T>, Queue<T>, Stack<T>, Dictionary<TKey, TValue>는 각각 ArrayList, Queue, Stack, Hashtable의 일반화 버전이다.

이들 클래스의 기능과 동작 방식은 똑같다.

 

1. List<T>

List<T> 클래스는 비일반화 클래스인 ArrayList와 같은 기능을 하며 사용법 역시 동일하다.

차이점이라면 List<T> 클래스는 인스턴스를 만들 때 타입 매개변수가 필요하다는 것과, 한 컬렉션에 '아무' 타입의 객체나 마구 집어넣을 수 있었던 ArrayList와 달리 List<T>는 타입 매개변수에 입력한 타입 외에는 입력을 허용하지 않는 것이다.

 

2. Queue<T>

Queue<T> 클래스는 타입 매개변수를 요구한다는 점만 다르고, 비일반화 클래스인 Queue와 같은 기능을 하며 사용법도 같다.

 

3. Stack<T>

Stack<T> 클래스는 타입 매개변수를 요구한다는 점만 다르고, 기능과 사용방법은 Stack과 동일하다.

 

4. Dictionaray<TKey, TValue>

Hashtable의 일반화 버전이다. TKey는 Key, TValue는 Value를 위한 타입이다.

 

11.6 foreach를 사용할 수 있는 일반화 클래스

foreach를 사용할 수 있는 클래스를 만드는 데 필요한 것은 IEnumerable 인터페이스를 상속하는 것이다.

일반화 클래스도 IEnumerable 인터페이스를 상속하면 일단은 foreach문을 순회할 수 있지만, 요소를 순회할 때마다 타입 변환을 수행하는 오버로드가 발생한다는 문제가 있었다.

System.Collections.Generic 네임스페이스에 있는 IEnumberable의 일반화 버전인 IEnumberable<T> 인터페이스가 해결법이다. 이 인터페이스를 상속받으면 타입 변환으로 인한 성능 저하가 없으면서도 foreach 순회가 가능한 클래스를 작성할 수 있다.

 

다음은 구현해야 할 IEnummerable<T> 인터페이스의 메소드이다.

메소드 설명
IEnumerator GetEnumerator() IEnumerator 타입의 객체를 반환(IEnumerable로부터 상속받은 메소드)
IEnumerator<T> GetEnumerator() IEnumerator<T> 타입의 객체를 반환

 

IEnumerable<T> 인터페이스는 GetEnumerator() 메소드를 두 개나 갖고 있다. 표를 보면 이름이 같긴 하지만 반환 타입이 다르다. IEnumerator를 반환하는 버전의 GetEnumerator() 메소드는 IEnumerable<T> 인터페이스가 IEnumerable 인터페이스로부터 상속을 받아 얻어온 것이다. 그리고 IEnumerator<T>를 반환하는 버전은 IEnumerable<T>에서 새로 선언된 메소드이다. 두 가지 버전을 모두 구현해야 한다.

 

다음은 IEnumerator<T>의 메소드와 프로퍼티이다.

메소드 또는 프로퍼티 설명
boolean MoveNext() 다음 요소로 이동한다. 컬렉션의 끝을 지난 경우에는 false, 이동이 성공한 경우에는 true를 반환한다.
void Reset() 컬렉션의 첫 번째 위치의 '앞'으로 이동한다. 첫 번째 위치가 0번일 때, Reset()을 호출하면 -1번으로 이동하는 것이다. 첫 번째 위치로의 이동은 MoveNext()를 호출한 다음에 이루어진다.
Object Current{get;} 컬렉션의 현재 요소를 반환한다(IEnumerator로부터 상속받은 프로퍼티).
T Current{get;} 컬렉션의 현재 요소를 반환한다.

 

IEnumerator<T>도  Current 프로퍼티가 두 가지 버전을 갖고 있다. 하나는 IEnumerator로부터 상속받은 버전, 또 다른 하나는 IEnumerator<T>에서 선언된 일반화를 지원하는 버전이다. 둘 다 구현해야 한다.

IEnumerable<T> 나 IEnumerator<T>는 타입 매개변수를 제외하면 IEnumerable 및 IEnumerator와 다른 점이 거의 없다.

using System;
using System.Collections;
using System.Collections.Generic;

namespace PracticeCSharp
{
    class MyList<T> : IEnumerable<T>, IEnumerator<T>
    {
        private T[] array;
        int position = -1;

        public MyList() 
        {
            array = new T[3];
        }

        public T this[int index]
        {
            get
            {
                return array[index];
            }

            set
            {
                if(index >= array.Length)
                {
                    Array.Resize(ref array, index + 1);
                    Console.WriteLine($"Array Resized : {array.Length}");
                }

                array[index] = value;
            }
        }

        public int Length
        {
            get {return array.Length;}
        }

        public IEnumerator<T> GetEnumerator()
        {
            return this;
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return this;
        }

        public T Current
        {
            get { return array[position]; }
        }

        object IEnumerator.Current
        {
            get { return array[position]; }
        }

        public bool MoveNext()
        {
            if(position == array.Length-1)
            {
                Reset();
                return false;
            }
            position++;
            return (position < array.Length);
        }

        public void Reset()
        {
            position = -1;
        }

        public void Dispose()
        {

        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyList<string> str_list = new MyList<string>();
            str_list[0] = "abc";
            str_list[1] = "def";
            str_list[2] = "ghi";
            str_list[3] = "jkl";
            str_list[4] = "mno";

            foreach (string str in str_list)
                Console.WriteLine(str);

            Console.WriteLine();

            MyList<int> int_list = new MyList<int>();
            int_list[0] = 0;
            int_list[1] = 1;
            int_list[2] = 2;
            int_list[3] = 3;
            int_list[4] = 4;

            foreach (int no in int_list)
                Console.WriteLine(no);

        }
    }
}