정신과 시간의 방

전체 글 323

카테고리 설명
게임 프로그래머가 되기 위한 청춘의 기록
  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 예외처리 파트에서 배우고 느낀 점을 정리해보았다. 예외처리란? - 예상치 못한 예외적인 상황에 대한 처리를 뜻한다. - 게임 쪽에서는 잘 안쓰는 경향이 있다. -> 왜? 게임에서는 예외처리를 따로 하기보다는 크래쉬가 나도록 두고, 빠르게 수정하는 쪽으로 처리함. (한 사람이 예외가 발생한다는 건 다른 사람들도 모두 똑같이 예외가 발생한다는 것이므로 한 사람만 따로 예외처리하는 것이 의미가 없다.) -> 단, 심각한 예외가 아니거나 네트워크 관련 예외의 경우에는 catch해서 처리할 수 있다. 예외처리 문법 (try ~ catch, finally) static void Main(string[] args) { try // 1. 먼저 try..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 이벤트 파트에서 배우고 느낀 점을 정리해보았다. 이벤트를 사용하는 이유? - 델리게이트가 의도하지 않은 곳에서 멋대로 호출될 수 있다는 단점을 가지고 있으므로, 이를 통한 문제 발생을 막기 위해 델리게이트를 맵핑하기 위해서이다. 예제 // 구독자를 모집한 다음 특정 이벤트가 발생했을 때 구독자에게 // 메시지를 뿌리는 방식을 옵저버 패턴이라고 한다. class InputManager // 사용자가 키보드나 마우스를 입력하는 것을 캐치하여 다른 게임 로직, // 프로그램에 알려주는 중간 매개 역할을 하는 클래스 { public delegate void OnInputKey(); // 델리게이트 형식의 보호수준 이벤트와 // 변수 보호수준은..

    C#

    220822 Event (이벤트) NEW

    2022.08.22
    댓글
  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 델리게이트 파트에서 배우고 느낀 점, 보충 공부한 것들을 정리해보았다. 델리게이트의 정의와 필요성 - 델리게이트 : 함수 자체를 인자로 넘겨주는 것 - 델리게이트는 함수에 대한 참조를 가진다. 다른 함수의 메모리 주소를 가지는 함수 포인터로, C/C++에서의 함수 포인터와 비슷하다. - 델리게이트는 함수 위치의 목록이 포함된 주소록으로 시각화가 가능하다. 따라서 여러 함수 포인터를 가지며 한 번에 여러 개의 함수를 호출 할 수 있다. /// ///업체 사장의 부재 -> 사장님의 비서와 연락 /// 우리의 연락처/용건 남기면 -> 사장님이 연락을 줌 (콜백) /// UI 작업을 할 때 이런 일이 빈번하게 일어남. /// UI와 게임 로직..

    C#

    [C#] Delegate (대리자) NEW

    2022.08.21
    댓글
  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 프로퍼티 파트에서 배우고 느낀 점을 정리해보았다. 프로퍼티의 필요성 // 객체지향 -> 은닉성 // 프로퍼티 class Knight { protected int hp; // 그렇기 때문에 보통 주요 변수는 보호수준을 높이고 int GetHp() { return hp; } // 이렇게 getter setter 함수를 이용해서 할당 및 수정 해줄 수 있음. public void SetHp(int hp) { this.hp = hp; } } static void Main(string[] args) { Knight knight = new Knight(); knight.hp = 100; // 기본 변수 할당 -> 실수로 hp에 접근해서 임의로 값..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 인터페이스 파트에서 배우고 느낀 점을 정리해보았다. 1. 추상 클래스와 추상 함수 1-1 추상 클래스 - 기능 : abstract 를 사용해 추상 클래스를 선언할 수 있다. - 용도 : 반드시 이 함수를 재정의(오버라이드)해야 한다고 강요해야 할 때 사용할 수 있다. - 유의점 : 추상 클래스는 인스턴스를 만들 수 없으므로 더 이상 new 키워드로 생성할 수 없다. 1-2 추상 함수 - 특징 : 추상 클래스의 내부 함수는 추상 함수로 만들 수 있다. 또한 추상 함수는 빈 생성자만을 가질 수 있기 때문에, 생성자 안에 아무것도 선언할 수 없다. * 추상 클래스와 가상 클래스의 차이 추상 클래스의 경우 파생 클래스에서의 재정의를 강제할 수..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 일반화 파트에서 배우고 느낀 점, 추가로 공부한 점을 정리해보았다. 0. 일반화란? 사용자가 클래스를 선언하고 인스턴스화 할 때까지 타입 형식을 지연하는 것을 허용 하는 C#의 기능으로, 클래스가 제네릭 이라고 부를 때는 정의된 오브젝트 타입을 가지지 않아 어떤 타입이라도 될 수 있다. 이를 활용하면 C++에서의 템플릿과 같은 역할을 하는 클래스를 만들 수 있다. 1. 왜 일반화 형식을 사용해야 하는가? 1-1. 각 형식에 대한 클래스 만들기와 object 타입 사용하기 class MyIntList // 나만의 리스트를 만들고 싶다고 가정할 때의 잘못된 방식 { int[] arr = new int[10]; } class MyFloatL..

    C#

    [C#] 일반화(Generic) NEW

    2022.08.16
    댓글
  • '게임 프로그래밍 패턴' 교재를 학습하며 핵심 내용을 정리해보았다. CH1. 구조, 성능, 게임 1.1 소프트웨어의 구조 소프트웨어 구조의 핵심 목표 - 작업에 들어가기 전에 알아야 할 지식을 줄이는 것 - 생산성(코드를 더 유연하고 변경하기 쉽게 만듦)을 높이기 위한 좋은 구조를 갖추는 것 - 추상화, 모듈화, 디자인 패턴, 소프트웨어 구조를 사용하는 이유 역시 생산성을 높이기 위해서이다. 좋은 구조란 무엇인가? - 구조는 변경과 관련이 있기 때문에, 얼마나 쉽게 변경할 수 있느냐가 코드 설계 평가의 척도가 된다. - 코드를 거의 건드리지 않고도 적당한 함수 몇 개를 호출하여 원하는 작업을 할 수 있어야 한다. 좋은 구조 만들기 1. 코드를 변경하는 방법 - 코드를 고치기 전에 먼저 기존 코드를 이해해..

  • C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 객체지향 파트에서 배우고 느낀 점을 정리해보았다. 배열의 기초 class Program { static void Main(string[] args) { // int a; int[] scores = new int[5]; // scores라는 배열은 int 형식의 데이터를 5개 가지고 있다는 뜻 // 0 1 2 3 4 scores[0] = 10; // 배열에 값 할당 scores[1] = 20; for(int i = 0; i < scores.length; i++) // 이미 할당된 scores의 length가 5이므로, // 이에 벗어나는 범위(ex. 10)의 범위를 for문에서 읽고자 하면 오류가 발생한다. // 오류를 방지하기 위해 l..

    C#

    220816 배열과 컬렉션 NEW

    2022.08.16
    댓글
카테고리
작성일
2022. 8. 22. 23:14
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 예외처리 파트에서 배우고 느낀 점을 정리해보았다.


  • 예외처리란?
    - 예상치 못한 예외적인 상황에 대한 처리를 뜻한다.
    - 게임 쪽에서는 잘 안쓰는 경향이 있다.
      -> 왜? 게임에서는 예외처리를 따로 하기보다는 크래쉬가 나도록 두고, 빠르게 수정하는 쪽으로 처리함. (한 사람이 예외가 발생한다는 건 다른 사람들도 모두 똑같이 예외가 발생한다는 것이므로 한 사람만 따로 예외처리하는 것이 의미가 없다.)
      -> 단, 심각한 예외가 아니거나 네트워크 관련 예외의 경우에는 catch해서 처리할 수 있다.

  • 예외처리 문법 (try ~ catch, finally)

        static void Main(string[] args)
        {
            try // 1. 먼저 try구문 안의 코드를 한 번 실행해보고
            {

                // 여기서 예외적인 상황이란?
                // : 프로그램이 정상적으로 실행되지 않는 상황으로 아래와 같은 예시가 있음
                // 1. 값을 0으로 나눌 때. 0으로 나누면 아무것도 나눠지지 않으므로 오류발생
                // 2. 잘못된 메모리를 참조하는 경우
                // (null || 캐스팅 오류로 잘못된 타입을 가지는 경우)
                // 3. 복사시 너무 많은 데이터를 복사하여 오버플로우가 발생한 경우
                // 4. 스택이나 힙 메모리를 잘못 건드렸을 때

                // 1번 예외를 활용한 예제
                int a = 10;
                int b = 0;
                int result = a / b; // 0으로 값을 나눴기 때문에 예외가 발생함
            }

            catch (DivideByZeroException e) // 세부예외처리 -> 0으로 나누는 경우.
            {

            }

            catch (Exception e) // 2. 실행도중 예외적인 결과가 발생한다면
                                // 해당 예외에 대한 처리를 여기서 실행함
                                // Exception의 경우 모든 예외를 커버할 수 있고,
                                // 그 밖에 세부적으로 예외를 처리할 수도 있다.
                                // 단, Exception이 전체 예외를 커버하기 때문에
                                // 세부 예외처리보다 Exception이
                                // 앞에 오면 오류가 발생하여 실행이 되지 않는다.
                                // 그러므로 이 부분이 DivideByZeroException의 뒤에 와야 한다.
            {

            }

            finally  // try 문에서 예외가 발생하면 바로 catch문으로 빠지기 때문에
                     // try 스코프 안에서  예외가 발생한 줄 아래에 있는 코드들은 실행되지 않는다.
                     // 따라서 try catch문과 상관없이 반드시 실행되어야 하는 경우에는
                     // 여기에 해당 코드를 입력해서 실행되도록 따로 빼주어야 한다.
                     // 이 키워드의 활용으로는 파일 입출력이나 데이터베이스 연결같은 작업을 할 때,
                     // try 스코프에서 접속 코드를, finally 스코프 안에서는
                     // 해당 DB, 파일 내용을 정리하는 부분을 넣는 방법이 있다.
            {

            }
        }

 

  • 직접 에러 Exception을 만드는 방법
    class Program
    {
        class TestException : Exception // 커스팀 예외처리 클래스를 만들기 위해서는
                                        // 반드시 Exception을 상속받아야 한다.
        { 
            
        }

        static void Main(string[] args)
        {
            try 
            {
                throw new TestException(); // 새로 만든 예외를 던지면 catch가 받아줌
                
            }

            catch (Exception e) 
            {

            }

        }

    }

 

[결론]

try~catch 구문을 사용하여 예외 상황에 대한 처리를 간편하게 할 수 있다.

다양한 상황에서 발생하는 예외에 대해서 대처할 수 있으며,

네트워크 통신 오류 등의 덜 치명적인 상황에 대해서도 catch해서 별도로 처리할 수 있다.

카테고리
작성일
2022. 8. 22. 22:11
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 이벤트 파트에서 배우고 느낀 점을 정리해보았다.

 


  • 이벤트를 사용하는 이유?
    - 델리게이트가 의도하지 않은 곳에서 멋대로 호출될 수 있다는 단점을 가지고 있으므로, 이를 통한 문제 발생을 막기 위해 델리게이트를 맵핑하기 위해서이다.
  • 예제
   // 구독자를 모집한 다음 특정 이벤트가 발생했을 때 구독자에게
    // 메시지를 뿌리는 방식을 옵저버 패턴이라고 한다.

    class InputManager // 사용자가 키보드나 마우스를 입력하는 것을 캐치하여 다른 게임 로직,
                       // 프로그램에 알려주는 중간 매개 역할을 하는 클래스
    {

        public delegate void OnInputKey(); // 델리게이트 형식의 보호수준 이벤트와 
                                          // 변수 보호수준은 서로 같아야함.
        public event OnInputKey InputKey;  
        // 외부에서 사용 가능한 것은
        // 앞자리를 대문자로 바꾸는 것이 강사님 스타일

        public void Update()
        {
            if (Console.KeyAvailable == false)
                return;

            ConsoleKeyInfo info = Console.ReadKey();
            if (info.Key == ConsoleKey.A)
            {
                // 모두에게 알려준다! -> 이 부분을 어떻게 만들 것인가?
                // 여기에 강제로 코드를 넣는 것은 
                // 다른 코드와 inputManager와의 의존성을
                // 증가시키기 때문에 비효율적인 방식.
                // -> 이를 해결하기 위해 사용하는 것이 콜백 방식.
                // -> 콜백을 구현하기 위해 사용하는 문법이 델리게이트


                            // A라는 키를 누르면
                InputKey(); // 함수가 실행되며 모두에게 알려준다!
                            // 이 이벤트에 관심이 있다면 구독신청을 하면 된다.


            }
        }

    }
   class Program 
    {
        static void OnInputTest()
        { 
            Console.WriteLine("Input Received!"); 
        }

        static void Main(string[] args)
        {
            InputManager inputManager = new InputManager();
            inputManager.InputKey += OnInputTest; // 해당 함수를 구독함
//            inputManager.InputKey -= OnInputTest; // 구독을 취소함.  

            while (true)
            {
                inputManager.Update(); // a키가 눌리면 정상적으로 함수가 호출됨
                                       // 즉 "Input Received!"가 출력됨
            }

          // 이벤트와 델리게이트의 차이점
          //  inputManager.InputKey(); // 이벤트는 델리게이트와 달리 멋대로 직접 호출할 수 없음.
                                     // 구독 신청 or 취소만 가능하며 실질적으로 호출하는 것은 불가능함.
                                     // 내부적으로는 큰 차이가 없으며 이벤트 또한 콜백 방식의 일종임. 
        }

    }

 

  • 이벤트와 델리게이트의 차이점과 공통점
    - 차이점 : 이벤트는 델리게이트와 달리 멋대로 직접 호출할 수 없다. 오직 구독 신청 or 취소만 가능하며 실질적으로 호출하는 것은 불가능하다.
    - 공통점 : 이벤트와 델리게이트는 내부적으로 큰 차이가 없으며 이벤트 또한 델리게이트 처럼 콜백 방식의 일종이다.
카테고리
작성일
2022. 8. 21. 23:26
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 델리게이트 파트에서 배우고 느낀 점, 보충 공부한 것들을 정리해보았다.


  • 델리게이트의 정의와 필요성
     - 델리게이트 : 함수 자체를 인자로 넘겨주는 것
     - 델리게이트는 함수에 대한 참조를 가진다. 다른 함수의 메모리 주소를 가지는 함수 포인터로, C/C++에서의 함수 포인터와 비슷하다.
    - 델리게이트는 함수 위치의 목록이 포함된 주소록으로 시각화가 가능하다. 따라서 여러 함수 포인터를 가지며 한 번에 여러 개의 함수를 호출 할 수 있다.

    /// <대리자 ; Delegate>
    ///업체 사장의 부재 -> 사장님의 비서와 연락
    /// 우리의 연락처/용건 남기면 -> 사장님이 연락을 줌 (콜백)
    /// UI 작업을 할 때 이런 일이 빈번하게 일어남.
    /// UI와 게임 로직과 관련된 코드는 서로 분리되어야 한다.
    /// 그러나 함수 내부는 수정할 수 없는 경우가 많음. (EX. 유니티의 코어한 함수들은 변경 불가능)
    /// 
    /// 이럴 때 사용할 수 있는 것은 버튼이 눌렸을 때 호출해야 할 함수 자체를 인자로 넘겨주어서 
    /// 함수를 호출하도록 처리하는 것이다. 
    /// </summary>
    class Program 
    {
        static void ButtonPressed(/* 함수 자체를 인자로 넘겨주고 */)
        { 
            // 함수를 호출();
            // 인자를 넘겨 필요할 때 함수를 역으로 호출하는 것을 콜백이라고 한다.
        }
        
        static void Main(string[] args)
        {
           ButtonPressed(/* 실제 인자를 넣어주어 함수가 호출되도록 함 */);
        }

    }

 

  • 델리게이트 사용
   class Program 
    {

        delegate int OnClicked(); 
        // 1. 함수가 아니라 이 자체가 하나의 형식임. 함수 자체를 인자로 넘겨주는 형식
        // 반환 : int 입력 : void
        // OnClicked 가 delegate 형식의 이름이 됨.

        static void ButtonPressed(OnClicked clickedFunction/* 함수 자체를 인자로 넘겨주고 */) 
        // 2. delegate 형식을 보고 
        {
            // 함수를 호출();
            clickedFunction();
        }

        static int TestDelegate() // 4. 이 함수가 호출됨. 
        // 실질적으로 프로그램 작성시 내용을 구현해야 할 부분은 여기임
        {
            Console.WriteLine("Hello Delegate");
            return 0;
        }
        
        static void Main(string[] args)
        {
            ButtonPressed(TestDelegate/* 실제 인자를 넣어주어 함수가 호출되도록 함 */); 
            // 3. 실제로 호출되길 바라는 함수를 넣음
        }

    }
        static void Main(string[] args)
        {  // C++의 함수 포인터와 유사하지만 내부적으로 보자면 다음과 같음            
            OnClicked clicked = new OnCilcked(TestDelegate);
            clicked();
            
           ButtonPressed(/* 실제 인자를 넣어주어 함수가 호출되도록 함 */);
        }

2번 예제처럼 객체를 생성해서 사용하면 델리게이트 체이닝을 할 수 있다.

즉 하나의 함수만 넘기는 것이 아니라 여러 개의 함수를 동시에 넘겨주는 동작이 가능해진다.

 

  • 델리게이트 체이닝
    class Program 
    {

        delegate int OnClicked(); 


        static void ButtonPressed(OnClicked clickedFunction)
        {
            clickedFunction();
        }

        static int TestDelegate()
        {
            Console.WriteLine("Hello Delegate");
            return 0;
        }

        static int TestDelegate2()
        {
            Console.WriteLine("Hello Delegate");
            return 0;
        }


        static void Main(string[] args)
        {      
            OnClicked clicked = new OnClicked(TestDelegate);
            clicked += TestDelegate2;

            ButtonPressed(clicked);
        }

    }

델리게이트가 체이닝 되어 두 개의 함수가 동시에 동작한다.

 

  • 델리게이트 사용 시 장점
    1. 코드에 직접 접근할 수 없을 때 간접적으로 함수를 호출할 수 있어 유용하다.

    2. 실제로 코드를 접근할 수 있는 상황에서도 다양한 방식으로 동작하는 하나의 함수를 만들 수 있다는 점에서 유용하다.

    ex. 리스트를 이용해서 내용을 소팅해야하는 경우, 비교하는 함수에 따라서 어쩔 때는 작은 순서대로 정렬되고, 어쩔 때는 큰 순서대로 정렬되어야 할 때 숫자 중에 어느 숫자의 우선순위가 높은지를 판별하는 부분만 델리게이트를 사용해 외부에서 정해줄 수 있게 해주면 다양한 동작이 가능하다.

 

카테고리
작성일
2022. 8. 20. 23:12
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 프로퍼티 파트에서 배우고 느낀 점을 정리해보았다.


  • 프로퍼티의 필요성
    // 객체지향 -> 은닉성
    // 프로퍼티
    class Knight
    {
        protected int hp; // 그렇기 때문에 보통 주요 변수는 보호수준을 높이고

        int GetHp() { return hp; } // 이렇게 getter setter 함수를 이용해서 할당 및 수정 해줄 수 있음.
        public void SetHp(int hp) { this.hp = hp; }

    }

    static void Main(string[] args)
    {

        Knight knight = new Knight();
        knight.hp = 100;

        // 기본 변수 할당 -> 실수로 hp에 접근해서 임의로 값을 변경해버릴 위험이 있음.
        // 이 경우 어디에서 값이 바뀌었는지 알아내기가 어려움.
        knight.hp = 40;

        // 기본 변수 할당의 보완 -> getter setter 함수의 활용
        knight.SetHp(100);
    }

하지만 위의 개선된 코드에도 문제점이 있다. 바로 변수가 늘어나면 getter setter 함수도 그만큼 늘어난다는 것이다.
C++이라면 따로 방법이 없으니 getter setter를 쓰는게 최선이겠지만,

C#에서는 이를 편리하게 해결하기 위한 프로퍼티가 있으니 이를 활용하는 것이 좋다.

  • 프로퍼티 기본형
        public int Hp
        {
            get { }
            set { }
        }

 

  • 사용 예제
        class Knight
        {
            protected int hp;

            public int Hp
            {
                get { return hp; }
                set { this.hp = value; } // 디폴트로 value를 사용함.
            }
        }

        static void Main(string[] args)
        {
            Knight knight = new Knight();
            knight.Hp = 100; // set -> value가 100이 되어 할당됨.

            int hp = knight.Hp; // get -> get함수에서 return 값으로 지정한 변수를 사용하여 가져옴
        }


반드시 get, set 함수가 동시에 사용될 필요가 없기 때문에 경우에 따라 한 쪽 함수만 선언하여 사용하기도 한다.

    class Program 
    {
        class Knight
        {
            protected int hp;

            public int Hp
            {
                get { return hp; }
            }
        }

        static void Main(string[] args)
        {
            Knight knight = new Knight();

            int hp = knight.Hp; // get -> get함수에서 return 값으로 지정한 변수를 사용하여 가져옴
        }
    }

 

또한 get 함수는 외부에 노출해 사용하지만, set함수는 안에서만 사용이 가능하도록 막고 싶을 때의 경우에는 다음과 같이 set 앞에 private 선언을 해주어서 외부 접근이 불가능하도록 만들 수 있다.

        class Knight
        {
            protected int hp;

            public int Hp
            {
                get { return hp; }
                private set { hp = value; }
            }

            void Test()
            {
                Hp = 100;
            }
        }

 

  • 자동화된 프로퍼티
    - 이전의 코드들의 내용을 한 줄로 줄일 수 있는 더욱 간소화된 자동 프로퍼티가 있다.
    // 더욱 간소화된 자동구현 프로퍼티
    class Program 
    {
        class Knight
        {
           public int Hp { get; set; }
            public int Mp { get; set; } = 100; // c# 7.0부터 이처럼 초기화 값을 함께 넣어줄 수 있게 되었음

            // 위의 자동구현 프로퍼티는 아래의 코드 3줄과 같은 의미를 가짐
            private int _hp;
            public int GetHp() { return _hp; }
            public void SetHp(int value) { _hp = value; } 
        }

        static void Main(string[] args)
        {
            Knight knight = new Knight();

            int hp = knight.Hp; // 최종적으로 이렇게 사용할 수 있음
        }

    }

 

[결론]

프로퍼티는 객체지향의 은닉성을 구현하기 위한 수단으로, 외부에서 중요한 내부 변수에 함부로 접근할 수 없게끔 해준다.

또한 프로퍼티는 함수의 형태를 띄고 있지만 public 변수를 접근하고 변경하는 것처럼 편리하게 변수를 다룰 수 있다는 장점이 있다.

따라서 은닉성과 사용의 편리성을 한 번에 챙길 수 있는 문법이라고 할 수 있다.

 

+) 식 본문 멤버

C# 6.0부터 사용할 수 있는 문법 중에는 속성이나 메서드의 Body 블록이 간단한 경우 람다식처럼 사용할 수 있는 식 본문이라는 것이 있다.

프로퍼티를 만들 때 get만 가능한 읽기 전용 프로퍼티를 만들 때 이 식 본문을 활용할 수도 있기에 추가로 학습해 보았다.

 

  • 식 본문 예제
    - 아래의 링크에서 학습한 예제는 다음과 같다. 기존의 람다식처럼 => 기호를 사용하여 입력되는 값 => 반환할 값을 할당해주면 된다.
// 기존의 속성
public int Area
{
    get 
    {
        return Height * Width;
    }
}

// C# 6.0의 Expression-bodied member 표현
public int Area => Height * Width;

 

https://www.csharpstudy.com/CS6/CSharp-Expression-bodied-member.aspx

 

C# 6.0 Expression-bodied - C# 프로그래밍 배우기 (Learn C# Programming)

Expression-bodied member 사용 C#의 속성이나 메서드는 보통 여러 문장(statement)들로 구성된 블럭을 실행하게 된다. 하지만 속성이나 메서드의 Body 블럭이 간단한 경우, Statement Block을 사용하는 대신 간

www.csharpstudy.com

 

'C#' 카테고리의 다른 글

220822 Event (이벤트)  (0) 2022.08.22
[C#] Delegate (대리자)  (0) 2022.08.21
220817 Interface (인터페이스)  (0) 2022.08.17
[C#] 일반화(Generic)  (0) 2022.08.16
220816 배열과 컬렉션  (0) 2022.08.16
카테고리
작성일
2022. 8. 17. 16:02
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 인터페이스 파트에서 배우고 느낀 점을 정리해보았다.


1. 추상 클래스와 추상 함수

 1-1 추상 클래스

 - 기능 : abstract 를 사용해 추상 클래스를 선언할 수 있다.

 - 용도 :  반드시 이 함수를 재정의(오버라이드)해야 한다고 강요해야 할 때 사용할 수 있다.

 - 유의점 : 추상 클래스는 인스턴스를 만들 수 없으므로 더 이상 new 키워드로 생성할 수 없다.

 

 1-2 추상 함수

 - 특징 : 추상 클래스의 내부 함수는 추상 함수로 만들 수 있다. 또한 추상 함수는 빈 생성자만을 가질 수 있기 때문에, 생성자 안에 아무것도 선언할 수 없다.

 

* 추상 클래스와 가상 클래스의 차이

추상 클래스의 경우 파생 클래스에서의 재정의를 강제할 수 있지만 가상 클래스에서는 할 수 없다.

추상 클래스의 경우 객체 생성이 불가하지만(인스턴스화X) 가상 클래스는 new 키워드로 객체를 생성할 수 있다.(인스턴스화O)

    class Program
    {
        abstract class Monster // abstract -> 기능 : 추상 클래스로 해당 클래스를 선언한다.
        {
            public abstract void Shout();
        }

        class Orc : Monster
        {
            public override void Shout()
            {
                Console.WriteLine("록타르 오가르!");
            }

        }

        class Skeleton : Monster
        {
            // 가상 함수에서는 하단의 함수 재정의 부분이 없다면 Monster 내부의 shout()이 실행됨
            // 추상 함수에서는 하단의 함수 재정의 부분이 없다면 오류가 발생함.
            public override void Shout()
            {
                Console.WriteLine("꾸에에엑!");
            }
        }

        static void Main(string[] args)
        {

        }

    }

 

2. 인터페이스

- C#은 문법상 C++과 달리 다중 상속이 불가능하다. (다이아몬드 문제 때문)
- 이를 대신해 인터페이스는 상속되지만 구현부는 파생 클래스에서 알아서 할 수 있도록 인터페이스 기능을 제공한다.

- 인터페이스로 선언되면 public이나 private 같은 접근 제한자 없이 인터페이스 함수의 뼈대 선언만 하게 된다.

- 일반적으로 인터페이스의 컨벤션으로 변수 앞에 I를 붙인다.

 

* (죽음의) 다이아몬드 문제란?

하나의 클래스가 여러 개의 상위 클래스를 상속받았을 때, 동일한 이름을 가진 메소드가 2개 이상 존재할 경우 어떤 클래스의 메소드를 상속받아야 하는지 모호해지는 문제를 말한다.

 

- 인터페이스 사용 예제

        abstract class Monster
        {
            public abstract void Shout();
        }

        interface IFlyable
        {
            void Fly();
        }

        class Orc : Monster
        {
            public override void Shout()
            {
                Console.WriteLine("록타르 오가르!");
            }

        }

        class FlyableOrc : Orc, IFlyable
        {
            public void Fly() // 상속받은 인터페이스의 함수를 선언하지 않으면 오류가 발생함.
            				  // 반드시 해당 부분을 구현해야 한다.
            { 
            
            }

        }

 

- 인터페이스를 사용했을 때의 유용함

        static void DoFly(IFlyable flyable)
        {
            flyable.Fly();
        }

        static void Main(string[] args)
        {
            FlyableOrc orc = new FlyableOrc();
            DoFly(orc);
        }

다양한 클래스를 구현할 때 인터페이스를 이용하면 공통적인 기능을 쉽게 추가할 수 있다.

또한 위의 코드처럼 인터페이스를 상속받는 클래스만 접근이 가능한 함수를 만들 수도 있고,

다른 클래스와 인터페이스를 함께 상속한 클래스의 경우 인터페이스 속성을 사용해 클래스를 불러올 수도 있어 편리하다.

'C#' 카테고리의 다른 글

[C#] Delegate (대리자)  (0) 2022.08.21
220820 Property (프로퍼티) + 식 본문 멤버 (Expression-bodied members)  (0) 2022.08.20
[C#] 일반화(Generic)  (0) 2022.08.16
220816 배열과 컬렉션  (0) 2022.08.16
220718 C# 연습문제 풀이  (0) 2022.07.18
카테고리
작성일
2022. 8. 16. 23:39
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 일반화 파트에서 배우고 느낀 점, 추가로 공부한 점을 정리해보았다.


0. 일반화란?

사용자가 클래스를 선언하고 인스턴스화 할 때까지 타입 형식을 지연하는 것을 허용 하는  C#의 기능으로,

클래스가 제네릭 이라고 부를 때는 정의된 오브젝트 타입을 가지지 않아 어떤 타입이라도 될 수 있다.

이를 활용하면 C++에서의 템플릿과 같은 역할을 하는 클래스를 만들 수 있다.

 

1. 왜 일반화 형식을 사용해야 하는가?

 1-1. 각 형식에 대한 클래스 만들기와 object 타입 사용하기 

        class MyIntList // 나만의 리스트를 만들고 싶다고 가정할 때의 잘못된 방식
        {
            int[] arr = new int[10];
        }

        class MyFloatList
        {
            float[] arr = new float[10];
        }

        // 지원할 수 있는 형식마다 하나씩 클래스를 만드는 건 매우 비효율적.
        // 타입이 몇개인지 예측할 수 없고, 새로운 형식을 만들 때도
        // 그 때마다 클래스를 추가해야하기 때문에 유지보수가 너무 어려워짐.
        
        static void Main(string[] args)
        {
            // 위의 문제를 해결하기 위해 object 타입을 사용해본다.
            // object 타입의 장점은 어떤 타입이든 저장 및 사용(캐스팅시)이 가능하다는 점이다.

            object obj = 3; 
            object obj2 = "hello world";

            int num = (int)obj;
            string str = (string)obj2;
            
       }

 

 1-2 object 타입?

 - object 타입의 장점은 어떤 타입이든 저장 및 사용(캐스팅시)이 가능하다는 점이다.
 - 어떻게 모든 타입을 저장할 수 있는가? : c#의 최상위 부모 클래스가 object이며, string, int등의 타입이 이러한 object를 상속받아 구현되었기 때문이다.

 

 * var 타입과 object 타입의 비교
 - var 타입도 object 처럼 여러 타입의 저장이 가능하지만, 두 타입은 엄연히 다르다. var는 string을 명시적으로 입력하지 않아도 컴파일러가 뒤에 오는 코드를 보고 타입을 변환해주는 방식으로 사용된다. 반면 object는 타입이 오브젝트라는 형식 자체가 된다.

 

 1-3 그렇다면 모든 자료형을 object로 통합하면 되지 않을까?

 - object는 복사 타입의 데이터(데이터가 heap에 들어가는 것이 아니라 stack에 저장되는) int 와 같은 타입과 달리 오브젝트는 반드시 참조 형식으로만 동작하기 때문에, heap에서 해당 정보를 빼와서 stack에 저장해야하는 박싱/언박싱 이란 복잡하고 무거운 작업을 거친다. 따라서 모든 자료형을 object로 사용하는 것은 성능 상의 문제를 야기하므로 다른 방법인 일반화를 사용하여야 한다.

 

2. 일반화의 사용 - 클래스 일반화

class Program
    {
        // < Generic ; 클래스 일반화 >
        // 선언
        class MyList<T> // <T> -> 일반화 되어 T타입에 어떤 값을 대응하더라도 작동됨.
        {
            T[] arr = new T[10];

            public T GetItem(int i) // i번째 아이템을 반환하는 함수
            {
                return arr[i];
            }
        }

        class Monster
        { 
        
        }

        // 사용
        static void Main(string[] args)
        {
            MyList<int> myIntList = new MyList<int>();
            myIntList.GetItem(0); // int타입으로 반환하게 됨

            MyList<short> myShortList = new MyList<short>();
            MyList<Monster> myMonsterList = new MyList<Monster>();
        }
   }

 

3. 일반화의 사용 - 함수 일반화

        static void Test<T>(int input)
        { 
            
        }

 

4. 일반화의 사용 - 응용

        // < Generic ; 일반화 응용 >
        // 선언
        class MyList<T> where T : struct // 어떤 값을 T에 대응하더라도 상관없다.
                                         // 단, T는 struct 형식이여야 한다.
        {
            T[] arr = new T[10];

            public T GetItem(int i)
            {
                return arr[i];
            }
        }
        class MyList<T> where T : class // T가 반드시 참조형식이여야 하는 경우
        {
            T[] arr = new T[10];

            public T GetItem(int i)
            {
                return arr[i];
            }
        }

 

+) 자주 사용되는 응용

       // < Generic ; 일반화 응용 >
        // 선언
        class MyList<T> where T : new() // 반드시 어떠한 인자도 받지 않는
                                        // 기본 생성자가 있어야 하는 경우
        {
            T[] arr = new T[10];

            public T GetItem(int i)
            {
                return arr[i];
            }
        }
        // < Generic ; 일반화 응용 >
        // 선언
        class MyList<T> where T : Monster // T가 반드시 몬스터를 상속받는 클래스여야 하는 경우
        {
            T[] arr = new T[10];

            public T GetItem(int i)
            {
                return arr[i];
            }
        }

 

[결론]

- 일반화를 활용하면 클래스를 남발하지 않아도 여러 타입에 대응이 가능하기 때문에 프로그램의 유지보수가 용이해짐.

- 특정 타입에 대한 일반화가 필요하다면 ':'을 이용해 조건을 세부적으로 정해줄 수 있음.

'C#' 카테고리의 다른 글

220820 Property (프로퍼티) + 식 본문 멤버 (Expression-bodied members)  (0) 2022.08.20
220817 Interface (인터페이스)  (0) 2022.08.17
220816 배열과 컬렉션  (0) 2022.08.16
220718 C# 연습문제 풀이  (0) 2022.07.18
[C#] 문법 정리  (0) 2022.07.17
카테고리
작성일
2022. 8. 16. 21:08
작성자
risehyun

'게임 프로그래밍 패턴' 교재를 학습하며 핵심 내용을 정리해보았다.


CH1. 구조, 성능, 게임

1.1 소프트웨어의 구조

  • 소프트웨어 구조의 핵심 목표
    - 작업에 들어가기 전에 알아야 할 지식을 줄이는 것
    - 생산성(코드를 더 유연하고 변경하기 쉽게 만듦)을 높이기 위한 좋은 구조를 갖추는 것
    - 추상화, 모듈화, 디자인 패턴, 소프트웨어 구조를 사용하는 이유 역시 생산성을 높이기 위해서이다.

  • 좋은 구조란 무엇인가?
    - 구조는 변경과 관련이 있기 때문에, 얼마나 쉽게 변경할 수 있느냐가 코드 설계 평가의 척도가 된다.
    - 코드를 거의 건드리지 않고도 적당한 함수 몇 개를 호출하여 원하는 작업을 할 수 있어야 한다.

  • 좋은 구조 만들기
    1. 코드를 변경하는 방법
    - 코드를 고치기 전에 먼저 기존 코드를 이해해야 한다.
    - 문제를 해결하기 위한 코드를 작성한다.
    - 컴파일에 성공했다면 테스트(자동화된 단위 테스트 등)를 작성한다.
    - 코드 리뷰를 요청하기 전에 추가한 코드를 이전 코드들과 깔끔하게 통합되도록 정리한다.

    2. 디커플링을 이용한다
    - 커플링이란? 양쪽 코드 중에 한쪽이 없을 때 코드를 이해할 수 없으면 서로 커플링 되어 있다고 본다.
    - 디커플링이란? 어느 한 코드를 변경했을 때 다른 코드를 변경하지 않아도 되는 것
    - 디커플링시 기대할 수 있는 효과는? 한 번에 이해해야 하는 코드의 양을 최대한 줄일 수 있다.

1.2 비용은?
- 하지만, 무조건 디커플링을 통해 좋은 구조를 만드는 것이 정답은 아니다.
- 좋은 구조를 만들고 유지하는 데에는 많은 노력과 원칙이 필요하다.
- 기능을 추가하거나 변경할 때마다 나머지 코드와 깔끔하게 통합되도록 노력해야만 한다.

  • 코드를 통합 하기 위한 노력이란?
    - 프로그램 어디를 디커플링하고 추상화할지 고민
    - 추후 쉽게 변경할 수 있으려면 어디를 확장성 있게 설계할지 미리 정해야함

    * 확장성의 딜레마
    - 예측되는 필요에 따라 유연성을 발휘해야 하는데, 이를 위해 코드를 추가하다보면 시간이 더 걸리게 된다.
    - 또한 만들어놓은 모듈이 실제로 써먹지 못하면 지금까지의 작업이 헛수고가 된다.
    - 확장성에만 매몰 되면 코드에 이것저것 살이 붙어 계층이 복잡해지고 구조가 막 나가게 된다.


1.3 성능과 속도

- 빠른 프로토타이핑을 위해 프로그램을 유연하게 만들면 성능상 비용이 발생한다.
- 반면 코드를 최적화하면 유연성이 떨어진다.

- 즉, 유연성(비용)은 최적화(성능)와 반비례한다.
- 코드에 유연함이 필요하다는 확신이 없다면 추상화와 디커플링을 적용하려 애쓰지 말고 추후 기획이 확실해졌을 때 추상 계층을 제거해 성능을 높이는 타협안도 있다.

- 저수준의 핵심 최적화는 가능한 늦게 해야 한다.

1.4~1.6 균형을 잡기 위한 단순함
- 오랫동안 같은 코드로 계속 작업해야 하는 경우 구조화가 잘된 코드를 작성하는 것이 좋다.
- 하지만 기획 확인 수준의 코드라면 구조를 멋지게 만들기보다 빨리 만들되 버릴 코드는 확실히 버릴 수 있게끔 적당히 작업 해야한다.
- 제약을 완화하기 위해 필요없는 코드를 최대한 빼서 간결하고 적은 로직으로도 많은 유스케이스를 정확히 해결하게끔 짜야 한다.
- 즉, 우선적으로 자료구조와 알고리즘을 순서에 맞춰 먼저 잡아놓은 다음 다른 방법을 찾아야 한다.

'CS > 디자인패턴' 카테고리의 다른 글

관찰자 패턴(Observer Pattern)  (0) 2022.08.29
3장. 경량 패턴(Flyweight Pattern)  (0) 2022.08.28
2장. 명령 패턴(Command Pattern)  (0) 2022.08.23
카테고리
작성일
2022. 8. 16. 17:22
작성자
risehyun

C#과 유니티로 만드는 MMORPG 게임 개발 시리즈 강의를 수강하며 객체지향 파트에서 배우고 느낀 점을 정리해보았다.


  • 배열의 기초
   class Program
    {
        static void Main(string[] args)
        {
            // <배열>
            int a;
            int[] scores = new int[5]; // scores라는 배열은 int 형식의 데이터를 5개 가지고 있다는 뜻

            // 0 1 2 3 4
            scores[0] = 10; // 배열에 값 할당
            scores[1] = 20;

            for(int i = 0; i < scores.length; i++) // 이미 할당된 scores의 length가 5이므로, 
            // 이에 벗어나는 범위(ex. 10)의 범위를 for문에서 읽고자 하면 오류가 발생한다.
            // 오류를 방지하기 위해 length 키워드를 사용하고 범위 내에서만 작동하도록 한다.
            {
                Console.WriteLine(scores[i]); // 10, 20, 0, 0, 0이 출력 됨
                // 0, 1번째 인덱스는 값이 할당되어 있으나 이후로는 할당값이 없으므로
                // 모두 기본값인 0을 출력하게 됨

            }

        }
    }

 

     - 배열을 선언하는 또 다른 방법은 다음과 같다. 이 경우에는 미리 할당된 배열 크기만큼 반드시 값이 할당되어야 하므로 10, 20까지만 할당하고 나머지는 비워두는 0번 방식과 동일하게 사용할 수 없다.

 

int[] scores = new int[5]; // 0번 방법, scores라는 배열은 int 형식의 데이터를 5개 가지고 있다는 뜻
scores[0] = 10; // 배열에 값 할당
scores[1] = 20;

int[] scores = new int[5] { 10, 20, 30, 40, 50 }; // 1번 방법

int[] scores = new int[] { 10, 20, 30, 40, 50 };   // 2번 방법

int[] scores = { 10, 20, 30, 40, 50 };            // 3번 방법

 

    - 만약 동적으로 배열 값을 할당하고 싶을 때는 2번 방법처럼 new int[5]에 있는 [] 값을 비워두는 방법이 있다.

    - 2번 방법의 약식인 3번 방법을 사용해도 결과는 동일하다.

    - 강사님이 주로 사용한다고 언급한 방법은 1번 방법이다. 배열을 동적할당한다는 것을 명시적으로 나타낼 수 있기 때문. 

 

  • 배열 오류를 막기 위한 foreach문의 활용
            for(int i = 0; i < 5; i++)
            {
                Console.WriteLine(scores[i]); // 10, 20, 0, 0, 0이 출력 됨
            }

            foreach (int score in scores) // for문에서 가독성과 간결성을 높인 foreach문
                                          // 인덱스 오류를 방지할 수 있음
            {
                Console.WriteLine(score); // for문과 동일하게 10, 20, 0, 0, 0이 출력 됨
            }

 

  • 배열의 복사
    class Program
    {
        static void Main(string[] args)
        {
            // <배열 : 복사>

            int[] scores = new int[5] { 10, 20, 30, 40, 50 };
            int[] scores2 = scores; // 위에 선언한 배열의 값의 원본을 2에 할당함

            scores2[0] = 9999; // 원본 값을 할당했기 때문에 scores와 scores2 둘다
                               // 0번 인덱스의 할당 값이 9999가 됨


            for(int i = 0; i < 5; i++)
            {
                Console.WriteLine(scores[i]); // 9999, 20, 30, 40, 50이 출력됨
            }

            foreach (int score in scores)
            {
                Console.WriteLine(score); // for문과 동일하게 출력됨
            }

        }
    }

 

  • 연습문제
    class Program
    {
        static int GetHighestScore(int[] scores) // 제일 높은 값을 리턴하는 함수
        {
            int maxValue = 0;
            foreach (int score in scores)
            {
                if (score > maxValue)
                    maxValue = score;
            }
            return maxValue;
        }

        static int GetAverageScore(int[] scores) // 배열 값의 평균을 리턴하는 함수
        {
            int sum = 0;

            if (scores.Length == 0) // 배열의 크기가 0인 경우 그대로 실행하면
                                    // 오류가 발생하기 때문에 연산 이전에 미리 예외처리해줌 
            {
                return 0;
            }

            foreach (int score in scores)
            {
                sum += score;
            }

            return sum / scores.Length;
        }

        static int GetIndexOf(int[] scores, int value) // 스코어 변수에서 원하는 값이 있는지 체크하고
                                                       // 해당하는 인덱스 반환, 없으면 -1 반환하는 함수
        {

            for (int i = 0; i < scores.Length; i++) // 인덱스를 반환하는게 목표이므로 for문을 사용함
            {
                if(scores[i] == value)
                {
                    return i;
                }
            }
            return -1;
        }

        static void Sort(int[] scores) // 크기 순서대로 배열을 정렬해주는 함수
        {
            for (int i = 0; i < scores.Length; i++)
            {
                // [i~scores.length-1]의 범위 내에서 제일 작은 숫자가 있는 index를 찾는다.
                int minIndex = i;
                for (int j = i; j < scores.Length; j++)
                {
                    if (scores[j] < scores[minIndex])
                        minIndex = j;
                }

                // 현재 위치와 제일 작은 값이 있는 위치를 swap 해줌
                int temp = scores[i];
                scores[i] = scores[minIndex];
                scores[minIndex] = temp;
            }
        }

        static void Main(string[] args)
        {
            // <배열 : 복사>

            int[] scores = new int[5] { 10, 30, 40, 20, 50 };


            int hightestScore = GetHighestScore(scores);
            Console.WriteLine(hightestScore);

            int avgScore = GetAverageScore(scores);
            Console.WriteLine(avgScore);

            int index = GetIndexOf(scores, 15);
            Console.WriteLine(index);

            Sort(scores);

        }
    }

 

    - ✔ 소팅의 경우 실무에서 직접 구현할 일은 없지만 면접 시에 기본적인 소팅 알고리즘을 4개 정도 출제 되니 관련해서 추가적인 공부가 필요하다. 

 

  • 다차원 배열
   class Program
    {
        class Map
        {
            // 1은 갈 수 없는 곳, 0은 갈 수 있는 곳이라고 가정하고 맵 생성
            int[,] tiles = {
                { 1, 1, 1, 1, 1},
                { 1, 0, 0, 0, 1},
                { 1, 0, 0, 0, 1},
                { 1, 0, 0, 0, 1},
                { 1, 1, 1, 1, 1}
            };


            public void Render()
            {

                ConsoleColor defaultColor = Console.ForegroundColor; 
                // 콘솔 출력 텍스트의 색깔이 변하지 않도록 전경색을 저장해줌

                for (int y = 0; y < tiles.GetLength(1); y++) 
                // 왼쪽에서 오른쪽으로 읽기 때문에 (1)은 [여기,] 를 의미
                {
                    for (int x = 0; x < tiles.GetLength(0); x++) // (0)은 [,여기]를 의미
                    {
                        if (tiles[y, x] == 1) // 한 줄의 마지막 부분이면
                            Console.ForegroundColor = ConsoleColor.Cyan;

                        else
                            Console.ForegroundColor = ConsoleColor.Magenta;
                        Console.Write('\u25cf');
                    }
                    Console.WriteLine(); // 한 줄의 표현이 끝났으므로 구분하기 위해 줄을 한 칸 띄움
                }

                Console.ForegroundColor = defaultColor;
            }

        }

        static void Main(string[] args)
        {
            /*            // <다차원 배열>
                        // 하나는 층, 하나는 호수를 나타낸다고 생각하면 된다.
                        // 다차원 배열을 선언하려면 ','를 사용한다.
                        // 읽을 때는 오른쪽에서 왼쪽 순서이며
                        // 오른쪽은 1차원의 크기, 왼쪽이 2차원의 크기를 의미한다.

                        // 2F [ . . . ]
                        // 1F [ . . . ]

                        int[,] arr = new int[2, 3] { { 1, 2, 3 }, { 1, 2, 3 } }; // 초기화 문법

                        // 1차원 배열과 마찬가지로 선언시 앞 부분의 값은 생략이 가능하다.
                        // int[,] arr = new int[,] { { 1, 2, 3 }, { 1, 2, 3} };

                        arr[0, 0] = 1; // 값 세팅, 접근하는 방법
                        arr[1, 0] = 1;
            */

            Map map = new Map();
            map.Render();

        }
    }

 

  • 가변 배열
    - 활용되는 경우가 거의 없긴 하지만, 특수하게 사용하게 될 수 있으므로 알아두자.
   class Program
    {
        // < 다차원 배열 : 가변 배열(배열의 배열, 특수한 경우)>

        static void Main(string[] args)
        {

            int[,] map = new int[2, 3]; // 특정 행 또는 열이 특수하게 더 많거나 적은 경우

            // [ . . . ]
            // [ . . . . . . . ]
            // [ . . . ]

            int[][] a = new int[3][];
            a[0] = new int[3];
            a[1] = new int[6];
            a[2] = new int[2];

            a[0][0] = 1;
        }
    }

a[0], a[1], a[2]가 각각의 배열로 생성되어 있음을 알 수 있다.

 

  • List
    - 배열의 경우 생성시에 이미 크기가 정해져있기 때문에, 원래 설정해둔 크기 이상의 데이터를 담을 수 없다.
    - 이를 해결하기 위해 배열의 크기를 확신할 수 있다면 여유롭게 1000개쯤 최대값을 선언할 수도 있지만, 실질적으로 사용하는 공간이 5개 정도 밖에 없다면 남은 995개만큼의 공간에 대한 메모리 낭비가 될 수 있다.
    - 이러한 문제를 해결하기 위해 필요에 따라 배열의 크기를 동적으로 할당할 수 있는 List라는 것이 존재한다.
    - List를 사용하기 위해서는 using System.Collections.Generic;를 선언해주어야 한다.
    class Program
    {
        static void Main(string[] args)
        {
            // <List ; 동적 배열>
            List<int> list = new List<int>(); 
            // <> 안에는 사용할 리스트의 타입을 기입한다.
            // 이러한 것을 Generic타입이라고 하며, 어떤 타입이든 담을 수 있게 해준다.
            // List는 일종의 class이기 때문에, 참조 형태라서 사용시에 new 키워드를 사용해야한다.

            for (int i = 0; i < 5; i++)
            {
                list.Add(i); // 리스트의 맨 끝에 새로운 데이터를 밀어 넣어준다.
            }

            list[0] = 1; // 이러한 방식으로 리스트에 접근 할 수 있다.
                         // 단, 리스트가 비어있는 상태로 list에 접근하면
                         // 오류가 발생하므로 반드시 확인해주어야 한다.
        }
    }

배열과 달리 리스트는 자동으로 count가 5로 인식됨을 알 수 있다.

 

    - 리스트는 사용시에 배열과 동일한 방법으로 접근할 수 있다.

    class Program
    {
        static void Main(string[] args)
        {
            // <List의 사용>
            List<int> list = new List<int>(); 
            
            for (int i = 0; i < 5; i++)
                list.Add(i);

            for (int i = 0; i < 5; i++)
                Console.WriteLine(list[i]);

            foreach(int num in list)
                Console.WriteLine(num);
        }
    }

 

    - 리스트의 삽입

   class Program
    {
        static void Main(string[] args)
        {
            List<int> list = new List<int>();
            
            for (int i = 0; i < 5; i++)
                list.Add(i);

            // <List의 삽입>
            // 맨 끝 부분에 데이터를 추가하는 add와 달리 특정 인덱스에
            // 원하는 값을 할당하고 싶을 때 사용할 수 있는 방법
            list.Insert(2, 999); // 2번째 인덱스에 999를 할당함
                                 // 배열의 구성이 [ 0 1 999 2 3 4 ] 로 바뀜 됨

            for (int i = 0; i < list.Count; i++)
                Console.WriteLine(list[i]);

            foreach(int num in list)
                Console.WriteLine(num);
        }
    }

 

    - 리스트의 삭제 : Remove와 RemoveAt, Clear 함수를 사용하여 리스트의 데이터를 삭제할 수 있다.

       1. Remove

Remove는 bool 값을 리턴하기 때문에 디버깅을 해보면 true가 반환됨을 알 수 있다.

또한 Remove() 함수에 아이템으로 3을 할당했기에 리스트 내부에 '3' 이 삭제되고 나머지 값만 남아있음을 알 수 있다.

 

       2. RemoveAt

RemoveAt은 void 타입의 함수이기 때문에 반환 값이 없으며, 지정된 인덱스에 저장된 데이터 값을 삭제한다.

위의 코드에서 0번째 인덱스를 삭제하였기 때문에 배열의 Count는 4로 줄어들고, 1번째 인덱스에 든 값부터 0번째 배열에 할당되어 하나씩 자릿수가 줄었음을 알 수 있다.

 

       3. Clear

list.Clear(); // 리스트의 내용을 전체 삭제한다.

지정된 리스트의 내용을 모두 삭제하여 초기화한다.

 

    - 리스트 사용시 주의점과 단점

리스트를 중간에 삽입/삭제하는 것은 효율적이지 않다.

해당 작업을 수행할 때 반드시 나머지 배열의 값을 복사하여 이동시키고 빈 공간에 할당해야 하기 때문에 시간복잡도면에서 적합하지 않기 때문이다.

 

또한, 강사님이 짚어주신 바로는 아래에 서술할 문제 때문에 경우에 따라 list를 무작정 사용하는 것은 문제가 된다.

MMORPG를 만든다고 가정했을 때, 필드에 몬스터와 플레이어가 매우 많다.
이때 각종 생명체를 구분하기 위해 대부분 ID를 부여하여 식별자로 활용하게 된다.

예를 들어 103이라는 ID를 가진 몬스터를 공격하고 싶을 때는
10 103 -> 이런 식으로 공격이란 네트워크 요청에 해당하는 아이디 10과 함께 몬스터의 아이디인 103 넣어주게 된다.이런 상황에서 만약 리스트로 모든 것을 관리하고 있다면 해당 데이터를 찾는 것이 문제가 된다.

만약 몬스터 리스트의 count가 100만개라고 하면 이 중에서 103번에 해당하는 몬스터를 찾으려면 처음부터 끝까지 루프를 돌면서 모든 몬스터를 하나씩 체크하여 103번인지 아닌지를 판단하는 방법 밖에 없다. 즉, 매우 비효율적

이를 해결하기 위해서는 특징 key를 사용하여 value를 찾는 방법인 Dictionary가 가장 적합하다.

 

  • Dictionary
    - Dictionary는 List와 달리 Key와 Value가 한쌍으로 존재한다는 특징이 있다.

    - Key를 사용하여 특정 데이터의 Value를 매우 빠르게 찾을 수 있다는 장점이 있다.

    - 단, 데이터를 얻기 위해서는 반드시 Key를 사용하여야 하며 반대로 Value만 알고 있는 경우 해당 Value값에 대한 Key값은 알 수 없다.

    - 딕셔너리 값 할당

 

    - 딕셔너리 값 사용

 

    - 딕셔너리 값 삭제

 

    - 딕셔너리의 구조 : HashTable

: HashTable에 대한 예시
ex) 아주 큰 박스에 공이 들어있는 경우 [ ......... ] 1만개(1~1만개의 데이터 존재)
특정 공을 찾기 위해서 공을 하나씩 꺼내보는 것이 아니라,
아주 큰 박스 하나가 아니라 박스를 여러개(1천개쯤)로 쪼개어
1에서 1만 숫자 사이의 데이터를 잘 배분하여 10개씩 집어넣는다.
[11~20] [] [] [] [] -> 이렇게 1천개의 상자 만듦 -> 찾기가 더 수월함!
777번 공을 찾고자 한다 -> 10개씩 나눴으므로 777번째 상자에 공이 있음을 알 수 있음.
리스트를 하나씩 순회하는 것보다 훨씬 데이터 찾기가 수월해지지만,
메모리 상으로는 여러개의 박스를 준비해야하므로 큰 손해를 보게 된다.
결국 해시테이블을 사용하는 방법은 메모리를 내주고 성능을 취하는 방법이라고 할 수 있다.

 

+) 딕셔너리와 HashTable에 대해서는 전에도 공부한적이 있기 때문에 그 때의 자료를 추가하여 별도의 포스팅으로 남기는 것이 좋겠다.

'C#' 카테고리의 다른 글

220817 Interface (인터페이스)  (0) 2022.08.17
[C#] 일반화(Generic)  (0) 2022.08.16
220718 C# 연습문제 풀이  (0) 2022.07.18
[C#] 문법 정리  (0) 2022.07.17
220716 C# 람다식  (0) 2022.07.16