C# [4주차]

인터페이스 (Interface)

인터페이스란 클래스가 구현해야 하는 메서드/속성/이벤트의 계약을 정의한걸 의미한다!
( Class가 어떤 유형인지 정해주는 느낌인거 같은데.. )

인터페이스의 이점

  1. 코드의 재사용성
    인터페이스를 사용하면 다른 Class에서 해당 인터페이스를 구현하여 동일한 기능을 공유할 수 있다!

  2. 다중 상속 제공
    C#에서는 Class는 단일 상속만을 지원하지만, 인터페이스는 다중 상속을 지원한다.
    -> Class가 여러 인터페이스를 구현(다중 상속)함으로써 여러 개의 기능을 조합할 수 있다!

  3. 유연한 설계
    인터페이스를 사용하면 Class와 인터페이스 간에 느슨한 결합을 형성할 수 있다! (Class는 인터페이스를 구현하기만 하면 되므로)
    Class의 내부 구현에 대한 변경 없이 인터페이스의 동작을 변경하거나 새로운 인터페이스를 추가할 수 있다!
    -> 유연하고 확장 가능한 소프트웨어 설계 가능

인터페이스 사용법

인터페이스 형태로 메서드/속성/이벤트 를 정해주어 이를 상속받은 Class는 무조건 형식에 맞춰 메서드/속성/이벤트를 생성해주어야 한다!

  • 인터페이스 선언!
// 인터페이스 1
public interface IPickable
{
    // 픽업 메서드 만들어!
    void PickUp();
}

// 인터페이스 2
public interface IDroppable
{
    // 드랍 메서드 만들어!
    void Drop();
}
  • 인터페이스 상속!
// 아이템 클래스 ( 다중 상속 )
public class Item : IPickable, IDroppable
{
    public string Name { get; set; }

    // 정해진 메서드를 만들기 
    public void PickUp()
    {
        Console.WriteLine("아이템 {0}을 주웠습니다.", Name);
    }

    public void Drop()
    {
        Console.WriteLine("아이템 {0}을 버렸습니다.", Name);
    }
}
  • 정의한 Class와 인터페이스를 응용해 사용!
// 플레이어 클래스
public class Player
{
    // 줍기 가능한 아이템 인터페이스를 상속받은 것만!
    public void InteractWithItem(IPickable 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);
}

열거형 (Enums)

열거형은 서로 관련된 상수들의 집합을 정의하는 걸 뜻한다. 기본적으로 열거형의 각 상수는 정수 값으로 지정된다!

열거형의 이점

  1. 가독성
    열거형을 사용하면 일련의 연관된 상수들을 명명할 수 있다!
    -> 코드의 가독성이 향상, 상수를 사용할 때 잘못된 값 할당 방지

  2. 자기 문서화(Self-documenting)
    열거형은 의미 있는 이름을 사용하여 상수를 명명할 수 있다. -> 코드의 가독성이 향상, 상수의 의미를 명확하게 설명가능

  3. 스위치 문과의 호환성
    열거형을 사용하면 스위치 문에서 다양한 상수 값에 대한 분기를 쉽게 작성할 수 있다.

열거형 사용예제

  • 열거형 선언!
enum MyEnum
{
    // 값도 지정 가능!
    Value1 = 10,
    Value2,
    Value3
}
  • 열거형 사용 + 형변환
MyEnum myEnum = MyEnum.Value1;

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;
}

예외 처리

전에 Node.js에서는 try{}catch(err){} 를 통해 진행하였다. C#에서는 예외 처리 유형에 따라 다른 로직을 적용할 수 있는 것 같다!

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

사용자 정의 예외

사용자는 필요에 따라 자신만의 예외 클래스를 작성할 수 있다. ( 나는 이 예외처리를 해봤어요! )

  • 사용자 정의 예외 클래스는 Exception 클래스를 상속받아 작성
    -> 추가적인 기능이나 정보를 제공할 수 있음
public class NegativeNumberException : Exception
{ 
    // 생성자! 뒤에 : base(message)는 생성자 실행 전에 상속된 Exception을 실행하고 온다는 의미이다!
    public NegativeNumberException(string message) : base(message)
    {
    }
}

try
{
    int number = -10;
    if (number < 0)
    {
        throw new NegativeNumberException("음수는 처리할 수 없다네요..");
    }
}
catch (NegativeNumberException ex)
{
    Console.WriteLine(ex.Message);
}
catch (Exception ex)
{
    Console.WriteLine("예외가 발생!! " + ex.Message);
}

값/참조 형

사실 이 부분도 얕은 복사 / 깊은 복사를 배우며 어느정도 개념은 잡혀있었다!

  • 값형은 변수에 값을 직접 저장 ( int, float, double, bool, struct 등)

  • 참조형은 변수가 데이터에 대한 참조(메모리 주소)를 저장 (클래스, 배열, 인터페이스 등)

  • 참조형의 얕은 복사

class MyClass
{
    public int Value;
}

MyClass obj1 = new MyClass();
obj1.Value = 10;

// obj2는 obj1과 동일한 객체를 참조
MyClass obj2 = obj1; 

obj2.Value = 20;

// 출력 결과: 20
Console.WriteLine(obj1.Value); 

박싱(Boxing)과 언박싱(Unboxing)

  • 박싱(Boxing) : 값형을 참조형으로 변환하는 과정

  • 언박싱(Boxing) : 박싱된 객체를 다시 값형으로 변환하는 과정

  • object사용예제
    (object.NET Common Type System(CTS)의 일부이며, 모든 클래스의 직간접적인 상위 클래스)

using System;

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

        // 참조형
        int[] arr1 = new int[] { 1, 2, 3 };
        int[] arr2 = arr1;
        arr2[0] = 4;
        // 출력 결과: 4
        Console.WriteLine("arr1[0]: " + arr1[0]); 
        // 출력 결과: 4
        Console.WriteLine("arr2[0]: " + arr2[0]);

        // 박싱과 언박싱
        int num1 = 10;
        // 박싱
        object obj = num1; 
        // 언박싱
        int num2 = (int)obj; 
        // 출력 결과: 10
        Console.WriteLine("num1: " + num1); 
        // 출력 결과: 10
        Console.WriteLine("num2: " + num2); 

        /* ------ !!List 활용 예제!!------- */
        List<object> myList = new List<object>();

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

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

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

    • 박싱과 언박싱은 값형과 참조형 사이의 변환 작업이므로 성능에 영향을 미칠 수 있음

    • 박싱된 값형은 참조로 전달되므로 메모리 오버헤드발생할 수 있음

    • 박싱된 객체는 힙 영역에 할당되므로 가비지 컬렉션의 대상이 될 수 있음
      (-> 메모리 관리에 유의)

    • 잘못된 형식으로 언박싱하면 런타임 에러가 발생할 수 있음

델리게이트 (Delegate)

델리게이트(delegate)는 메서드참조하는 타입으로 다른 언어에서는 함수 포인터라는 용어를 사용하기도 한다.
-> 메서드를 매개변수로 전달하거나 변수에 할당하는데 사용가능!

  • 델리게이트 선언!
// 매개변수는 델리게이트에 대입할 메서드와 동일해야 함!
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);
        Console.WriteLine("결과: " + result);
    }
}

델리게이트 응용

  • 여러 메서드 동시 사용(선언부)
delegate void MyDelegate(string message);

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

static void Method2(string message)
{
    Console.WriteLine("Method2: " + message);
}
  • 여러 메서드 동시 사용(실행부)
class Program
{
    static void Main()
    {
        // 델리게이트 인스턴스 생성 및 메서드 등록
        MyDelegate myDelegate = Method1;
        myDelegate += Method2;

        /* 출 Method1: Hello!
           력 Method2: Hello! */
        myDelegate("Hello!");
    }
}
  • 콜백 받기(선언부)
// 델리게이트 선언
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);
}

람다 (Lambda)

익명 메서드를 만드는 방법으로 메서드의 이름 없이 메서드를 만들 수 있다! ( 이거 화살표 함수자나.. )

  • 예제
using System;

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

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

        myDelegate += (message) => Console.WriteLine("람다식을 두번째 메시지: " + message);

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

Func 과 Action

Func과 Action은 델리게이트를 대체하는 미리 정의된 제네릭 형식이다!

  • Func는 값을 반환하는 메서드 형식
// Func를 사용하여 두 개의 정수를 더하는 메서드
int Add(int x, int y)
{
    return x + y;
}

// Func를 이용한 메서드 호출
// 매개변수 2개와 마지막 반환 값 지정 
Func<int, int, int> addFunc = Add;
int result = addFunc(3, 5);
// 결과: 8
Console.WriteLine("결과: " + result);
  • Action은 값을 반환하지 않는 메서드 형식 ( void 처럼 실행용! )
void PrintMessage(string message)
{
    Console.WriteLine(message);
}

// Action을 이용한 메서드 호출
Action<string> printAction = PrintMessage;
printAction("Hello, World!");

LINQ (Language Integrated Query)

  • .NET 프레임워크에서 제공되는 쿼리 언어 확장 기능!

  • 데이터 소스에서 데이터를 쿼리하고 조작하는데 사용
    (예: 컬렉션, 데이터베이스, XML 문서 등)

  • 데이터베이스 쿼리와 유사한 방식으로 데이터를 필터링, 정렬, 그룹화, 조인 등 다양한 작업을 수행

  • 쿼리를 던지고 필터링하느라 성능에 영향을 많이 미칠 수 있음!

  • 기본 형태

var result = from 변수 in 데이터소스
             [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);
}
  • var 키워드는 결과 값의 자료형을 자동으로 추론!

  • from 절에서는 데이터 소스를 지정

  • where 절은 선택적으로 사용하며, 조건식을 지정하여 데이터를 필터링

  • orderby 절은 선택적으로 사용하며, 정렬 방식을 지정

  • select 절은 선택적으로 사용하며, 조회할 데이터를 지정

고급 자료형 기능

Nullable

C#에서 null 값을 가질 수 있는 값형에 대한 특별한 형식으로 약간 Prisma 썼을 때 문법과 똑같다!
(변수 Type + “?”)

  • Nullable 선언
int? nullableInt = null;
double? nullableDouble = 3.14;
bool? nullableBool = true;
  • Nullable 활용(메서드)
// 값 할당 및 접근
nullableInt = 10;
int intValue = nullableInt.Value;

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

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

문자열 빌더 (StringBuilder)

  • Append(), Insert(), Replace(), Remove() 등 다양한 메서드를 제공하여 문자열에 대한 추가, 삽입, 치환, 삭제 작업을 수행할 수 있음

  • 내부 버퍼를 사용하여 문자열 조작을 수행하므로 크기를 동적으로 조정할 수 있음 (= 추가적인 메모리 할당이 발생 X)
    (StringBuilder의 용량이 무제한은 아니며 최대 용량을 넘어서게 되면 새 공간이 자동 할당되 용량이 두 배로 증가..)

  • 주요 메서드

    • Append: 문자열을 뒤에 추가

    • Insert: 문자열을 지정한 위치에 삽입

    • Remove: 지정한 위치에서 문자열을 제거

    • Replace: 문자열의 일부를 다른 문자열로 대체

    • Clear: StringBuilder의 내용을 모두 지움

  • 메서드 활용

StringBuilder sb = new StringBuilder();

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

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

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

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

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

한줄 평 + 개선점

  • C# 문법에 대해선 대부분 끝났고, 남은건 알고리즘이다!

  • 새로운 언어의 새로운 규칙을 배워가는 것도 나름 즐겁다!