다중 상속은 이름이 같은 멤버가 있을 때 어떤 부모 클래스의 것을 사용해야 하는지 모호해지고, 이렇게 이름이 충돌했을 때 이를 명확히 해주어야 하기 때문에 코드가 복잡해진다. 따라서 단일 상속을 통해서 관계를 명확하고 단순하게 해서 코드의 가독성과 이해도를 높인다. 하지만 경우에 따라서 다중 상속이 필요할 때도 있다. 이럴 때는 인터페이스라는 개념을 사용한다.
[ 인터페이스 ]
public interface IItemPickable
{
void PickUp();
}
public interface IUseable
{
void Use();
}
public class Item : IItemPickable, IUseable
{
public string Name { get; set; }
public void PickUp()
{
Console.WriteLine("아이템 {0}을 주웠습니다.", Name);
}
public void Use()
{
Console.WriteLine("아이템 {0}을 사용했습니다.", Name);
}
}
public class Player
{
public void PickUpItem(IItemPickable item)
{
item.PickUp();
}
public void UseItem(IUseable item)
{
item.Use();
}
}
static void Main()
{
Player player = new Player();
Item item = new Item { Name = "Health Potion" };
player.PickUpItem(item);
player.UseItem(item);
}
인터페이스는 클래스가 구현해야 할 멤버들을 정의한 것으로, 인터페이스에서는 선언만 포함하며 구체적인 행동은 클래스에서 구현한다. 인터페이스를 상속받은 클래스는 반드시 모든 인터페이스 멤버를 구현해야 하며, 다중 상속이 가능하다. 인터페이스명은 맨 앞에 대문자 i(I)를 붙여준다.
다른 클래스에서 인터페이스를 통해 동일한 기능을 공유하여 코드를 재사용하고, 다중 상속을 통해 여러 개의 기능을 조합할 수 있다. 또한 클래스에서 내부에서의 변경 없이 인터페이스를 구현함으로써 유연하고 확장 가능한 설계가 가능하게 한다.
[ enum ]
enum Week
{
Mon, //0
Tue, //1
Wed, //2
Thu, //3
Fri, //4
Sat, //5
Sun, //6
}
static void Main(string[] args)
{
Week enumWeek = Week.Tue; //Tue
}
상수들에 의미 있는 이름을 사용해서 하나로 그룹화함으로써 더 의미 있게 프로그래밍을 할 수 있다. 만약 enum이 아닌 배열이었다면, Week[1]로 값을 할당했을 것이다. 하지만 1이라는 것은 정확히 어떤 것을 의미하는지 알 수 없다. 하지만 위의 코드처럼 enum을 사용하면, 어떤 값을 사용하고 있는지 바로 알 수 있다는 장점이 있다.
열거형을 정의할 때 기본값은 0부터 시작해서 1씩 증가하는 정수가 들어있다. 상수값을 지정할 때는 할당을 하면 되고, 상수값이 지정되지 않은 열거형은 앞 상수값에 1을 더한 값이 된다.
enum은 메서드 내에서는 정의될 수 없고, 클래스, 구조체, 네임스페이스와 같은 넓은 스코프 내에서만 정의할 수 있다.
int intWeek = (int)Week.Tue; //1
Week enumWeek = (Week)intWeek; //Tue
형변환은 앞에 소괄호에 자료형을 적어준다. 열거형 → 정수는 int, 정수 → 열거형은 열거형의 타입명을 적는다.
[ 델리게이트 ]
public delegate void EnemyAttackHandler(float damage);
public class Enemy
{
public event EnemyAttackHandler OnAttack;
public void Attack(float damage)
{
OnAttack?.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;
enemy.Attack(10.0f);
}
델리게이트는 메서드를 참조하는 타입으로, 메서드를 매개변수로 전달하거나 변수에 할당할 수 있다. 한 번에 여러 개의 메서드를 등록하고 호출하는 것도 가능하다.
위 코드를 가지고 델리게이트가 작동되는 과정을 자세히 알아보자.
- float 타입의 damage를 매개변수로 받는 메서드를 참조하는 EnemyAttackHandler 델리게이트를 선언한다. 이는 즉 float 타입 파라미터를 받고 void를 반환하는 어떤 메서드든 참조할 수 있다는 것을 의미한다.
- Enemy 클래스에서 EnemyAttackHandler 델리게이트 타입을 사용하여 OnAttack이라는 메서드를 선언한다. event 키워드를 통해 OnAttack 메서드가 호출됐을 때 이 메서드를 구독하고 있는 클래스나 객체에게 알림을 보낸다.
- ? 연산자를 이용해서 Attack 메서드가 호출 됐을 때, OnAttack의 구독자가 있다면 구독자(메서드)를 호출하고 damage 파라미터를 전달한다. 구독자는 해당 값을 받아서 본인 메서드에서 사용할 수 있다. 구독자가 없다면 OnAttack 이벤트는 호출되지 않는다.
- Player 클래스에서 damage를 파라미터로 받는 HandleDamage 메서드를 생성한다.
- HandleDamage가 OnAttack 메서드를 구독함으로써, OnAttack 메서드가 호출됐을 때 구독자인 HandleDamage도 호출되어 받은 damage 파라미터를 처리한다.
[ 람다 ]
delegate void Mydelegate(string message);
static void Main(string[] args)
{
//한 줄로 표현
Mydelegate mydelegate = (message) => Console.WriteLine("메시지: " + message);
//중괄호로 표현
Mydelegate mydelegate = (message) =>
{
Console.WriteLine("메시지: " + message);
};
mydelegate("내일배움캠프 12일차");
}
//출력결과
메시지: 내일배움캠프 12일차
람다 표현식은 메서드를 정의하지 않고 동작을 표현하는 익명 메서드를 만드는 방법이다. 메서드의 이름이 없기 때문에 간결하게 표현할 수 있다.
왼쪽 소괄호 내의 파라미터를 가지고 수행할 동작을 => 연산자 다음에 작성한다.
한 줄이라면 중괄호 생략하고 바로 옆에 작성할 수 있고, 여러 줄이라면 다른 함수들처럼 중괄호 안에 작성하면 된다. 조금 다른 점은 중괄호 마지막에 세미콜론을 붙여줘야 한다.
[ Func, Action ]
Func와 Action은 델리게이트 대신 사용할 수 있는 제네릭 타입이다.
int Add(int x, int y)
{
return x + y;
}
Func<int, int, int> addFunc = Add;
int sum = addFunc(1, 2);
Console.WriteLine("합계: " + sum);
//출력결과
합계: 3
Func은 반환값이 있는 메서드를 참조한다. 매개변수는 2개 이상 받을 수 있고, 첫 번째는 입력 받는 값의 타입이고 두 번째는 반환하는 값의 타입이다. 3개부터는 맨 마지막 1개만 반환하는 값의 타입이다. 예를 들어 Func<int, string>이면 int 값을 받아서 string 값으로 반환하는 것이다.
void PrintMessage(string message)
{
Console.WriteLine("메시지: " + message);
}
Action<string> printAction = PrintMessage;
printAction("TIL을 꾸준히 작성합시다.");
//출력결과
메시지: TIL을 꾸준히 작성합시다.
Action은 반환값이 없는 메서드를 참조한다. 리턴 값이 없을 때 사용한다. Func처럼 입력 매개변수를 받을 수는 있지만, 반환 매개변수는 없다. 예를 들어 Action<string>은 string 값을 입력받아 출력할 수는 있지만, 해당 값을 리턴할 수는 없다.
[ LINQ ]
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var divTwo = from num in numbers
where num %2 == 0
orderby num
select num;
foreach (int num in divTwo)
{
Console.WriteLine(num);
}
//출력결과
2
4
LINQ는 'Language INtegrated Query'의 약자로, .NET 프레임워크에서 제공하는 데이터를 쿼리하는 방법이다.
조건문과 반복문을 사용하는 대신, 데이터 필터링, 정렬, 그룹화 등의 기능을 통해 데이터 로직을 쉽게 정리할 수 있다.
LINQ를 사용하면 코드가 훨씬 깔끔해지지만, 오남용하면 성능 부분의 문제와 다른 사람들이 이해하기 힘들어질 수 있다.
변수를 선언할 때는 컴파일러가 자동으로 타입을 추론할 수 있는 var 타입을, 특정 자료형을 사용하려면 List<int>와 같은 컬렉션 타입을 작성한다.
for은 데이터를 담을 변수명, in은 데이터를 가져올 변수명이다.
where은 조건을 만족하는 요소를 필터링한다. orderby는 결과를 정렬하는 방법이다. 기본값은 오름차순으로 되어있고, 내림차순으로 정렬하려면 뒤에 descending 키워드를 적어준다. select는 반환할 결괏값이다.
[ Nullable ]
//변수 선언
int? nullableInt = null;
string? nullableStr = null;
bool? nullableBool = true;
nullableStr = "안녕"; //할당
string strValue = nullableStr; //접근
if (nullableBool.HasValue)
{
Console.WriteLine("nullableBool값: " + nullableBool.Value);
}
else
{
Console.WriteLine("nullableBool 값은 null 입니다.");
}
//null 병합 연산자
int intValue = nullableInt ?? 5;
Console.WriteLine("nullableInt값: " + intValue);
null은 아무것도 없음을 의미한다. 값형은 null을 허용하지 않지만, Nullable을 이용해서 값형 변수에 null 값을 가질 수 있도록 한다. 이를 통해 값형 변수가 null인지 아닌지를 확인할 수 있다.
Nullable 형식은 자료형 뒤에 ? 연산자를 입력한다. null 병합 연산자는 할당할 때 변수명 ?? 반환값으로 사용되는데, 변수가 null이면 ?? 뒤의 값을 반환한다.
[ 회고 ]
델리게이트라는 개념을 이해하는 데 어려움이 있었다. 클래스처럼 다양한 함수들을 포함하고, 그 안에서 호출하는 것으로 오해하고 있어서 구조를 쉽게 이해하지 못했다. 하지만 동일한 구조의 함수를 참조하고 이벤트 키워드를 이용해서 트리거를 발생시키는 것까지 알게 되었다.
enum은 특강에서도 몇 번 들었었는데, 개념을 알고 보니 생각보다 더 유용하게 쓰이는 기능이었다. 텍스트 게임을 만들 때에도 직관적으로 코드를 작성할 수 있게 해주는 장점이 있었다.
'Coding > C#' 카테고리의 다른 글
[내일배움캠프 14일차 TIL] 추상 클래스, 인터페이스 (0) | 2024.05.02 |
---|---|
[내일배움캠프 13일차 TIL] 클래스 메모리 구조, 기본값 세팅, 박싱/언박싱 (0) | 2024.05.01 |
[내일배움캠프 11일차 TIL] 배열과 리스트 비교, 로직 설계 방법 (0) | 2024.04.29 |
[내일배움캠프 10일차 TIL] 멤버, 클래스 상속, 유니티 라이프 사이클 (0) | 2024.04.26 |
[내일배움캠프 9일차 TIL] Txt 게임 만들기 (1) | 2024.04.25 |