남은 4주차 강의 다 듣기 성공 🙌
[델리게이트]
델리게이트
델리게이트란?
메서드를 참조하는 타입(객체)로, 메서드를 변수에 저장하듯이 활용할 수 있다.
메서드는 참조타입 또는 값타입과 같은 분류에 속하지 않는다. 따라서 메서드의 인수로 전달하는 등의 실행이 불가능하다. 이럴 때 델리게이트 객체를 활용하면 메서드를 저장하여 인수로서 작용하도록 하거나 변수로 참조하는 것이 가능해진다.
예제
// 델리게이트 선언
public delegate void EnemyAttackHandler(float damage);
// 적 클래스
public class Enemy
{
// 공격 이벤트
public event EnemyAttackHandler OnAttack;
// 적의 공격 메서드
public void Attack(float damage)
{
// 이벤트 호출
OnAttack?.Invoke(damage);
// null 조건부 연산자
// null 참조가 아닌 경우에만 멤버에 접근하거나 메서드를 호출
}
}
// 플레이어 클래스
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);
}
- 델리게이트 선언: deligate 반환타입 사용자 정의 자료형 명 (파라미터)
- 여기서 사용자 정의 자료형 명은 그냥 내가 원하는 이름을 갖다 붙이면 이게 하나의 타입이 된다는 뜻
- 델리게이트를 선언하면, 메서드 참조 객체를 여러 개 만들 수도 있고, 하나의 델리게이트 객체가 여러 개의 메서드를 연결해 한 번의 행위에 의해 처리해야 하는 여러 실행을 전부 예약 걸어둘 수 있다.
OnAttack?.Invoke(damage);
여기서 OnAttack?부분은 이 델리게이트가 null이 아닌 경우에만 연산 뒤의 메서드나 속성에 접근하도록 하는 표현이다 (조건부 연산자). 아무런 매서드가 연결되지 않은 상태에서 Invoke를 호출하지 않기 위한 안전장치라고 보면 된다.
Invoke(damage) 부분이 당황스러웠는데, 이전의 예제에서는 Invoke를 명시하지 않은 채로 OnAttack(damage); 이렇게 델리게이트를 호출하여 연결된 메서드들을 실행하도록 했기 때문이다.
- Invoke는 델리게이트의 내부적인 메서드로, 델리게이트에 참조된 메서드를 호출하는데 사용된다. 암시적으로 호출이 가능하기 때문에 델리게이트 인스턴스를 호출함으로써 자동적으로 실행시킬 수 있다.
- 하지만 조건부 연산자와 함께 쓸 경우, 조건부 연산자가 델리게이트의 Invoke 메서드에 접근하게 하기 위해서는 명시적으로 써야 한다. 아래의 두 표현은 모두 동일한 실행을 의미한다.
OnAttack?.Invoke(damage); // Invoke의 명시적인 호출
if (OnAttack != null)
{
OnAttack(damage); // Invoke의 암시적인 호출 (조건부 연산자를 쓸 수 없으므로 조건문 활용)
}
null 조건부 연산자
- 위에서 정리한 Invoke의 명시적 호출과 암시적인 호출 예시를 통해 조건부 연산자의 역할을 파악할 수 있다.
- 해당 조건부 연산자가 붙은 객체가 null인지 아닌지 판단한 후, 뒤의 내용을 실행하게 된다.
event
위의 예제에서는 EnemyAttackHandler라는 델리게이트가 객체화될 때, 일반적인 객체가 되는 것이 아니라 이벤트가 되는 형태를 띄고 있다.
이벤트는 특별한 종류의 델리게이트 객체라고 볼 수 있다. 일반적인 델리게이트 객체는 외부에서의 접근 및 값 조작이 가능한 반면, 이벤트는 외부에서 값을 직접 변경할 수 없으며, 오로지 +=(메서드 추가) 또는 -=(메서드 제거) 연산만 가능하다.
델리게이트는 메서드 참조를 위한 것인 반면, 이벤트는 해당 클래스나 구조체에서 특정 상황이 발생했을 때, 외부에 알림을 보내는데 사용되는 경우가 많다. 즉, 일반적으로 이벤트는 객체의 상태 변화나 특정 사건 발생을 외부에 알릴 때 주로 사용되며 안전성이 높다는 장점을 가지고 있으나, 특정 동작이나 연산을 나타내는 데는 일반 델리게이트 변수가 더 적합하다.
[람다]
람다
람다는 익명 메서드 표현식을 의미한다.
(parameter_list) => expression의 구조로 생겼는데, 여기서 expression 부분(코드부)이 길어질 경우 블록으로 표현할 수 있다. (expression 부분이 메서드의 실행 내용과 같은 구조이고, parameter_list에서 파라미터를 정의한다. 하는 일은 메서드와 같으나, 이름이 없는 것을 확인할 수 있다.)
원데 델리게이트의 활용에서는 델리게이트 선언 후 이를 객체화(이벤트 포함)하여 다른 메서드들을 연결해서 썼는데,
람다는 자신이 생성한 익명 메서드를 객체화된 델리게이트에 바로 전달해서 저장하게 된다.
람다와 메서드를 비교해보면, 람다는 간단한 실행을 요구할 경우 메서드가 필요한 시점에서 일어난 실행들을 바로 확인할 수 있다는 장점이 있고, 메서드는 보다 복잡한 구조를 갖거나 체계성, 구조성이 중요한 경우에 활용할 수 있다.
* 람다의 타입
사실상 람다 자체는 메서드가 아니다. 익명의 메서드를 정의하는 방법 중 하나로, 컴파일 시점에서 익명 메서드로 변환되는 것이다. 그런데 특이한 점은 델리게이트와 호환되는 메서드시그니처를 가질 때(델리게이트와 같은 파라미터 구조라고 생각하면 된다.) 해당 델리게이트 타입의 인스턴스로 변환될 수 있다. (마치 자동 형변환처럼?) 아래의 예제에서 알게된 사실
class GameCharacter
{
public void SetHealthChangedCallback(Action<float> callback) // 함수에서 델리게이트 객체를 파라미터로 받고 있음
{
healthChangedCallback = callback;
}
}
GameCharacter character = new GameCharacter();
character.SetHealthChangedCallback(health => // 함수 호출 과정에서 인자로 람다를 넣고 있음
{
if (health <= 0)
{
Console.WriteLine("캐릭터 사망!");
}
});
// 캐릭터의 체력 변경
character.Health = 0;
예제
using System;
// 델리게이트 선언
delegate void MyDelegate(string message);
class Program
{
static void Main()
{
// 델리게이트 인스턴스 생성 및 람다식 할당
MyDelegate myDelegate = (message) =>
{
Console.WriteLine("람다식을 통해 전달된 메시지: " + message);
};
// 델리게이트 호출
myDelegate("안녕하세요!");
Console.ReadKey();
}
}
[Func & Action]
Func과 Action
Func과 Action은 모두 델리게이트를 대체하는 미리 정의된 제네릭 형식이다.
그냥 델리게이트의 일종인데, 따로 선언할 필요 없이 객체 생성 시에 앞에 붙여주면 된다고 보면 될 듯하다.
Func - 반환값이 있는 델리게이트
Action - 반환 없이 실행만 하는 델리게이트
델리게이트는 메서드를 참조하는 타입이라고 했다. 델리게이트를 통해 여러 메서드를 연결시켜두고, 연속적으로 실행시키는 역할을 한다.
앞에서 정리했듯, 델리게이트 객체에 연결되는 메서드들은 델리게이트가 정의될 때 설정된 구조와 동일한 형태를 띄어야 한다. 반환값이 없는 void인지, int나 string 등을 반환하는 지, 파라미터는 어떻게 받는지 등을 따져 동일한 구조의 메서드만 연결할 수 있다.
Func를 쓰냐 Action을 쓰냐는 여기서 연결하고자 하는 메서드가 return값이 있는, 그러니까 앞에 int, string, bool 이런 데이터타입이 붙었는지, 아니면 return값 없이 void로 선언된 메서드인지에 따라 달라진다. 반환값이 있으면 Func, 없으면 Action
이 두 형식은 Func<int, int>, Action<int, string>이런 식으로 <> 안에 데이터 타입을 설정함으로써 생성되는 델리게이트 객체의 파라미터를 지정한다. Func<T>와 Action<T>는 모두 0 ~ 16개의 인자를 받을 수 있다고 하는데, Func<T>의 마지막 인자는 반환타입이 된다. (연결되는 메서드의 반환타입과 Func의 마지막 파라미터의 타입이 같아야 한다.)
[LINQ]
LINQ
LINQ는 .NET 프레임워크에서 제공하는 쿼리 언어의 확장이다.
쿼리 언어란, 데이터를 검색, 추출, 필터링하기 위한 요청이나 명령을 뜻한다. 따라서 링큐를 활용하면 컬렉션이나 배열, 데이터베이스, XML 등의 데이터가 뭉쳐 있는 자료 구조에게 코드를 통해 쿼리를 던질 수 있다.
(DB 쿼리와 유사한 방식으로 데이터를 필터링, 정렬, 그룹화, 조인 등 다양한 작업이 수행 가능해진다.)
구조
// 데이터 소스 정의 (컬렉션)
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);
}
예제에서 중간의 "쿼리 작성" 부분을 살펴보자.
해당 코드는 numbers라는 리스트에서 값들을 필터링해 evenNumbers라는 변수에 저장하고 있다.
var 키워드를 통해 변수를 선언하는 이유는 LINQ의 경우 반환되는 타입이 복잡하거나 길 수 있기 때문에 암시적 타입 지역 변수를 선언하도록 하는 것이다.
이 때 evenNumbers의 타입이 궁금해져서 (리스트의 요소를 필터링 한 다음 모아서 저장하기 때문에) 찾아보았더니, 구체적인 타입명을 알 수는 없다는 사실을 알게 되었다.
쿼리문을 통해 저장되는 데이터는 '열거 가능한' 혹은 '순회 가능한' 타입의 변수에 저장이 되는데, evenNumbers가 이 '열거 가능한', '순회 가능한' 타입에 해당한다. 이러한 타입은 IEnumerable 인터페이스를 구현하고 있는 클래스인데 (해당 인터페이스가 열거가능한 제약을 걸고 있기 때문), foreach문을 통해 데이터 하나하나를 순회할 수 있게 된다.
IEnumerable 인터페이스는 리스트, 배열 등과 같이 여러 데이터값을 순회하면서 하나씩 살펴볼 수 있는, 즉 foreach문을 사용할 수 있는 타입의 클래스에서 구현된다. 따라서 위 예제의 evenNumbers는 우리가 흔히 쓰는 배열, 리스트와 비슷한 계열의 타입으로 여러 데이터를 보관할 수 있다. (.NET 라이브러리의 내부 클래스이므로 구체적으로 어떤 클래스이며 어떤 특징을 갖는지는 노출되지 않기 때문에 알 수 없다고 한다.)
키워드
- var 키워드는 결과 값의 자료형을 자동으로 추론한다.
- from 절에서는 데이터 소스를 지정한다.
- where 절은 선택적으로 사용하며, 조건식을 지정하여 데이터를 필터링한다.
- orderby 절은 선택적으로 사용하며, 정렬 방식을 지정한다.
- select 절은 선택적으로 사용하며, 조회할 데이터를 지정한다.
[Nullable]
null
null은 참조형 변수가 어떠한 객체를 참조하지 않을 때를 의미한다. 즉, 값 타입의 데이터와는 원래 상관이 없는 말이고, 참조 타입의 변수가 비어 참조를 갖고 있느냐 없느냐에 대한 말이다.
* 여기서 틱택토 만들 때 string 변수가 null일 때의 조건을 걸었는데, string은 참조 형태이기 때문에 가능해했다.
nullable
nullabledms null값을 가질 수 있는 값형에 대한 특별한 형식이다.
값형 변수에 null값을 지정할 수 있는 방법을 제공하는데, 이는 여러 상황에서 유용하게 쓰인다.
- 조건부 사용자를 쓸 때: 값 타입의 데이터를 담고 있는 변수라 할지라도 조건부 사용자를 쓸 수 있게 되어 코드가 간결해진다.
- 값 타입의 경우에는 임의로 값을 할당하지 않으면 자동으로 초기값이 들어가게 되는데, 이러한 초기값을 활용해야 하고, 임의로 어떤 값을 할당하지 않은 상태를 따로 관리할 필요가 있을 때 nullable을 활용할 수 이싿.
- 예를 들어, int 변수를 생성했는데, 음수부터 0, 양수까지 모든 값을 활용할 가능성이 있고, 값을 할당하지 않은 상태를 따로 조건으로 걸 필요가 발생할 수 있다. 이 때, 일반적으로 int에 어떠한 값을 할당하지 않으면 0을 초기값으로 가지고 있는데, 0은 할당될 수 있는 의미 있는 수이기 때문에 이를 조건으로 걸지 못할 수 있다. 이 때 nullable을 쓰면 할당되지 않은 상태를 따로 관리할 수 있다.
형태는 자료형 + ?의 형태로 간단하다. (int의 경우 nullable은 int?라고 타입을 명시)
null 병합 연산자
int? nullableInt = null;
int nonNullableInt = nullableInt ?? 0;
Console.WriteLine("nonNullableInt 값: " + nonNullableInt);
nullable이 값 형태의 데이터를 null로 표현할 수 있게 해주는 역할을 한다면, null 병합 연산자는 값이 null인 경우 대체 값을 제공하는 역할을 한다.
위의 예시를 보면, nullableInt는 값 타입의 변수이지만, null값을 가지고 있다. 이러한 경우, nonNullableInt와 같이 nullable이 아닌 변수에 그대로 할당될 수 없는데, null 병합 연산자를 활용하면 null을 0이라는 값으로 대체해서 할당할 수 있게 된다.
예시와 같이 nullable로 선언된 값 타입의 변수가 아니라, 참조 타입의 변수일 경우에도 참조를 갖지 않아 null인 상태이면 이를 null 병합 연산자를 통해 특정한 값 타입의 데이터로 바꾸어서 값 타입의 변수에 할당할 수 있게 된다.
[StringBuilder(문자열 빌더)]
문자열 빌더
문자열 조작과 관련된 작업을 더 효율적으로 수행할 수 있게 도와주는 클래스이다. 이 클래스를 객체화해서 활용하면 문자열과 관련된 정보를 저장하고 조작할 수 있는데, 이 때 문자열 타입으로 데이터를 저장하는 것이 아니라, 문자열의 요소를 내부적인 버퍼에 저장하고 필요할 때 문자열로 만들어줘서 활용할 수 있다.
*버퍼는 데이터를 일시적으로 저장하거나 전송 및 변환 처리를 위한 임시저장소를 의미한다.
일반적인 문자열 연산의 문제점
문자열에 빈번한 조작을 가할 경우 메모리나 성능의 문제가 발생할 수 있다. (문자열은 내부적으로 배열의 형태로 저장되는 참조 타입의 데이터이기 때문) 그 외에도 추가적인 문제가 있는데, 문자열의 연산 과정에서 데이터가 증식되는 현상이 발생하기 때문이다.
"A" + "B" + "C"라는 연산이 있다고 하자. 우리는 이 연산을 단순히 "ABC"라는 결과를 얻기 위해 사용한다.
이 문자열을 이어주는 작업 또한 컴퓨터 입장에서는 "연산"이 되는데, 이 과정에서 연산되는 결과물이 전부 필요할지 모르는 데이터라고 판단해서 각각의 값을 저장하게 된다. 그 결과 "A", "B", "C", "AB", "ABC"가 모두 저장되는 비효율이 발생한다.
이러한 문제를 피하기 위해서 문자열 빌더 혹은 포맷을 활용하는 것이 권장된다.
문자열 빌더의 활용
문자열 빌더는 기본적으로 클래스이며, 기능을 활용하기 위해서는 일반적인 클래스와 같이 객체를 생성하고 메서드에 접근하면 된다.
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);
처음 생성하면 null 값이 저장되고, Append()를 통해 값을 할당(과 같은 역할을 수행...)할 수 있다.
문자열 빌더는 내가 저장한 정보를 문자열의 형태로 보관하고 있는 것이 아니기 때문에, 이를 문자열처럼 쓰고 싶다면 ToString()메서드를 활용하면 된다.
'👾 내일배움캠프 > 🎮 TIL & WIL' 카테고리의 다른 글
내일배움캠프 (휴일) TIL - 블랙잭 C# (0) | 2023.08.20 |
---|---|
내일배움캠프 2주차 WIL - C# 기초 공부 (1) | 2023.08.20 |
내일배움캠프 9일차 TIL - C# 기초 문법 (3) (0) | 2023.08.18 |
내일배움캠프 8일차 TIL - 스네이크 게임 C# (0) | 2023.08.17 |
내일배움캠프 7일차 TIL - C# 기초 문법 (2) (0) | 2023.08.16 |