공부 기록/C#

[C#/TIL] C# 문법 종합반 중간지점에서 배운 내용 정리

톰마토 2025. 1. 31. 20:17
728x90
이 글에서 다룬 내용들 
컬렉션에 대하여
.NET Framework에 대하여
틱택토 게임 만들기
구조체 VS 클래스
프로퍼티
다형성(가상 메서드, 추상 메서드, 오버로딩과 오버라이딩)
out & ref
인터페이스
Enum

컬렉션에 대하여

일단 여기서 컬렉션 = 자료구조임. 

C#의 컬렉션은 .NET Framework에서 제공하는 데이터 구조들을 포괄하는 개념이고, 자료구조뿐만 아니라 데이터 조작, 탐색, 필터링 등의 부가 기능을 포함하는 개념이라고 함.

List

가변적인 크기를 갖는 배열. 연결리스트가 아니었다.! 인덱스로 접근도 가능.

C#의 List<T>는 노드 기반이 아니고 동적 배열이라고 함. 노드 기반 리스트로는 LinkedList<T>가 따로 있다. 

(C#의 List<T>와 LinkedList<T>는 각각 C++의 std::vector<T>와 std::list<T>에 대응된다.)

 

Dictionary - C++의 std::map

HashSet - C++의 std::set

.NET Framework에 대하여

C# 문법을 공부하다보니 .NET Framework 라는 말이 자꾸 들린다.

.NET Framework는 C#을 비롯한 프로그래밍 언어를 실행하고 개발할 수 있도록 지원하는 마이크로소프트의 개발 플랫폼이다. C#이 실행되기 위한 환경(=런타임환경)과 개발을 돕는 라이브러리들의 집합이다. Java의 JVM같은 존재였던 것!!!! 

 

C++은 컴파일하면 기본적으로 OS에서 바로 실행 가능한 네이티브 코드가 생기기 때문에 이런 런타임 환경이 필요하지 않다. 그래서 .NET Framework같은 중간 계층이 필요가 없어서 나에게 생소하게 느껴졌나보다..!!

2주차 강의 숙제 - 틱택토 게임 만들기

틱택토 게임 실행 화면

더보기
static char[,] board = new char[3, 3];
static void PrintBoard()
{
    for (int i = 0; i < 3; i++)
    {
        Console.WriteLine("\t|\t|\t|");
        for (int j = 0; j < 3; j++)
        {
            Console.Write($"   {board[i, j]}\t|");
        }
        Console.WriteLine("\n\t|\t|\t|");
        Console.WriteLine(" -------|-------|-------|");
    }
}

static bool PlayerTurn(char mark)
{
    int user = int.Parse(Console.ReadLine()) - 1;
    int x = user / 3;
    int y = user % 3;
    if (x > 3) return false;
    if (board[x, y] == 'X' || board[x, y] == 'O')
    {
        return false;
    }
    else
    {
        board[x, y] = mark;
        return true;
    }
}

static bool isClear(char curMark)
{
    //가로 성공 확인
    for (int i = 0; i < 3; i++)
    {
        bool horizontal = true;
        for (int j = 0; j < 3; j++)
        {
            if (board[i, j] != curMark)
            {
                horizontal = false;
            }
        }
        if(horizontal)
        {
            return true;
        }
    }

    //세로 성공 확인
    for (int i = 0; i < 3; i++)
    {
        bool vertical = true;
        for (int j = 0; j < 3; j++)
        {
            if (board[j, i] != curMark)
            {
                vertical = false;
            }
        }
        if (vertical)
        {
            return true;
        }
    }

    //대각선 확인 
    if (board[0, 0] == curMark && board[1, 1] == curMark && board[2, 2] == curMark) return true;
    if (board[0, 2] == curMark && board[1, 1] == curMark && board[2, 0] == curMark) return true;

    return false;
}

static void Main(string[] args)
{
    int curTurn = 1, attempt = 9;
    char curMark = ' ';
    //초기화
    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 3; j++)
        {
            board[i, j] = char.Parse((i * 3 + j + 1).ToString());
        }
    }

    while (true)
    {
        --attempt;
        if(attempt < 0)
        {
            Console.WriteLine("무승부!");
            break;
        }
        Console.Clear();
        Console.WriteLine("플레이어 1 : X 와 플레이어 2 : O");


        Console.WriteLine($"플레이어 {curTurn}의 차례");

        // 보드 그리기
        PrintBoard();

        // 
        if(curTurn == 1)
        {
            curMark = 'O';
            while (!PlayerTurn(curMark))
            {
                Console.WriteLine("입력이 유효하지 않음");
            }
            if(isClear(curMark))
            {
                Console.WriteLine($"플레이어 {curTurn}의 승리!");
                break;
            }
            curTurn = 2;
            continue;
        }
        
        if(curTurn == 2)
        {
            curMark = 'X';
            while (!PlayerTurn(curMark))
            {
                Console.WriteLine("입력이 유효하지 않음");
            }
            if (isClear(curMark))
            {
                Console.WriteLine($"플레이어 {curTurn}의 승리!");
                break;
            }
            curTurn = 1;
            continue;
        }
    }
}

구조체와 클래스 차이

C#에서의 구조체 struct와 클래스 class의 차이가 인상 깊었다. 헷갈리지 않기 위해 C++의 차이도 함께 정리했다.

C++에서 구조체 VS 클래스

C++에서는 구조체와 클래스의 차이는 단 하나뿐이다. 디폴트 접근제한자가 구조체는 public, 클래스는 private이라는 차이점 외에는 없다. C++의 구조체는 상속도 가능하기 때문에 거의 동일하게 사용 가능하다.

C++에서는 구조체든 클래스든 값 타입으로도 참조 타입으로도 생성할 수 있다. 

값 타입이면 멤버에 . 점으로 접근, 참조 타입이면 멤버에 -> 화살표로 접근.

C#에서 구조체 VS 클래스

C#에서는 구조체와 클래스의 차이가 크다.

 

C#의 구조체

  • 구조체는 값 타입으로, 스택에 직접 저장되기 때문에 GC 부담이 없어 속도가 빠른 편이다.
  • 할당 시 값이 복사된다.
  • 클래스를 상속 받을 수 없다!
  • 인터페이스를 상속받아 구현할 수 있다. (다중 인터페이스 구현 가능)
Point p1 = new Point { X = 10, Y = 20 };
Point p2 = p1;  // 값 복사 (새로운 메모리 할당)

 

C#의 클래스는 참조 타입이기 때문에 new 키워드로 생성해야 하고, 힙에 저장된다. 객체끼리 할당할 때 참조 복사가 이루어진다.

프로퍼티

아직 생소하게 느껴지는 프로퍼티에 대해서 배웠다. 예전에 C# 문법 톺아보기 때 작성한 글이 있어서 새롭게 알게 된 내용을 덧붙여 작성하는 것으로 대체했다.

https://ramenkirby.tistory.com/16

 

[C#] 기초 문법 톺아보기 (1) - 입력과 출력, 배열, 필드와 프로퍼티

[ 유니티를 배우기 전, 기초 문법 톺아보기(1) ]내일배움캠프 사전캠프의 과제를 해결하며 C#과 유니티의 예습을 하고 있다. 유니티를 본격적으로 배워나가기 전에 C# 문법에 익숙해지기에 좋은

ramenkirby.tistory.com

상속

C#에서는 클래스는 단일 상속만 가능하다. (인터페이스를 다중 상속 가능하다.)

부모의 함수를 재구현해서 사용할 수 있는데, 이게 다형성을 유지하는 방법은 아니다. 

아래 코드를 예시로, Marine을 Marine으로만 사용하고 Zergling을 Zergling으로만 사용한다면 큰 문제없이 사용 가능하다.

public class Unit
{
    public void Move()
    {
        Console.WriteLine("두 발로 걷기");
    }

    public void Attack()
    {
        Console.WriteLine("Unit 공격");
    }
}

public class Marine : Unit
{

}

public class Zergling : Unit 
{
    public void Move()
    {
        Console.WriteLine("네 발로 걷기");
    }
}

static void Main(string[] args)
{
    Marine marine = new Marine();
    marine.Move();      // 두 발로 걷기 
    marine.Attack();    // Unit 공격

    Zergling zergling = new Zergling();
    zergling.Move();    // 네 발로 걷기
    zergling.Attack();  // Unit 공격
}

 

자식 클래스들을 부모 클래스로 묶어 관리를 할 때는 그게 문제가 된다.

== Marine과 Zergling을 묶어 관리할 때는 문제가 된다. "두 발로 걷기"만 출력됨! 이때 필요한 것이 가상 메서드.

다형성

다형성 : 같은 타입이지만 다양한 동작을 수행할 수 있는 능력

가상(Virtual) 메서드 

키워드 : virtual - override

부모 클래스에서 가상 메서드를 정의하고 자식 클래스에서 필요에 따라 재정의할 수 있다. (자식이 재정의를 했을 수 있다)

자식 클래스에서 가상 메서드를 재정의한 경우에는 자식 클래스에 있는 함수를 사용한다.

virtual -> 실형태가 다를 수 있으니 실형태에 재정의가 되어있는지 확인해 봐라. 안 되어 있으면 내꺼 써라.
public class Unit
{
    public virtual void Move()
    {
        Console.WriteLine("두 발로 걷기");
    }

    public void Attack()
    {
        Console.WriteLine("Unit 공격");
    }
}

public class Marine : Unit
{

}

public class Zergling : Unit 
{
    public override void Move()
    {
        Console.WriteLine("네 발로 걷기");
    }
}

static void Main(string[] args)
{
    // 참조의 형태는 Unit이고, 실제 형태는 Marine과 Zergling임
    List<Unit> list = new List<Unit>();
    list.Add(new Marine());
    list.Add(new Zergling());

    foreach (Unit unit in list)
    {
        unit.Move();
    }
}

virtual 예제 실행 결과

추상(Abstract) 클래스와 메서드 

키워드 : abstract - override

직접 인스턴스를 생성할 수 없는 클래스로, 주로 상속을 위한 베이스 클래스로 사용됨. 

선언만 해놓고 구현을 안 함. 그 대신 자식 클래스에서 반드시 구현되어야 한다. 

abstract class Shape
{
    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");
    }
}
static void Main(string[] args)
{
    List<Shape> list = new List<Shape>();
    list.Add(new Circle());
    list.Add(new Square());
    list.Add(new Triangle());

    foreach (Shape shape in list)
    {
        shape.Draw();
    }
}

위 코드에서 우리는 Shape가 추상 클래스이므로 무조건 Draw가 정의되어 있음을 알 수 있음. 

virtual보다 강제성이 있는 느낌쓰. 

오버라이딩과 오버로딩 

오버라이딩과 오버로딩도 다형성을 나타내는 개념이다. 이미 위에서 다 다룬 개념들이다.

  • 오버라이딩을 통해서 부모 클래서에서 정의된 메서드를 자식 클래스에서 '나에게 맞는' 동작을 재구현할 수 있다.
  • 오버로딩은 동일한 이름의 메서드가 여러 개 있는 것임. 매개변수의 개수나 타입이 다른! 

out & ref 키워드

out

메서드의 반환값을 out 매개 변수를 통해 전달할 수 있다.

out으로 넘겨주는 매개 변수는 실제 그 변수의 위치, 즉 참조를 넘기는 것이다. 그 변수를 직접 건드리는 것임.

 

아래 오류를 보면 out 매개변수를 초기화를 안 해줬다고 한다.

=> out으로 넣어주는 매개변수는 무조건 값이 변화를 가졌겠구나~

out 매개 변수 초기화 안하면 오류남

ref

ref는 대입을 안 한다고 에러가 나지 않음. (사용할지 안 할지 모른다) 

메서드에서 매개변수를 수정하여 원래 값에 영향을 주는 경우에 사용. 

 

원래 참조형인 애들은 이렇게 동작하고 있었겠지만? 값형 변수들한테도 out과 ref를 붙여서 입력받을 수 있다~

둘 다 직접 값을 복사하는 것이 아니기 때문에 성능상 이점이 있다. 그러나 남발하면 코드의 가독성이 떨어지고 유지보수가 어려워질 수 있다. 

인터페이스

인터페이스란 클래스가 구현해야 하는 멤버들을 정의하는 것이다. 필드를 가질 수 없다. 프로퍼티는 가능. 

클래스가 인터페이스를 구현할 경우, 모든 인터페이스 멤버를 구현해야 한다. 

 

추상 클래스와 차이가 있다면 인터페이스는 다중 상속이 가능하고, 인터페이스는 아예 구현이 불가능하다. (추상 클래스는 abstract 메서드 외에 일반 메서드도 포함할 수 있기 때문에 일부 구현 가능)

 

인터페이스를 활용한 예시이다. Player 클래스의 UseItem 메서드의 매개 변수 타입을 Item이 아니라 IUsable로 사용한 것이 포인트이다. 

public interface IUsable
{
    void Use();
}
public class Item : IUsable
{
    public string Name { get; set; }

    public void Use()
    {
        Console.WriteLine("아이템 {0}을 사용했습니다", Name);
    }
}

public class Player
{
    public void UseItem(IUsable item) // <- 여기서 Item이 아니고 IUsable로 
    {
        item.Use();
    }
}
static void Main(string[] args)
{
    Player player = new Player();
    Item item = new Item() { Name = "Health Potion" }; // 이건 생성자가 아니고 이렇게 초기화해줄뿐임 
    player.UseItem(item);
}

아이템 추가 시

소모성 아이템, 장착 아이템, 스텟 아이템 등이 있을 수 있고 이것들을 다양한 클래스로 구현하게 될 텐데 거기에 IUsable만 상속받아서 만들어주면 Player.UserItem을 그냥 사용할 수 있다는 큰 장점이 있다. 

열거형

C++의 Enum class와 같다. enum값을 int값으로 사용하려면 캐스팅을 해줘야 한다. 

enum의 값을 사용할 때 멤버 접근하듯이 '.' 점으로 접근한다.

public enum Month
{ 
    Jan = 1,
    Feb,
    Mar,
    Apr,
    May,
    June,
    Jul,
    Aug,
    Sep,
    Oct,
    Nov,
    Dec
}

public static void ProcessMonth(int month)
{
    if(month >= (int)Month.Jan && month <= (int)Month.Dec)
    {
        Month selectMonth = (Month)month; // int를 enum인 Month로 캐스팅
        Console.WriteLine("선택한 월은 {0} 입니다.", selectMonth);
    }
    else
    {
        Console.WriteLine("올바른 월을 입력해주세요.");
    }
}

static void Main(string[] args)
{
    ProcessMonth(7);
    ProcessMonth(13);
    ProcessMonth(1);
}

enum 예제 실행 결과

 


공부하며 느낀 점

C#에 대해 공부할수록 C++과의 차이점이 점점 발견된다. 처음에는 별차이 없다고 생각했었는데 뭐가 많이 다른 듯!? ㅋㅋ 신기하고 당황스럽다!!! 🙃 언어를 아예 처음 배울 때보다는 수월하게 느껴지지만 오히려 둘 다 애매하게 배운 사람이 되어있을까 봐 걱정이 된다..  C++에 익숙해지는 것도 오래 걸렸는데, 이제 C#을 공부하게 되어서 둘을 헷갈리며 사용하는 사람이 되면 안 될 텐데. C# 문법을 많이 쳐봐야겠다. 일단 강의 과제들부터 ㅜㅅㅜ. 강의 듣는 속도가 생각보다 느리다. 부지런히 파이팅.

728x90