프로그래밍 C#언어

컬렉션(Collections)

게임첫걸음 2024. 9. 9. 22:36

컬렉션이란? 아래의 기능을 사용하여 여러 개의 데이터를 하나의 구조로 그룹화하여 관리하는 기능입니다. 

using System.Collections;
using System.Collections.Generic; //컬렉션에서 제네릭을 사용하기 위한 코드

 

 

 데이터의 집합을 저장하고 조작하는데 필요한 다양한 기능을 제공하며, C#의 컬렉션 클래스와 인터페이스는 다음과 같은 주요 기능과 특징을 갖습니다.

  • Array: 고정된 크기의 데이터 집합을 저장, 인덱스를 통해 요소에 접근할 수 있습니다.
//배열 Array 타입 예시
int[] arr = { 5, 1, 6, 3, 7 };
Array.Sort(arr);    //Sort, 오름차순 정리
foreach (int i in arr)
{
    Console.WriteLine(i);   //오름차순 정리된 arr값들 출력
}
  • ArrayList
//리스트 중 비제네릭 컬렉션 ArrayList 타입 예시
ArrayList arrList = new ArrayList();
//1. 자료형 상관없이 어떤 타입의 객체든 저장할 수 있습니다.
//2. 내부적으로 모든 항목은 object 타입으로 저장됩니다.
//3. 그 때문에 타입 안전성이 보장되지 않습니다.
//4. 쓸 때마다 자료형을 지정해야함.
arrList.Add(5);
arrList.Add(3.14);
arrList.Add("Hello");

//Boxing, Unboxing
object obj = "World";
string str = (string)obj;

 ArrayList는 비제네릭 컬렉션으로 어떤 자료형, 데이터 타입 상관없이 object 타입으로 변환하여 저장합니다. 이로 인해 타입 안전성이 보장되지 않으며, 요소를 사용할 때마다 적절한 타입으로 캐스팅해야합니다. 그리고 이 과정에서 Boxing, Unboxing에 대해 알아보아야합니다.

    • Boxing: int, float, bool, string 등 기본 데이터 타입을 object 타입으로 변환하는 과정입니다. 기본 타입 값을 object 타입의 참조 타입으로 감싸는 것입니다.
    • 위 코드 중 arrList는 요소를 꺼낼 때마다 적절한 타입으로 캐스팅이 필요합니다.

ArrList 그림 예시

 

  • List<T>는 제네릭 컬렉션으로 특정 타입의 요소만 저장할 수 있습니다. 이로 인해 타입 안전성이 보장됩니다. 지정된 타입은 모두 T로 저장되며, 타입 캐스팅이 필요없습니다.
//리스트 중 제네릭 컬렉션 List<T> 타입 예시
List<double> list = new List<double>();
list.Add(1.1);
list.Add(2.2);
list.Add(3.3);
list[0] = 11.1;
list.Sort();
//배열은 Range, List는 Count

 

위의 코드에서는 double 타입의 요소만 저장할 수 있습니다. 여기서 list의 구성 요소는 {1.1, 2.2, 3.3}입니다. 그런데 list[0] = 11.1;이 첫번째 요소를 11.1로 초기화시키는 코드이기 때문에 {11.1, 2.2, 3.3}이 됩니다.

foreach (double d in list)
    Console.WriteLine(d);

만약 위의 출력문을 밑에 추가로 작성한다면 2.2 \n 3.3 \n 11.1 이 출력됩니다. 그 이유는 list.Sort();가 있기 때문입니다. Sort()는 구성 요소들을 오름차순으로 재배열해달라는 코드입니다.

  • Dictinary<TKey, TValue> : 키와 쌍을 저장하는 컬렉션입니다. 키를 통해 값에 빠르게 접근할 수 있습니다.
 Dictionary<string, int> dic = new Dictionary<string, int>();
 dic.Add("Hello", 100);
 dic.Add("World", 200);
 dic["Hello"] = 300;

 위의 코드에서는 dic이라는 Dictinary 컬렉션에서 string과 int를 다루고 있으며, Hello 키에 100, World 키에 200을 추가했습니다. 그리고 Hello라는 키에 대한 값을 300으로 초기화시켜줍니다. 아래의 출력문에 주석으로 출력값을 표시했습니다.

 Console.WriteLine(dic["Hello"]);	//출력값: 300
 //Console.WriteLine(dic[1]);  //배열 형식의 출력문은 안됨.
 Console.WriteLine(dic.ContainsKey("Hello"));	//해당 키가 있으면 True 없으면 False 출력값: True
 //dic.Keys
 foreach (KeyValuePair<string, int> kvp in dic)
 {
     Console.WriteLine("Key: {0} / Value: {1}", kvp.Key, kvp.Value);
 }
 // foreach문 출력값
 // Key: Hello / Value: 300
 // Key: World / Value: 200

 

이외에 종류 3가지 컬렉션 타입은 간략하게 설명하겠습니다.

  • HashSet<T>: 중복 없는 고유한 요소의 집합을 저장, 집합 연산(합, 교집합 등)을 지원합니다.
//HashSet<T> 예시
HashSet<int> uniqueNumbers = new HashSet<int> { 1, 2, 3, 4, 5 };
  • Queue<T>: 선입선출 (First In First Out) 원칙에 따라 요소를 관리합니다. 데이터를 순서대로 처리하는 데 사용합니다.
//Queue<T> 예시
Queue<string> queue = new Queue<string>(); 
queue.Enqueue("First"); 
queue.Enqueue("Second");
  • Stack<T>: 후입선출(Last In First Out) 원칙에 따라 요소를 관리합니다. 데이터를 최근 항목을 먼저 처리합니다.
//Stack<T> 예시
Stack<string> stack = new Stack<string>(); 
stack.Push("First"); 
stack.Push("Second");

 

이제 위를 응용해보겠습니다.

//DataHolder.cs
class DataHolder : IComparable<DataHolder>    //DataHolder를 이용한
{
    public string str = string.Empty;
    public int i = 0;
    public float f = 0.0f;

    public DataHolder(string _str, int _i, float _f)
    {
        str = _str;
        i = _i;
        f = _f;
    }

    public int CompareTo(DataHolder _other) //IComparable 관련
    {
        return this.f.CompareTo(_other.f);
    }

    public override string ToString()
    {
        return string.Format("< {0} / {1} / {2} >", str, i, f);
    }
}

위는 DataHolder이름의 스크립트로 str, i ,f를 사용하는 DataHolder(str, i, f)라는 객체를 만들어줍니다. 그리고 두 개의 메서드를 사용합니다.

  • CompareTo: IComparable 인터페이스를 구현하여 DataHolder 객체를 f 필드(부동 소수점 숫자)를 기준으로 비교하여 정렬 기준을 제공합니다.
  • ToString: DataHolder 객체의 상태를 문자열로 표현하여 쉽게 출력할 수 있게 해줍니다.

이제 이를 다루는 Program 스크립트를 살펴보겠습니다.

using System;
using System.Collections;
using System.Collections.Generic;   //제네릭 컬렉션 사용

//유니티가 아닌 일반 C#을 다루면 기본 using System;이 깔린 상태
//Collections도 포함.
class Program
{
    static int CompareDataHolder(DataHolder _lhs, DataHolder _rhs) //DataHolder.cs의 좌변, 우변 비교
    {
        //_lhs.i - _rhs.i DataHolder.cs의 int자료형의 i를 가지고있는 좌변, 우변의 값을 비교.
        return _lhs.i.CompareTo(_rhs.i);    //좌변에 i를 우변의 i와 비교하는 것.
    }

    public class DataHodlerComparer : IComparer<DataHolder>
    {
        int IComparer<DataHolder>.Compare(DataHolder _lhs, DataHolder _rhs)
        {
            return _lhs.str.Length - _rhs.str.Length;  
        }
    }
}
  • CompareDataHolder는 DataHolder.cs에서 int 자료형 i를 가지고 있는 좌변, 우변을 비교하는 메서드입니다. _lhs.i.CompareTo(_rhs.i)가 그 역할을 해줍니다.
  • DataHolderComparer는 IComparer<DataHolder> 인터페이스를 구현하여 두 DataHolder _lhs, _rhs의 str 필드의 길이를 비교합니다. 

이제 이 두 문을 이용하는 코드를 보겠습니다.

class Program
{
	static void Main()
    {
        //DataHolder[] dataHodlers;
        List<DataHolder> dataHolderList = new List<DataHolder>();
        dataHolderList.Add(new DataHolder("A", 10, 10.1f));
        dataHolderList.Add(new DataHolder("B", 2, 0.2f));

        //1
        //Comparison<DataHolder> comparison = new Comparison<DataHolder>(CompareDataHolder);
        //dataHolderList.Sort(comparison);    //위의 비교문의 오름차순의 Sort(); A의 값이 3이상이면 B가 먼저 호출
        //dataHolderList.Sort();  //()괄호 안에 ctrl, shift, space 누르면 Sort관련이 나옴.
        //익명, 무명 메소드
        Comparison<DataHolder> comparison = new Comparison<DataHolder>(delegate (DataHolder _lhs, DataHolder _rhs)
        {
            return _lhs.i.CompareTo(_rhs.i);
        }
        );
        dataHolderList.Sort(comparison);
        //2
        //dataHolderList.Sort(new DataHolderComparer());

        //3
        //dataHolderList[0].CompareTo(); 
        //dataHolderList.Sort();

        foreach (DataHolder data in dataHolderList)
        {
            //Console.WriteLine("{0} / {1} / {2}", data.str, data.i, data.f);
            Console.WriteLine(data.ToString());
        }
    }
}

 List<T>를 이용하여 DataHolder를 다루는 dataHolderList 제네릭 리스트를 만듭니다. 그리고 요소에 추가합니다. 이제 델리게이트(delegate)에 대해 알아봐야합니다.

  • 대리자(delegate): 메서드를 참조할 수 있는 타입입니다. 대리자는 메서드의 참조를 담고 있는 변수라고 생각할 수 있습니다. 이를 통해 메서드를 변수처럼 다루거나 메서드를 인자로 전달할 수 있습니다.
  • 대리자의 정의: 대리자는 메서드의 시그니처(반환형과 매개변수 목록)를 정의하는 타입입니다. 이 시그니처에 맞는 메서드들을 대리자 변수에 할당할 수 있습니다.
  • 콜백 메서드: 콜백이란 특정 작업이 끝난 후 호출되는 메서드입니다. 대리자는 이 콜백 메서드를 저장하고 호출할 때 사용됩니다. 즉, 특정 이벤트나 작업이 완료된 후 호출되기를 기다리는 메서드입니다.

대리자 그림 예시

이제 구문을 해석하겠습니다. 여기에는 익명(무명) 메서드라는 개념이 등장합니다.

  • 위의 코드에서 Comparison<DataHolder>는 DataHolder 타입의 두 객체를 비교하고 정수 값을 반환하여 정렬 순서를 결정하는 메서드의 대리자 타입입니다. (Comparison<T>라는 대리자(delegate) 타입 문법이 존재합니다.)
  • new Comparison은 익명, 무명 메서드를 사용하여 Comparison<DataHolder> 타입의 대리자를 생성하였습니다. 

익명, 무명 메서드(Anonymous Method)란?

  • 익명, 무명 메서드(Anonymous Method): 이름이 없는 메서드를 정의할 수 있는 기능을 제공하는 C#의 구문입니다.
  • 코드에서 메서드를 간단히 정의하고 사용할 수 있게 하며 대리자(delegate)와 함께 사용합니다.
  • 짧은 코드로 간단한 메서드를 정의할 수 있습니다.

위에서 delegate를 통해 Comparison<DataHolder> 타입의 대리자를 생성하였고 DataHolder의 좌변과 우변을 비교하고 정수값을 반환합니다. 여기서 익명 메서드의 본문은 

return _lhs.i.CompareTo(_rhs.i);

이 코드가 됩니다ㅣ int 타입의 i값을 가진 좌변과 우변을 DataHolder.cs의 Compare.To로 비교한 값을 반환한다는 뜻입니다. 이제 foreach()문을 해석합니다.

foreach (DataHolder data in dataHolderList)
        {
            //Console.WriteLine("{0} / {1} / {2}", data.str, data.i, data.f);
            Console.WriteLine(data.ToString());
        }

 

 DataHolder 타입의 data 요소를 참조하는 변수를 넣어놓은 dataHolderList를 순회, 반복하라는 뜻입니다.

주석처리한 출력문의 출력값은 처음에 초기화 시켰던 

{A} / {10} / {10.1}
{B} / {2} / {0.2} 가 출력됩니다.

밑의 출력문은 DataHolding.cs의 ToString() 메서드를 사용한 것으로 

public override string ToString()
    {
        return string.Format("< {0} / {1} / {2} >", str, i, f);
    }

이를 이용합니다. override는 덮어쓴다는 표현이라고 설명했었죠? 그에 따라. 위의 출력값과 같은

{A} / {10} / {10.1}
{B} / {2} / {0.2} 가 출력됩니다.

 

'프로그래밍 C#언어' 카테고리의 다른 글

대리자(delegate)  (0) 2024.09.10
템플릿(Template)  (1) 2024.09.09
상속3  (0) 2024.09.09
상속2  (5) 2024.09.05
상속 (Inheritance)  (0) 2024.08.30