상세 컨텐츠

본문 제목

[C#] 일반화 프로그래밍

.NET/C#

by 클리엘 클리엘 2021. 10. 14. 03:35

본문

728x90

일반화 프로그래밍은 처리의 대상이 되는 '데이터'에서 '타입'이라는 개념만을 분리해 공통적으로 취급하고자 하는 것을 말합니다. 예를 들어 다음과 같이 매개변수를 받은 정수 1개를 그대로 반환하는 메서드가 존재하는 경우

class Cal
{
    public int Print(int i)
    {
        return i;
    }
}

이 상태에서 float형에 대한 Print() 메서드를 추가해야 한다면 다음과 같이 할 수 있을 것입니다.

class Cal
{
    public int Print(int i)
    {
        return i;
    }

    public float Print(float i)
    {
        return i;
    }
}

문제가 되는 점은 똑같은 처리를 하는 메서드가 단지 데이터의 '타입'이 다르다는 이유만으로 중복해서 생성되고 있다는 점인데 바로 이런 문제를 해결하고자 일반화 프로그래밍 기법이 등장하게 되었습니다.

 

1. 일반화 메서드

 

메서드의 데이터 형식을 일반화한 메서드입니다. 위 예제에서 사용된 Print() 메서드를 일반화 버전으로 바꿔보면 다음과 같이 할 수 있습니다.

class Cal
{
    public T Print<T>(T i)
    {
        return i;
    }
}

우선 T는 데이터 형식을 대체하기 위한 것입니다. 때문에 메서드도 T형식을 반환하고 매개변수도 T형식을 전달받습니다. 이 T형식은 메서드를 호출할 때 구체적으로 어떤 형식이 사용될지가 정해집니다.

class Program
{
    static void Main(string[] args)
    {
        Cal c = new Cal();
        WriteLine(c.Print<int>(100));
    }
}

메서드를 호출할때 메서드 이름 옆에 <int>형식으로 데이터의 형식을 표현하고 있습니다. 이 형식은 실제 메서드에 있는 Print<T>를 통해 그대로 전달되는데 만약 int형에서 다른 형식으로 바뀐다 할지라도 float나 string처럼 데이터 형식을 그대로 지정해 주기만 하면 됩니다.

 

2. 일반화 클래스

 

일반화의 범위를 단순히 클래스 쪽으로 옮겨놓은 형태일 뿐 특별히 다른 것은 없습니다.

class Cal<T>
{
    public T Print<T>(T i)
    {
        return i;
    }
}

위와 같이 하면 클래스를 사용할 때 지정된 형식으로 데이터를 다룰 수 있습니다.

class Program
{
    static void Main(string[] args)
    {
        Cal<int> c = new Cal<int>();
        WriteLine(c.Print(100));
    }
}

특정 데이터 형식을 일관적으로 클래스 전체에 걸쳐 사용하고자 하는 경우 일반화 클래스를 구현합니다.

 

3. 형식 범위 제한

 

기본적으로 T에는 모든 데이터 형식을 지정할 수 있지만 때에 따라서는 특정 형식으로 제한해야 하는 경우도 있습니다. 이런 경우에는 where절을 사용해 T의 형식을 제한하는데, 예를 들어 T가 반드시 참조 형식이어야 한다면 다음과 같이 구현할 수 있습니다.

class Cal<T> where T : class
{
    public T Print<T>(T i)
    {
        return i;
    }
}

'where T : 제한조건'형식을 사용하며 예제에서 사용된 class는 참조 형식임을 의미하므로 아래와 같이 값 형식으로는 클래스를 사용할 수 없게 됩니다.

class Program
{
    static void Main(string[] args)
    {
        Cal<int> c = new Cal<int>(); //오류 int는 값형식임
        WriteLine(c.Print(100));
    }
}

기타 '제한조건'에 올 수 있는 것들은 다음과 같습니다.

struct 값형식만 가능
class 참조형식만 가능
new() 매개변수가 없는 생성자가 반드시 있어야 함
클래스명 해당 클래스이거나 해당 클래스를 상속받은 클래스여야 함
인터페이스명 반드시 해당 인터페이스를 구현한 클래스여야 함(인터페이스는 여러개가 지정될 수 있음)
U 메서드에 사용할 수 있으며 소속 클래스에서 제약조건으로 지정된 클래스를 상속하는 클래스여야 함

마지막에 U에서 약간의 설명을 덧붙이자면 우선 아래와 같이 클래스가 작성되었을 때

class Cal<U> where U : Base
{
    public T Print<T>(T i) where T : U
    {
        return i;
    }
}

클래스에서 U의 제한조건으로 Base클래스가 지정된 것을 볼 수 있습니다. 이 상태에서 Cal 클래스를 사용하는 경우에는 Base클래스 자체나 혹은 Base클래스를 상속하는 또 다른 클래스를 형식 매개변수인 U의 대상으로 지정해야 합니다.

 

이때 소속 메서드인 Print()에서는 자신의 T에 대한 제한조건을 U로 설정했는데 여기서 U라면 Cal에서 제한조건으로 설정했던 Base를 의미하고 결국 Print()의 T는 이 Base에서 상속된 클래스만 올 수 있다는 것을 의미하게 됩니다. 클래스에서 Base를 지정한 것과 다른 점이라면 Cal은 Base나 Base를 상속한 클래스 모두 올 수 있지만 Print()는 오로지 Base를 상속한 클래스만 올 수 있다는 것이 다릅니다. Base는 자시 자신일 뿐이지 상속된 클래스가 아니기 때문입니다.

 

4. 일반화 컬렉션

 

컬렉션을 일반화한다는 것은 기존의 컬렉션이 가지고 있던 문제를 극복하고자 하는 것인데 그 문제라는 것이 바로 성능 문제입니다.

class Program
{
    static void Main(string[] args)
    {
        ArrayList myList = new ArrayList();
        myList.Add(10);
        myList.Add(20);

        WriteLine(myList[1]); //30입니다.
    }
}

컬렉션은 태생적으로 object형이기 때문에 모든 형식의 데이터를 담아낼 수 있습니다. 예제에서는 정수 값을 담았지만 내부적으로는 이 값들을 object로 취급합니다.

 

object로 데이터 형식을 다루게 되면 값을 넣을 때 '박싱'을, 값을 가져올 때는 '언박싱'을 수행해야 하므로 이 과정에서 오는 오버헤드가 큰 것입니다.

class Program
{
    static void Main(string[] args)
    {
        List<int> myList = new List<int>();
        myList.Add(10);
        myList.Add(20);

        WriteLine(myList[1]); //30입니다.
    }
}

List는 ArrayList의 일반화 버전으로 List클래스 사용 시 일반화 매개변수를 지정한다는 것만 빼면 사용법은 완전히 동일합니다. 예제에서는 형식 매개변수로 int형을 지정했으므로 Add()메서드를 통해 담을 수 있는 데이터는 오로지 int형식의 데이터뿐입니다. 자유롭게 모든 형식을 담아낼 수는 없지만 '박싱'이나 '언박싱'을 수행하지 않으므로 성능적으로는 우위에 있는 것입니다.

 

ArrayList와 HashTable만을 제외하고 다른 컬렉션인 Queue나 Statck은 형식 매개 변수만 지정해 주면 일반화로 구현이 가능합니다. ArrayList는 일반화인 List로 대체되고 HashTable은 일반화 버전인 Dictionary로 대체됩니다.

 

5. 배열 순회를 위한 일반화

 

다음 글에서 IEnumerable인터페이스를 상속하여 클래스에 배열을 순회하는 방법에 대해 알아본 적이 있습니다.

 

[.NET/C#] - [C#] 배열, 컬렉션, 인덱서

 

[C#] 배열, 컬렉션, 인덱서

1. 배열 배열의 기본적인 개념은 '같은 성격의 데이터를 여러 개 모아놓은 것'이라고 볼 수 있습니다. 예를 들어 어떤 학급의 학생별 시험 점수를 처리하기 위해 다음과 같이 변수를 선언했다면 c

lab.cliel.com

일반화 클래스의 경우에도 마찬가지로 IEnumerable인터페이스를 상속하면 동일하게 배열 순회가 가능합니다. 다만 IEnumerator와 IEnumerable인터페이스 모두 일반화 클래스에 맞는 IEnumerator<T>와 IEnumerable<T>형식이 존재하므로 해당 인터페이스를 상속받아서 클래스를 구현해야 합니다. IEnumerator와 IEnumerable인터페이스를 그대로 사용해도 배열을 순회하는 데는 아무런 문제가 없지만 배열을 하나씩 순회할 때마다 형식 변환을 거쳐야 해서 성능이 저하되는 문제가 발생하기 때문입니다. 형식 변환으로 인한 성능 저하 문제를 해결하기 위해 일반화 클래스를 구현했는데 정작 배열을 순회하는 과정에서 형식 변환으로 인한 성능 문제가 발생한다면 일반화 클래스를 구현하는 게 아무런 의미가 없는 것이죠.

 

IEnumerable<T> 인터페이스를 상속하는 경우에는 IEnumerator와 IEnumerator<T>를 반환하는 2가지의 메서드를 구현해야 합니다. IEnumerable<T> 인터페이스가 IEnumerable 인터페이스를 상속받아 만들어졌기 때문에 기존에 인터페이스에서 갖고 있던 메서드도 같이 구현해야 하는 것입니다. IEnumerator<T>인터페이스도 마찬가지로 IEnumerator 인터페이스를 상속하고 있으므로 IEnumerator에 있던 Object를 반환하는 Current와 IEnumerator<T>에 있는 T형식을 반환하는 Current 프로퍼티를 같이 구현해야 합니다.

class Student<T> : IEnumerator<T>, IEnumerable<T>
{
    private T[] students;
    private int position = -1; //현재 배열 위치값

    public Student()
    {
        students = new T[3];
    }

    public T this[int idx]
    {
        get
        {
            return students[idx];
        }

        set
        {
            students[idx] = value;
        }
    }

    //IEnumerator 인터페이스 구현 - 현재 요소값 변환
    object IEnumerator.Current
    {
        get {
            return students[position];
        }
    }

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

    //IEnumerator 인터페이스 구현 - 다음 요소로 이동
    public bool MoveNext()
    {
        if (position >= students.Length - 1) {
            Reset();
            return false;
        }
        else {
            ++position;

            return (position < students.Length);
        }
    }

    //IEnumerator 인터페이스 구현 - 요소 위치 초기화
    public void Reset()
    {
        position = -1;
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this;
    }

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

    public void Dispose()
    {

    }
}

위 예제는 이전에 구현했던 클래스를 바탕으로 일반화 클래스로 변경한 것입니다. 일반화 클래스를 위해 클래스명 옆에 <T>형식을 지정하였으며 내부 students배열도 T형식으로 바뀌었고 인덱서도 마찬가지로 T형식으로 변경되었습니다.

 

인터페이스도 일반화 버전인 IEnumerator<T>와 IEnumerable<T>인터페이스를 상속받았으므로 Current 프로퍼티와 GetEnumerator() 메서드도 각각의 인터페이스에 구현된 2가지 버전이 만들어졌습니다. 그런데 T가 아닌 일반 버전의 경우에는 프로퍼티와 메서드의 이름이 T버전과 충돌하므로 이를 방지하기 위해 IEnumerator.Current와 IEnumerable.GetEnumerator() 처럼 직접 인터페이스를 지정해서 구현해야 합니다.

 

마지막으로 클래스에 Dispose() 메서드가 구현되었는데 이 메서드는 IEnumerator<T>에서 가지고 있는 메서드입니다.

class Program
{
    static void Main(string[] args)
    {
        Student<string> s = new Student<string>();
        s[0] = "홍길동";
        s[1] = "홍길순";
        s[2] = "홍길석";

        foreach(string sd in s)
        {
            WriteLine(sd);
        }
    }
}

참고로 위에서 작성했던 Student클래스는 IEnumerable<T>인터페이스만을 상속하여 GetEnumerator()메서드를 직접구현해 주면 컴파일러가 알아서 IEnumerator<T>인터페이스를 상속한 클래스를 생성해줍니다.

class Student<T> : IEnumerable<T>
{
    private T[] students;

    public Student()
    {
        students = new T[3];
    }

    public T this[int idx]
    {
        get
        {
            return students[idx];
        }

        set
        {
            students[idx] = value;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        foreach(T s in students) {
            yield return s;
        }
    }

    public IEnumerator<T> GetEnumerator()
    {
        foreach(T s in students) {
            yield return s;
        }
    }
}
728x90

'.NET > C#' 카테고리의 다른 글

[C#] 대리자와 이벤트  (0) 2021.10.18
[C#] 예외처리  (0) 2021.10.15
[C#] 일반화 프로그래밍  (0) 2021.10.14
[C#] 배열, 컬렉션, 인덱서  (0) 2021.10.13
[C#] 프로퍼티(Property)  (0) 2021.10.07
[C#] 인터페이스와 추상클래스  (0) 2021.10.07

태그

관련글 더보기

댓글 영역