[TIL] C# 공부 - 클래스, 예외 처리, 고급 문법 등
📕학습 개요
유니티 캠프의 2주차가 시작되었다.
앞으로 2주 간 유니티관련 내용은 일절 학습하지 않으며, C#에 대한 공부와 프로젝트를 진행한다고 한다. 평소에 유니티를 다루며 C#도 자연스래 많이 사용하게 되었지만, 정작 C#을 제대로 공부해본 적은 한번도 없는 것 같다. 물론 Java에 익숙해서 적응하는 데 어렵지는 않았지만, 약간 익숙하지 않은 키워드를 마주한 적도 많았다. 이번 기회를 통해 C#을 제대로 학습하고, 문법에 익숙해졌으면 좋겠다.
📖학습 내용
1. 구조체 vs 클래스
구조체와 클래스는 데이터를 담는 사용자 정의 타입이라는 점에서 언뜻 보면 비슷하다고 생각할 수 있지만, 메모리 사용 방식, 성능, 기능 측면에서 큰 차이가 있다.
| 항목 | 구조체 (struct) | 클래스 (class) |
|---|---|---|
| 형식 | 값 형식 (Value Type) | 참조 형식 (Reference Type) |
| 메모리 위치 | 스택(Stack)에 저장 | 힙(Heap)에 저장 (레퍼런스는 스택에) |
| 기본 상속 | System.ValueType 상속 (상속 불가) |
object 상속, 상속 및 다형성 지원 |
| 생성자 | 매개변수 없는 생성자 정의 불가 (자동 제공) | 생성자 자유롭게 정의 가능 |
즉 구조체는 위치, 색상, 좌표 등 단순 데이터 묶음을 표현할 때 사용하고, 클래스는 더욱 복잡한 객체를 표현하고 상속 및 메서드 같은 다양한 기능을 제공하기 위해 사용하면 적합하다.
2. 예외 처리
예외 처리는 말 그래도 프로그램 실행 중 발생하는 예기치 않은 상황에 대비하는 방식으로, 프로그램을 안정적으로 유지하는 데 큰 도움을 준다. 대표적인 예로 DivideByZeroException이 있다.
try
{
int a = 10;
int b = 0;
int result = a / b; // 여기서 예외 발생
Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"에러 발생: {ex.Message}");
}
finally
{
Console.WriteLine("예외 처리 완료");
}
try블록 내에서 예외가 발생할 가능성이 있는 코드를 작성하고,catch블록에서 예외처리한다.catch블록은switch문 처럼 위에서부터 순서대로 실행된다.finally블록은 예외 발생 여부와 상관 없이 항상 실행되며, 생력 가능하다.
사용자 정의 예외 처리
만약 필요하다면, 사용자가 직접 예외 클래스를 작성할 수 있다.
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);
}
위 코드와 같이 Exception 클래스를 상속받아 작성한다.
3. 제너릭
제너릭(Generics)은 데이터 형식에 독립적인 코드를 작성하게 해주고, 재사용성, 타입 안정성, 성능을 모두 챙길 수 있는 매우 강력한 기능이다.
- 제너릭 클래스 예제
public class Box<T>
{
public T Value { get; set; }
public void PrintValue()
{
Console.WriteLine($"Box contains: {Value}");
}
}
Box<int> intBox = new Box<int> { Value = 123 };
intBox.PrintValue(); // 출력: Box contains: 123
Box<string> strBox = new Box<string> { Value = "Hello" };
strBox.PrintValue(); // 출력: Box contains: Hello
- 제너릭 메서드 예제
public class Util
{
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
}
int x = 1, y = 2;
Util.Swap<int>(ref x, ref y);
Console.WriteLine($"{x}, {y}"); // 출력: 2, 1
string a = "A", b = "B";
Util.Swap(ref a, ref b);
Console.WriteLine($"{a}, {b}"); // 출력: B, A
잘못된 타입을 할당하는 것을 사전에 방지하여 형변환 오류를 예방해주는 것이 제너릭의 큰 장점인 것 같다.
제너릭을 잘 사용한다면 클래스나 메서드를 일반화시켜, 코드의 재사용성을 크게 높일 수 있다. 흔히 사용하는 List<>도 대표적인 제너릭 클래스이다.
4. out, ref 키워드
out, ref키워드는 메서드에서 매개변수를 전달할 떄 사용하며, 메서드 내에서 값을 직접 변경하거나 여러 값을 반환할 때 유용하다. 서로 비슷해보이지만 사용 목적과 규칙이 조금 다르다.
out예제
void GetPlayerInfo(out string name, out int score)
{
name = "Alice";
score = 100;
}
string playerName;
int playerScore;
GetPlayerInfo(out playerName, out playerScore);
Console.WriteLine($"{playerName}, {playerScore}"); // 출력: Alice, 100
ref예제
void Add(ref int number)
{
number += 10;
}
int x = 5;
Add(ref x);
Console.WriteLine(x); // 출력: 15
| 항목 | ref |
out |
|---|---|---|
| 초기화 여부(메서드 호출 전) | 반드시 초기화되어야 함 | 초기화되지 않아도 됨 |
| 메서드 내부에서 | 수정할 수도 있고 안 할 수도 있음 | 반드시 값을 할당해야 함 |
| 사용 목적 | 값을 전달하고 변경된 값을 받아오고 싶을 때 | 여러 값을 반환할 때(출력 전용) |
| 공통점 | 참조로 전달되어 원본 변수에 영향을 줌 |
🎮콘솔 텍스트 게임 만들기
콘솔창을 통해 플레이 할 수 있는 간단한 게임 두 가지를 C#을 통해 구현해 보았다. 강의를 통해 배운 내용을 최대한 사용해 보기 위해 노력했다.
🐍스네이크 게임
스네이크 게임의 핵심 기능은 다음과 같다.
- 스네이크 이동 : 플레이어가 wasd 키를 입력할 때마다 한 칸씩 이동한다
- 몸통 추가 : 먹이를 먹으면 스네이크의 길이가 증가함
- 충돌 판정 : 벽이나 자기 몸에 부딪히면 게임 오버
- 먹이 리스폰 : 빈 공간에 무작위로 먹이를 생성
구현 코드
public enum TileType //게임 맵 타일의 종류
{
Empty, //빈 칸
SnakeBody, //뱀의 몸
SnakeHead, //뱀의 머리
Food, //음식
Wall //벽
}
internal class Program
{
static int boardSize = 20; //보드 크기 (20 x 20)
static TileType[,] gameBoard = new TileType[boardSize, boardSize];
static List<Point> snake = new List<Point>();
static bool isGameOver = false;
static int score = 0; //게임 스코어
static void Main(string[] args)
{
for (int i = 0; i < boardSize; i++)
{ //벽 채우기
gameBoard[0, i] = TileType.Wall;
gameBoard[boardSize - 1, i] = TileType.Wall;
gameBoard[i, 0] = TileType.Wall;
gameBoard[i, boardSize - 1] = TileType.Wall;
}
snake.Add(new Point(5, 9));
snake.Add(new Point(4, 9));
snake.Add(new Point(3, 9));
GenerateFood();
//게임 시작 전 설명
Console.WriteLine("스네이크 게임에 오신 걸 환영합니다!");
Console.WriteLine();
Console.WriteLine("1. WASD 키로 뱀을 움직여보세요!");
Console.WriteLine("2. 음식을 먹을 때 마다 뱀의 길이가 1 증가합니다.");
Console.WriteLine("3. 벽이나 뱀의 몸에 머리가 닿으면 게임 오버입니다.");
Console.WriteLine();
Console.WriteLine("게임을 시작하려면 아무 키나 입력하세요.");
Console.ReadKey();
MoveSnake(1, 0);
//게임 시작
while (!isGameOver)
{
//Console.Clear();
DrawGame();
ConsoleKeyInfo input = Console.ReadKey();
switch (input.Key)
{
case ConsoleKey.W:
MoveSnake(0, -1);
break;
case ConsoleKey.A:
MoveSnake(-1, 0);
break;
case ConsoleKey.S:
MoveSnake(0, 1);
break;
case ConsoleKey.D:
MoveSnake(1, 0);
break;
default:
Console.WriteLine("WASD키로 조작 가능합니다.");
continue;
}
}
}
static void DrawGame() //게임 보드 그림
{
Console.SetCursorPosition(0, 0);
for (int i = 0; i < boardSize; i++)
{
for (int j = 0; j < boardSize; j++)
{
switch (gameBoard[i, j])
{
case TileType.Empty:
Console.Write(" ");
break;
case TileType.Wall:
Console.Write("▦");
break;
case TileType.SnakeHead:
Console.Write("◆");
break;
case TileType.SnakeBody:
Console.Write("■");
break;
case TileType.Food:
Console.Write("●");
break;
default:
Console.Write(" ");
break;
}
}
Console.WriteLine();
}
}
static void MoveSnake(int moveX, int moveY)
{
//일단 머리가 벽이나 몸에 부딪히는지 확인
Point nextHead = new Point(snake[0].x + moveX, snake[0].y + moveY);
if (gameBoard[nextHead.y, nextHead.x] == TileType.Wall ||
gameBoard[nextHead.y, nextHead.x] == TileType.SnakeBody)
{
GameOver();
return;
}
//머리가 음식에 부딪히는 경우
else if (gameBoard[nextHead.y, nextHead.x] == TileType.Food)
{
EatFood(snake[snake.Count - 1]);
}
//뱀 움직이기
for (int i = snake.Count - 1; i >= 0; i--)
{
if (i == 0)
{
snake[i] = nextHead;
}
else
{
snake[i] = snake[i - 1];
}
}
// 보드 초기화
for (int i = 0; i < boardSize; i++)
{
for (int j = 0; j < boardSize; j++)
{
if (gameBoard[i, j] != TileType.Wall &&
gameBoard[i, j] != TileType.Food)
gameBoard[i, j] = TileType.Empty;
}
}
//뱀 위치 할당
for (int i = 0; i < snake.Count; i++)
{
if (i == 0)
{
gameBoard[snake[i].y, snake[i].x] = TileType.SnakeHead;
}
else
{
gameBoard[snake[i].y, snake[i].x] = TileType.SnakeBody;
}
}
}
static void EatFood(Point lastTail) //뱀이 음식을 먹었을 시 호출
{
//뱀의 꼬리 뒤(움직인 방향과 정반대)에 뱀 칸 추가
snake.Add(new Point(snake[snake.Count - 1].x - lastTail.x,
snake[snake.Count - 1].y - lastTail.y));
score++;
GenerateFood();
}
static void GenerateFood() //음식을 빈칸 중 랜덤 위치에 생성
{
Random rand = new Random();
while (true)
{
int x = rand.Next(1, boardSize - 1);
int y = rand.Next(1, boardSize - 1);
if (gameBoard[y,x] == TileType.Empty)
{
gameBoard[y, x] = TileType.Food;
break;
}
}
}
static void GameOver() //게임 오버 시 호출
{
isGameOver = true;
Console.WriteLine("게임 오버");
Console.WriteLine($"점수 : {score}");
}
struct Point //좌표를 저장하기 위한 구조체
{
public int x;
public int y;
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
public bool Equals(Point p)
{
if (this.x == p.x && this.y == p.y)
return true;
else
return false;
}
}
}
🃏블랙잭 게임
블랙잭 게임의 핵심 기능은 다음과 같다.
- 카드 덱 생성 및 셔플
- 플레이어/딜러의 카드 분배
- Hit/Stand 입력 처리
- Ace 카드의 유동적인 점수 계산
- 최종 승패 판단 및 결과 출력
구현 코드
public enum Suit
{
Heart,
Spades,
Diamond,
Club
}
public enum Rank
{
Ace = 1,
Two,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King
}
internal class Program
{
static List<Card> deck = new List<Card>(); //게임에 사용할 덱.조커를 뺸 총 52장의 카드
//카드를 뽑을 시 덱에서 완전히 제거하기 위해 배열 대신 리스트 사용
static List<Card> playerCards = new List<Card>(); //플레이어의 패
static List<Card> dealerCards = new List<Card>(); //딜러의 패
static Random rand = new Random();
static void Main(string[] args)
{
Console.OutputEncoding = Encoding.UTF8;
int playerScore;
int dealerScore;
bool isPlaying;
ConsoleKeyInfo playerInput;
while (true)
{
//게임 환경 초기화
Console.Clear();
ShuffleDeck();
playerScore = 0;
dealerScore = 0;
playerCards.Clear();
dealerCards.Clear();
isPlaying = true;
//서로 두장씩 뽑고 시작
playerCards.Add(DrawCard());
dealerCards.Add(DrawCard());
playerCards.Add(DrawCard());
dealerCards.Add(DrawCard());
ShowCards(playerCards); //플레이어의 핸드를 보여줌
while (isPlaying)
{ //플레이어의 점수가 21점을 초과하거나, 스탠드를 선택할때 까지반복
Console.WriteLine("\n1. Hit 2. Stand");
playerInput = Console.ReadKey();
if (playerInput.Key == ConsoleKey.NumPad1 ||
playerInput.Key == ConsoleKey.D1)
{
playerCards.Add(DrawCard());
ShowCards(playerCards);
playerScore = AddScore(playerCards);
if (playerScore > 21) //점수가 21을 초과한 경우
{
isPlaying = false;
}
}
else if (playerInput.Key == ConsoleKey.NumPad2 ||
playerInput.Key == ConsoleKey.D2)
{
isPlaying = false;
}
else
{
Console.WriteLine("1 이나 2를 입력하세요");
}
}
if(playerScore > 21)
{
Console.WriteLine("21점을 초과했습니다.");
Console.WriteLine("당신의 패배입니다.");
}
else
{
DealerRule();
dealerScore = AddScore(dealerCards);
playerScore = AddScore(playerCards);
Console.SetCursorPosition(0, Console.CursorTop);
Console.Write("딜러 : ");
ShowCards(dealerCards);
Console.Write("플레이어 : ");
ShowCards(playerCards);
if (dealerScore > 21) //딜러의 점수가 초과된 경우
{
Console.WriteLine("딜러의 점수가 21점을초과했습니다");
Console.WriteLine("당신의 승리입니다!");
}
else
{
Console.WriteLine($"딜러 : {dealerScore}, 플레이어 :{playerScore}");
if (playerScore > dealerScore)
{
Console.WriteLine("당신의 승리입니다!");
}
else if (playerScore == dealerScore)
{
Console.WriteLine("무승부입니다!");
}
else
{
Console.WriteLine("당신의 패배입니다.");
}
}
}
Console.WriteLine("\n아무 키나 눌러서 다시 시작");
Console.ReadKey();
}
}
static Card DrawCard() //카드 한장 랜덤 드로우
{
int index = rand.Next(0, deck.Count);
Card draw = deck[index];
deck.RemoveAt(index);
return draw;
}
static void ShowCards(List<Card> cards) //패를 보여주는 메서드
{
foreach (Card card in cards)
{
Console.Write($"{card} ");
}
Console.WriteLine();
}
static void DealerRule() //딜러는 자신의 패가 17점 이상 될때 까지무조건 Hit
{
if(AddScore(dealerCards) < 17)
{
dealerCards.Add(DrawCard());
DealerRule();
}
else
{
return;
}
}
static int AddScore(List<Card> cards) //패의 점수를 계산
{
int result = 0;
int aceCnt = 0;
foreach (Card card in cards)
{
if(card.Rank == Rank.Ace)
{
aceCnt++;
}
else
{
result += card.Value();
}
}
result += aceCnt * 11; //에이스의 특수성을 고려하여 따로 계산
while (result > 21 && aceCnt > 0)
{
result -= 10; //에이스를 11대신 1로 계산
aceCnt--;
}
return result;
}
static void ShuffleDeck() //덱 초기화
{
deck.Clear();
//덱 생성. 모든 카드 조합 만들기
foreach (Suit suit in Enum.GetValues(typeof(Suit)))
{
foreach (Rank rank in Enum.GetValues(typeof(Rank)))
{
deck.Add(new Card(suit, rank));
}
}
}
class Card
{
public Suit Suit { get; private set;}
public Rank Rank { get; private set;}
public Card(Suit suit, Rank rank)
{
Suit = suit;
Rank = rank;
}
public int Value()
{
if(Rank >= Rank.Two && Rank <= Rank.Ten)
{
return (int)Rank;
}
else if(Rank >= Rank.Jack && Rank <= Rank.King)
{
return 10;
}
else
return 11;
}
public override string ToString()
{
switch (Suit)
{
case Suit.Heart:
return $"♥{Rank}";
case Suit.Spades:
return $"♠{Rank}";
case Suit.Diamond:
return $"♦{Rank}";
case Suit.Club:
return $"♣{Rank}";
default:
return "";
}
}
}
}
🏁오늘 배운 핵심 한줄 요약
- 클래스는 참조 형식, 구조체는 값 형식
- 제너릭은 다양한 타입에 대해 재사용 가능한 클래스/메서드/인터페이스 정의
ref,out키워드를 통해 메서드 내에서 변수 수정 및 다중 반환try-catch문으로 예외 처리 -> 선택이 아닌 필수
댓글남기기