오늘 학습 키워드

조건문, 반복문, 배열, 컬렉션, 메소드, 구조체, 클래스, 객체, 상속, 다형성, 제네릭(제너릭),

오늘 학습 한 내용을 나만의 언어로 정리하기

C# 기본 문법

조건문

  • if / else / else if
if(playerScore >= 70)
{
 
}
else if (playerScore >= 40)
{
 
}
else {
 
}
  • switch
switch(command)
{
	case 'A' :
		...
		break;
	case 'B' :
		...
		break;
	default :
		...
		break;
}
  • 3항 연산자
string result = (currentExp >= requiredExp) ? "레벨업 가능" : "레벨업 불가능";

반복문

  • for
for (int i = 0; i < 10; i++) 
{
	Console.WriteLine(i);
}
  • while
while(i < 10)
{
	Console.WriteLine(i);
	i++;	
}
  • do-while : do 에 있는 걸 무조건 한 번은 실행함
do
{
	Console.WriteLine(i);
	i++;
} while(i < 10);
  • foreach
foreach(string item in inventory) 
{
	Console.WriteLine(item);
}
  • break : 반복문 중지
  • continue : 다음 반복으로 진행

배열

  • 배열 : 동일한 자료형의 값들이 연속적으로 저장되는 자료구조
int[] array1 = new int[5];
string[] array2 = new string[3];
 
array1[0] = 1;
...

컬렉션

  • 컬렉션 : 자료를 모아 놓은 데이터 구조
  • 배열과는 다르게 크기가 가변적임
  • 사용하기 위해서는 System.Collections.Generic 네임스페이스 추가 필요함

List

  • 가변적인 크기를 갖는 배열
  • List를 생성할 때에는 List에 담을 자료형을 지정함
List<int> numbers = new List<int>(); // 빈 정수형 리스트 생성
numbers.Add(1);
numbers.Add(2);
numbers.Remove(2);

Dictionary

  • 키, 값으로 구성된 데이터를 저장
  • 중복된 키를 가질 수 없음. 키와 값을 쌍으로 저장함
Dictionary<string, int> scores = new Dictionary<string, int>();
scores.Add("Alice", 100);
scores.Add("Bob", 80);
scores.Add("Charlie", 90);
scores.Remove("Bob");
 
foreach(KeyValuePair<string, int> pair in score)
{
	//
}

Stack

  • 후입선출(LIFO) 구조를 가짐
Stack<int> st = new Stack<int>();
 
st.Push(1);
st.Push(2);
st.Push(3);
 
int value = st.Pop(); // => 가장 마지막에 넣은 3이 value로 들어감

Queue

  • 선입선출(FIFO) 구조를 가짐
Queue<int> qu = new Queue<int>;
 
qu.Enqueue(1);
qu.Enqueue(2);
qu.Enqueue(3);
 
int value = qu.Dequeue(); // => 가장 먼저 추가된 1이 value로 들어감

배열과 리스트의 차이

  1. 크기 변경 가능 여부
    • 배열은 크기가 고정
    • 리스트는 동적 배열
  2. 메모리 사용 방식
    • 리스트는 내부적으로 배열을 사용함
    • 공간이 부족해질 경우 더 큰 배열을 새로 생성하고 데이터를 복사하는데, 일시적으로 메모리 사용량이 증가는 함. 일반적인 상황에서는 큰 문제가 되진 않음
  3. 기능적 차이
    • 배열은 인덱스 접근 용이
    • 리스트는 다양한 기능을 제공. 복잡한 자료 조작에서 유리
  4. 성능 고려
    • 배열 : 반복 처리에서 빠르고 예측 가능한 성능 제공
    • 리스트 : 유연하지만, 반복 처리에서는 성능 저하의 원인이 될 수 있음.
  • 어떤 상황에서 사용해야 할까?
    • 크기가 명확, 고정형 배열
    • 게임 루프에서 반복적 접근 배열
    • 데이터 개수 유동적, 추가/삭제 잦음 리스트
    • 데이터 동적 구성, 정렬/검색/필터링 필요 리스트

메소드

  • 메소드 : 특정 작업을 위해 사용되는 독립적인 기능 단위
  • 메소드의 역할 및 중요성
    • 코드 재사용성, 모듈화, 가독성 및 유지보수성, 코드 중복 제거, 코드 추상화
접근제한자 리턴타입 메서드 (매개변수)
{
	// 메소드 실행 코드
}
 
public int AddNumbers(int a, int b)
{
	return a + b;
}
  • 메소드 오버로딩 : 같은 이름의 메소드를 다른 매개변수 목록으로 정의함
// 오버로딩 예시
 
void PrintMessage(string message) 
{
	Console.WriteLine("Message: " + message);
}
 
void PrintMessage(int number)
{
	Console.WriteLine("Number : " + number);
}
  • 재귀호출 : 자기 자신을 부르는 행위
    • 주의 : 종료조건을 명확히 하지 않으면 무한 호출이 될 수 있음
// 재귀호출 예시
void CountDown(int n)
{
	if (n <= 0)
	{
		Console.WriteLine("Done");
	}
	else
	{
		Console.WriteLine(n);
		CountDown(n - 1);
	}
}
 
CountDown(5);

구조체

  • 여러 개의 데이터를 묶어서 하나의 사용자 정의 형식으로 만듬
struct Person
{
	public string name;
	public int age;
 
	public void PrintInfo()
	{
		Console.WriteLine($"Name : {name}, Age : {age}");
	}
}
 
Person person;
person.Name = "John";
person.Age = 25;
person.PrintInfo();

틱택토 만들기

class TicTacToe
{
    char[,] board = new char[3, 3]
            { { '_', '_', '_' },
              { '_', '_', '_' },
              { '_', '_', '_' }
            };
    char currentPlayer;
    char aiPlayer;
 
    int humanSum;
    int aiSum;
 
    bool isPlayerFirst; // 플레이어가 먼저 시작하는지 여부
    bool isGameDone;
 
    public TicTacToe()
    {
        while (true)
        {
            Console.WriteLine("틱택토 게임을 시작합니다!");
            Console.WriteLine("O와 X 중, 사용할 말을 입력 해 주세요.");
            Console.Write("말 입력 (O, X, 소문자 가능): ");
            string str = Console.ReadLine().ToUpper();
            if (str == "O" || str == "X")
            {
                currentPlayer = str[0]; 
                aiPlayer = (currentPlayer == 'O') ? 'X' : 'O'; 
                break;
            }
            else
            {
                Console.WriteLine("잘못된 입력입니다. O 또는 X를 입력하세요.");
                continue;
            }
        }
 
        humanSum = currentPlayer * board.GetLength(0);
        aiSum = aiPlayer * board.GetLength(0);
 
        Console.WriteLine("선공을 지정합니다.");
        Random random = new Random();
        isPlayerFirst = random.Next(0, 2) == 0; // 0 또는 1을 랜덤으로 생성하여 선공 여부 결정. 0이면 선공임.
        if (isPlayerFirst)
        {
            Console.WriteLine("플레이어가 선공입니다.");
        }
        else
        {
            Console.WriteLine("AI가 선공입니다.");
        }
 
        while(!isGameDone)
        {
            Console.Clear();
            PrintBoard();
            if (isPlayerFirst)
            {
                PlayerMove();
                AIMove();
            }
            else
            {
                AIMove();
                PlayerMove();
            }
            Console.Clear();
            PrintBoard();
            char winPlayer;
            if (CheckWin(out winPlayer))
            {
                if(winPlayer == currentPlayer)
                {
                    Console.WriteLine($"당신이 이겼습니다!");
                }
                else if (winPlayer == aiPlayer)
                {
                    Console.WriteLine($"AI가 이겼습니다!");
                }
                isGameDone = true;
            }
        }
 
    }
 
 
    // 틱택토 보드 출력 함수
    public void PrintBoard()
    {
        Console.WriteLine("-----------");
        for (int i = 0; i < board.GetLength(0); i++)
        {
            for (int j = 0; j < board.GetLength(1); j++)
            {
                Console.Write("{0, 3}", board[i, j]);
            }
            Console.WriteLine();
        }
        Console.WriteLine("\n-----------");
    }
 
    public bool Move(int row, int col, char player)
    {
        if (row < 0 || row >= board.GetLength(0) || col < 0 || col >= board.GetLength(0))
        {
            // 잘못된 위치인 경우
            return false;
        }
        if (board[row, col] != '_')
        {
            // 이미 선택된 위치인 경우
            return false;
        }
        board[row, col] = player;
        return true;
    }
 
    public void AIMove()
    {
        Random random = new Random();
        int row, col;
        row = random.Next(0, 3);
        col = random.Next(0, 3);
        while (!Move(row, col, aiPlayer))
        {
            row = random.Next(0, 3);
            col = random.Next(0, 3);
        }
    }
 
    public void PlayerMove()
    {
        while(true)
        {
            Console.WriteLine("행과 열을 입력하세요 (0-2, 공백으로 구분): ");
            string input = Console.ReadLine();
            string[] inputs = input.Split(' ');
 
            int playerRow = -1 , playerCol = -1;
 
            bool isValidInput = inputs.Length == 2 &&
                int.TryParse(inputs[0], out playerRow) &&
                int.TryParse(inputs[1], out playerCol);
 
            if (!isValidInput)
            {
                Console.WriteLine("잘못된 입력입니다. 행과 열을 0-2 사이의 숫자로 입력하세요.");
                continue;
            }
                
            if (Move(playerRow, playerCol, currentPlayer))
            {
                break;
            }
            else
            {
                Console.WriteLine("해당 위치에는 돌을 둘 수 없습니다. 다시 입력하세요.");
            }
        }
    }
    
 
    public bool CheckWin(out char winPlayer)
    {
        char player;
 
        // 가로 검사
        for (int i = 0; i < board.GetLength(0); i++)
        {
            int rowSum = 0;
            for(int j = 0; j < board.GetLength(1); j++)
            {
                rowSum += board[i, j];
            }
            if(CheckPlayerSum(rowSum, out player))
            {
                winPlayer = player;
                return true;
            }
        }
 
        // 세로 검사
        for(int i = 0; i < board.GetLength(1); i++)
        {
            int colSum = 0;
            for(int j = 0; j < board.GetLength(0); j++)
            {
                colSum += board[j, i];
            }
            if(CheckPlayerSum(colSum, out player))
            {
                winPlayer = player;
                return true;
            }
        }
 
        // 대각선 검사
        for(int i = 0; i < board.GetLength(0); i++)
        {
            int diagSum1 = 0;
            int diagSum2 = 0;
            for (int j = 0; j < board.GetLength(1); j++)
            {
                diagSum1 += board[j, j]; // 왼쪽 위에서 오른쪽 아래 대각선
                diagSum2 += board[j, board.GetLength(0) - 1 - j]; // 오른쪽 위에서 왼쪽 아래 대각선
            }
            if (CheckPlayerSum(diagSum1, out player) || CheckPlayerSum(diagSum2, out player))
            {
                winPlayer = player;
                return true;
            }
        }
 
        // 승리자가 없는 경우 return false;
        winPlayer = '_'; 
        return false;
    }
 
 
    // 이긴사람이 있으면 true, 없으면 false 반환.
    // 그리고 out player에 이긴 사람 저장
    public bool CheckPlayerSum(int sum, out char player)
    {
        
 
        if (humanSum == sum)
        {
            player = currentPlayer;
            return true;
        }
        else if (aiSum == sum)
        {
            player = aiPlayer;
            return true;
        }
        else
        {
            player = '_'; 
            return false;
        }
    }
}

승리 판정을 할 때, 일일히 같은지 아닌지 체크하는 것 보다 특정 플레이어의 char 값을 곱하기 3 해서 같은지 확인하는게 더 빨라보여서 그렇게 했음. 메인 함수에서 Tictactoe 클래스 생성해주면 알아서 게임 시작됨

클래스와 객체

객체지향 프로그래밍 (OOP)

  • 특징
    1. 캡슐화 : 관련 데이터와 기능을 하나의 단위로 묶는 것
    2. 상속 : 기존의 클래스를 확장하여 새로운 클래스를 만드는 것
    3. 다형성 : 하나의 인터페이스나 기능을 다양한 방식으로 구현하거나 사용하는 것
    4. 추상화 : 복잡한 시스템이나 개념을 단순화하여 필요한 기능에 집중하는 것
    5. 객체 : 클래스로부터 생성된 실체

클래스

  • 구성요소
    1. 필드 : 변수
    2. 메소드 : 동작
    3. 생성자 : 객체가 생성될 때 호출됨
    4. 소멸자 : 객체가 소멸할 때 호출됨
class Player
{
	private string name; // 필드
	public void Attack()  // 메소드
	{
	 
	}
 
	public Player() // 매개변수가 없는 생성자
	{
	}
 
	public Player(string name) // 매개변수가 있는 생성자
	{
		this.name = name
	}
 
	~Player() // 소멸자
	{
		
	}
}
  • 클래스란 객체를 생성하기 위한 템플릿 혹은 설계도 역할을 함
  • 객체는 클래스의 인스턴스, 실체화 형태

구조체 vs 클래스

  • 구조체 : 값 형식, 스택에 할당, 값으로 전달
  • 클래스 : 참조 형식, 힙에 할당, 참조로 전달
  • 구조체는 상속 불가, 클래스는 상속 가능
  • 구조체는 작은 크기의 데이터 저장이나 단순한 데이터 구조
  • 클래스는 복잡한 객체 표현용
// 구조체와 클래스의 생성 차이
struct A { /**/ }
class B { /**/ }
 
A a; // 구조체
B b = new B(); // 클래스

접근제한자

  • public : 외부 접근 가능
  • private : 같은 클래스 내부만 가능
  • protected : 같은 클래스 내부와 상속받은 클래스에서만 접근 가능

프로퍼티

  • 클래스 멤버. 객체의 필드값을 읽거나 설정하는데 사용되는 접근자 메소드의 조합
// 예시
class Person
{
    private string name;
    private int age;
 
    public string Name
    {
        get { return name; }
        set { name = value; }
    }
 
    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}
  • 프로퍼티에 접근 제한자 적용과 유효성 검사를 진행할 수 있음
// 예시
class Person
{
    private string name;
    private int age;
 
    public string Name
    {
        get { return name; }
        private set { name = value; } // private라 set이 작동하지 못함
    }
 
    public int Age
    {
        get { return age; }
        set
        {
            if (value >= 0) // value가 0 이상일 때에만 작동함
                age = value;
        }
    }
}
  • 자동 프로퍼티 : 알아서 get/set이 완성됨
// 예시
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

상속과 다형성

상속

  • 정의 : 기존의 클래스를 확장하거나 재사용하여 새로운 클래스를 만드는 것
  • 장점 : 코드 재사용, 계층 구조 표현, 유지 보수성 향상
  • 종류 : 단일 상속, 다중 상속, 인터페이스 상속
  • 특징 : 부모 클래스 멤버에 접근 가능, 메소드 재정의 가능, 상속 깊이 다르게 가능

다형성

  • 정의 : 같은 타입이지만 다양한 동작을 수행할 수 있는 능력
  • 가상 메소드 : 부모 클래스에서 선언한 가상 메소드는 자식 클래스에서 재정의할 수 있음.
// 예시
public class Unit
{
    public virtual void Move()
    {
        Console.WriteLine("두발로 걷기");
    }
 
    public void Attack()
    {
        Console.WriteLine("Unit 공격");
    }
}
 
public class Marine : Unit
{
	// 마린은 오버라이드하지 않았기 때문에 부모 클래스인 Unit의 Move()를 그대로 따라감
}
 
public class Zergling : Unit
{
	// 저글링은 오버라이드 하였기 때문에 부모 클래스인 Unit의 Move()를 따라가지 않음
    public override void Move()
    {
        Console.WriteLine("네발로 걷기");
    }
}
  • 추상 클래스 / 메소드 : 직접 구현한게 아님. 자식 클래스에서 반드시 구현되어야 함
// 예시
abstract class Shape
{
	// Shape를 상속받는 모든 클래스는 Draw() 메소드를 구현해야 함
    public abstract void Draw(); 
}
 
class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a circle");
    }
}
 
class Square : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a square");
    }
}
 
class Triangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("Drawing a triangle");
    }
}
 
  • 오버라이딩 : 부모 클래스에서 정의된 메소드를 자식 클래스에서 재정의 하는 것.
  • 오버로딩 : 같은 이름의 함수를 다른 매개변수를 받아 다르게 처리하는 것.

C# 고급 문법

제네릭

  • 정의 : 클래스나 메소드를 일반화 시켜서 다양한 자료형에 대응할 수 있는 기능
  • <T> 형태로 사용함
// 예시
class Stack<T> // T 부분에 뭐든 가져옴
{
    private T[] elements; // T의 배열을 만듬
    private int top;
 
    public Stack()
    {
        elements = new T[100];
        top = 0;
    }
 
    public void Push(T item) // T를 매개변수로 받는 함수
    {
        elements[top++] = item;
    }
 
    public T Pop() // 반환형이 T인 함수
    {
        return elements[--top];
    }
}
 
// 제너릭 클래스 사용 예시
Stack<int> intStack = new Stack<int>(); // 여기서 T가 int로 대입됨
intStack.Push(1);
intStack.Push(2);
intStack.Push(3);
Console.WriteLine(intStack.Pop()); // 출력 결과: 3
 
// T가 두 개인 예시
class Pair<T1, T2>
{
    public T1 First { get; set; }
    public T2 Second { get; set; }
 
    public Pair(T1 first, T2 second)
    {
        First = first;
        Second = second;
    }
 
    public void Display()
    {
        Console.WriteLine($"First: {First}, Second: {Second}");
    }
}
 
Pair<int, string> pair1 = new Pair<int, string>(1, "One");
pair1.Display();
 
Pair<double, bool> pair2 = new Pair<double, bool>(3.14, true);
pair2.Display();

out

  • 메소드에서 매개변수를 전달할 때 쓰임
  • 메소드의 반환 값을 매개변수로 전달하는 경우에 쓰임
  • 다른 사람한테 종이(변수)를 주고 이름을 써서 돌려달라고 하는 것과 비슷
// 예시
void Divide(int a, int b, out int quotient, out int remainder)
{
    quotient = a / b; 
    remainder = a % b;
    // 3. 종이에다가 값을 써서 돌려줌
}
 
int quotient, remainder; // 1. 여기서 종이를 준비
Divide(7, 3, out quotient, out remainder); // 2. 종이를 Divide에게 넘겨줌
Console.WriteLine($"{quotient}, {remainder}"); // 출력 결과: 2, 1
 
  • out 사용 시 주의사항
    • out 매개변수는 메소드 내에서 무조건 값을 할당해 주어야 함 == 해당 변수의 이전 값이 덮어씌워짐

ref

  • 매개변수의 값을 함수 내에서 직접 수정하는 경우
// 예시
void Swap(ref int a, ref int b)
{
    int temp = a;
    a = b;
    b = temp;
}
 
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"{x}, {y}"); // 출력 결과: 2, 1
  • ref 사용 시 주의사항
    1. 값 변경 가능성
    2. 성능 : ref가 값 복사로 가져가는게 아니라서 성능이 좋기는 한데 코드 읽기가 힘들 수 있음

스네이크 게임 만들기

namespace SnakeGame
{
 
    class SnakeGame
    {
        // 스네이크 게임 만들기
 
        // 1. 뭔가 먹는게 맵에 랜덤하게 나온다.
        // 2. 그걸 먹으면 몸이 길어진다.
        // 3. 벽에 닿으면 죽는다.
        // 4. 자기 몸에 닿아도 죽는다.
 
        int boardSize = 50;
 
        char[,] board; // 게임 보드
        int snakeLength = 1; // 뱀의 길이
        int snakeHeadX = 24; // 뱀 머리의 X 좌표
        int snakeHeadY = 24; // 뱀 머리의 Y 좌표
        Random random = new Random(); // 랜덤 객체
        int foodX; // 음식의 X 좌표
        int foodY; // 음식의 Y 좌표
        ConsoleKey lastInputKey = ConsoleKey.RightArrow; // 마지막으로 입력한 방향 (기본값 : 오른쪽)
 
        char blankChar = ' '; // 빈 공간을 나타내는 문자
        char snakeChar = 'O'; // 뱀 머리를 나타내는 문자
        char foodChar = 'X'; // 음식을 나타내는 문자
 
        List<(int, int)> snakeBody = new List<(int, int)>(); // 뱀의 몸을 저장하는 리스트
 
 
        public SnakeGame()
        {
            Console.CursorVisible = false;
            snakeBody.Insert(0, (snakeHeadX, snakeHeadY)); // 뱀 머리 위치 초기화
            InitializeBoard();
            MakeFood(); 
            PlaceFood();
            // 게임 루프 시작
            DrawBoard();
 
            while (true)
            {
                MoveSnake();
                // DrawBoard();
 
            }
            
        }
 
        public void InitializeBoard()
        {
            board = new char[boardSize, boardSize];
            for (int i = 0; i < boardSize; i++)
            {
                for (int j = 0; j < boardSize; j++)
                {
                    board[i, j] = blankChar; // 빈 공간
                }
            }
            board[snakeHeadY, snakeHeadX] = snakeChar; // 뱀 머리 표시
        }
 
        public void DrawBoard()
        {
            Console.Clear();
            for (int i = 0; i < boardSize; i++)
            {
                for (int j = 0; j < boardSize; j++)
                {
                    Console.Write(board[j, i]);
                }
                Console.WriteLine();
            }
        }
 
        public void MakeFood()
        {
            do
            {
                foodX = random.Next(0, boardSize);
                foodY = random.Next(0, boardSize);
            } while (board[foodY, foodX] != blankChar); 
        }
 
        public void PlaceFood()
        {
            Console.SetCursorPosition(foodX, foodY);
            Console.Write(foodChar); // 음식 표시
        }
 
        public void MoveSnake()
        {
            if (Console.KeyAvailable)
            {
                ConsoleKeyInfo keyInfo = Console.ReadKey(true);
                // 바로 반대 방향으로는 이동할 수 없음.
                if (keyInfo.Key == ConsoleKey.UpArrow && lastInputKey != ConsoleKey.DownArrow)
                {
                    lastInputKey = ConsoleKey.UpArrow;
                }
                else if (keyInfo.Key == ConsoleKey.DownArrow && lastInputKey != ConsoleKey.UpArrow)
                {
                    lastInputKey = ConsoleKey.DownArrow;
                }
                else if (keyInfo.Key == ConsoleKey.LeftArrow && lastInputKey != ConsoleKey.RightArrow)
                {
                    lastInputKey = ConsoleKey.LeftArrow;
                }
                else if (keyInfo.Key == ConsoleKey.RightArrow && lastInputKey != ConsoleKey.LeftArrow)
                {
                    lastInputKey = ConsoleKey.RightArrow;
                }
            }
            
            switch (lastInputKey)
            {
                case ConsoleKey.UpArrow:
                    snakeHeadY = (snakeHeadY - 1); // 위로 이동
                    break;
                case ConsoleKey.DownArrow:
                    snakeHeadY = (snakeHeadY + 1); // 아래로 이동
                    break;
                case ConsoleKey.LeftArrow:
                    snakeHeadX = (snakeHeadX - 1); // 왼쪽으로 이동
                    break;
                case ConsoleKey.RightArrow:
                    snakeHeadX = (snakeHeadX + 1); // 오른쪽으로 이동
                    break;
            }
            CheckCollision();
 
            snakeBody.Insert(0, (snakeHeadX, snakeHeadY)); // 뱀 머리 위치 추가
            board[snakeHeadY, snakeHeadX] = snakeChar; // 뱀 머리 위치 업데이트
            Console.SetCursorPosition(snakeHeadX, snakeHeadY);
            Console.Write(snakeChar);
 
            
            // 0.1초 대기 (게임 속도 조절)
            Thread.Sleep(500);
        }
 
 
        public void CheckCollision()
        {
            // 벽에 닿았는지 확인
            if (snakeHeadX < 0 || snakeHeadX >= boardSize || snakeHeadY < 0 || snakeHeadY >= boardSize)
            {
                Console.WriteLine("Game Over! You hit the wall.");
                Environment.Exit(0);
            }
            // 자기 몸에 닿았는지 확인 
            if (board[snakeHeadY, snakeHeadX] == snakeChar)
            {
                Console.WriteLine("Game Over! You hit yourself.");
                Environment.Exit(0);
            }
            // 음식 먹었는지 확인
            if (snakeHeadX == foodX && snakeHeadY == foodY)
            {
                snakeLength++; // 뱀 길이 증가
                MakeFood();
                PlaceFood(); 
            }
            else
            {
                while(snakeBody.Count != snakeLength)
                {
                    // 뱀의 꼬리 위치를 지우고 리스트에서 제거
                    Console.SetCursorPosition(snakeBody[snakeBody.Count - 1].Item1, snakeBody[snakeBody.Count - 1].Item2);
                    Console.Write(blankChar);
                    board[snakeBody[snakeBody.Count - 1].Item2, snakeBody[snakeBody.Count - 1].Item1] = blankChar; // 꼬리 위치 빈 공간으로 설정
                    snakeBody.RemoveAt(snakeBody.Count - 1); // 뱀의 꼬리 제거
                }
                PlaceFood(); // 음식 위치 다시 표시
            }
        }
 
 
    }
    internal class Program
    {
        static void Main(string[] args)
        {
            SnakeGame game = new SnakeGame();
        }
    }
}
 

전각/반각 이슈때매 한시간을 날림… 이슈 정리는 밑에 있음

블랙잭 만들기

namespace BlackJack
{
    class BlackJack
    {
        List<(int, int)> dealerHands = new List<(int, int)>();
        List<(int, int)> playerHands = new List<(int, int)>();
 
        int dealerScore = 0;
        int playerScore = 0;
 
        bool isGameOver = false;
        bool isPlayerWin;
        bool needToOpenDealer = false;
 
        public BlackJack()
        {
            Console.WriteLine("블랙잭 게임을 시작합니다!");
            Hit(isDealer: true);
            Hit(isDealer: false);
            Hit(isDealer: true);
            Hit(isDealer: false);
            PrintHands(needToOpenDealer);
 
            needToOpenDealer = true;
            while (!isGameOver)
            {
                Console.WriteLine("플레이어의 차례입니다. (h: 히트, s: 스탠드");
                string input = Console.ReadLine().ToLower();
                if (input == "h")
                {
                    Hit(isDealer: false);
                }
                else if (input == "s")
                {
                    Stand();
                }
                else
                {
                    Console.WriteLine("잘못된 입력입니다. 다시 입력해주세요.");
                    continue;
                }
                PrintHands(needToOpenDealer);
                BustCheck();
            }
 
            if(isPlayerWin)
            {
                Console.WriteLine("플레이어가 이겼습니다!");
            }
            else
            {
                Console.WriteLine("딜러가 이겼습니다!");
            }
        }
 
        public void PrintHands(bool needToOpenDealer)
        {
            Console.Clear();
            Console.WriteLine("딜러의 카드 :");
            if (!needToOpenDealer)
            {
                Console.WriteLine($"({(CardSuit)dealerHands[0].Item1}, {(CardNumber)dealerHands[0].Item2})");
            }
            else
            {
                foreach (var hand in dealerHands)
                {
                    Console.WriteLine($"({(CardSuit)hand.Item1}, {(CardNumber)hand.Item2})");
                }
            }
 
            Console.WriteLine($"딜러의 점수 : {dealerScore}");
            Console.WriteLine("플레이어의 카드 :");
            foreach (var hand in playerHands)
            {
                Console.WriteLine($"({(CardSuit)hand.Item1}, {(CardNumber)hand.Item2})");
            }
            Console.WriteLine($"플레이어의 점수 : {playerScore}");
 
        }
 
        public void Hit(bool isDealer)
        {
            CardSuit cardSuit = GetRandomEnumValue<CardSuit>();
            CardNumber cardNumber = GetRandomEnumValue<CardNumber>();
            if (isDealer)
            {
                dealerHands.Add(((int)cardSuit, (int)cardNumber));
                dealerScore += GetCardValue(cardNumber, isDealer);
            }
            else
            {
                playerHands.Add(((int)cardSuit, (int)cardNumber));
                playerScore += GetCardValue(cardNumber, isDealer);
            }
        }
 
        public void Stand()
        {
            if(dealerScore < 17)
            {
                Hit(isDealer: true);
            }
        }
 
        public void BustCheck()
        {
            if (playerScore > 21)
            {
                isPlayerWin = false;
                isGameOver = true;
            }
            else if (dealerScore > 21)
            {
                isPlayerWin = true;
                isGameOver = true;
            }
            else if (playerScore == 21)
            {
                isPlayerWin = true;
                isGameOver = true;
            }
            else if (dealerScore == 21)
            {
                isPlayerWin = false;
                isGameOver = true;
            }
        }
 
        public static T GetRandomEnumValue<T>() // Enum 주면은 랜덤 값 줌
        {
            Array values = Enum.GetValues(typeof(T));
            Random random = new Random();
            int index = random.Next(values.Length);
            return (T)values.GetValue(index);
        }
 
        public int GetCardValue(CardNumber cardNumber, bool isDealer)
        {
            if (cardNumber == CardNumber.Ace)
            {
                if (!isDealer)
                {
                    while (true)
                    {
                        Console.WriteLine("A가 나왔습니다. A는 1 또는 11로 계산할 수 있습니다.");
                        Console.Write("A를 1로 계산하시겠습니까? (y/n): ");
                        string str = Console.ReadLine().ToLower();
                        if (str == "y")
                        {
                            return 1;
                        }
                        else if (str == "n")
                        {
 
                            return 11;
                        }
                        else
                        {
                            Console.WriteLine("잘못된 입력입니다. 다시 입력해주세요.");
                        }
                    }
                }
                else
                {
                    if (dealerScore + 11 > 21) return 1;
                    else return 11;
                }
            }
            else if (cardNumber >= CardNumber.Ten)
            {
                return 10; 
            }
            else
            {
                return (int)cardNumber; 
            }
        }
    }
 
    public enum CardNumber
    {
        Ace = 1,
        Two = 2,
        Three = 3,
        Four = 4,
        Five = 5,
        Six = 6,
        Seven = 7,
        Eight = 8,
        Nine = 9,
        Ten = 10,
        Jack = 11,
        Queen = 12,
        King = 13
    }
 
    public enum CardSuit
    {
        Spades = 1,
        Hearts = 2,
        Diamonds = 3,
        Clubs = 4,
    }
 
 
 
    internal class Program
    {
        static void Main(string[] args)
        {
            BlackJack game = new BlackJack();
        }
    }
}
 

고급 룰은 안넣었음.

인터페이스와 열거형

다중상속을 사용하지 않는 이유

  1. 다이아몬드 문제 : 한 클래스가 두 개 이상의 부모 클래스로부터 동일한 멤버를 상속받을 수 있는데, 같은 이름의 멤버를 가지고 있을 때 어떤 부모 클래스의 멤버를 사용해야 하는지가 모호해짐.
  2. 설계 복잡성 증가 : 클래스 간의 관게가 복잡해짐
  3. 이름 충돌 및 충돌 해결 난해 : 여러 부모 클래스로부터 상속받은 멤버들의 이름이 충돌할 수 있음
  4. 설계 일관성 및 단순성 유지

인터페이스를 사용하는 이유

  1. 코드 재사용성
  2. 인터페이스 다중 상속 지원
  3. 유연한 설계

인터페이스

  • 클래스가 구현해야 하는 멤버를 정의하는 것.
  • 클래스가 인터페이스를 구현하려고 할 경우, 모든 멤버를 구현해야 함
interface IMyInterface
{
	void Method1();
	int Method2(string str);
}
  • 참고 : 추상 클래스와는 다르다! 추상 클래스는 일부 동작의 구현을 가질 수 있다!

열거형

  • 사용 이유

    1. 가독성
    2. 자기 문서화 : 의미 있는 이름을 사용해 상수에 이름을 붙일 수 있어서
    3. 스위치 문과 호환성이 좋음
  • 열거형의 특징 : 각 상수는 정수 값으로 지정됨

enum MyEnum
{
	Value1 = 10,
	Value2,
	Value3 = 20
}
 
int intValue = (int)MyEnum.Value1; // 열거형 -> 정수
MyEnum enumValue = (MyEnum)intValue; // 정수 -> 열거형

예외 처리 및 값형과 참조형

예외 처리

  • try-catch로 사용함
// try-catch 예시
try
{
	// 예외가 발생할 수 있는 코드
}
catch (ExceptionType1 ex)
{
	// ExceptionType1 에 해당하는 예외
}
catch (ExceptionType2 ex)
{
	// ExceptionType2 에 해당하는 예외
}
finally
{
	// 예외 발생 여부와 상관없이 항상 실행되는 코드
}
  • 예외 타입은 가장 상위 예외 타입의 catch 블록이 먼저 실행됨
  • 사용자 지정 예외 만들려면 Exception 클래스를 상속받아 작성할 수 있음
// 사용자 지정 예외 예시
public class NegativeNumberException : Exception
{
    public NegativeNumberException(string message) : base(message)
    {
    }
}
 
try
{
    int number = -10;
    if (number < 0)
    {
        throw new NegativeNumberException("음수는 처리할 수 없습니다.");
    }
}
catch (NegativeNumberException ex)
{
    Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine("예외가 발생했습니다: " + ex.Message);
}
 

값형과 참조형

값형

  • 값형은 변수에 값을 직접 저장함
  • int, float, double, bool 등 기본 데이터 + struct가 값형임

참조형

  • 참조형은 변수가 데이터에 대한 참조(메모리 주소)를 저장함
  • 클래스, 배열, 인터페이스 등이 참조형임

박싱 / 언박싱

  • 박싱 : 값형 참조형
  • 언박싱 : 참조형 값형
  • 박싱된 객체는 힙 영역에 할당되기 때문에 가비지 컬렉션의 대상이 될 수 있음.

학습하며 겪었던 문제점 & 에러

문제 1

  • 문제&에러에 대한 정의

원래 처음에는 함수를 internal class Program 내에 넣어서 실행시키고자 했음. 근데 오류가 나서 안됨.

이 때 발생했던 오류는 다음과 같음

CS0120: 객체 참조가 필요합니다.
  • 해결 방법

Tictactoe 클래스를 따로 만들어서, 그 클래스 내부에 함수를 만들어다가 메인에서 실행함

  • 새롭게 알게 된 점

static 인 클래스 안에서는 static이 아닌 멤버에 접근할 수 없음.

  • 이 문제&에러를 다시 만나게 되었다면?

정적인지 아닌지 잘 확인해 보아야겠다.

문제 2

  • 문제&에러에 대한 정의
int playerRow, playerCol;
 
bool isValidInput = inputs.Length == 2 &&
    int.TryParse(inputs[0], out playerRow) &&
    int.TryParse(inputs[1], out playerCol);
 
if (!isValidInput)
{
    Console.WriteLine("잘못된 입력입니다. 행과 열을 0-2 사이의 숫자로 입력하세요.");
    continue;
}
 
if (Move(playerRow, playerCol, currentPlayer))  // ← 여기가 문제
 

위의 코드에서 playerRow와 playerCol에 값이 할당되어있지 않다는 이유로 에러가 났음

  • 내가 한 시도

위에다가 int playerRow, playerCol 을 따로 만들어줬음. 애초에 out playerRow를 거치니까 분명 playerRow에 값이 들어가 있을 것이라고 생각했음

  • 해결 방법
int playerRow = -1 , playerCol = -1;

값을 대입해줬음.

  • 새롭게 알게 된 점

out으로 받아오는 변수값의 경우, 컴파일러가 값이 할당되어 있지 않다고 판단할 수 있음.

// 예시
int result;
bool success = int.TryParse("abc", out result);
 
if (!success)
{
    Console.WriteLine("잘못된 입력입니다.");
}
 
Console.WriteLine(result); // 이 타이밍에 값이 보장되지 않을 수 있음
 
  • 이 문제&에러를 다시 만나게 되었다면?

out이 있다고 하더라도, 값을 미리 대입해서 문제가 없도록 해야겠다.

문제 3

  • 문제&에러에 대한 정의

스네이크 게임 만들기에서, 사용자의 입력을 수시로 받아야 하는데 어떻게 할까 생각했음

  • 내가 한 시도

쓰레드를 쓸 까 했음

  • 해결 방법

Key.Available을 사용해서 키 입력이 있을 경우에만 받는 방법을 알아냄

  • 새롭게 알게 된 점

ReadLine 말고도 입력을 받는 방법은 많다.

  • 이 문제&에러를 다시 만나게 되었다면?

상황에 따라 입력을 어떻게 받을 지 찾아봐야겠다.

문제 4

  • 문제&에러에 대한 정의

스네이크 게임 만들기에서, Console.Clear()를 자주 호출하다 보니 너무 깜빡거려서 보기가 불편했음

  • 해결 방법

Console.SetCursorPosition을 통해 특정 위치로 커서를 이동시킬 수 있다는 사실을 알아냄.

  • 새롭게 알게 된 점

꼭 모든걸 초기화 해야만 하는 것은 아니다.

  • 이 문제&에러를 다시 만나게 되었다면?

화면 깜빡임을 줄이기 위해 SetCursorPosition을 사용해야겠다.
+추가로, Console.CursorVisible을 False로 하면 커서 깜빡임도 없다.

문제 5

  • 문제&에러에 대한 정의

스네이크 게임 만들기에서, 위 아래로 움직일 때에만 꼬리가 사라졌다.

  • 해결 방법

반각이 문제였다!! 스페이스바 두 번 해서 전각마냥 크기를 늘렸다.

  • 새롭게 알게 된 점

뭔가 표기가 이상하면 인코딩을 생각해 볼 필요도 있다.

  • 이 문제&에러를 다시 만나게 되었다면?

전각 반각 잘 봐야지..

내일 학습 할 것은 무엇인지

C# 고급 문법