[C#] array, enum, struct

Lingtea_luv·2025년 3월 17일
0

C#

목록 보기
6/37
post-thumbnail

배열


배열과 List는 관련이 있는 동일한 자료형의 데이터를 묶는 꾸러미라고 보면 된다. 이때 배열은 크기를 미리 지정한 뒤에 데이터를 저장하여 크기가 변하지 않고, List는 저장한 데이터에 따라 크기가 달라진다는 차이가 있다.

학습 목표

  • 배열의 이해
  • 배열의 사용법
  • 참조형의 이해

배열의 이해

배열의 기본 사용식은 다음과 같다.

자료형[] = new 자료형[배열의 크기]{데이터...}

여기서 데이터의 저장 방식이 기존 변수와 조금 다른데, new를 사용하여 배열의 크기를 지정하면 해당 배열의 크기를 가진 데이터 공간이 생성되며, 해당 데이터 공간에 들어갈 요소들은 데이터 값이 아닌 주소값으로 저장된다. 즉 배열 요소의 데이터가 바로 배열의 크기에 해당하는 공간에 바로 들어가는 것이 아닌, 주소값이 저장되어 이를 불러오는 방식으로 배열을 나타낼 수 있다는 것이다. 이를 참조형 변수라고 하며, 데이터의 실제 값은 해당 주소값에 데이터의 크기만큼 할당되어 저장된다.

int[] intArray = new int[4]{1,2,3,4};
foreach(int output in intArray)
{
  Console.Write(output);
}
1234

위처럼 intArray 에 1~4의 숫자를 크기 4인 배열에 저장한다고 했을때 실제로 저장되는 것은 1234를 저장한 주소값이다.

int[] intArray = new int[4];
intArray[0] = 1;
intArray[1] = 2;
intArray[2] = 3;
intArray[3] = 4;
foreach(int output in intArray)
{
  Console.Write(output);
}
1234

배열을 선언할 때 배열의 크기만 지정하고 요소는 따로 추가하는 것도 가능하다. 다만 배열은 0부터 시작하기에 배열의 첫번째에 저장하기 위해서는 배열[0] 에 저장해야하는 것을 주의하자. 또한 요소를 추가하지 않을 경우 배열에는 각 자료형의 기본값이 저장된다.

int[] intArray = new int[4];
intArray[0] = 1;
intArray[1] = 2;
intArray[3] = 4;
foreach(int output in intArray)
{
  Console.Write(output);
}
1204

배열의 크기

int[] intArray = new int[4];
Console.WriteLine(intArray.Length);
4

배열의크기.length 로 배열의 크기를 불러올 수 있다. 함수나 구문에 활용하는 경우 다음과 같이 불러와서 사용한다. 또한 배열의 크기가 굉장히 커서 디버깅 과정에 어려움이 생길 수 있는데, 이 경우에는 조사식을 사용하여 배열의 특정 순서 데이터 변화를 보는 것이 가능하니 참고하자.

int[] intArray = new int[4]{1,2,3,4};
for(int i = 0; i < intArray.Length; i++)
{
  Console.Write(intArray[i]);
}
1234

배열 안에 있는 요소를 모두 표현하고자 할 때 for문과 배열의 길이를 사용하면 편하다.

다차원 배열

게임에서 프리셋 변경을 생각해보자.
첫 번째 단축키 프리셋 : 1. 포션 2. 폭탄 3. 물약 4. 부적
두 번째 단축키 프리셋 : 1. 물약 2. 깃발 3. 폭탄 4. 포션
해당 프리셋을 구현하기 위해서는 크기 4인 배열을 2개 사용해야하지만, 다음과 같이 2차원 배열로 지정하는 것도 가능하다.

string[,] stringArray = new string[2, 4]
{
    { "포션", "폭탄", "물약", "부적" },
    { "물약", "깃발", "폭탄", "포션" }
};
for (int i = 0; i < stringArray.GetLength(0); i++)
{
    for (int j = 0; j < stringArray.GetLength(1); j++)
    {
        {
            Console.Write(stringArray[i,j] + "\t");
        } 
    }
    Console.WriteLine();
}
포션	폭탄	물약	부적
물약	깃발	폭탄	포션

2차원 배열의 경우 배열의 선언은 다음과 같이 이루어진다.
자료형[,] 배열의 이름 = new 자료형[행의 크기, 열의 크기]
양쪽의 형식을 똑같이 맞춰주어야한다는 것을 유의하여 선언을 하면 되고, 다만 2차원 배열의 크기를 알기 위해 배열의 이름.Length 를 사용하면 행의 크기 x 열의 크기가 출력되어 요소의 총 개수가 출력된다. 만약 특정 행의 열 크기를 알고 싶다면 배열의 이름.GetLength(행 번호) 를 사용하면 된다.

string[,] stringArray = new string[2, 4]
{
    stringArray[0,0] = "포션";
    stringArray[0,1] = "폭탄";
    stringArray[0,2] = "물약";
    stringArray[0,3] = "부적";
    stringArray[1,0] = "물약";
    stringArray[1,1] = "깃발";
    stringArray[1,2] = "폭탄";
    stringArray[1,3] = "포션";
};
for (int i = 0; i < stringArray.GetLength(0); i++)
{
    for (int j = 0; j < stringArray.GetLength(1); j++)
    {
        {
            Console.Write(stringArray[i,j] + "\t");
        } 
    }
    Console.WriteLine();
}
포션	폭탄	물약	부적
물약	깃발	폭탄	포션

2차원 배열에 요소를 추가하는 방식은 다양하기에 요소를 한 번에 추가하는지, 하나씩 추가하는지에 따라 다르게 활용하면 된다.

문자열

기존에 배웠던 string 또한 문자가 나열된 것이므로 문자의 배열이라고 볼 수 있다. 다만 읽기 전용이라는 점에서 char[] 와의 차이가 존재한다.

string name = "abcde";
char[] arrayName = name.ToCharArray();
string name = "abcde";
for(int i = 0; i < name.Length; i++)
{
    Console.Write(name[i]);
}
abcde

이렇게 배열의 크기를 표현하는 .Length 의 사용도 가능하며, 배열을 출력할 때 처럼 string을 표현할 수 있다.

string name = "aBcde";
char[] arrayName = name.ToCharArray();
arrayName[0] = char.ToUpper(arrayName[0]);
arrayName[1] = char.ToLower(arrayName[1]);
arrayName[2] = 'x';
for (int i =0; i<name.Length; i++)
{
    Console.Write(arrayName[i]);
}
Abxde

만약 string 문자열의 일부만 변형시키고 싶은 경우, ToCharArray 를 통해 char[] 로 변경 후에 요소 값을 바꿔주는 것으로 가능하다.

참고. char.ToUpper : 대문자 변환 / char.ToLower : 소문자 변환
참고. string.Split() : 괄호 기준 문자열 쪼개기 / string.Replace : 문자열 치환

함수에서의 배열

위에서 언급했듯이 배열은 데이터 자체를 저장하는 것이 아니라 주소값을 저장한다. 따라서 함수에서 매개변수의 값은 ref 를 붙여 참조하는 것이 아닌 이상 변하지 않지만, 배열을 매개변수로 사용할 경우 애초에 배열의 요소가 참조 형태이기 때문에 함수 내에서 배열의 요소 값을 바꾸면 실제로 변하는 것을 볼 수 있다.

static void ArrayTest(int[] array)
{
  	array[2] = 999;
}

static void Main(string[] args)
{
	int[] test = new int[4]{1,2,3,4};
    Console.WriteLine("함수 전 값 : {test[2]}");
    ArrayTest(test);
    Console.WriteLine("함수 후 값 : {test[2]}");
}
함수 전 값 : 3
함수 후 값 : 999

열거형


switch 구문으로 데이터의 종류를 다루는 형식의 경우 혼자 작업할 때는 큰 문제가 생기지 않지만, 타인과 협업하여 점점 코드가 길어진다면 가독성이 문제가 되는 상황이 분명히 오게 된다. 이때 활용하는 열거형은 숫자에 별명을 붙여 알아보기 쉽게 만들어주는 기능이다.

선언

string commend = "가위";
switch(commend)
{
	case "가위" :
    	Console.WriteLine("가위를 냅니다");
        break;
    case "바위" :
    	Console.WriteLine("바위를 냅니다");
    	break;
    case "보" :
   		Console.WriteLine("보를 냅니다");
   		break;
    default :
    	Console.WriteLine("잘못 냈습니다");
   		break;
}

위와 같은 경우 case가 3가지 밖에 없어서 비교적 단순해 보이지만, case가 몇십, 몇백가지라고 가정해보자. 사람이기에 오타를 생길 수 밖에 없고, 백번양보해서 나는 실수가 없다고 하더라도 협업하는 사람까지 실수가 없을 것이라는 보장을 할 수 없다.

enum RockScissorPaper
{
	가위,
    바위,}

RockScissorPaper commend = RockScissorPaper.바위;
switch(commend)
{
	case RockScissorPaper.가위 :
    	Console.WriteLine("가위를 냅니다");
        break;
    case RockScissorPaper.바위 :
    	Console.WriteLine("바위를 냅니다");
    	break;
    case RockScissorPaper.:
   		Console.WriteLine("보를 냅니다");
   		break;
    default :
    	Console.WriteLine("잘못 냈습니다");
   		break;
}

열거형은 enum 으로 키워드를 묶어 사용하는데, 이렇게 하면 인덱스 commend 에는 enum 으로 묶은 가위 바위 를 제외하고 다른 단어가 쓰일 수 없다. 즉, 실수로 잘못 입력하게 되는 경우나 오탈자의 위험을 없애주기 때문에 관리하기 훨씬 편하고 가독성이 좋다는 장점을 갖는다. 다만 위에서는 예시를 위해 한글로 작성했지만, 웬만하면 영어를 쓰자.

활용

enum RockScissorPaper
{
	가위, 바위,}

다시 열거형 선언부로 돌아가서 가위, 바위, 보에 마우스를 가져다대면 각각 0,1,2라는 값을 갖는다는 것을 볼 수 있다. 앞서 말했듯이 열거형은 우리가 헷갈리지 않게 하기 위해 단순히 숫자에 별명을 붙인 것이기에, 열거형에 나열된 단어들은 0부터 시작하는 정수의 별명이라고 볼 수 있다.

enum RockScissorPaper
{
	가위 = 1, 바위,}
enum RockScissorPaper = RockScissorPaper.가위

다만 위와 같이 앞의 단어에 가위 = 1 처럼 작성하면 시작 번호가 0이 아닌 1로 설정되어 마우스를 가져다대면 각각 1,2,3의 값을 갖는다는 것을 볼 수 있다.
따라서 enum RockScissorPaper = RockScissorPaper.가위int index = 1 로 치환이 가능하다. 다만 이 경우 앞서 말한 문제점을 가지고 있기에 열거형을 만든 이상 이를 활용하는 것이 좋다.

형변환

enum RockScissorPaper
{
	가위,바위,}
RockScissorPaper commend = (RockScissorPaper)1;

위에서 말했듯이 열거형의 요소들은 각각 정수와 1대1 대응되기 때문에, 열거형과 정수 자료형 int는 서로 변환이 가능하다. 위의 코드에서 결국 commend에 들어가는 것은 가위가 되는 것이다.

enum RockScissorPaper
{
	가위 = 1,바위,}
Enum.IsDefined(typeof(RockScissorPaper),3);

이를 활용하여 열거형에서 해당 요소가 존재하는지 여부를 다음과 같은 함수를 사용하여 확인이 가능하다. Enum.IsDefined 는 열거형 RockScissorPaper 에서 3과 대응되는 요소는 있는가? 라는 bool값을 반환하고, true인 경우 3과 대응되는 요소가 존재한다는 뜻으로 해석이 가능하다.

enum RockScissorPaper
{
	가위 = 1,바위,}
static void Main(string[] args)
{
	Console.WriteLine("가위바위보!!\n1. 가위 2. 바위 3. 보");
	RockScissorPaper commend;
	Enum.TryParse(Console.ReadLine(), out commend);

	switch (commend)
	{
    case RockScissorPaper.가위:
        Console.WriteLine("가위를 냅니다");
        break;
    case RockScissorPaper.바위:
        Console.WriteLine("바위를 냅니다");
        break;
    case RockScissorPaper.:
        Console.WriteLine("보를 냅니다");
        break;
    default:
        Console.WriteLine("잘못 냈습니다");
        break;
	}
}

열거형을 활용한 가위바위보 텍스트 출력의 완성본을 보면 이와 같다. 사용자는 번호를 입력하거나, 번호에 해당하는 가위바위보를 입력할 경우 본인이 낸 것을 콘솔창에서 확인할 수 있다.

참고 응용예시

ConsoleKey key = Console.ReadKey().Key;

가장 자주 쓰게 되는 ConsoleKey 역시 열거형을 활용하여 만들어졌다.

구조체


지금까지 동일한 자료형을 묶어 관리하는 배열, List, 열거형 등을 알아보았다면, 마지막으로 다양한 자료형을 하나의 박스에 넣어 관리하는 구조체에 대해 알아보자.

선언

구조체는 새롭게 자료형을 정의하는 것이다.

enum Type {fire, water, wind, earth}
struct Skill
{
    public string name;
    public float attack;
    public float coolTime;
    public int cost;
    public float range;
    public Type type;
}    
static void Main(string[] args)
{
    Skill fireball = new Skill;
    fireball.name = "파이어볼";
    fireball.attack = 5;
    fireball.coolTime = 2.5f;
    fireball.cost = 3;
    fireball.range = 3.5f;
    fireball.type = Type.fire;

    Skill smash = new Skill;
    smash.name = "윈드커터";
    smash.attack = 20;
    smash.coolTime = 10;
    smash.cost = 12;
    smash.range = 15.5f;
    smash.type = Type.wind;
}

새롭게 스킬을 만든다고 가정을 해보자. 스킬 하나에도 굉장히 많은 자료형의 데이터를 가지고 있기에 만들어둔 스킬이 많아짐에 따라 데이터 관리에 어려움을 갖게 될 것이다. 따라서 이름부터 속성까지 다양한 자료형을 묶어 Skill이라는 새로운 자료형을 생성하면 다음과 같이 Skill fireball; 이라고 선언했을 때, .name .type 처럼 스킬이 가지고 있는 데이터들을 직관적으로 표현하는 것이 가능해진다.

struct Monster      // 구조체 선언
    {
        public string name;
        public int attack;
    }

    class StructArray
    {
        static void Print(string name, int age)    // 출력 전담 함수
            Console.WriteLine($"{name}의 공격력은 {attack} 입니다.");

        static void Main()
        {
            Monster orc;                        //구조체 형식 변수 선언
            orc.name = "오크";
            orc.attack = 200;
            
            Print(orc.name, orc.attack);

            Monter[] names = new Monster[2];    //구조체 형식 배열 선언
            names[0].name = "고블린"; names[0].attack = 30;
            names[1].name = "웨어울프"; names[1].attack = 500;
            
            for (int i = 0; i < names.Length; i++)
            {
                Print(names[i].name, names[i].attack);
            }
        }
    }

구조체 형식의 선언 방식에는 2가지가 있다. 하나는 Monster orc 와 같이 변수로 선언하는 것이며, 다른 하나는 Monster[] names 와 같이 배열로 선언하는 것이다. 배열로 선언하는 경우 코드가 꽤나 복잡해지기 때문에 많이 써서 숙달될 필요가 있다.

참고 기능


숫자의 제곱 표시

int value = 1;
for(int i = 0; i < 3; i++)
{
	value *= 4;
}

int value = (int)Math.Pow(4,3);

반복문을 통해 제곱을 표현이 가능하다. 또는 Math 함수의 Pow가 애초에 제곱을 나타내는 함수로 정의되었기 때문에 이를 가져와서 사용하는 것도 좋다.

foreach

int[] intArray = new int[4]{1,2,3,4};
foreach(int output in intArray)
{
  Console.Write(output);
}

foreach의 경우 배열과 굉장히 잘 어울리는 반복문이다. 배열의 요소를 나열할 때 많이 쓰이지만, 읽기만 가능할 뿐 배열의 요소 값을 바꿀 수 없기 때문에 변동이 필요한 경우 for이나 while 등으로 바꿔주어야한다.

List

요소의 개수가 정해진 경우 배열을 사용하여 효율적으로 메모리를 관리할 수 있으나, 요소의 개수를 알 수 없는 경우, 가변적인 경우에는 List를 사용하여 메모리 누수를 방지하는 것이 가능하다.

List<int> arrayNum2 = new List<int>();

for (int num10 = 824748; num10 != 0; num10 = num10 / 2)
{
    arrayNum2.Add(num10 % 2);
}
    arrayNum2.Reverse();
foreach (int num2 in arrayNum2)
{
    Console.Write(num2);
}    
11001001010110101100

위의 코드는 List를 활용하여 정수를 이진수로 바꾸는 것을 구현한 것인데, 여기서 배열이 아닌 List를 사용한 이유는 입력값에 따라 이진수의 자릿값이 변경되기 때문이다. 만약 배열을 사용하고자 한다면, 입력값의 범위를 제한하고, 최대 값을 미리 지정하면 가능하다. 하지만 이 경우 입력값이 작은 경우 배열에 저장되고 남은 공간은 모두 기본값인 0으로 저장되어 출력 시 이상하게 보일 뿐더러, 쓸모없는 데이터로 메모리를 낭비하게 된다.

참고 List의 이름.Reverse() 를 넣으면 List의 요소를 뒤집어서 저장한다.

null

배열의 요소에 값을 할당하지 않을 경우 기본값이 저장되는데 int, float 등은 0으로 bool은 false, string은 null로 저장된다. 이때 null이라는 개념이 생소할텐데 쉽게 말하면 아예 텅 비어있다고 생각하면 된다. 예를 들어 화장실에 갔을 때 화장지가 다 떨어져 빈 휴지심이 있었을 때는 0이라는 값이지만, 휴지 자체가 없는 경우를 null이라고 생각하면 편하다.

string[] name = new string[4];
name[1] = "b";
name[2] = "c";
name[3] = name[0] ?? "d";

foreach (string output in name)
{
    Console.WriteLine(output);
}

b
c
d

이처럼 name[0] 값이 null 인 경우 name[4] 의 값이 d가 되도록 구현을 했을 때, name[0] 에는 값이 할당되어있지 않았기 때문에 string의 기본값인 null이 되어 name[4]가 d 가 되는 것이다.

접근 제한자

접근 제한자 혹은 접근 한정자는 말 그대로 사용을 제한하는 정도를 나타낸 것이다. C#에서 기본값은 private이다.

  • public : 아무 제한 없이 사용 가능
  • private : 동일 클래스, 구조체 내의 멤버만 사용 가능
  • internal : 해당 프로젝트에서만 사용 가능, 클래스 선언시 기본값
  • protected : 해당 클래스 및 하위 클래스에서 사용 가능

과제


아이템과 인벤토리의 크기를 입력 받아 아이템 정보를 출력하는 함수를 구현할 것.

  1. 열거형과 구조체를 활용하여 Item Inventory 설정
//보유중인 아이템 열거형 작성
enum ItemList
{
    potion = 1, bomb, amulet, grenade
}
//Item 구조체 작성 아이템 이름, 고유 ID 자료형 설정
struct Item
{
    public string name;
    public int id;
}
//Inventory 구조체 작성. Item 구조체 배열로 자료형 설정 
struct Inventory
{
    public Item[] itemConfg;
}
  1. 아이템 입력값에 따른 출력 함수 구현
static void ItemOutput(string input1)
{
    ItemList inputItem;

    Enum.TryParse(input1, out inputItem);

    Item item1 = new Item();
    item1.name = "potion";
    item1.id = 0;

    Item item2 = new Item();
    item2.name = "bomb";
    item2.id = 1;

    Item item3 = new Item();
    item3.name = "amulet";
    item3.id = 2;

    Item item4 = new Item();
    item4.name = "grenade";
    item4.id = 3;

    switch (inputItem)
    {
        case ItemList.potion:
            Console.WriteLine($"아이템의 이름 : {item1.name}\n아이템 고유 ID : {item1.id}");
            break;
        case ItemList.bomb:
            Console.WriteLine($"아이템의 이름 : {item2.name}\n아이템 고유 ID : {item2.id}");
            break;
        case ItemList.amulet:
            Console.WriteLine($"아이템의 이름 : {item3.name}\n아이템 고유 ID : {item3.id}");
            break;
        case ItemList.grenade:
            Console.WriteLine($"아이템의 이름 : {item4.name}\n아이템 고유 ID : {item4.id}");
            break;
        default:
            Console.WriteLine("다시 입력해주세요.");
            Enum.TryParse(Console.ReadLine(), out inputItem);
            break;
    }
}
  1. 인벤토리 크기 입력값에 따른 출력 함수 구현
static void InvenOutput(string input2)
{
    int invenSize = int.Parse(input2);

    Inventory inventory = new Inventory();
    inventory.itemConfg = new Item[invenSize];

    inventory.itemConfg[0] = item1;
    inventory.itemConfg[1] = item2;
    inventory.itemConfg[2] = item3;
    inventory.itemConfg[3] = item4;

    for (int i = 0; i < invenSize; i++)
    {
        if (inventory.itemConfg[i].name == null)
        {
            break;
        }
        Console.Write($"{inventory.itemConfg[i].name},{inventory.itemConfg[i].id}\n");
    }
}
  1. 메인 메서드에서의 함수 구현
static void Main(string[] args)
{
    Console.Write("아이템을 입력해주세요 \n1. potion 2. bomb 3. amulet 4. grenade : ");
    string inputItemBasic = Console.ReadLine();

    ItemOutput(inputItemBasic)

    Console.Write("인벤토리의 크기를 말해주세요 : ");
    string inputInvenSize = Console.ReadLine();

    ItemOutput(inputInvenSize)
}
profile
뚠뚠뚠뚠

0개의 댓글