본문 바로가기

Unity 개발 공부

[내배캠] 본캠 8일차. 인터페이스, 열거형, 예외처리, 참조형, 값형, 델리게이트, 람다, LINQ

다중상속 다이아몬드 문제

클래스와 객체

캡슐화:
관련된 데이터와 기능을 하나의 단위로 묶는것을 의미.
정보를 은닉하고, 외부에서 직접적인 접근 제한, 안정성 유지보수성 높임


상속:
기존의 클래스를 확장하여 새로운 클래스를 만듬

예를들어
사람아래에
직장인, 학생으로 확장가능하다.

코드 중복 줄이고
유지보수 용이함

다형성:

하나의 메서드 이름이 다양한 객체에서 다르게 동작하도록 하는것으로 
오버로딩과 오버라이딩을 통해 구현

추상화:

복잡한 시스템을 단순화하고 기능에 집중하는것

세부구현을 감추고 개념에 집중하는것. 세부구현은 차후에 하는걸로 미뤄주는것이다.


객체:

데이터와 메서드를 가지며 상호작용하고 프로그램이 동작하는것
모듈화 재사용성 높임

-----------------
클래스구성요소:

필드 : 클래스에서 사용되는 변수, 객체의 상태를 나타내는 데이터 저장용

메서드 : 클래스에서 수행되는 동작이 정의됌, 객체 동작 구현을 위해 사용

생성자 : 객체를 초기화하는 역할. 객체 생성시 자동으로 호출되며, 필드를 초기화하는 등의 작업 수행

소멸자 : 객체가 소멸될때 호출되는 메서드, 메모리나 리소스의 해제등의 작업을 수행함.

----------

클래스는 객체를 생성하기위한 템플릿, 설계도 역할을함
객체를 생성하기위해서 클래스를 사용하여 인스턴스를 만들어야함

붕어빵틀로 비유하면 클래스는 붕어빵을 만들기위한 틀이라고함.

----------
객체는 클래스의 인스턴스로 클래스가 실체화된 형태라고할 수 있다.

객체는 클래스로부터 생성되고, 각 객체는 독립적인 상태다.

붕어빵 틀로 비유하면 객체는 붕어빵이라고할수잇다.

-----------

클래스 선언과 인스턴스

클래스는 사용자 정의타입이다. 구조체와 같으면서 다른데, 

        class Person
        {
            public string Name;
            public int Age;

            public void PrintInfo()
            {
                Console.WriteLine("Name: " + Name);
                Console.WriteLine("Age: " + Age);
            }
        }

        static void Main(string[] args)
        {
            Person p = new Person();// 구조체에서는 그냥 Person p; 하면 int a와 동일하게 변수가 생성되었었음
            //하지만 클래스는 레퍼런스 타입이다. Person p는 공간이 실제 잡힌게아님
            //Person의 주소를 저장할수있는 p가 있음, new는 따른공간에 Person을 생성하고 p라는곳에 연결해놨다라는 뜻임
           // 즉, 객체화 해줘야 쓸수있다.
p.Name = "John";
            p.Age = 30;
            p.PrintInfo(); // 출력: Name: John, Age: 30
        }

-------------
구조체 vs 클래스

구조체와 클래스 모두 사용자 정의 형식을 만드는데 사용할 수 있다. 원하는 기능들을 뭉쳐놓은 상태다.

구조체는 값 형식이며, 메모리의 스택에 할당되고 복사될때 값이 복사된다. 
클래스는 참조형식이며, 메모리의 힙에 할당되고 참조로 전달되면서 동적할당된다. 

구조체는 상속받을 수 없다. 
클래스는 단일 상속, 다중 상속이 가능하다.

구조체는 작은 크기의 데이터 저장이나 단순한 데이터 구조에 적합
클래스는 더 복잡한 객체를 표현하고 다양한 기능을 제공하기 위해 사용
---------------------
메모리의 스택영역과 힙영역 차이

스택영역
1. 자동할당, 스택은 함수호출시 생성되는 지역변수나 매개변수같은 일시적인 데이터를 저장하는 메모리 영역이다.
이 영역은 함수가 호출될때 할당되고, 함수가 종료되면 자동으로 해제된다.
2. 빠른 접근 및 관리: 스택은 메모리 할당과 해제가 매우 빠르고, 포인터 연산이나 간단한 주소 증감연산으로
관리된다.
3. 메모리 크기제한: 일반적으로 제한된 크기를 가지고, 너무 많은 데이터를 할당하면 스택 오버플로우가 발생한다.
4.구조체는 값 타입으로 간주되어 스택영역에 저장된다. 값타입은 변수에 직접 저장되어 할당시마다 복사된다.

힙영역
1. 동적할당, 힙은 프로그램 실행중에 동적으로 메모리를 할당할때 사용되는 영역이고, 개발자가 필요에 따라 메모리를
할당하고 해제한다. 이 메모리는 가비지컬렉션이나 명시적인 해제에 의해 관리된다.
2.유연한 할당: 필요한 만큼 메모리를 동적으로 할당할 수 있어, 크기가 변동되는 데이터나 객체에 적합하다.
하지만 할당과 해제시 오버헤드가 있고 , 메모리 단편화 문제가 발생가능하다.
오버헤드란 간단하게말해 단순한 스택할당이아닌 힙영역의 할당은 요청,기록, 추가연산이 이루어지고 cpu사이클이나 시간
을 소비하게되어 프로그램 실행속도에 영향을 주는 것을 말하며,
메모리 단편화문제란 할당과 해제작업을 반복하는 과정에서 사용 가능한 메모리가 여러개의 작은 조각으로
나뉘어 버리는 현상을 말한다.
2-1 외부 단편화(External Fragmentation):
전체 사용할 수 있는 메모리의 총 크기는 충분하더라도, 연속된 큰 메모리 블록을 찾기 어려울 수 있습니다.
예를 들어, 메모리 전체가 100MB여도, 연속된 50MB의 큰 블록이 없다면 50MB 크기의 데이터를 할당받지 못할 수 있습니다.

2-2 내부 단편화(Internal Fragmentation):
메모리를 할당할 때, 요청된 크기보다 조금 더 큰 블록이 할당되면서 실제 사용하지 않는 낭비된 메모리가 발생할 수 있습니다.

3. 클래스 인스턴스가 힙에 할당된다. 이 경우, 스택에는 객체를 참조하는 포인터(또는 참조값)만 남게되고, 실제
데이터는 힙에 존재한다.
4. 구조체가 다른 클래스나 구조체 안에 포함되어있다면 해당 객체의 멤버 변수로서 힙에 함께 할당될 수 있다.
----------
포인터
포인터는 다른 변수의 메모리 주소를 저장하는 변수이다. 
예를 들어 어떤 정수형 변수의 주소를 저장할때 정수형 포인터를 사용한다.

컴퓨터의 메모리는 여러개의 셀로 구성되는데, 각 셀은 고유한 주소를 갖고있다.
포인터는 이 주소를 값으로 가지게 된다.

int num = 10;          // 정수 변수 num 선언 및 초기화
int *ptr = #       // ptr은 num의 주소를 저장하는 포인터

여기서 &num은 변수 num의 메모리 주소를 의미하고, ptr은 해당 주소를 저장하는 포인터다.

선언:
포인터는 데이터 타입과 함께 선언해야한다. 예를들어 int *는 정수형 데이터를 가리키는 포인터를 나타내고
char *, float * 등도 존재한다.

초기화:
포인터는 사용전에 반드시 유효한 메모리 주소로 초기화 해야한다.
그렇지 않으면 Dangling Pointer(죽은포인터) Null Pointer를 사용하여 프로그램 오류를 발생시킨다.

int value = 20;
int *pValue = &value;  // 올바른 초기화
int *pNull = NULL;     // NULL로 초기화하여 아직 유효한 주소가 없음 표시

C#은 가비지 컬렉션등 기본적으로 메모리가 관리되는 언어라 일반적인 상황에서는 포인터 사용이 거의 필요하지않다.
대신 "참조"를 통해 대부분의 작업을 수행한다
포인터를 사용해야하는 상황(성능 최적화나 하드웨어관련작업)이 있다면 unsafe라는 특별한 코드블록내에서
포인터를 사용 할 수는 있다.


---------
접근제한자

public: 외부에서 자유롭게 접근가능

private: 같은 클래스 내부에서만 접근 가능하다.

protected: 같은 클래스 내부와 상속받은 클래스에서만 접근 가능하다.

class Person
{
    public string Name;         // 외부에서 자유롭게 접근 가능
    private int Age;           // 같은 클래스 내부에서만 접근 가능
    protected string Address;  // 같은 클래스 내부와 상속받은 클래스에서만 접근 가능
}

-------
필드와 메서드

클래스의 구성요소중

1. 필드: 클래스나 구조체 내의 객체의 상태를 저장하는 변수
객체의 특징이나 속성을 표현하며 멤버변수로 선언되고 보통 private으로 선언하여
필요한 경우 프로퍼티나 함수로 간접적으로 접근하게한다.

2. 메서드:
클래스나 구조체에서 동작을 정의하는 함수
입력값을 받아 처리하고 ,결과값을 받아 반환하기도한다.
보통 public으로 만들어 외부에서 호출가능하게 만든다.
class Player
{
    // 필드
    private string name;
    private int level;

    // 메서드
    public void Attack()
    {
        // 공격 동작 구현
    }
}

Player player = new Player();  // Player 클래스의 인스턴스 생성, new Player() 부분이 실제로 생성되는 인스턴스다.
player.Attack();  // Attack 메서드 호출


---------------

생성자와 소멸자

1. 생성자는 객체가 생성될때 호출되는 특별한메서드
객체를 초기화하고 필요한 초기값을 설정하는 역할을 수행한다.
클래스와 동일한 이름을 가지며 반환타입이 없다.
객체를 생성할때 new 키워드와 함께 호출된다.

2. 생성자는 객체를 초기화하는 과정에서 필요한 작업을 수행할 수 있다.
생성자는 여러개 정의할 수 있으며 매개변수의 개수와 타입에 따라 다른 생성자를 호출 할 수있고
이것을 생성자 오버로딩이라고한다.
기본적으로 구현하지않았다면 매개변수가 없는 디폴트 생성자가 자동으로 생성된다.
하지만 하나라도 우리가 만든다면 디폴트 생성자는 만들어지지않는다.

        class Person
        {
            public string Name;
            public int Age;

            public void PrintInfo()
            {
                Console.WriteLine("Name: " + Name);
                Console.WriteLine("Age: " + Age);
            }
        }
        class Person2
        {
            private string name;
            private int age;

            public Person2() // 디폴트 생성자 처음에 자동으로 생성되서 안보인다. 반환형은 안쓰며 이름은 클래스랑 같다.
            {
                // 아무것도 안하고 아무것도 안받고 아무것도 반환안함
                
            }

            public Person2( string newName, int newAge) // 디폴트 생성자가 아닌 오버로딩상태
                //매개변수가 생겻다. 나중에 new로 객체를 생성할때 매개변수가 있는 상태로 생성자를
                //만들어놓았다면, 디폴트 생성자처럼 new Person2 (); 할때 소괄호안을 공란으로 인스턴스
                //생성은 불가능하다. 이를 가능케하려면 자동으로 생성되는것처럼 디폴트 생성자를 따로
                //위에 써줘야한다. 현재는 위에 써져있다.
            {
                name = newName;
                age = newAge;
            }

            public void PrintInfo()
            {
                Console.WriteLine($"Name: {name}, Age: {age}"); 
            }
        }



        Person person1 = new Person();



        static void Main(string[] args)
        {
            Person p = new Person();// 구조체에서는 그냥 Person p; 하면 int a와 동일하게 변수가 생성되었었음
            //하지만 클래스는 레퍼런스 타입이다. Person p는 공간이 실제 잡힌게아님
            //Person의 주소를 저장할수있는 p가 있음, new는 따른공간에 Person을 생성하고 p라는곳에 연결해놨다라는 뜻임
            p.Name = "John";
            p.Age = 30;
            p.PrintInfo(); // 출력: Name: John, Age: 30

            Person2 person1 = new Person2();
            Person2 person2 = new Person2("John", 25); // 생성자란 객체가 생성될때 호출한다.
        }

3. 소멸자

소멸자란 객체가 소멸되는 시점에서 자동으로 호출되는 특별한 메서드다
클래스와 동일한 이름을 가지며, 이름 앞에 ~기호를 붙여 표현한다.
C#에서는 가비지컬렉터에 의해 관리되는 메모리 해제를 담당하므로, 명시적으로 소멸자를 호출하는 것은
일반적으로 권장되지않는다.

소멸자의 역할: 파일핸들, 네트워크 연결, 데이터베이스 연결등 외부 리소스를 사용한 경우,
사용할 수 있다.

오버로딩이 불가능한데, 왜냐면 우리가 호출한것이 아님, 우리가 컨트롤하는게아니다.

class Person
{
    private string name;

    public Person(string newName)
    {
        name = newName;
        Console.WriteLine("Person 객체 생성");
    }

    ~Person()
    {
        Console.WriteLine("Person 객체 소멸");
    }
}

-------------------
프로퍼티

private 멤버등을 외부에서도 접근가능하게 하는 통로 역할을 한다.
데이터 유효성 검사, 접근제어를 가능하게한다.


[접근제한자] [데이터 타입] 프로퍼티명
{
get
{
// return을 해줘야함, 필드를 반환하거나 다른 로직수행
}

set
{
//필드의 값을 설정하거나 다른 로직 수행. value라는 값으로 대입하여 받아온다.
}


class Person
{
    private string name;
    private int age;

    public string Name // 대문자로 프로퍼티 이름 명명, 필드 데이터타입 확인해서 앞에 자료형 작성
    {
        get { return name; } // 필드인 name을 반환해줌
        set { name = value; } // 필드인 name에 값을 할당해줌
    }

    public int Age
    {
        get { return age; }
        set { age = value; }
    }
}

Person person = new Person();
person.Name = "John";   // Name 프로퍼티에 값 설정, "John"이라는  string 값을 set을 통해 할당
person.Age = 25;        // Age 프로퍼티에 값 설정, 25라는 int 값을 set을 통해 할당

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); 
 // .(닷)을 이용해 클래스 person의 Name프로퍼티에 접근해서 get을 통해 값을 꺼내온다. John을 꺼내오는것

---------
프로퍼티 접근제한자 적용 & 유효성 검사 예제

class Person
{
    private string name;
    private int age;

    public string Name
    {
        get { return name; } // 값을 반환은 public으로 가능하다
        private set { name = value; } // 값을 할당하는것은 class 내부에서만 하겠다.
    }

    public int Age
    {
        get { return age; }
        set // 값을 할당하는 것은 value >=0 일때만 age 필드에 하겠다.
        {
            if (value >= 0)
                age = value;
        }
    }
}

Person person = new Person();
person.Name = "John";     // 컴파일 오류: Name 프로퍼티의 set 접근자는 private입니다.
person.Age = -10;         // 유효성 검사에 의해 나이 값이 설정되지 않습니다.

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // Name과 Age 프로퍼티에 접근하여 값을 출력합니다.


--------
자동 프로퍼티
자동 프러포터리란 필드의 선언과 접근자 메서드의 구현을 컴파일러가 자동으로 처리하여 개발자가 간단한 구문으로
프로퍼티를 정의할 수 있다.
프로퍼티가 필드의 역할도 같이 진행해버린다.
[접근 제한자] [데이터 타입] 프로퍼티명 { get; set; }

그예시로

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Person person = new Person();
person.Name = "John";     // 값을 설정
person.Age = 25;          // 값을 설정

Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");  // 값을 읽어 출력

-----------

상속

상속은 기존의 클래스(부모클래스 또는 상위클래스)를 확장하거나 재사용하여 새로운
클래스(자식클래스 또는 하위클래스)를 생성하는것을 말한다.

자식 클래스는 부모클래스의 멤버(필드, 메서드, 프로퍼티 등)을 상속 받아 사용할 수 있다.
상속을 통해 기능 확장, 수정 가능한 새로운 클래스를 정의한다.

장점: 코드를 재사용, 계층구조로 코드 표현, 유지보수성 향상

상속종류
1. 단일상속
하나의 자식 클래스가 하나의 부모 클래스만 상속받는다. c#에서는 단일 상속만을 지원한다.
2. 다중상속
c#에서는 지원안함

3. 인터페이스 상속
c#에서는 인터페이스를 다양한 클래스가 상속받을수있다.


상속의 특징
1. 부모클래스의 멤버에 접근가능

2. 메서드 재정의 가능: 자식 클래스는 부모 클래스의 메서드를 재정의하여 자신에게 맞게 수정가능

3. 상속의 깊이: 다수의 계층적인 상속 구조를 가질 수 있다.
할아버지 - 아버지 - 아들 - 손자 ...

상속의 깊이가 깊어질수록 클래스 간의 관계가 복잡해질 수 잇어 적절한 상속의 깊이를 유지해야한다.

---------
접근제한자와 상속
상속관계에서 멤버의 접근제한자는 중요한 역할을한다.


예제

        public class Animal
        {
            public string Name { get; set; }
            public int Age { get; set; }

            public void Eat()
            {
                Console.WriteLine("Animal is eating.");
            }

            public void Sleep()
            {
                Console.WriteLine("Animal is sleeping.");
            }
        }

        // 자식 클래스
        public class Dog : Animal 
        {
            public void Bark()
            {
                Console.WriteLine("Dog is barking");
            }
        }

        public class Cat : Animal
        {
            public void Meow()
            {
                Console.WriteLine("Cat is meow");
            }

            public void Sleep()
            {
                Console.WriteLine("Cat sleep");
            }
        }
        static void Main(string[] args)
        {
            Dog dog = new Dog();
            dog.Name = "Bobby"; //Animal을 상속받았기때문에 가능
            dog.Age = 3; //Animal을 상속받았기때문에 가능

            dog.Eat(); //Animal을 상속받았기때문에 가능
            dog.Bark();
            dog.Sleep(); //Animal을 상속받았기때문에 가능

            Cat cat = new Cat();
            cat.Name = "kkami";
            cat.Age = 10;

            cat.Eat();
            cat.Sleep(); // 부모의 sleep(); 말고 cat의 sleep();이 더가깝다 이걸 사용함, 부모것은 숨김
            cat.Meow();
        }


물론 위와 같이 상속을 숨기며 받는 방법이,
다형성을 유지하는 좋은 방법은아니다

--------
다형성

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

1. 가상메서드를 사용해본다.
기본적으로 부모클래스에서 정의되고 자식클래스에서 재정의되는것

가상메서드는 virtual 키워드를 사용하여 선언되며 자식 클래스에서 필요에 따라 재정의 될 수 있다.
이 말은 꼭 재정의 될 필요는 없다는 말이기도하다.

아래 예제를 보자.

        public class Unit
        {
            public virtual void Move() // 가상메서드로 선언하면 자식들이 재정의를 했을 수 있다.
            {
                Console.WriteLine("두발로 걷기");
            } // virtual이면 이부분은 참조인데 객체를 생성한 클래스부분의 실형태가 다를수있으니 실형태에 재정의가 되어있는지 확인해봐라는 뜻.

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

        public class Marine : Unit
        {

        } // 실형태를 override 하지않았네?

        public class Zergling : Unit
        {
            public override void Move() // virtual을 여기서 override로 재정의했다.
            {
                Console.WriteLine("네발로 걷기");
            }

        }
        static void Main(string[] args)
        {
            //Marine marine = new Marine();
            //marine.Move();
            //marine.Attack();

            //Zergling zergling = new Zergling();
            //zergling.Move();
            //zergling.Attack();

            //저글링 100마리, 마린 100마리 전부 하기엔 너무 복잡해지니
            //Unit이라는 클래스로 관리를하자
            //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, override 없이 실행시키면 두발로 걷기만 실행된다.
                             // 마린이나 저글링이나 Unit에 해당하는 부분은 동일하기때문에 이부분을 참조하고
                             // 이 Unit 부분에서 Move();를 찾으면 저글링안에 새로 함수를 작성하여도 Unit에 있는 함수가 동작한다
                             // 이때 가상메서드를 사용한다. 부모는 virtual, 자식은 override로 함수 반환형 앞에 작성
                             // Marine() 클래스는 재정의 안했고, Zergling은 재정의햇으므로 재정의한 Move(); 함수 사용

            }
        }


정리하자면, 다양한 유닛 클래스를 Unit(부모) 참조의 형태로 관리를 할때는, Move(); 함수를 호출하면 
재정의 되어있는 경우엔 재정의 된 것을 쓰고 아니라면 부모의 것을 쓰는것이다.

----------
비가상 메서드를 사용할시 다형성부분에서 
실제 게임에서 적용할때 문제가 되는 더 자세한 예제도 있다.

using System;
using System.Collections.Generic;

namespace GameExample_NonVirtual
{
    // 기본 적 클래스 (비가상 메서드 사용)
    class Enemy
    {
        // 기본적인 피해 계산: 그대로 피해를 받음
        public void TakeDamage(int damage)
        {
            Console.WriteLine("Enemy takes " + damage + " damage.");
        }
    }

    // 고블린: 피해를 회피하는 특수 효과
    class Goblin : Enemy
    {
        // 같은 이름의 메서드를 새로 정의하지만, 이는 숨김(hiding) 처리됨
        public new void TakeDamage(int damage)
        {
            int reducedDamage = Math.Max(0, damage - 2);
            Console.WriteLine("Goblin dodges! It takes " + reducedDamage + " damage.");
        }
    }

    // 트롤: 피해를 절반만 받음 (내구성이 좋음)
    class Troll : Enemy
    {
        // Troll 전용 피해 계산
        public new void TakeDamage(int damage)
        {
            int reducedDamage = damage / 2;
            Console.WriteLine("Troll is resilient! It takes " + reducedDamage + " damage.");
        }
    }

    class DamageSystem
    {
        // 여러 적에게 피해를 적용하는 시스템
        public void ApplyDamage(List<Enemy> enemies, int damage)
        {
            foreach (Enemy enemy in enemies)
            {
                // 여기서 enemy의 참조형은 Enemy지만 실제 객체는 Goblin이나 Troll일 수 있음
                // 비가상 메서드의 경우, 컴파일 타임에 결정되어 기본 클래스의 메서드만 호출됨
                enemy.TakeDamage(damage);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 리스트에 Enemy 타입으로 여러 적들을 저장
            List<Enemy> enemies = new List<Enemy>
            {
                new Goblin(),  // 실제로는 Goblin 타입이지만, List에는 Enemy로 저장됨
                new Troll() // enemies.Add(new Troll()); 와 같은 초기화작업을 한번에 해버린다.
            };

            DamageSystem damageSystem = new DamageSystem(); // damageSystem이라는 객체 생성
            damageSystem.ApplyDamage(enemies, 10); // 클래스의 함수 읽어오기

            // 결과:
            // Enemy takes 10 damage.
            // Enemy takes 10 damage.
        }
    }
}

핵심 문제:
여기서는 Goblin과 Troll 모두 자신들만의 TakeDamage 메서드를 선언했지만, 이들은 new 키워드를 사용하여 부모 클래스의 메서드를 **숨기기(hiding)**만 합니다.
그런데, DamageSystem에서는 적들을 모두 Enemy 타입으로 다루기 때문에, 컴파일러는 호출할 메서드를 Enemy 클래스의 TakeDamage로 결정합니다.
결과적으로, 실제로는 Goblin과 Troll이 고유의 피해 계산 로직을 가지고 있음에도 불구하고, 모두 기본 Enemy의 로직(그냥 10의 피해)을 실행하게 됩니다.

문제점 발생:
게임에서는 각 적마다 고유의 반응(예: 고블린은 일부 피해를 회피, 트롤은 피해를 반으로 줄임)이 있어야 하는데, 
비가상 메서드를 사용하면 이런 다형성이 구현되지 않아 기대했던 효과가 나타나지 않습니다. 


다시 가상메서드를 사용하면

using System;
using System.Collections.Generic;

namespace GameExample_Virtual
{
    // 기본 적 클래스 (가상 메서드 사용)
    class Enemy
    {
        // 가상 메서드로 선언하여 파생 클래스에서 재정의할 수 있도록 함
        public virtual void TakeDamage(int damage)
        {
            Console.WriteLine("Enemy takes " + damage + " damage.");
        }
    }

    // 고블린: 피해 회피 효과 적용
    class Goblin : Enemy
    {
        // 부모의 가상 메서드를 재정의
        public override void TakeDamage(int damage)
        {
            int reducedDamage = Math.Max(0, damage - 2);
            Console.WriteLine("Goblin dodges! It takes " + reducedDamage + " damage.");
        }
    }

    // 트롤: 피해 절반 감소 효과 적용
    class Troll : Enemy
    {
        // 부모의 가상 메서드를 재정의
        public override void TakeDamage(int damage)
        {
            int reducedDamage = damage / 2;
            Console.WriteLine("Troll is resilient! It takes " + reducedDamage + " damage.");
        }
    }

    class DamageSystem
    {
        // 여러 적에게 피해를 적용하는 시스템
        public void ApplyDamage(List<Enemy> enemies, int damage)
        {
            foreach (Enemy enemy in enemies)
            {
                // 동적 바인딩에 의해 실제 객체의 타입에 맞는 메서드가 호출됨
                enemy.TakeDamage(damage);
            }
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            // 리스트에 Enemy 타입으로 여러 적들을 저장
            List<Enemy> enemies = new List<Enemy>
            {
                new Goblin(),
                new Troll()
            };

            DamageSystem damageSystem = new DamageSystem();
            damageSystem.ApplyDamage(enemies, 10);

            // 결과:
            // Goblin dodges! It takes 8 damage.
            // Troll is resilient! It takes 5 damage.
        }
    }
}

가상 메서드와 재정의:
Enemy 클래스의 TakeDamage 메서드를 virtual로 선언하고, Goblin과 Troll에서는 override를 사용하여 각각의 고유한 로직을 구현합니다.

동적 바인딩(dynamic binding):
리스트는 여전히 Enemy 타입의 참조를 사용하지만, ApplyDamage를 호출하면 런타임에 실제 객체 타입(Goblin, Troll)에 맞는 TakeDamage 메서드가 실행됩니다.
이를 통해 각 적은 자신만의 특화된 피해 계산 방식대로 반응하게 됩니다.

게임 개발에서의 중요성:
게임 개발에서는 다양한 적, 캐릭터, 스킬 등이 동일한 인터페이스를 공유하면서 서로 다르게 동작해야 하는 경우가 많습니다.
가상 메서드를 사용하면 데이터 구조나 이벤트 처리 시스템에서 기본 클래스 타입의 참조를 통해 객체를 관리할 때에도, 각 객체의 고유한 행동을 정확하게 실행할 수 있어 
드의 유지보수성과 확장성이 크게 향상됩니다.

----------------

추상클래스

추상클래스는 직접적으로 인스턴스를 생성할 수 없는 클래스.
주로 상속을 위한 베이스 클래스로 사용된다.

추상 클래스는 추상 메서드를 포함 할 수 있다. abstract 키워드를 사용하며 선언한다. 구현은안한다.
이걸 상속받은 자식클래스에서 무조건 구현해야한다.
상속 받은 자식클래스는 무조건 이 추상 메서드를 구현했을거라는 확답을 받을 수 있다.


            abstract class Shape
            {
                public abstract void Draw();
            }

            class Circle : Shape // 상속받는 순간 Draw() 라는 메서드를 무조건 구현해야함
            {
                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)
            {
                // Shape shape = new Shape(); // 추상형식의 클래스는 객체 생성불가
                List<Shape> list = new List<Shape>()
               { new Circle(), new Square(),new Triangle() };

                foreach(Shape shape in list)
                {
                    shape.Draw();
                }
            }
      
-------------
오버라이딩 과 오버로딩 헷갈리지말자

오버라이딩은 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 재정의하는것을 의미
이는 상속관계가 있는 클래스간에 발생, 메서드의 이름, 매개변수 및 반환 타입이 동일해야함.

오버라이딩을 통해 자식 클래스는 부모 클래스의 메서드를 재정의하여 자신에게 맞는 동작 구현가능

오버로딩은 똑같은 이름의 메서드인데 매개변수의 개수, 타입 또는 순서가 다른 것임
public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public int Add(int a, int b, int c)
    {
        return a + b + c;
    }
}

Calculator calc = new Calculator();
int result1 = calc.Add(2, 3);         // 5
int result2 = calc.Add(2, 3, 4);      // 9

---------------------

고급 문법 및 기능


c#의 제너릭, out, ref 키워드


제너릭: 클래스나 메서드를 일반화 시켜 다양한 자료형에 대응할 수 있는 기능
제너릭을 사용하면 코드의 재사용성을 높일 수 있습니다.

<T>형태의 키워드를 이용하여 제너릭을 선언한다.
사용할때는 <T> 대신 구체적인 자료형을 넣어준다.

        class Stack<T> // 선입 후출이므로 저장공간필요함 <T> 타입의 Stack이라는 클래스 선언함
        {
            private T[] elements; // T 타입 자료형의 elements 배열을 선언, 헷갈린다면 int [] elements = new int[3]; 식을 생각
            private int top; // int 형 변수 top 선언

            public Stack() // 반환형 없는 함수식이므로 생성자다, 객체 생성시 호출되며 값들을 초기화해주는 녀석
            {
                elements = new T[100]; // 생성자안에 T타입 elements 배열을 new로 100개짜리 배열로 생성함, T[] elements = new T[100];
                top = 0; // top을 0으로 초기화
            }

            public void Push(T item) // 반환하지않는 Push 메서드로 매개변수 T타입의 item 변수를 인자로 넣을 수있다.
            {
                elements[top++] = item; // 후위증감, 즉 top에 0이 대치되면, elements[0]은 집어넣은 item 변수이고, top을 1증감해서 1이된다.
            }

            public T Pop() // T 자료형의 Pop 메서드 , T타입을 반환한다.
            {
                return elements[--top]; // T타입의 elements 배열의 요소를 반환한다, 전위증감형이라 Top이 1이면, 미리 1 빼서 elements[0]을 반환한다.
            }

        }

     
        static void Main(string[] args)
        {
            Stack<int> intStack = new Stack<int>(); // Stack<T> 의 자료형 타입을 int로 하여 컬렉션 객체 생성, 비슷한 걸로 List<int> = new List<int>(); 같은 컬렉션
            intStack.Push(1); // 만든 객체 intStack에서 Push(1) 메서드 실행 하는데 item에 1을 넣음 1은 elements[0]에 있는데 여하튼 top은 1로 바뀜
            intStack.Push(2); // 2 top 2
            intStack.Push(3); // 3 top 3
            Console.WriteLine(intStack.Pop()); // pop()을 실행하면 element[--top] 을 반환하는데, 여기서 미리 -1 감소해서 3이 아닌 element[2]를 반환해줌
      // 즉, element[2]는 3이므로 3을 출력한다.
        }

실제로 리스트, 딕셔너리, 스택이 이런식으로 구현되어있다.

----------

제너릭을 두개 이상 사용하는 예도 있다.
T로 구분해주는데 꼭 T가 아니어도 되지만 T1, T2로 만들어서 사용해볼 수 있다.

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과 ref 키워드

보통은 매개변수를 줘서 반환값으로 받았는데
직접적으로 매개변수로 받아올 수 있게하는 키워드들이다.

out 키워드는 메서드에서 반환값을 매개변수로 전달하는 경우에 사용
ref 키워드는 메서드에서 매개변수를 수정하여 원래 값에 영향을 주는 경우에 사용.

out, ref 키워드를 사용하면 메서드에서 값을 반환하는것이 아니라, 매개변수를 이용하여 값을 전달 할 수 있음.

        // out 키워드 사용 예시
        static void Divide(int a, int b, out int quotient, out int remainder)
        {// out이 있는애들은 무조건 값이 바뀌어서 돌아온다고 할 수 있음. 할당해서 초기화해야하기때문
            quotient = a / b;
            remainder = a % b;
        }

        // ref 키워드 사용 예시
        static void Swap(ref int a, ref int b)
        {

        }



        static void Main(string[] args)
        {
            int quotient, remainder;
            Divide(7, 3, out quotient, out remainder); // 7,3이 복사해서 a와 b에 들어간다,
                                                       // 그런데 out 인자가 있는애들은 바깥의 선언된 인자를 직접적으로 만진다.

            Console.WriteLine($"{quotient}, {remainder}");
        }
        
보는것과 같이 static void Divide( ) 소괄호 안에는 out 인자가 두개 매개변수로 들어가게 만들어졌고

main 함수에서 
선언된 
이 인자들과 같다. 그뜻은 결국 Divide() 함수는 out 인자를 매개변수로 받기때문에
무조건 내용물에 인자를 할당해줘야만하고 main함수에서 선언된 이 인자들의 값은 바뀐다고 보면된다.


그다음은 ref의 사용예시다.
       static void Swap(ref int a, ref int b)
       {
           int temp = a;
           a = b;
           b = temp;
       }

        static void Main(string[] args)
        {
            int x = 1, y = 2;
            Swap(ref x, ref y);
            Console.WriteLine($"{x},{y}");
        }
        
위의 함수 Swap()의 경우 인자들을 ref 로받아 이러면 temp에 a값을 임시보관하고, a 에 b의 값이 대입되고
b에는 임시보관한 a값을 꺼내어 대입한다.
그러면서 호출한 값들에도 영향을 준다.

따라서 x =2 가되고 y =1이 된다.

참조형태가아닌 값형인 변수애들도 반환값을 내지않고 호출쪽으로 값을 내보내 바꿔줄수있는것이다.
ref는 사용할지않할지 모르는 상태. out 처럼 반드시 사용해야하는것과는 좀 다르다.


주의사항
ref 매개변수는 사용하면 메서드 내에서 해당 변수의 값을 직접 변경하는데,
이는 예기치 않는 동작을 초래할 수 있다.

성능이슈도 있다. ref자체는 값을 복사하는게아니니 성능상 더 빠르긴하다.
너무 많은 매개변수를 ref로 전달하면 코드 가독성이 떨어지고 유지보수가 어려워진다.
적절한 상황에서 ref를 사용하는것이 좋다.

----------
인터페이스 

다중상속을 사용하지않는이유:
다이아몬드 문제 - 한클래스가 두개 이상의 부모 클래스로부터 동일한 멤버를 상속받을 수 있음.
이 경우 어떤 부모 클랫의 멤버를 사용해야할지 모호해지고, 규칙을 더해줘야해서 코드가 복잡해지고 가독성저하

설계의 복잡성 증가 하며, 이름 충돌과 충돌해결이 어려워진다.

따라서 C#은 단일 상속을 통해 설계의 일관성 단순성 유지한다.

------------
인터페이스 사용이유

1.코드의 재사용성 -

2.다중상속제공 -

3.유연한 설계 -

인터페이스의 특징: 
클래스가 구현해야하는 멤버들을 정의한다.
클래스의 일종은 아니며, 클래스에 대한 제약조건을 명시하는것
인터페이스 구현시, 모든 인터페이스의 멤버를 구현해야함.
인터페이스는 다중상속을 지원함

인터페이스 구현:

멤버정의
interface IMyInterface // 앞에 inferface 키워드 쓰고 I로 주로 시작한다.
{
void Method();
int Method2(string str);
}

상속받으면 처음엔 에러가 뜸. 인터페이스의 멤버를 구현하지않았기때문에 바로 구현해줘야함

class MyCass : IMyInterface
{

  public void Method()
  {

  }

  public int Method2(string str)
  {

   return 0;
  }

}

사용 예제

        public interface IMovable
        {
            void Move(int x, int y);
        }


        public class Player :IMovable
        {
            public void Move(int x, int y) 
            {
                //이동구현
            }
        }

        public class Enemy : IMovable
        {
            public void Move(int x, int y)
            {
                //이동구현
            }

        }

        static void Main(string[] args)
        {
            IMovable movableObject1 = new Player();
            IMovable movableObject2 = new Enemy();

            movableObject1.Move(1,2);
            movableObject2.Move(1,9);
        }

Move를 가지고있는 인터페이스를 통해
통합이 가능하다. 별개의 이동 코드를 구현한다하더라도 하나로 묶어서 이용할 수 있는것이다.
반드시 Move(); 메서드는 구현되어야한다.


사용예제2

        public interface IUsable
        {
            void Use();

        }

        public class Item : IUsable
        {
            public string Name { get; set; }

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

        public class Player
        {
            public void UseItem(IUsable item)
            {
                item.Use();
            }
        }


        static void Main(string[] args)
        {
            Player player = new Player();
            Item item = new Item() { Name = "Health Position"}; // 초기화를 위해 값을 세팅해준것
            player.UseItem(item);
        }

사용해야하는 아이템의 종류가 많을텐데, 이걸 하나의 클래스로 구현하는게아닐것.
IUsable로 상속받아서 구현하면 Player.UseItem() 코드 하나로 전부 사용할 수 있엇지는것.

여기서  Item item = new Item() { Name = "Health Position"};   구문은 객체 생성과 동시에
객체 초기화 구문을 사용한것인데, 유연성 간결성에 있어서 좋다.

클래스 내부에 하드코딩하여 Name값을 고정하게되면 유연성이 부족하고 재사용성이 제한 받을 수 있다.
구렇기에 구현과 사용을 분리하는게 좋다.

만약 객체 생성시에 값을 반드시 주입받아야 한다면, 생성자를 자동으로 생성시키는게 아니라, 매개변수가 있는
생성자를 만들어준다. 단, 이때는 객체생성시에 생성자와 같이, 소괄호안에 인자를 넣어줘야만 한다.

public class Item : IUsable
{
    public string Name { get; set; }

    // 생성자를 통해 Name 초기화
    public Item(string name)
    {
        Name = name; 
    }

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

그리고

Item item = new Item("Health Position");
처럼 작성가능하다.
Item item = new Item(); // 는 불가능해진다. 생성자에 매개변수가 생겼기때문

번외로 하드코딩하는 방법도있다.

프로퍼티에 바로 박아넣어서 초기화해주거나, 생성자 내부에 적어준다.

public class Item : IUsable
{
    public string Name { get; set; } = "Health Position";

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

하거나,, 

public class Item : IUsable
{
    public string Name { get; set; }

    // 기본 생성자
    public Item()
    {
        Name = "Health Position";
    }

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

---------------
다중 상속 구현 예제

아이템을 주워지기도, 버려지기도 가능하게 만들어야함.

// 인터페이스 1
public interface IItemPickable
{
    void PickUp();
}

// 인터페이스 2
public interface IDroppable
{
    void Drop();
}

// 아이템 클래스
public class Item : IItemPickable, IDroppable // 쉼표로 다중상속표현
{
    public string Name { get; set; }

    public void PickUp()
    {
        Console.WriteLine("아이템 {0}을 주웠습니다.", Name);
    }

    public void Drop()
    {
        Console.WriteLine("아이템 {0}을 버렸습니다.", Name);
    }
}

// 플레이어 클래스
public class Player
{
    public void InteractWithItem(IItemPickable item)
    {
        item.PickUp();
    }

    public void DropItem(IDroppable item)
    {
        item.Drop();
    }
}

// 게임 실행
static void Main()
{
    Player player = new Player();
    Item item = new Item { Name = "Sword" };

    // 아이템 주울 수 있음
    player.InteractWithItem(item);

    // 아이템 버릴 수 있음
    player.DropItem(item);
}


----------------
가상메서드, 인터페이스와 추상클래스 차이

1. 가상메서드가 포함된 일반 클래스
기본 클래스에서 기본 구현을 제공하고 필요에따라 파생클래스에서 오버라이드 가능
파생클래스는 기본 구현 그대로 사용하거나 일부만 변경가능하다



2.인터페이스는 추상적인 동작만 정의하고 구현을 갖지않음
다중 상속 가능
인터페이스에서 정의한 메서드는 자식클래스에서 반드시 구현해야한다
인터페이스는 오직 메서드, 속성, 이벤트등의 계약만을 정의하며 인스턴스의 데이터를 저장할 수 
있는 필드나 상태를 직접 보유하는 기능은 제공하지 않는다. 멤버가 없다.
참고로 속성(프로퍼티)는 계약할 수 있기때문에 - 
public interface IUsable
{
    string Name { get; set; }
    void Use();
}
이런식의 작성이 가능하다. 이때 상속받는 클래스에서 반드시 멤버로 필드를 내부에서 사용하거나, 자동구현
프로퍼티를 통해 해당 기능을 제공해야한다.
public class Item : IUsable
{
    // 컴파일러가 backing field를 자동으로 생성
    public string Name { get; set; } = "Default Item";

    public void Use()
    {
        Console.WriteLine($"아이템 {Name}을 사용했습니다.");
    }
}
이때 인터페이스의 내의 string Name { get; set; } 는 필드가 아니라 프로퍼티에 대한 '계약'이다.
실제 저장소(필드)는 인터페이스를 구현하는 자식클래스가 제공하는것이다. 인터페이스 자체는 상태(데이터)를
보유하지않는다. 컴파일러가 자식클래스에서 자동으로 숨겨진 필드를 생성한다.
참고로 
string Name { get; set; } 을 풀어쓰면

private string name;// 숨겨진 필드

public string Name
{
 get 
  { 
   return name;
  }
  set
  {
   name = value;
  }
}
이다.



3.추상클래스는 일부 동작의 구현을 가지며, 추상메서드를 포함 할 수 있음, 가상메서드도 포함가능하다.
단일 상속만 가능
추상클래스에 정의한 추상메서드는 자식 클래스에서 반드시 구현해야한다.
추상클래스에서 구현한 virtual(가상) 메서드는 자식클래스에서 반드시 구현할필요는 없다. (필요하면쓴다)
추상클래스는 객체 생성이 안된다. 따라서 추상클래스를 상속받은 클래스들은
리스트로 콜렉션에 넣은뒤 foreach 구문으로 메서드를 실행시킬 수 있다.

추상클래스에 반드시 자식클래스가 구현해야하는 추상메서드를 넣지않을 수도있다.
그냥 프로퍼티와 일반 메서드정도만 존재할 수 있고.. 이것은 해당 추상클래스가 인스턴스 화 할 수없다는게
중요하다. 
public abstract class BaseEnemy
{
    public string Name { get; set; }

    // 모든 적이 공통으로 사용할 수 있는 기본 이동 메서드
    public void Move()
    {
        Console.WriteLine($"{Name}가 이동합니다.");
    }
}

public class Zombie : BaseEnemy
{
    // 추가 기능이 필요하면 별도로 구현 가능
    public void Groan()
    {
        Console.WriteLine($"{Name}가 으르렁거립니다.");
    }
}

위 코드에서 BaseEnemy는  추상클래스지만 반드시 자식 클래스가 오버라이드해야하는 추상메서드는 포함안한다
따라서 이경우 BaseEnemy는 인스턴스화 되지 않도록 의도된 디자인일뿐 자식 클래스에 강제사항은없다.
참고로 단순히 공통코드만 있고 추상메서드가 없다면 굳이 추상클래스로 만들지않고
일반 클래스를 사용해도 무방할수있다. 어차피 일반클래스도 상속이 가능하고 자식 클래스에서 재정의가능하다
특히나 일반클래스에서의 virtual 메서드 같은것도 자식클래스에서 그냥 오버라이드해서 똑같이 쓸수있다.

----------------

열거형 (Enums) 

사용하는 이유: 
연관된 상수들을 명명할 수 있음.
숫자와 연결된 특정한 이름을 사용하면 이게 어떤 코드인지 바로 보이며 가독성을 높인다.

스위치문과의 호환성이 좋다.

특징:
서로 관련된 상수들의 집합을 정의함
열거형의 각 상수는 정수값으로 지정됌. 실수형을 사용 x

정의:
enum MyEnum
{

 value1, //지정안하면 0
 value2, //지정안하면 1
 value3, // 지정안하면 2

}

열거형 사용:
MyEnum myEnum = MyEnum.Value1;


열거형 상수값지정:

enum MyEnum
{
 value1 = 10, // 지정가능하여 10
 value2, // 지정안했으므로 바로 하나 올려서 11
 value3= 20 // 지정하였으므로 20
}

열거형 형변환
명시적 캐스팅가능

int intValue = (int)MyEnum.Value1; // 열거형 값을 정수로 변환
MyEnum enumValue = (MyEnum)intValue; // 정수를 열거형으로 변환

스위치문과 사용가능

switch(enumValue)
{
 case MyEnum.Value1:
// Value1 에대한 처리
break;
 case MyEnum.Value2:
// Value2 에대한 처리
break;
 case MyEnum.Value3:
// Value3에 대한 처리
break;
default:
//기본처리
break;
}

-------
열거형 사용예제

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

        public static void ProcessMoth(int month)
        {
            if(month >=(int)Month.Jan && month <=(int)Month.Dec) // 1부터 12까지라는걸 enum형 Month 안에서 1로 지정해준뒤 표현해주는데, int형 형변환해줘서 표현한다.
            {
                Month selectMonth = (Month)month; // 선언한 selectMonth라는 지역변수에 int 형 month 매개변수를 할당하는데, Enum 형으로 형변환을 해준다.
                Console.WriteLine($"선택한 월은 {selectMonth}입니다.");
            }
            else
            {
                Console.WriteLine("올바른 월을 입력하시오");
            }
        }

        static void Main(string[] args)
        {
            int userInput = 7;
            ProcessMoth(userInput);  // int형 매개변수 7을 month에 집어넣어준다. 
        }
------------
이런식으로 사용될수있다.
// 게임 상태
enum GameState
{
    MainMenu,
    Playing,
    Paused,
    GameOver
}

// 방향
enum Direction
{
    Up,
    Down,
    Left,
    Right
}

// 아이템 등급
enum ItemRarity
{
    Common,
    Uncommon,
    Rare,
    Epic
}

---------------
예외처리는 예기치않은 상황을 대비하여 프로그램을 안정적으로 유지한다.
오류알람을 띄우거나 우회할수있는 코드를 띄운다.
디버깅에 용이하다.

try-catch 블록을 사용한다.

try
{
    // 예외가 발생할 수 있는 코드
}
catch (ExceptionType1 ex)
{
    // ExceptionType1에 해당하는 예외 처리
}
catch (ExceptionType2 ex)
{
    // ExceptionType2에 해당하는 예외 처리
}
finally
{
    // 예외 발생 여부와 상관없이 항상 실행되는 코드
}

-------------
예외처리 catch 블록은 위에서부터 순서대로 실행됌.
예외 타입에 해당하는 첫 번째 catch 블록이 실행됨.
상속관계가 있을 경우 상위 예외타입의 catch 블록이 먼저 실행됌.

다중 catch 블록
여러개의 catch 블록을 사용하여 다양한 예외처리 가능

예외 객체를 사용하여 예외에 대한 정보를 엑세스할수있다.
소괄호 안에 ExceptionType1 ex 이런 매개변수가 예외에 대한 정보를 알려준다.

finally 블록은 예외가 발생했든 안했든 무조건 실행함.
마지막 단계로, 필요한처리들을하는데 예외발생이 해서 해야하는 처리, 예외를 위해 준비해놨던
것들을 해지한다거나 해야함.
물론 생략이 가능하다. 필요하지않다면 생략가능.

예외가 발생했다면, 모든게 끝난 이후에 finally블록이 실행
발생안하면 finally블록이 실행
-------------
사용자 정의 예외:
클래스를 제작해서 자신만의 예외클래스를 사용
Exception 클래스를 상속받아 작성할수있음.
catch문 소괄호 ExceptionType2 이런것들을 우리가 만든 클래스 이름만 사용하면 바로 쓸수있다.

예제
try
{
    int result = 10 / 0;  // ArithmeticException 발생 컴퓨터는 나누지않고 연속해서 빼는데, 무한히 빼게된다.
    Console.WriteLine("결과: " + result);
}
catch (DivideByZeroException ex) //이때 이 구문으로 내려온다.
{
    Console.WriteLine("0으로 나눌 수 없습니다.");
}
catch (Exception ex)
{
    Console.WriteLine("예외가 발생했습니다: " + ex.Message);
}
finally
{
    Console.WriteLine("finally 블록이 실행되었습니다.");
}

미리 정의된 예외타입들이 존재한다. 소괄호안에 들어간 DivideByZeroException 가 바로 타입이자 클래스다.
그뒤에 ex는 매개변수다.
.NET 프레임워크에서는 이미 다양한 예외 클래스들이 제공된다.
NullReferenceException
DivideByZeroException
ArgumentException 등... 모두 System.Exception에 상속받고있다.
catch 블록의 소괄호 의 ex는 해당 예외 인스턴스를 참조하는 변수다.
즉 try 블록에서 오류가 발생하면 이 오류는 객체화되고, catch 블록에 넘어오면서 ex라는 이름으로 할당된다.
즉 객체를 참조하고있기때문에 ex 변수를 사용하여 catch 블록에서는 예외에 관한 자세한 정보에 접근할 수 있다.
ex.Message는 Message라는 프로퍼티를 접근하여  예외와 관련된 메시지를 문자열 형태로 반환하는 블록이다.
참고로 ex는 관례상 쓰는 이름으로 다른 이름으로 써도 된다.
try
{
    int result = 10 / 0;
}
catch (DivideByZeroException exception)
{
    Console.WriteLine("0으로 나눌 수 없습니다. " + exception.Message);
}

이런식으로 ex를 exception이라고 써도된다.

----------------

사용자정의 예외처리 예제

 public class NegativeNumberException : Exception // 사용자 예외처리 클래스는 Exception 클래스를 상속받음
 {
     public NegativeNumberException(string message) : base(message) // string message를 매개변수로받는
    // 생성자를 생성
    // 근데, :(콜론)을 써서 Exception 클래스의 base(message)를 우선 출력하도록 만듬
     {
     }
 }




 static void Main(string[] args)
 {
     try
     {
         int number = -10;
         if (number < 0)
         {
             throw new NegativeNumberException("음수는 처리할 수 없습니다."); // throw는 일부러 코드에러를 발생시킬때 쓴다
  // new 한 NegativeNumberException(string message) 를 발생시킨다. 이 에러는 인스턴스화되었다
         }
     }
     catch (NegativeNumberException ex) // ex는 새로만든 NegativeNumberException 타입의 매개변수 (객체화된 에러를 할당받는다)
   // 출력할때 ex.Message 로 string 타입으로 에러를 설명하라고 반환 받는 Message 프로퍼티로 접근
     {
         Console.WriteLine(ex.Message);
     }
     catch (Exception ex)
     {
         Console.WriteLine("예외가 발생했습니다: " + ex.Message);
     }

 }

-------------
실사용 예제1

// 플레이어 이동
try
{
    // 플레이어 이동 코드
    if (IsPlayerCollidingWithWall())
    {
        throw new CollisionException("플레이어가 벽에 충돌했습니다!"); // 에러 출력: 플레이어가 벽에충돌했다는 구문을 띄우는 해당 오류 클래스 객체생성
    }
}
catch (CollisionException ex) // 해당 클래스의 ex 변수 받아오고
{
    // 충돌 예외 처리
    Debug.Log(ex.Message); // 에러가 디버그에 출력되도록 하고
    // 예외에 대한 추가 처리 
}


// 리소스 로딩
try
{
    // 리소스 로딩 코드
    LoadResource("image.png");
}
catch (ResourceNotFoundException ex)
{
    // 리소스가 없는 경우 예외 처리
    Debug.Log(ex.Message);
    // 예외에 대한 추가 처리
}
catch (ResourceLoadException ex)
{
    // 리소스 로딩 중 오류가 발생한 경우 예외 처리
    Debug.Log(ex.Message);
    // 예외에 대한 추가 처리
}


// 게임 상태 전이. 게임이 실행중이지않을때 상태가 넘어가면안된다.
// 뭔가 사용자의 환경등외부적인 요인으로 게임실행에 문제가될때 예외처리하는 방법이다.
try
{
    // 상태 전이 코드
    if (currentGameState != GameState.Playing)
    {
        throw new InvalidStateException("게임이 실행 중이 아닙니다!");
    }
    // 게임 상태 전이 실행
}
catch (InvalidStateException ex)
{
    // 상태 예외 처리
    Debug.Log(ex.Message);
    // 예외에 대한 추가 처리
}


주의사항

예외처리를 할때 신경써야할경우
엄청 긴 코드를 try문안에 넣어버리면 오히려 처리하기 어려워짐
우려되는 부분에 한해서만 예외처리를 진행시켜주는것이 좋다.

실제 게임코드에서는
리소스로딩, 네트워크 통신, 외부 데이터 파싱등 불확실한 작업을 할때 try-catch문을 사용함.

try-catch 문은 실행블록내, 즉 실제로 프로그램이 실행되는 코드 영역 내에서 사용할 수 있는데
좀더 풀어말하면 메서드 내에서 실제 로직이 구현되는 부분에 쓸수있다.

public void ComplexLogic()
{
    // 이 부분이 실행 블록입니다.
    try
    {
        // 예외가 발생할 수 있는 코드
    }
    catch (Exception ex)
    {
        // 예외 처리 코드
    }
}


혹은 생성자 내부

public MyClass()
{
    try
    {
        // 생성자 내 실행 블록
    }
    catch (Exception ex)
    {
        // 예외 처리
    }
}

이벤트 핸들러: 이벤트가 발생했을때 실행되는 메서드 내의 코드블록

private void Button_Click(object sender, EventArgs e)
{
    try
    {
        // 버튼 클릭시 실행되는 코드
    }
    catch (Exception ex)
    {
        // 예외 처리
    }
}
---------------

실제로 어떤식으로 구조가 짜이는가?

스크립트를 따로 관리하면서
하나는 Exception 전용 스크립트에서 사용자 정의 exception 클래스를 만들어두고
내용에 각종 예외처리를 정의한다. 
다양한 생성자로 제공하여 에러메시지와 내부예외를 전달할 수 있게 public으로 정의 한다.
그리고 특정 스크립트의 게임 실행 로직내에서 
try - cath문으로 그 내용들을 구체화 해서 쓰게되는데,
마치 인터페이스와 상속 클래스의 관계와 비슷하다.

먼저 Exception 관련된 cs에 아래와 같이 관리한다.

using System;

// 보통 프로젝트의 Exceptions 폴더나 별도의 네임스페이스에 둡니다.
namespace Game.Exceptions
{
    public class PlayerNotFoundException : Exception
    {
        public PlayerNotFoundException() { }

        public PlayerNotFoundException(string message)
            : base(message)
        { }

        public PlayerNotFoundException(string message, Exception inner)
            : base(message, inner) // inner는 내부예외를 의미함. 어떤 예외가 발생했는데 근본적인 다른예외가 원인이라면
    // 그 예외의 체인을 구성할 수 있도록 도와준다.
    // 예를들어 플레이어를 찾을수없는 에러의 근본 원인은 네트워크 연결실패예외인것이다
        { }
    }
}

PlayerNotFoundException 이라는 예외를 정의하고
다양한 생성자를 제공하여 에러메시지와 내부예외를 전달 할 수 있게 만들었다.
이 클래스는 별도의 네임스페이스에서 관리하게끔 짜여졌다.
public으로 작성하여 들고올 수 있게 만든다.

// 그다음 사용자 정의 예외를 활용하는 코드를 특정 클래스에서 사용 할 수 있다.

using System;
using Game.Exceptions; // using 문을 추가해준다.

public class GamePlayManager
{
    // 플레이어를 찾는 메서드 예시
    public void FindPlayer(string playerName)
    {
        try
        {
            // 플레이어를 찾는 로직 - 실제 게임 로직에 맞게 구현
            bool found = SearchPlayerInDatabase(playerName);

            if (!found)
            {
                // 사용자 정의 예외를 던집니다.
                throw new PlayerNotFoundException("플레이어가 존재하지 않습니다: " + playerName);
            }

            // 플레이어가 존재할 경우 이후 로직 실행
            Console.WriteLine("플레이어를 찾았습니다.");
        }
        catch (PlayerNotFoundException ex)
        {
            // 해당 예외에 대해 구체적인 처리를 합니다.
            Console.WriteLine("예외 발생: " + ex.Message);
            // 예를 들어, 사용자에게 오류 메시지 표시 또는 대체 로직 실행
        }
        catch (Exception ex)
        {
            // 다른 예외를 처리합니다.
            Console.WriteLine("일반 예외 발생: " + ex.Message);
        }
    }

    private bool SearchPlayerInDatabase(string playerName)
    {
        // 실제 데이터베이스 조회 로직은 생략.
        // 여기서는 테스트를 위해 항상 false를 반환하도록 하겠습니다.
        return false;
    }
}

플레이어 검색 중 조건이 맞지않을경우 만들어 두었던 PlayerNotFoundException을 가져와
try문에서 
오류 객체를 생성하고 (throw new PlayerNotFoundException("플레이어가 존재하지 않습니다: " + playerName);

catch 구문에서 해당 사용자 정의 예외를 잡아 구체적인 처리를 한다.

------------------

값형과 참조형

값형은 struct, 참조형은 class 등

1. 값형은 변수에 값을 직접 저장함.
변수가 실제 데이터를 보유하고있으며, 해당 변수를 다른 변수에 할당하거나 전달할때는 값의 복사가 일어난다.
값형 변수의 수정은 해당 변수의 값만 변경하므로 다른 변수에 영향을 안준다.
int, float, double, bool 등의 기본 데이터 타입들이 값형에 해당한다.

값형의 예제는 아래와 같다.
struct MyStruct
{
    public int Value;
}

MyStruct struct1 = new MyStruct();
struct1.Value = 10;

MyStruct struct2 = struct1; // struct2는 struct1의 값 복사가 일어난다. struct2에 10저장

struct2.Value = 20; // struct2의 값을 20으로 재할당

Console.WriteLine(struct1.Value); // 출력 결과: 10 // 그런다고 struct1의 값이 20이 되진않음


2. 참조형은 참조 (메모리주소)를 저장
변수가 실제 데이터를 가리키는 참조를 갖고있음(데이터를 갖고있는게아니라 주소)
해당 변수를 다른 변수에 할당하거나 전달할때 값의 복사가아니라 참조가 복사됨
참조형 변수의 수정은 동일한 데이터를 가리키고 있는 다른 변수에 영향을 준다. 
클래스, 배열, 인터페이스 등이 이 참조형에 해당한다.

class MyClass
{
    public int Value;
}

MyClass obj1 = new MyClass(); // obj1는 비어있음, 할당하면 MyClass(); 인스턴스가 생김 obj1는 해당 객체의 주소를 가져감
obj1.Value = 10; // 해당 객체의 value를 10으로 만들어준다

MyClass obj2 = obj1; // obj2에 obj1을 복사하면, 마찬가지로 해당 객체의 주소를 가져간것까지 복사됌
   // 즉 같은 객체를 바라보고있다.
obj2.Value = 20; // 같은 객체의 value를 20으로 바꿔주는것이다.

Console.WriteLine(obj1.Value); // 그러니까 결국 obj1을 출력해도 참조하는 값이 바뀌었으니 출력 결과: 20이 나온다.

여기서 재밌는 점은 객체는 new를 통해 생성될때 변수로 할당해서 관리해주는데
이 변수는 어디까지나 참조형 주소일뿐이다. 객체 자체의 이름은 아니다.
객체 자체에 이름을 부여하지않는 이유는 메모리 관리의 효율성과 추상화를 위함이다.
모든 객체에 이름을 명명한다면 복잡성, 오버헤드가 늘어난다.
그래서 이런 세부정보들 그러니까 객체의 식별자나 주소값은 내부적으로 숨기고 런타임 시스템 내부에서나
사용되는 정보다.
obj1, obj2는 객체 자체의 이름이아니라 주소(참조)를 저장하고있는 변수라보면되고
여기 이 코드 블록에서는 MyClass obj2 = obj1; 함으로서 같은 주소를 가진다고 보면된다.
비유하자면 집에 이름은 굳이 알필요없고 두명이 같은 집에 대해서 모두 같은 주소지가 적힌 종이를 복사해서 가지고있는것이다.
그리고 그 집안에 value( 페인트 칠등)이 바뀐다면 어차피 두명은 같은 주소를 알고있으니, 열어서 확인할 수 있다.

값복사의 경우는 같은 내용을 적은 종이를 한장 더 복사해서 나눠 가지고, 복사한 종이내용을 수정한다고
기존의 종이 내용이 바뀌지는 않는다는 개념으로 비유할 수 있다.

--------------
클래스, 배열, 딕셔너리, 리스트등 컬렉션타입의 클래스는 참조형, 인터페이스도 참조형,
나머지는다 값형이라 생각하는게 편하다.
참조형은 힙영역에 실제 데이터가 저장되고 변수는 그 데이터가 있는 위치(주소)를 참조하는것이다. 
값형은 보통 스택메모리에 저장된다.

힙메모리, 스택메모리 조금 복습해보자면
먼저 스택메모리는
접근속도가 빠르고, 호출시 지역변수등을 저장하는데 사용된다.
변수가 선언되면 자동으로할당되고, 해당 스코프가 끝나면 자동으로 해제된다.
힙메모리는
조금더 크고 복잡한 데이터를 저장하는 영역, 동적으로 할당한다.
프로그램 실행중에 크기가 변할 수 있는 데이터를 저장하는데 적합하다.
가비지컬렉션과 같은 메커니즘에 의해 관리된다.
new 키워드로 클래스 객체 같은 참조형이 힙에 생성된다.
변수(참조변수)는 힙에 저장된 객체의 주소를 저장한다.
MyClass obj1 = new MyClass();  일케하면 
실제 객체는 힙에 있고 obj는 그 객체의 주소를 가지고 일반적으로 스택에 저장된다.

클래스의 멤버 변수(객체의 필드로 선언된 참조변수)는 해당 객체가 저장된 힙 영역안에 같이 위치한다.
class MyClass
{
    public int Value;
}

void SomeMethod() 
{
    // obj는 로컬 변수이므로 스택에 저장됨.
    MyClass obj = new MyClass();  // new MyClass()로 생성된 인스턴스는 힙에 저장됨.
    obj.Value = 10;
}

여기서 obj는 메서드 내 로컬변수고, 객체 생성시 주소를 가르킨다. 스택영역에 저장된다.
여기서 public int Value; 는 객체 생성시 힙에 같이 저장된다.

class MyClass
{
    public int Value;
    
    public MyClass(int initialValue)
    {
        Value = initialValue;
    }
}

MyClass obj = new MyClass(10);

객체의 필드 Value는 10으로 초기화되며 함께 힙에 저장된다.
생성자가 있다면 그내용또한 힙에 저장될것이다.



--------------
박싱과 언박싱

박싱: 값형을 참조형으로 바꿔버림 ->값형을 스택이아니라 힙영역에 동적할당하는 것
언박싱: 참조형을 다시 값형으로 돌려놓음

박싱에서 값형을 참조형으로 바꿀때 값형은 사라지는게아니라 그대로있고
참조형으로 새롭게 복사된다. 메모리를 더 쓰기때문에 성능저하가 일어나는경우가있다.

언박싱은 참조형으로 된 값들을 가비지 컬렉션에서 다 없애버림

-----------
박싱과 언박싱예제

object는 모든 클래스의 직간접적인 최상위 클래스의 형식이다. Int,, float 등등 모두다..

using System;

class Program
{
    static void Main()
    {
        // 값형
        int x = 10;
        int y = x;
        y = 20;
        Console.WriteLine("x: " + x); // 출력 결과: 10
        Console.WriteLine("y: " + y); // 출력 결과: 20

        // 참조형
        int[] arr1 = new int[] { 1, 2, 3 }; // arr1은 주소를 가짐, new로 객체가 3칸짜리(비유)가 생성됌
        int[] arr2 = arr1; // arr2도 arr1과 같은 주소를 가지게 할당함
        arr2[0] = 4; // arr2에서 0번째 인덱싱 값을 바꾸면 결국 같은 객체에 대해서 바뀌게됌
        Console.WriteLine("arr1[0]: " + arr1[0]); // 출력 결과: 4
        Console.WriteLine("arr2[0]: " + arr2[0]); // 출력 결과: 4

        // 박싱과 언박싱
        int num1 = 10; 
        object obj = num1; // 박싱 , 최상위 타입 object로 참조형으로 바꿔줌, 이때는 값형 10이랑 다르게 참조형 10이 새로이 생긴것
        int num2 = (int)obj; // 언박싱 , 해당 참조형 10을 다시 값형으로 바꿔줌, 복사가 일어난상태이므로 num1의 값 10과는 독립적인 10임
        Console.WriteLine("num1: " + num1); // 출력 결과: 10
        Console.WriteLine("num2: " + num2); // 출력 결과: 10
    }
}
------------
박싱 언박싱의 리스트 활용예제

List<object> myList = new List<object>();

// 박싱: 값 형식을 참조 형식으로 변환하여 리스트에 추가
int intValue = 10;
myList.Add(intValue); // int를 object로 박싱하여 추가

float floatValue = 3.14f;
myList.Add(floatValue); // float를 object로 박싱하여 추가

// 언박싱: 참조 형식을 값 형식으로 변환하여 사용
int value1 = (int)myList[0]; // object를 int로 언박싱
float value2 = (float)myList[1]; // object를 float로 언박싱

----------------
델리게이트, 람다 및 LINQ

델리게이트:  메서드를 참조하는 타입

구현:
사용하고자하는 메서드의 형식대로 써주되, 이름은 사용할 변수명, 사용자정의 명으로 만들어줌

메서드는 사용하고자 한 형식과 동일해야함.

나중에 연결해줄 수 있음. 그리고 메서드 쓰듯이 델리게이트를 써주면됨.
근데 왜쓰느냐? 

실제로는 특정 메서드에 접근이 불편한 상황이많고 이래서 델리게이트를 쓴다.

delegate int Calculate(int x, int y); // 델리게이트를 써줄 메서드형식을 하나 써줌, 이름은 사용성있게

static int Add(int x, int y) // 델리게이트와 연결한 메서드도 존재함.
{
    return x + y;
}

class Program
{
    static void Main()
    {
        // 메서드 등록
        Calculate calc = Add; // 메서드와 연결해줌

        // 델리게이트 사용
        int result = calc(3, 5); // 호출할때 Add와 동일하게 가능
        Console.WriteLine("결과: " + result);
    }
}


----------
하나 이상의 메서드를 등록할때도 delegate를 쓸수있다.

        delegate void MyDelegate(string message);

        static void Method1(string message)
        {
         Console.WriteLine("Method1:" + message);
        }


        static void Method2(string message)
        {
         Console.WriteLine("Method2:" + message);

        }


        static void Main(string[] args)
        {
            MyDelegate myDelegate = Method1; // 메서드 등록은 변수 선언, 할당하듯이 참조형 인스턴스 생성
            myDelegate += Method2; // 참조형 인스턴스 Method1에 Method2도 더해짐

            myDelegate("Hello!");
   
        }

하면
Method1:Hello!
Method2:Hello!
이렇게 출력되는데 실행코드블럭을 보면 WriteLine으로 줄바꿈을 하고있기때문.

------------
델리게이트를 이용해서 공격을 콜벡받을수있다.
먼저 사전에 개념적으로 접근해보자
백화점에 제품이 있고 재고가 있다. 여러 가게에서 입고가 되면 받아가겠다고 요청을 해놓은상태다
그리고 입고가 되면 그 재고에서 다시 가게들에게 나눠주는 형태를 떠올려보자

공격 콜백을 받아보자.
다음 예제에서는 event를 붙여서 사용했는데,
event란 델리게이트를 기반으로 한 특수한 필드다.
외부 객체가 특정상황이 발생했을때, 알림을 받을 수있도록 구독을 허용하는 역할을한다.
event는 할당 연산자 (=)를 사용할 수 없다. +=, -=는 사용하여 핸들러(메서드)를 구독하거나 제거할 수 있다.
클래스 외부에서는 직접 이벤트를 호출할수없다. 더 캡슐화한것임 해당 클래스를 정의한 내부에서만 호출할 수 있다.



// 델리게이트 선언
public delegate void EnemyAttackHandler(float damage);

// 적 클래스
public class Enemy
{
    // 공격 이벤트
    public event EnemyAttackHandler OnAttack; // Enemy는 OnAttack이라 명명한 EnemyAttackHandler를 만들어줌 특수필드
// 이 EnemyAttackHandler 타입의 Event OnAttack 변수가 곧 구독가능한 신호다.

    // 적의 공격 메서드
    public void Attack(float damage)
    {
        // 이벤트 호출
        OnAttack?.Invoke(damage);
// ? 는 null 조건부 연산자
// null 참조가 아닌 경우에만 멤버에 접근하거나 메서드를 호출
// .Invoke(damage)는 이벤트에 (구독)등록된 모든 메서드(핸들러)를 순서대로 호출하는 기능
    }
}

// 플레이어 클래스
public class Player
{
    // 플레이어가 받은 데미지 처리 메서드
    public void HandleDamage(float damage)
    {
        // 플레이어의 체력 감소 등의 처리 로직
        Console.WriteLine("플레이어가 {0}의 데미지를 입었습니다.", damage);
    }
}

// 게임 실행
static void Main()
{
    // 적 객체 생성
    Enemy enemy = new Enemy();

    // 플레이어 객체 생성
    Player player = new Player();

    // 플레이어의 데미지 처리 메서드를 적의 공격 이벤트에 추가
    enemy.OnAttack += player.HandleDamage; // 여기서는 += 연산자는 구독을 나타낸다.
          // 이벤트 호출목록에 해당 메서드 핸들러를 추가해준다.
          // HandleDamge(float damage)메서드는 반환값없고, float damage 매개변수를 받아 델리게이트 타입과 일치함
          // enemy 객체가 OnAttack 이벤트를 호출할때마다 해당 player객체의 메서드도 호출된다.
    // 적의 공격
    enemy.Attack(10.0f); // Attack 메서드를 실행하면 내부에 OnAttack 이벤트를 호출시키며 player가 데미지를 받는 메서드가 실행된다.
}

구독 해지도 간단하다.
enemy.OnAttack -= player.HandleDamage; 하면 된다. 

델리게이트는 함수의 참조를 저장할수있는 타입이다.
델리게이트 인스턴스란 메서드 참조를 저장하는 객체이다.
단일 메서드 또는 여러 메서드를 참조할 수 있다.

MyDelegate myDelegate = Method1; 
하면서 인스턴스를 생성하며 특정메서드(여기서는 Method1)의 참조를 할당한다.
콜백 리스트란 
델리게이트 인스턴스 내부에 저장된 메서드들의 순서있는 목록이다.
+= 연산자를 통해 추가하고 -=연산자를 통해 뺄수있다. 이 리스트에 있는 메서드들은 순서대로 전부 호출된다.

public event EnemyAttackHandler OnAttack; 특수필드선언시에는
기본적으로 null로 초기화되어있다. 즉 아무런 메서드도 구독되지않은상태이다.

구독(+=) 시점에서 객체가 생성되는데 
enemy.OnAttack += player.HandleDamage;
하는 순간 player.HanddleDamage 메서드의 참조가 OnAttack에 추가된다.
내부적으로 델리게이트의 Combine연산이 이루어지며 새로운 델리게이트 인스턴스가 생성된다.
이후 추가구독이 이루어지면
기존의 콜백리스트에 새로운 메서드가 연결되는식이다.

OnAttack?.Invoke(damage);
의경우 null이아니라면(즉, 적어도 하나의 메서드가 구독되어있다면) 내부에 저장된
델리게이트 인스턴스(콜백리스트)의 모든 메서드가 순차적으로 호출되게 하라는 실행문이다.


---------------
주의사항
Invoke()의 쓰임이 c#에서 상황에 따라 조금다르다.





델리게이트에서의 Invoke()
는 내부적으로 등록된 메서드들을 호출하기 위해 Invoke()메서드를 사용하는데
OnAttack.Invoke(damage);
등록된 모든 메서드들을 순차적으로 호출하고, 필요한 인자를 (여기서는 damage) 전달한다.
델리게이트 인스턴스는 실제로 메서드들이 모인 목록(멀티캐스트 델리게이트) 이기 때문에
Invoke()로 호출하면 그 목록에 잇는 모든 메서드가 실행되고 
그렇기에 구독과 취소로 ( += 나 -=) 이용하여 목록안에 넣고 뺄 수 있는 기능이 있는것이다.

그런데 별도로 Unity에는
UnityEngine의 MonoBehaviour 에 내장된 Invoke 메서드가 있다.
이 메서드는 델리게이트 처럼 함수 참조를 저장하는 용도가아니라 단순히 "나중에 이 함수를 실행해줘:
하고 예약하는 기능이다.

Invoke("메서드이름", 지연시간);
말고도  한번 생성할때 간격에 맞춰 반복생성해주는
InvokeRepeating("메서드이름", 시작딜레이, 반복간격);
도 있고
CancelInvoke(); 이라고 
호출해놓은 모든 Invoke 요청을 전체 취소해주는 메서드도있다.

정리:
 Invoke: 지정한 딜레이 후 한 번 메서드를 호출합니다.

InvokeRepeating: 지정한 딜레이 후 메서드를 시작하고, 이후 정해진 간격으로 반복 호출합니다.

CancelInvoke: 예약된 호출을 취소합니다.
--------------

람다

람다는 익명메서드를 만든다. 참조만을 가지고 컨트롤하고 델리게이트와 함께 등장할것.

(parameter_list) => expression 이런식으로 표현하는데,

정의해보자면

Calculate calc = (x, y) => 
{
return x + y;
};
// => 를 기점으로 스코프를 쓰면 좀더 긴 내용을 코드로 쓸수있고

Calculate calc = (x, y) => x + y;
// => 기점으로 단순하게 리턴값등을 빠르게 쓸거면 스코프 중괄호는 없어도된다.

( )에 받게되는 매개변수 인자를 가지고 => 뒤의 실행코드를 실행해준다고 생각할 수도있다.
---------------
람다 사용예제

using System;

// 델리게이트 선언
delegate void MyDelegate(string message);

class Program
{
    static void Main()
    {
        // 델리게이트 인스턴스 생성 및 람다식 할당
        MyDelegate myDelegate = (message) =>
        {
            Console.WriteLine("람다식을 통해 전달된 메시지: " + message);
        };

        // 델리게이트 호출
        myDelegate("안녕하세요!");

        Console.ReadKey(); 
    }
}

----------------------
델리게이트가 매개변수를 받지않는걸로 선언될때도 구독과 취소에 용의하게 
람다식을 쓸수있다.

// 델리게이트 선언
public delegate void GameEvent(); // GameEvent(); 는 매개변수가 없이 선언된 델리게이트 메서드

// 이벤트 매니저 클래스
public class EventManager
{
    // 게임 시작 이벤트
    public event GameEvent OnGameStart; // GameEvent 타입 특수필드 event 로 생성

    // 게임 종료 이벤트
    public event GameEvent OnGameEnd; // 마찬가지

    // 게임 실행
    public void RunGame()
    {
        // 게임 시작 이벤트 호출
        OnGameStart?.Invoke(); // null 값이 아닐때 OnGameStart 내의 구독된 메서드 모두 호출되게

        // 게임 실행 로직

        // 게임 종료 이벤트 호출
        OnGameEnd?.Invoke(); // null 값이 아닐때 OnGameEnd 내의 구독된 메서드 모두 호출되게
    }
}

// 게임 메시지 클래스
public class GameMessage
{
    public void ShowMessage(string message) // 매개변수 string 타입 message를 받는 ShowMessage 메서드 생성
    {
        Console.WriteLine(message); // 받은 변수를 출력
    }
}

// 게임 실행
static void Main()
{
    // 이벤트 매니저 객체 생성
    EventManager eventManager = new EventManager();

    // 게임 메시지 객체 생성
    GameMessage gameMessage = new GameMessage();

    // 게임 시작 이벤트에 람다 식으로 메시지 출력 동작 등록
    eventManager.OnGameStart += () => gameMessage.ShowMessage("게임이 시작됩니다.");
// 여기서 중요한것은 ()은 기존의 델리케이트 함수가 매개변수를 안받기때문에 비워둔것. 
//익명메서드를 참조형태로 만들었다. 이벤트의 데리게이트 시그니처와 맞지않는 메서드를 감싸서 호출하기위해 람다식을 썼다.
// 여기서는 구독하는동시에 ShowMessage( 매개변수) 안의 내용까지 입력해준것이다.
    // 게임 종료 이벤트에 람다 식으로 메시지 출력 동작 등록
    eventManager.OnGameEnd += () => gameMessage.ShowMessage("게임이 종료됩니다.");

    // 게임 실행
    eventManager.RunGame();
}

--------------
Func, Action

Func와 Action은 미리 제너릭 형식으로 정의된 델리게이트다
반환값, 매개변수등을 위에선 구현을 해줫었는데
Func는 반환값이 있는 메서드 
Action는 반환값이 없는 메서드다

 예를들어 
Func<int, string> 는 int는 매개변수로 입력받고 string은 반환값이다.

Action<int, string> 는 둘다 매개변수다 왜냐면 반환값은 없으니가.

이미 정의되어있어서 훨씬 안전하고 간결하다.
실제로도 델리게이트를 직접 만드는경우는 드물고 위에 두개를 더 많이 사용하게될것이다.

Func, Action 사용 예제

     static int Add(int x, int y)
     {
         return x + y;
     }

     static void PrintMessage(string message)
     {
         Console.WriteLine(message);
     }




     static void Main(string[] args)
     {
         Func<int, int, int> addFunc = Add; // 델리게이트 할당과 같은 방식, 여기서 Func<>안에 int가 세개인 이유는 매개변수가 2개고 반환을 하나 받을거기때문
         int result = addFunc(3, 5); // 메서드 사용하듯이 똑같이 델리게이트 방식으로 써주면됌
         Console.WriteLine("결과" + result);

         Action<string> printAction = PrintMessage; // 반환값없이 매개변수만 존재하게 할당
         printAction("Hello,World!"); // 매개변수 넣어주며 사용하면된다. 
 
     }
-------------
마지막 예제는 health 라는 변수를 활용하여 확장성을 늘리는 방법이다.

// 게임 캐릭터 클래스
class GameCharacter
{
    private Action<float> healthChangedCallback; // Action<float>은 내장된 델리게이트 타입
 // healthChangedCallback은 이 타입의 필드를 선언한것이다. 이벤트형식의 특수필드와는 다르다.
// 델리게이트 타입의 필드는 객체를 참조하는 변수다. 현재는 null 상태다.
    private float health; // 일반 변수 health 필드 선언

    public float Health // 해당 health 변수에 대한 프로퍼티를 만든다
    {
        get { return health; } // 값을 줄때는 그냥 준다.
        set 
        {
            health = value; // 값을 받아올때는 health 필드에 할당해준다. 
            healthChangedCallback?.Invoke(health); // 값을 받아올때 healthChangedCallback이 null 값이 아니면, health인자를 받은 구독된 메서드들을 전부 호출한다.
        } // Invoke( );의 경우 event 뿐아니라 일반 델리게이트 필드에 할당된 메서드들도 전부 호출한다.
    }

    public void SetHealthChangedCallback(Action<float> callback) // Action<float> 내장 델리게이트 타입의 callback(임의로명명) 매개변수를 받는다. 
          // 이 매개변수는 델리게이트타입으로 메서드의 참조를 값으로 받는다. 
    {
        healthChangedCallback = callback; // event는 구독을위해선 += 가필요하나 healthChangedCallback은 델리게이트 타입 일반 필드니 =로 받은 인자를 할당받을 수있다.
    }
}

// 게임 캐릭터 생성 및 상태 변경 감지
GameCharacter character = new GameCharacter(); // 객체를 생성해주고
character.SetHealthChangedCallback(health => // 해당객체의 SetHelathChangedCallback() 메서드를 실행하는데, 인자를 참조한 메서드값으로받을수있다.
{            // 여기서는 람다식으로 익명의메서드를 만들어서 받았다.
    if (health <= 0)
    {
        Console.WriteLine("캐릭터 사망!");
    }
});

// 캐릭터의 체력 변경
character.Health = 0; 
//캐릭터의 health 필드에 접근해서 0을 할당하면, 0 값을 받아서
속성의 set 부분의 healthChangedCallback.Invoke(0)을 통해 할당된 메서드를 호출한다
그런데 콜백을 할당시키는 메서드 내용을 보면 
그 메서드 자체에 람다식으로 애초에 인자를 받아서 메서드를 실행을 한번하게끔 코드를 작성했고
실행하는 순간 해당 인자가 healthChangedCallback 델리게이트 필드에 할당된다.
healthChangedCallback = health => { if (health <= 0)
    {
        Console.WriteLine("캐릭터 사망!");
    }
}

0을 넣어보면 <=0 조건식이 성립하므로
캐릭터 사망! 이라는 출력을 뱉어낸다.

-------------
매개변수 란에 일반메서드를 넣을 수는 없으나 델리게이트형식의 메서드 참조는 값으로 넣을 수있다.

public void MyMethod(int number)
{
 Console.WriteLine("Number: " + number);
}

이런 메서드가있고

public void ProcessNumber(Action<int> callback)
{
callback(42);
}

이런식으로 짜넣으면, Action<int> callback 란에 MyMethod를 집어넣어 줄수있다.
호출할때
ProcessNumber(MyMethod); 하면 
C# 컴파일러는 MyMethod를 Action<int> 델리게이트로 자동 변환하여 전달한다.
-------------
Player player = new Player();
는 Player 클래스의 객체를 생성해서 player라는 변수에 할당하여 참조하도록 한다.
player는 객체 참조 타입의 변수라고할수있다.
마찬가지로 델리게이트 필드는 예를들어 Private Action<int> HealthChangedCallback 은
Player player 와 같은 객체 참조타입의 변수다. 상단에서는 선언만해서 null인상태다.
Public event GameEvent OnGameStart 도 마찬가지인것이다.

--------------------
LINQ 
.Net 프레임워크에서 제공되는 Query 언어의 확장임

구조
var result = from 변수 in 데이터소스
             [where 조건식]
             [orderby 정렬식 [, 정렬식...]]
             [select 식];

- **`var`** 키워드는 결과 값의 자료형을 자동으로 추론합니다.
- **`from`** 절에서는 데이터 소스를 지정합니다.
- **`where`** 절은 선택적으로 사용하며, 조건식을 지정하여 데이터를 필터링합니다.
- **`orderby`** 절은 선택적으로 사용하며, 정렬 방식을 지정합니다.
- **`select`** 절은 선택적으로 사용하며, 조회할 데이터를 지정합니다.


예제

// 데이터 소스 정의 (컬렉션)
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 쿼리 작성 (선언적인 구문)
var evenNumbers = from num in numbers
                  where num % 2 == 0
                  select num;

// 쿼리 실행 및 결과 처리
foreach (var num in evenNumbers)
{
    Console.WriteLine(num);
}
----------------------------

고급 자료형 및 기능

Nullable 형

// Nullable 형식 변수 선언
int? nullableInt = null;
double? nullableDouble = 3.14;
bool? nullableBool = true;

// 값 할당 및 접근
nullableInt = 10;
int intValue = nullableInt.Value;

// null 값 검사
if (nullableDouble.HasValue)
{
    Console.WriteLine("nullableDouble 값: " + nullableDouble.Value);
}
else
{
    Console.WriteLine("nullableDouble은 null입니다.");
}

// null 병합 연산자 사용
// nullableInt ?? 0과 같이 사용되며, nullableInt가 null이면 0을 반환합니다.
int nonNullableInt = nullableInt ?? 0;
Console.WriteLine("nonNullableInt 값: " + nonNullableInt);

--------------

문자열 빌더

StringBuilder sb = new StringBuilder();

// 문자열 추가
sb.Append("Hello");
sb.Append(" ");
sb.Append("World");

// 문자열 삽입
sb.Insert(5, ", ");

// 문자열 치환
sb.Replace("World", "C#");

// 문자열 삭제
sb.Remove(5, 2);

// 완성된 문자열 출력
string result = sb.ToString();
Console.WriteLine(result);



---------------------
조건문 라이브강의


if와 switch 문에 대해서 알아보자

if문은 거의 모든 프로그래밍 언어에서 사용되는 제어문이다.

if문 코드 스타일 2개

BSD 스타일(Allman 스타일)
if(조건식)
{

}
else
{

}

c# 언어는 이걸 권장함

K&R스타일(InLine 스타일)
if (조건식) {

} else {

}

자바 같은애들은 이런 스타일을 쓰는데 c#은 쓰지않길 권한다.

BSD 스타일은 코드는 길어질수있지만 가독성이 좋다.

예제

bool isGameOver =false;
if(isGameOver)
{
 Console.WriteLine("게임이 종료되었습니다.");
}

불린타입이 변수면 소괄호안에 바로 표기안해도 식별되게 넣을수있다.
이건 isGameOver == true 일때 라는 뜻이다.

그다음

bool hasKey = false;

if (hasKey == true)
 Console.WriteLine("열쇠를 가지고 있습니다."); 

이런식으로 if문 조건식 안에 true라고 명시하는경우도 있다. 가독성면에서 유리할수도

그리고 중괄호가 없다. 라인이 한줄이면 바로 라인 다음을 실행하게 할수있다
그러나 중괄호 블럭을 써주는게 가독성면에서 유리하다.
return 같은게 바로 있다면... 중괄호 안할수잇겠지만
추가적인 내용을 작성할 수 있으니 웬만하면 중괄호를 써주는 습관을 가지자.

bool isPlayerDead = false;

if(isPlayerDead ==false)
{

}
if(!isPlayerDead) // true로 들어있는값을 바꿔준다. 위에 조건문과 같은 역할이다. isPlayerDead ==false 와 같은 뜻
{


}

보통 bool 타입의 변수는 부정형으로 쓰지말자. 헷갈린다.
! (not)을 붙일때 진짜 헷갈린다.

bool isGameOver = false;

if(isGameOver) / isGameOver가 true라면
{

}
else // false 라면
{

}

마찬가지로 라인한줄일경우엔  중괄호 생략하고  가능함. 그치만 중괄호써주자.

알아둘것.
= 는 대입연산자, 할당하는것
==는 같냐라는 비교연산자.

if- else if- else 구문

if(조건식1)
{

}
else if(조건식2)
{
 조건식1이 만족안할때 체크
}
else
{
 조건식 1, 2 모두 만족안할때 체크
}

...
비교연산자의 경우 범위를 잘 체크하자.
>

같은것들 특히.

삼항연산자

if-else를 간단하게 표현한다

변수 = (조건식)? 참일때값 : 거짓일때 값
단순 삼항연산자의 경우 코드의 가독성이 높아짐
하지만 복잡한 조건문을 사용할 경우는 가독성 떨어짐

int playerHealth = 30;
string msg;

msg = (playerHelath>0)? "플레이어 생존" : "게임 오버";
Console.WriteLine(msg); // 플레이어 생존을뱉어낸다.


switch문 

if문으로 다 가능한데, if elseif 등으로 하면 지저분해질수있는 코드를 간결하게 제어해보자라는 취지에서
쓸수있음

switch (변수)

{
 case 값1:
// 값1과 일치할때 실행되는 구문
break; 바로 스코프밖으로 빠진다 일치할경우
case 값2:
//
break;
default:
//
break;
}

예제

int weaponId= 2;
string weaponName;

switch (weaponId)
{
case1:
weaponName ="검";
break;

case2:
weaponName = "활";
break;

case3;
weaponName ="도끼";
break;
...
}

만약 case2의 break를 삭제하면 2를 골라도
weaponName에 도끼를 장착하고 
빠져나올수있다 case3을 적용해버리는거다
따라서 의도된게아니라면 case, break를 전부 작성하는 습관을 기르자.

활을 뱉어낼것.

case grouping도 가능하다

char buttonPressed = 'A';
string action;

switch (buttonPressed)
{
case 'W':
case 'w':
action = "앞으로 이동";
break;
...
}

대문자 소문자 모두 눌러도 되게 그룹핑된다.

한글일경우 문자열로 받는것으로 해서 다 받게해야함

-------------------------
그다음 주요 예제다

   namespace project2
{

    internal class Program
    {





        static void Main(string[] args)
        {
            var gameLogic = new GameLogic();
            gameLogic.StartGame();

        }

    }

    class GameLogic
    {
        private Player _player;
        private bool _isGameOver = false;
        public void StartGame()
        {
            Init();
            while(!_isGameOver)
            {
                InputHandler();
            }
            Console.WriteLine("게임이 종료되었습니다.");
        }

        private void InputHandler()
        {
            var input = Console.ReadKey();// var은 ConsoleKeyInfo 라는 타입
                                          // ConsoleKeyInfo는 구조체,
                                          // Console.ReadyKey()메서드는 Console 클래스의 정적메서드,
                                          // 콘솔창에서 키 입력을 받아 그결과를 ConsoleKeyInfo 구조체에 반환함,
                                          // 현재는 Input이라는 인스턴스를 생성해 그행위를 할당하였음
            if(input.Key == ConsoleKey.Escape) // ConsoleKeyInfo의 key는 프로퍼티, 누른키의 종류를 반환
                                               // 여기서 input 변수는 ConsoleKeyInfo 타입 인스턴스고 key속성은 ConsoleKey 열거형 타입
                                               // ConsoleKey는 콘솔프로그램에서 사용하는 키들의 집합 열거형 정의;
                                               // 정리하자면 input은 구조체, 그안에 적은 key는 열거형 ConsoleKey 타입 프로퍼티
                                               //Escape는 그 열거형(enum) 내용 중 하나. ESC를누른경우
            {
                _isGameOver = true;
            }

            //_isGameOver = (input.Key == ConsoleKey.Escape); 위의 로직을
            // 삼항연산자 아래 한줄로도 가능하다 여기선 true , false로 대응되니 심지어 뒷부분 ? 참값 : 거짓 값도 생략된다.

        }

        private void Init()
        {
            Console.Clear();
            Console.WriteLine("스파르타 던전에 오신것을 환영합니다.\n 이름을 입력하세요");
            string? playerName = Console.ReadLine(); // ?를 써주면 null이 들어갈수있다라는 뜻임

            if (_isGameOver) return; // 중괄호 생략함

            if(string.IsNullOrEmpty(playerName))
            {
              Console.WriteLine("잘못된 이름입니다.");
                Thread.Sleep(1000); // 잠깐 wait 해주는 기능 동기화되서 1초 기다림, 오류메시지를 인지하게 시간을준다.
                Init(); // 실제로 이렇게 사용하면 안됌. 재귀호출,
                        // 실전에서는 while 반복문으로 해결하는게 메모리문제를 방지한다.
       
            }
            else
            {
                _player = new Player(playerName); // Player 객체 _player를 생성하면서
                                                  // 생성자 호출 할때 입력값을 받아서 인자로 넣은게
                                                  // 객체의 변수가된다. 

                Console.WriteLine($"{_player.name}님, 입장하셨습니다.");
            }


            //별도의 메서드가 필요하긴하다만
            //직업선택
            Console.WriteLine("직업을 선택하세요. [1:전사 | 2:법사 | 3:궁수]");
            int job = int.Parse(Console.ReadLine());

            if(job >=1 && job <=3) // if(job is >=1 and <=3) 이게 C# 8.0부터 지원해줌 패턴매칭기술
            {
                _player.job = (Job)job; // 열거형 Job 타입으로 선언한 _player.job에 Job으로 형변환한 job(입력값 int로 받은) 할당

                switch (_player.job) // 그 할당값이 뭐냐에 따라서(여기선 1,2,3중 하나 )
                {
                    case Job.Warrior://case 뒤에 1이라고 하면 나중에 잘모를수있음 특히 협업때. enum으로 연결해준다.
                                     //여기서 내부적으로 Job enum에 Warrior 에는 상수 1이 지정되어있다.
                                     // 실질적으로 _player.job이 1이 할당된다면 Job enum 에서 Warrior을 가르킨다.
                        Console.WriteLine($"{_player.job}를 선택했습니다."); //ctrl+d로 복사하고 alt 화살표 아래 누르면 복사해서 내려가짐
                        break;
                    case Job.Wizzard:
                        Console.WriteLine($"{_player.job}를 선택했습니다.");
                        break;
                    case Job.Archor:
                        Console.WriteLine($"{_player.job}를 선택했습니다.");
                        break;
                }
            }
        }

    }

    class Player
    {
        public string name; //원래라면 프로퍼티로 할텐데 지금은 생략
        public Job job;
        public Player(string name) // 생성자 constructor
        {
            this.name = name; // 프로퍼티아니고 그냥 필드에 때려박음
        }
    }
        

    public enum Job
    {
        Warrior =1,
        Wizzard,
        Archor
    }