오늘 학습 키워드
조건문, 반복문, 배열, 컬렉션, 메소드, 구조체, 클래스, 객체, 상속, 다형성, 제네릭(제너릭),
오늘 학습 한 내용을 나만의 언어로 정리하기
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로 들어감
배열과 리스트의 차이
- 크기 변경 가능 여부
- 배열은 크기가 고정
- 리스트는 동적 배열
- 메모리 사용 방식
- 리스트는 내부적으로 배열을 사용함
- 공간이 부족해질 경우 더 큰 배열을 새로 생성하고 데이터를 복사하는데, 일시적으로 메모리 사용량이 증가는 함. 일반적인 상황에서는 큰 문제가 되진 않음
- 기능적 차이
- 배열은 인덱스 접근 용이
- 리스트는 다양한 기능을 제공. 복잡한 자료 조작에서 유리
- 성능 고려
- 배열 : 반복 처리에서 빠르고 예측 가능한 성능 제공
- 리스트 : 유연하지만, 반복 처리에서는 성능 저하의 원인이 될 수 있음.
- 어떤 상황에서 사용해야 할까?
- 크기가 명확, 고정형 → 배열
- 게임 루프에서 반복적 접근 → 배열
- 데이터 개수 유동적, 추가/삭제 잦음 → 리스트
- 데이터 동적 구성, 정렬/검색/필터링 필요 → 리스트
메소드
- 메소드 : 특정 작업을 위해 사용되는 독립적인 기능 단위
- 메소드의 역할 및 중요성
- 코드 재사용성, 모듈화, 가독성 및 유지보수성, 코드 중복 제거, 코드 추상화
접근제한자 리턴타입 메서드 (매개변수)
{
// 메소드 실행 코드
}
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)
- 특징
- 캡슐화 : 관련 데이터와 기능을 하나의 단위로 묶는 것
- 상속 : 기존의 클래스를 확장하여 새로운 클래스를 만드는 것
- 다형성 : 하나의 인터페이스나 기능을 다양한 방식으로 구현하거나 사용하는 것
- 추상화 : 복잡한 시스템이나 개념을 단순화하여 필요한 기능에 집중하는 것
- 객체 : 클래스로부터 생성된 실체
클래스
- 구성요소
- 필드 : 변수
- 메소드 : 동작
- 생성자 : 객체가 생성될 때 호출됨
- 소멸자 : 객체가 소멸할 때 호출됨
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 사용 시 주의사항
- 값 변경 가능성
- 성능 : 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();
}
}
}
고급 룰은 안넣었음.
인터페이스와 열거형
다중상속을 사용하지 않는 이유
- 다이아몬드 문제 : 한 클래스가 두 개 이상의 부모 클래스로부터 동일한 멤버를 상속받을 수 있는데, 같은 이름의 멤버를 가지고 있을 때 어떤 부모 클래스의 멤버를 사용해야 하는지가 모호해짐.
- 설계 복잡성 증가 : 클래스 간의 관게가 복잡해짐
- 이름 충돌 및 충돌 해결 난해 : 여러 부모 클래스로부터 상속받은 멤버들의 이름이 충돌할 수 있음
- 설계 일관성 및 단순성 유지
인터페이스를 사용하는 이유
- 코드 재사용성
- 인터페이스 다중 상속 지원
- 유연한 설계
인터페이스
- 클래스가 구현해야 하는 멤버를 정의하는 것.
- 클래스가 인터페이스를 구현하려고 할 경우, 모든 멤버를 구현해야 함
interface IMyInterface
{
void Method1();
int Method2(string str);
}
- 참고 : 추상 클래스와는 다르다! 추상 클래스는 일부 동작의 구현을 가질 수 있다!
열거형
-
사용 이유
- 가독성
- 자기 문서화 : 의미 있는 이름을 사용해 상수에 이름을 붙일 수 있어서
- 스위치 문과 호환성이 좋음
-
열거형의 특징 : 각 상수는 정수 값으로 지정됨
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# 고급 문법