본문 바로가기

C# 일기

16. 일반화 (Generic <T>)

어떠한 기능이 필요해서 함수를 선언하고 정의하다보면, 같은 기능인데 여러 자료형들도 사용할 수 있도록 만들고싶어질 때가 있다.

 

자료형을 가리지 않고 사용할 수 있는 함수를 구현하기 위해서, C++에서는 template을 사용했었다.

 

그러나 c#에서는 템플릿 대신 다른 개념이 등장하게 되었는데, 이번엔 그 개념에 대해서 알아보겠다.

 

 

 

 

특정 함수의 기능에 대해, 자료형을 특정하지 않고 여러 자료형들도 해당 기능을 사용하기 위해, c#에서는 함수를 일반화 한다. 

 

함수의 일반화란, 함수의 매개변수의 자료형을 특정하지 않고 일반화 하여, 어떤 자료형이든 매개변수로 넘길 수 있도록 하는 것이다.

 

함수의 일반화 방법은 다음과 같다.

 

 

using System;

namespace Favor
{
   internal class Program
   {
      public static void Swap<T> (ref T left, ref T right)
      {
         T temp = left;
         left = right;
         right = left;
      }
      static void Main(string[] args)
      {
         int a = 10;
         int b = 5;
         
         Swap<int>(a, b);
         
         Console.WriteLine($"{a}, {b}");
         
         // 출력값 5, 10
      }
   }
}

 

먼저, 일반화하길 원하는 함수의 이름 뒤에 <T>를 붙여준다. 이를 붙여줌으로써, 함수를 일반화할 것을 알려주는 것이다.

 

그 다음, 매개 변수의 자료형을 적는 대신, T를 붙여 T를 자료형 처럼 사용한다.

 

T는 어떤 자료형을 받든, 해당 자료형으로 형변환할 것이라고 정하는 것이다. 

 

위의 Swap<int>(a, b)는 Swap을 사용하되, 미리 내가 매개로 전달할 자료형을 알려주는 것이다.

 

여기서는 <int>로 int형을 매개로 전달할 것임을 정의하고 int변수 a와 b를 넘겼다.

 

물론, 매개변수를 통해 연산해야할 자료형이 추측이 가능한 경우에는 생략해도 무관하다.

 

여기서는 a와 b 모두 int 자료형이라 자료형이 추측이 가능하기 때문에, <int>부분은 생략이 가능하다.

 

 

 

 

위에서는 반환값이 없는 void 함수를 일반화 하였으나, 반환값도 T로 일반화가 가능하다.

 

어떤 자료형의 매개변수를 전달받든, 해당 자료형으로 다시 반환할 수 있는 함수로 일반화가 가능하다는 것이다.

 

 

 

 

함수를 일반화하는 것 외에, 클래스도 일반화할 수 있다.

 

일반화된 클래스는 이 클래스를 통해 객체를 인스턴스할 때 자료형을 지정해주면 된다.

 

이렇게 일반화된 클래스는, 같은 클래스를 통해 생성된 객체들 끼리 서로 다른 자료형을 다룰 수 있도록 생성됨으로써,

 

자료형 측면의 다형성을 만드는 것을 편리하고 직관적으로 만들어준다.

 

일반화 클래스의 생성 방법은 다음과 같다. 

 

using System;

namespace Favor
{
   internal class Program
   {
      class SafeArray<T>
      {
         T[] array;
         public SafeArray(int size) { array = new T[size]; }
      }
      static void Main(string[] args)
      {
         SafeArray<int> iArr = new SafeArray(6);
         SafeArray<float> fArr = new SafeArray(3);
      }
   }
}

 

함수이름 SafeArray뒤에 <T>일반화 선언을 해줌으로써, 클래스의 멤버를 일반화할 수 있어졌다.

 

여기서는 배열의 자료형을 일반화 하여, 같은 특징을 가진 자료형이 서로 다른 배열들을 생성할 수 있도록 하였다.

 

 

 

 

using System;

namespace Favor
{
   class SafeArray<T>
      {
         T[] array;
         public SafeArray(int size) { array = new T[size]; }
         
         public void Set(int index, T value)
         {
            // 예외처리.
            if(index < 0 || index >= array.Length) return;
            array[index] = value;
         }
         
         public T Get (int index)
         {
            if(index < 0 || index >= array.Length) return default;
            
            return array[index];
         }
      }
}

 

일반화한 클래스의 이용을 다루기위해 위와같이 클래스를 설계하여,

 

일반화 배열의 값에 직접 접근하지 않고 수정과 참조가 가능한 SafeArray라는 클래스를 만들었다.

 

위에서 보는 것처럼, 반환 값도 일반화가 가능하다.

 

이 때, 예외처리 한 부분이 있는데, 입력한 배열의 인덱스 값이 배열의 크기보다 크거나, 0보다 작을 경우엔 예외가 처리되지 않았다는 컴파일 에러가 발생하게 된다.

 

접근할 수 없는 값에 접근하려고 했기 때문이다.

 

따라서 예외 처리를 통해, 프로그램이 실행중 다운되지도 않으면서, 어디서 문제가 발생했는지 사용자가 능동적으로 제어할 수 있어야 한다.

 

 

 

 

 

여기까지만 보면, 일반화라는 개념은 정말 매력적인 것 같다.

 

아무 함수나 클래스에 일반화 선언을 하여 좀 더 다방면에서 해당 기능과 객체를 사용하고 싶어질 정도이다.

 

그런데, 만약 일반화한 클래스에서 사용하는 기능을 수행할 수 없는 자료형을 매개로 입력하면 어떻게 될까?

 

예를 들면, 매개변수들을 서로 더하는 함수에 char자료형을 매개로 전달하거나 배열을 전달하는 경우 말이다.

 

물론 이러한 예외상황이 생길 경우엔, 일반화 선언을 하지 않거나, 각 상황별로 예외를 처리하는 방법도 있다.

 

그러나, 일부 자료형들에 해당하는 예외상황 때문에 일반화를 선언하지 않고 일일히 자료형마다 기능을 생성하는 것은 비효율적이며, 너무 많은 예외상황에 대한 예외처리를 하는 것 역시 비효율적이다.

 

이를 위해, 일반화한 자료형을 제약하는 기능이 있다.

 

이에 대한 사용법은 다음과 같다.

 

 

class Banned<T> where T : struct
{
   // ...
}

 

where T : 를 활용한 제약을 통해, 일반화한 클래스나 함수에 접근하는 매개변수의 형식을 제약할 수 있다.

 

먼저 일반화를 선언한 다음, wher T : struct 등 제약조건에 해당하는 키워드를 입력하여 매개변수를 가려받을 수 있다.

 

제약 조건에 해당하는 키워드는 아래와 같다.  

 

where T : struct { } :  값 형식으로 제한. (struct는 값 형식)
where T : class { } : 참조 형식으로 제한. (class는 참조 형식)
where T : new() { } : 생성자의 매개변수가 없는 자료형으로 제한.
where T : 기반클래스(부모클래스) { } : 파생 클래스로 제한.
where T : interface { } : interface를 상속받은 자료형으로 제한.

 

예를들어, where T : struct 로 제한된 클래스일 경우,

 

클래스의 객체를 생성할 때 값 형식인 자료형으로만 생성이 가능하다.

 

따라서, 해당 자료형에 class나 Array, string등의 참조형식 자료형을 입력하게되면 생성이 불가능하다.

 

이를 통해 일반화한 클래스나 함수 내에서 기능을 수행할 때, 해당 기능을 수행하지 못하는 일부 자료형들을 제한함으로써, 예외를 미리 처리하는 동시에, 여러 자료형들로 쪼개어 다형성있는 객체를 생성할 수 있게 된다.

 

 

 

 

인터페이스의 경우도 마찬가지다. 만약 where T : IComparable로 매개변수의 형식을 제약했다면, 대입되는 자료형에는 IComparable을 상속받은 자료형만 입력해야한다.

 

이러한 인터페이스를 통한 제약은, 함수에서 사용할 자료형 혹은 클래스에서 다뤄야 할 자료형들이 어떠한 기능을 가져야만 하는지를 확실히 할 수 있게된다.

'C# 일기' 카테고리의 다른 글

18. 대리자 (Delegate)  (0) 2024.03.11
17. 예외처리(Exception Handling)  (0) 2024.03.11
15. 연산자 오버로딩  (0) 2024.03.08
14. Array 클래스  (0) 2024.03.08
13. String과 StringBuilder  (2) 2024.03.08