상세 컨텐츠

본문 제목

[C#] 배열및 컬렉션과 인덱서활용 그리고 나열하기

.NET/C#

by 클리엘 클리엘 2021. 2. 8. 10:47

본문

728x90

1. 배열

 

배열은 대괄호([])를 사용해 다음과 같이 선언할 수 있습니다.

int[] array = new int[5];

배열 변수는 '데이터형식[] 변수명'으로 선언하고 'new 데이터형식[배열크기]'형식으로 배열의 크기를 지정할 수 있습니다. 선언한 배열은 '변수명[인덱스]'와 같은 형태로 값을 읽거나 쓸 수 있습니다.

int[] array = new int[5];
array[0] = 10;
array[1] = 20;
array[2] = 30;
array[3] = 40;
array[4] = 50;

인덱스는 배열의 순서를 의미하며 순서는 0부터 시작하게 됩니다.

 

배열은 선언과 동시에 값을 초기화 할 수 있습니다.

int[] array = {10, 20, 30, 40, 50};

위와 같이 초기화 하면 요소의 개수에 맞게 초기화가 이루어집니다. 만약 요소의 수를 임의로 지정해야 한다면 아래와 같은 방법으로 초기화가 가능합니다.

int[] array = new int[5] {10, 20, 30, 40, 50};

배열은 System.Array클래스에 기반한 파생클래스에 해당하므로 System.Array에 속해있는 메서드 혹은 속성(프로퍼티)을 사용할 수 있습니다. 아래는 자주 사용되는 메서드를 정리한 것입니다.

메서드/속성 기능
Sort() 배열을 정렬합니다.
IndexOf() 배열을 검색하고 그 결과를 인덱스로 반환합니다.
Clear() 배열의 값을 초기화 합니다.
Length 배열의 길이를 반환합니다.
Rank 배열의 차원수를 반환합니다.
   

Rank는 배열의 차원수를 반환합니다. 이 말을 다르게 해석하면 배열은 다차원으로 이루어질 수 있음을 알 수 있습니다.

int[,] array = new int[2, 3];

위 에제는 길이가 3인 배열을 2개 선언한 것입니다. 따라서 다음과 같이 배열인덱스를 지정해 요소에 접근할 수 있습니다.

int[,] array = new int[2, 3];
array[0, 0] = 10;
array[0, 1] = 20;
array[0, 2] = 30;

array[1, 0] = 40;
array[1, 1] = 50;
array[1, 2] = 60;

다차원의 배열을 초기화 하는 경우에도 차원수에 맞게 요소를 배열해 주면 됩니다.

int[,] array = {{10, 20, 30}, {40, 50, 60}};

물론 위에서와 같이 차원수와 배열의 길이를 미리 지정해도 되지만 필요하다면 길이가 서로 다른 배열을 묶어주는 것도 가능합니다.

int[][] array = new int[3][];

예제에서는 대괄호([])를 2개를 붙여 사용했는데 이는 배열 자체를 담을 수 있는 배열이 선언된 셈이고 끝에 첨자 3이 있는 건 배열을 3개 담을 수 있다는 것을 의미합니다. 따라서 위 배열 변수는 다음과 같은 방식으로 배열을 담아낼 수 있습니다.

int[][] array = new int[3][];
array[0] = new int[3] {1, 2, 3};
array[1] = new int[] {1, 2};
array[2] = new int[] {1, 2, 3, 4, 5};

--또는

int[][] array = new int[3][] { new int[3] {1, 2, 3}, new int[] {1, 2}, new int[] {1, 2, 3, 4, 5} };

보시는 바와 같이 배열자체에 배열을 담고 있으며 2차원이나 3차원 배열처럼 차원수를 늘리는 배열과는 달리 각 배열마다 배열 길이가 다른 배열을 하나의 변수에 담아 사용할 수 있습니다.


2. 컬렉션

 

(1) ArrayList

 

ArrayList클래스는 다루고자 하는 데이터의 형식과 길이를 지정할 필요없이 다양한 형식의 데이터를 자유롭게 담고 삭제할 수 있습니다.

ArrayList list = new ArrayList();
list.Add(123);
list.Add("abc");
list.Add(10.1f);

list.RemoveAt(0);

list.Insert(0, 456);

Console.WriteLine($"{list[0]}");

ArrayList는 위와 같이 사용할 수 있으며 Add()메서드를 통해 컬렉션의 맨 끝에서부터 데이터를 추가할 수 있고, RmoveAt() 메서드로 특정 인덱스의 데이터를 제거할 수 있습니다. Insert() 메서드는 지정한 인덱스로 데이터를 추가하도록 합니다.

 

Add() 메서드나 Insert() 메서드를 보면 담을 수 있는 데이터형이 object이므로 모든 형식의 데이터를 자유롭게 담아낼 수 있습니다. 이러한 특징은 다른 컬렉션의 클래스에도 동일하게 적용됩니다.

 

컬렉션에서는 또한 배열을 통해서 초기화하거나

int[] arr = {1, 2, 3};
ArrayList list = new ArrayList(arr);

배열처럼 대괄호({})를 통해 초기화 하는 것이 가능합니다.

ArrayList list = new ArrayList() {1, 2, 3};

다만 배열을 이용한 초기화를 제외하고 대괄호를 통한 초기화 방식은 IEnumerable인터페이스를 상속하는 클래스만 가능합니다. 따라서 아래에서 설명할 Queue나 Stack에는 적용할 수 없습니다.

 

(2) Queue

 

Queue의 자료구조는 명확한데 가장먼저 들어온 데이터가 가정 먼저 나가는 구조입니다.

Queue que = new Queue();
que.Enqueue(1);
que.Enqueue(2);
que.Enqueue(3);

Console.WriteLine($"{que.Dequeue()}");

데이터를 담을 때는 Enqueue() 메서드가 사용되며 데이터를 꺼낼 때는 Dequeue() 메서드가 사용됩니다. 가정 먼저 들어온 데이터가 가장 먼저 나온다고 했으니 위 예제에서 결과를 1을 표시하게 됩니다.

 

(3) Stack

 

Stack은 Queue과 달리 가정먼저 들어온 데이터가 가장 마지막에 나가는 구조입니다.

Stack stk = new Stack();
stk.Push(1);
stk.Push(2);
stk.Push(3);

Console.WriteLine($"{stk.Pop()}");

Stack에서 데이터를 담을때는 Push()를, 꺼낼 때는 Pop() 메서드를 사용합니다. 가장 먼저 들어온 데이터가 가장 마지막에 나간다는 말을 뒤집어 보면 가장 마지막에 들어온 데이터가 가장 먼저 나가는 방식이 되므로 에제의 결과는 3이 됩니다.

 

(4) Hashtable

 

위에서 말한 컬렉션은 단순히 배열의 값을 꺼내고 담을 수 있는 구조인데 반해 Hashtable은 키값을 부여하고 '키'를 통해 데이터를 저장하거나 꺼낼 수 있는 구조입니다.

Hashtable ht = new Hashtable();
ht["car"] = "자동차";
ht["Mouse"] = "마우스";
ht["Orange"] = "오렌지";

Console.WriteLine($"{ht["Mouse"]}");

Hashtable의 키는 어떤 형식의 데이터든 임의로 부여할 수 있으며 데이터의 초기화도 키를 통해서 이루어집니다.

Hashtable ht = new Hashtable() { ["car"] = "자동차", ["Mouse"] = "마우스", ["Orange"] = "오렌지" };

3. 인덱서

 

인덱서를 사용하면 특정 개체를 마치 배열처럼 다룰 수 있게 됩니다.

class MyArray
{
	int[] array;

	public MyArray()
	{
		array = new int[5];
	}

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

		set {
			array[index] = value;
		}
	}
}

인덱서는 'public 데이터형 this[인덱서데이터형 인덱서식별자]'형태로 선언되며 내부에 프로퍼티와 동일한 구조로 인덱서의 식별자를 통해 get으로 데이터를 반환하고 set으로 데이터를 저장할 수 있도록 합니다.

 

이미 언급했듯이 인덱서는 개체를 배열처럼 다룰 수 있게 만들어 주는데 이것은 클래스 내부에서 단순히 배열을 선언해 놓는 것과는 좀 다릅니다. 배열 변수를 활용하는 것이 아니라 개체 스스로를 배열로 표현할 수 있다는 점에 주목해 주세요.

MyArray ma = new MyArray();
ma[0] = 1;
ma[1] = 2;
ma[2] = 3;
ma[3] = 4;
ma[4] = 5;

Console.WriteLine($"{ma[3]}");

하지만 인덱서는 개체가 배열을 흉내내고 있을 뿐 배열은 아닙니다. 일반 배열과 가장 다른 점은 열거가 불가능하다는 것인데

int[] i = {1, 2, 3, 4, 5};

foreach(int j in i) {
	Console.WriteLine($"{j}");
}

일반 배열은 foreach 구문을 통해 열거가 가능하지만 인덱서는 위와 같은 방식을 통해 순회가 불가능합니다. 이렇게 되는 이유는 배열은 IEnumerator와 IEnumerable 인터페이스를 상속받아 열거 관련 메서드와 속성이 구현되었지만 예제에서 예로 든 MyArray클래스는 그렇지 않기 때문입니다.

 

따라서 열거가 가능한 개체를 만들려면 IEnumerator, IEnumerable 인터페이스를 상속받아 각각에 해당하는 인터페이스의 메서드와 속성을 구현해야 합니다.

class MyArray : IEnumerator
{
	int[] array;

	public MyArray()
	{
		array = new int[5];
	}

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

		set {
			array[index] = value;
		}
	}
}

우선 IEnumerator 인터페이스부터 알아보도록 하겠습니다. 해당 인터페이스는 내부에 MoveNext(), Rest()메서드와 Current 속성을 구현해야 합니다.

class MyArray : IEnumerator
{
	int[] array;
	int index = -1;

	public MyArray()
	{
		array = new int[5];
	}

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

		set {
			array[index] = value;
		}
	}

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

		++index;
		return true;
	}
}

MoveNext() 메서드의 경우 다음 요소로 이동하기 위해 호출되는 메서드로 요소의 인덱스로 활용하기 위해 별도의 index변수를 사용하고 있습니다.

 

MoveNext() 메서드의 내부 구현은 현재 index값과 배열의 길이를 비교(index는 요소의 위치를 다루는데 사용되므로 array의 length를 위치 값과 비교하기 위해서는 -1을 해줘야 합니다.)하여 index와 값이 같거나 더 크면(더 클일은 논리적으로 발생하지 않지만...) Reset()메서드(밑에서 구현합니다.)를 호출하고 false를 반환하며 그렇지 않으면 index값을 늘려 다음 요소로의 이동을 가능하게 한 뒤 true를 반환합니다.

class MyArray : IEnumerator
{
	int[] array;
	int index = -1;

	public MyArray()
	{
		array = new int[5];
	}

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

		set {
			array[index] = value;
		}
	}

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

		++index;
		return true;
	}

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

Reset() 메서드는 간단합니다. 그저 현재 위치를 나타내는 index변수를 -1로 초기화시켜주기만 하면 됩니다. index를 -1로 하는 이유는 실제 foreach동작이 일어나면 MoveNext() 메서드가 호출되는데 MoveNext() 메서드에서는 index값을 +1로 하고 난 이후의 index를 반환하기 때문입니다. 따라서 만약 index를 배열 순서인 0부터 하게 되면 요소 값이 0번째가 아닌 1번째 요소부터 가져오게 되는 문제가 발생할 수 있습니다.

class MyArray : IEnumerator
{
	int[] array;
	int index = -1;

	public MyArray()
	{
		array = new int[5];
	}

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

		set {
			array[index] = value;
		}
	}

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

		++index;
		return true;
	}

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

	public object Current
	{
		get {
			return array[index];
		}
	}
}

Current 속성은 현재 인덱스에 해당하는 값을 가져오는 속성으로 인덱스변수를 통해 요소의 값을 반환하도록 하면 됩니다.

 

다음으로 IEnumerable입니다.

class MyArray : IEnumerator, IEnumerable
{
	int[] array;
	int index = -1;

	public MyArray()
	{
		array = new int[5];
	}

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

		set {
			array[index] = value;
		}
	}

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

		++index;
		return true;
	}

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

	public object Current
	{
		get {
			return array[index];
		}
	}
}

IEnumerable 인터페이스는 IEnumerator형식의 개체를 반환하는 GetEnumerator() 라는 메서드 하나만 구현하면 됩니다.

class MyArray : IEnumerator, IEnumerable
{
	int[] array;
	int index = -1;

	public MyArray()
	{
		array = new int[5];
	}

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

		set {
			array[index] = value;
		}
	}

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

		++index;
		return true;
	}

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

	public object Current
	{
		get {
			return array[index];
		}
	}

	public IEnumerator GetEnumerator()
	{
		for (int i = 0; i < array.Length; i++) {
			yield return array[i];
		}
	}
}

메서드는 그저 for문을 통해 배열을 순회하면서 배열의 값을 하나씩 반환하는 것이 전부입니다. 다만 주목해야 할 부분은 return에 yield를 사용했다는 점인데요.

 

yield 없이 그냥 return만 사용하게 되면 return이 실행됨과 동시에 곧바로 메서드를 빠져나오게 문제가 발생합니다. 배열 전체를 순회하면서 하나씩 요소 값을 반환해야 하는데 첫 번째 요소만 반환하고는 메서드 동작이 중지되어 버리는 것이죠.

 

그래서 yield를 사용하게되는데 yield return은 다음번 yield return이나 혹은 yield break문을 만날 때까지 반복하여 실행하게 되므로 자연스럽게 전체 배열을 순차적으로 반환할 수 있게 됩니다. 참고로 yield break는 다음번 yield return을 실행하지 않고 곧바로 순회를 중지합니다.

static void Main(string[] args)
{
	MyArray ma = new MyArray();
	ma[0] = 1;
	ma[1] = 2;
	ma[2] = 3;
	ma[3] = 4;
	ma[4] = 5;

	foreach(int i in ma) {
		Console.WriteLine($"{i}");
	}
}

 

728x90

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

[C#] 예외처리  (0) 2021.02.25
[C#] 일반화  (0) 2021.02.11
[C#] 배열및 컬렉션과 인덱서활용 그리고 나열하기  (0) 2021.02.08
[C#] 프로퍼티  (0) 2021.02.01
[C#] 인터페이스와 추상클래스  (0) 2021.01.28
[C#] 클래스  (0) 2021.01.26

관련글 더보기

댓글 영역