오늘 한 일
- 스파르타 코딩클럽 진도 (~5-2)
- 유니티 4기 등급 배치고사 보기
- 집중 강의 시간 Day7 - 인터페이스 / 추상클래스
인터페이스 - learn.microsoft에서의 설명
- 인터페이스는 계약(contract)을 정의합니다. 해당 인터페이스를 상속 받은 클래스, 레코드(record), 구조체는 인터페이스에 정의된 멤버들의 구현을 해야합니다. 인터페이스는 멤버에 대한 기본 구현을 정의할 수 있습니다. 공통적인 기능에 대해 단일 구현을 제공하기 위해 static(정적) 멤버를 정의할 수도 있습니다. C# 11부터는 static abstruct 또는 static virtual 멤버를 정의할 수 있습니다.
살짝 입맛대로 해석했다
(! 참고로 유니티 2022.3.24f1의 버전은 C# 9이다 !)
유니티 C# 컴파일러와 C# 9 일부 미지원 기능에 대한 설명
인터페이스의 선언과 사용하는 기본적인 방법
// 인터페이스 선언 방법 Interface ISampelInterface // 앞에 I를 붙이는 게 기본적인 컨벤션이다. { void SampleMethod(); } // 인터페이스 상속 받기 class ImplementationClass : ISampleInterface { // 반드시 인터페이스의 멤버들을 구현해야한다! void ISampleInterface.SampleMethod() { // 메소드 구현 } static void Main() { // 인터페이스 인스턴스 선언 ISampleInterface obj = new ImplementationClass(); // 멤버호출 obj.SampleMethod(); } }
인터페이스에서 정의가능한 멤버 목록
- 기본 액세스는 public이다
internal class Program { static void Main(string[] args) { IInterface iInterface = new Class(); iInterface.HelloWorld(); } } interface IInterface { void HelloWorld() { Console.WriteLine("Hello World!"); } } public class Class : IInterface // 이미 본문 선언이 된 메서드는 강제구현 사항이 아니다. { }
- 출력 결과
- 위와 같이 아무런 키워드를 붙이지 않아도 인터페이스 외부에서 꺼낼 수 있다!
클래스가 인터페이스를 상속 받을 때 지켜야하는 것
- 인터페이스로부터 상속 받은 멤버들을 구현할 때에는 public을 붙여야 원활하게 작동된다! (interface의 기본은 public 클래스의 기본은 private)
- 출력 결과 (상속 받은 클래스에 public을 안 붙인 경우)
- public을 붙인경우
추가적으로 발견한 사실
- 인터페이스에서 메서드를 정의할 때 ()에서 ; 끝맺음을 하지 않고 {}만 추가해버리면 그것도 본문 선언으로 인식하게 된다. (인터페이스에서 본문 선언이 된 메서드는 강제 구현할 필요가 없다.)
- 보면은 HelloWorld 메서드에 대한 오류가 발생하지 않았다..
- 본문 선언 기준을 알게 된 이유..
- 인터페이스를 상속받은 클래스들은 인터페이스의 멤버에 override 사용이 불가능하다 (정확하게 말해서는 인식을 못한다.)
- 인터페이스의 메서드를 구체화하는 과정의 '재정의'와
상속 받은 클래스가 부모 클래스의 메서드를 재정의하는 '오버라이딩'과는 약간 다르다고 한다.- 단순히 생각해보면 인터페이스에서 본문 선언된 메서드는 virtaul의 특징을 가지고 본문이 선언되지 않은인 메서드는 abstract의 특징을 가지는 것이다. 이미 인터페이스만의 특징을 가진 상태에서 추상 및 가상 클래스에 사용되는 override를 가져다대면 컴파일러가 못 찾는 것 같다.
- 동일한 기능이나 특징을 가진 애들을 하나로 묶어서 특정 메서드에 확정성 있게 사용하기 위해서 사용된다.
이를 바탕으로 시험삼아 만들어본 코드 (물약얻기 및 사용 코드입니다.)
internal class Program { static void Main(string[] args) { Player player = new Player(); bool playing = true; while (playing) { Console.Clear(); Console.WriteLine("테스트용 CS파일입니다."); Console.WriteLine("원하는 것을 고르세요"); Console.WriteLine(); Console.WriteLine("1. 스텟 보기"); Console.WriteLine("2. 인벤토리 보기 및 사용"); Console.WriteLine("3. 아이템 얻기"); Console.WriteLine("0. 종료하기"); bool isInt = int.TryParse(Console.ReadLine(), out int select); if (!isInt) { Console.WriteLine($"제대로 입력하세요"); continue; } switch (select) { case 0: Console.WriteLine("그럼 안녕히..."); playing = false; break; case 1: player.ShowStatus(); break; case 2: player.UseItem(); break; case 3: player.GetItem(); break; default: Console.WriteLine($"제대로 입력하세요"); break; } } } } // 플레이어 정보 public class Player { //인터페이스의 주요 활용처 1 List<IItem> inventory = new List<IItem>(); // 플레이어의 가방 int hp; public int Hp { get { return hp; } set { hp = value; } } int atk; public int Atk { get { return atk; } set { atk = value; } } public Player() { hp = 100; atk = 10; } public void ShowStatus() { Console.Clear(); Console.WriteLine("[ 플레이어의 스텟 ]"); Console.WriteLine($"현재 체력 : {Hp}"); Console.WriteLine($"현재 공격력 : {Atk}"); Console.WriteLine(); Console.WriteLine("돌아가려면 아무키나 입력하세요"); Console.ReadKey(); } public void UseItem() { Console.Clear(); if (inventory.Count == 0) { Console.WriteLine(" 보유 중인 아이템이 없습니다! "); Console.WriteLine(); Console.WriteLine("계속하려면 아무키나 입력하세요"); Console.ReadKey(); } else { bool selecting = true; while (selecting) { Console.Clear(); Console.WriteLine(" 보유 중인 아이템 "); for (int i = 0; i < inventory.Count; i++) { Console.Write(i + 1); inventory[i].ShowName(); } Console.WriteLine(); Console.WriteLine("사용하고 싶은 포션을 고르세요."); Console.WriteLine(); Console.WriteLine("0. 나가기"); bool isInt = int.TryParse(Console.ReadLine(), out int select); if (!isInt) { Console.WriteLine($"제대로 입력하세요"); continue; } else if (select == 0) { break; } else if (select <= inventory.Count) // 이와 같이 짜면 스위치 문처럼 case를 무진장 넓히지 않아도 되서 좋다. { inventory[select - 1].UseItem(this); inventory.RemoveAt(select - 1); Console.WriteLine("계속할려면 아무키나 입력하세요"); Console.ReadKey(); } else { Console.WriteLine("잘못된 입력입니다!"); Console.WriteLine("계속할려면 아무키나 입력하세요"); Console.ReadKey(); } } } } public void GetItem() { Console.Clear(); Console.WriteLine("# 가져가고 싶은 아이템을 고르세요! #"); Console.WriteLine($"1. 체력 포션 - 체력을 회복합니다."); Console.WriteLine($"2. 근력 포션 - 공격력을 올려줍니다."); Console.WriteLine(); Console.WriteLine($"0. 됐어요."); bool selecting = true; while (selecting) { bool isInt = int.TryParse(Console.ReadLine(), out int select); if (!isInt) { Console.WriteLine($"제대로 입력하세요"); continue; } switch (select) { case 0: selecting = false; break; case 1: inventory.Add(new HealthPotion()); Console.WriteLine($"체력 포션 획득!"); break; case 2: inventory.Add(new StrengthPotion()); Console.WriteLine($"근력 포션 획득!"); break; default: Console.WriteLine($"제대로 입력하세요"); break; } } } } // 아이템 인터페이스 interface IItem { void UseItem(Player player); // 인터페이스 주요 활용처 2 void ShowName(); } // 아이템을 상속받은 체력 포션 public class HealthPotion : IItem { public string name = "체력 포션"; public void UseItem(Player player) { player.Hp += 30; Console.WriteLine("30의 체력이 회복되었습니다."); } public void ShowName() { Console.WriteLine($"{name}입니다. 체력을 올려줍니다."); } } // 아이템을 상속받은 근력 포션 public class StrengthPotion : IItem { public string name = "근력 포션"; public void UseItem(Player player) { player.Atk += 10; Console.WriteLine("10의 공격력이 올랐습니다."); } public void ShowName() { Console.WriteLine($"{name}입니다. 공격력을 올려줍니다."); } }
코드 리뷰하면 진짜 지적하고 싶은 부분이 많긴 한데..
- 어쨌든 핵심만 짚어주자면 인벤토리와 아이템 사용에 대한 이 둘의 측면을 생각해보면 된다.
만약에 인벤토리에 담을 때 인터페이스를 활용하지 않고, 필드나 Array를 활용한다면 새로운 아이템이 생길 때마다 또 다시 그 부분에다가 추가적인 업데이트 사항을 수정해줘야한다.
// arr를 이용한 방식 int[] consumableItems = { 0, 0 } // 0번이 체력 포션 1번이 근력 포션일 때 만약에 인벤토리가 배열과 같은 방식인 상황에서 여기서 소모품 하나가 추가된다면 일일이 여기를 들어와서 또 추가시켜야된다. 만약에 한 두 개 정도의 소규모 종류 추가면 몰라도 10개 이상의 종류가 추가되버리면 이 방식의 확장성은 좋지 않는 방법이다. (장점이라면 소모품 종류 하나의 갯수를 리스트보다는 많이 받을 수 있지만, 종류에 확장성에 한해서는 리스트 인벤토리 방식을 사용하는 것이 좋다.) - 물론 제일 좋은 건 게임 기획의 의도에 따라서 더 간편하고 확실한 걸 선택해주면 된다.
- 아이템 사용 면에서는 서로 같은 상속을 받지 않은 다른 클래스일 경우에는 그때마다 메서드를 추가시키고 조건문으로 분류해줘야 하는 등 뭔가의 과정을 거쳐줘야하지만, 인터페이스를 활용한다면
UseItem()
으로 하나로 통일시켜서 굳이 추가적인 메서드를 만들지 않아도 바로 사용이 가능하고, 효과나 출력과 같은 구체적인 사항만 신경써주면 된다.
물론 내가 사용하는 방식보다 더 좋은 방식이 있으니 인터페이스를 잘 활용할 수 있다면 무궁무진한 시스템들을 만들어낼 수 있을 것이다.
선택적 매개변수
void Method(int i = 0) {}
이와 같은 방식으로 매개변수에 기본값을 지정해줄 수 있는데 이러면 매개변수 전달을 선택적으로 해줄 수 있다. 지켜야할 사항은 모든 필수 매개변수 이후에다가 적어줘야하는 것이다. (CS1737)
- 오류 사진
빅오 표기법 계산법
- 그냥 무식하게 n이 늘어날 때마다 그만큼 실행 횟수(시간 복잡도)와 연산에 활용되는 메모리 사용량(공간 복잡도)이 어떻게 변화하게 되는지를 계산해주면 된다.
for(int i = 0; i < n; i++)
라는 for문을 보면 n이 늘어날 때마다 for문의 실행 횟수가 똑같이 늘어나므로 시간 복잡도는 O(n)에 해당하고, 공간 복잡도를 계산할 때는int[] arr = new int[n];
와 같이 로직에서 새로운 공간을 사용하게 되면은 n이 늘어날 때마다 arr의 크기도 늘어나므로 공간 복잡도는 O(n) 그러니까 n의 요소라는 것부터 잘 찾고 그게 늘어날 때마다 어떻게 변화하는 지를 잘 보면 빅오 계산법의 원리는 이해가 될 것이다. (재귀 호출의 호출 스택 등등 이런 요소들도 초기에는 놓치기 쉽다.)
TIL을 작성하는데 너무 시간을 과하게 쓴 것 같다. 물론 아는 게 아직 부족한만큼 학습 방식이 효율적일 수 있을거라 생각은 안하지만, 차라리 새벽에 집중할 시간에 아침 시간대에 더 집중했으면 하는게 내 스스로에게 하는 바램이다.
아이템 코드를 작성해볼 때 밑바닥부터 깔면서 아.. 술술 적어지기는 하는데 막상 완성했을 때 지적할 게 한 두 가지가 아니라는 점에서 난 아직 많은 부족함을 느낀다. 뭔가 좋은 학습 방법이라는 건 밑바닥에서부터 시작하는 코딩 방법이 아니라 기존의 좋은 코드를 가져와서 해석하고 나만의 방식으로 만드는 게 어쩌면 좋은 코드를 짤 수 있는 좋은 방법이라고 생각한다.
그리고 코드를 짤 때 절대로 기획의 힘을 무시하지 말자. (어떻게 기획을 시작하느냐에 따라서 코드의 퀄리티가 천차만별로 갈리는 것 같다.)