공부/C#

익명 메서드, 예외처리, 열거자, 반복자, 시퀀스 조합

월러비 2025. 8. 18. 19:46

익명 메서드

개요

  • 익명 메서드(Anonymous Method)는 C# 2.0에서 도입된 기능임
  • 람다 식의 전신이 되는 기능으로, C# 3.0의 람다 식에 의해 대부분 대체됨
  • 메서드를 이름 없이 델리게이트 인스턴스로 직접 정의할 수 있음

기본 구문

  • delegate 키워드로 시작하여 매개변수 목록과 메서드 본문을 작성함
    • delegate 선언을 통해 반환형 추론이 가능하기에 반환형 생략이 가능하다.
  • 람다 식과 달리 표현식 구문을 지원하지 않음
  • 메서드 본문은 항상 구문 블록이어야 함
// 익명 메서드 예제
Transformer sqr = delegate (int x) { return x * x; };
Console.WriteLine(sqr(3)); // 출력: 9

delegate int Transformer(int i);

익명 메서드 특징

  • 매개변수 선언을 생략할 수 있음
  • 델리게이트의 시그니처와 일치하는 경우에만 사용 가능함
// 매개변수 선언 생략 예제
public event EventHandler Clicked = delegate { };  // 빈 이벤트 핸들러
Clicked += delegate { Console.WriteLine("clicked"); };

람다 식과의 차이점

  • 다음 기능들을 지원하지 않음:
    • 암시적 타입 매개변수
    • 표현식 구문
    • Expression\<T>로의 컴파일
  • 람다 식으로 다시 작성한 예제:
// 익명 메서드
Transformer sqr1 = delegate (int x) { return x * x; };

// 람다 식
Transformer sqr2 = x => x * x;

외부 변수 캡처

  • 람다 식과 동일하게 외부 변수를 캡처할 수 있음
  • 캡처된 변수는 메서드 호출 시점의 값을 사용함
// 외부 변수 캡처 예제
int factor = 2;
Func<int, int> multiplier = delegate(int n) { return n * factor; };
Console.WriteLine(multiplier(3));  // 출력: 6

factor = 10;
Console.WriteLine(multiplier(3));  // 출력: 30

정적(static) 익명 메서드

  • C# 9.0부터 static 키워드를 사용하여 외부 변수 캡처를 제한할 수 있음
  • 성능 최적화가 필요한 상황에서 유용함
// 정적 익명 메서드 예제
Func<int, int> multiplier = static delegate(int n) { return n * 2; };

int factor = 123;
// 아래 코드는 컴파일 에러 발생
// Func<int, int> multiplier = static delegate(int n) { return n * factor; };

실제 사용 예제

  • 이벤트 핸들러 등록에 자주 사용됨
  • 간단한 콜백 함수 구현에 유용함
// 이벤트 핸들러 예제
Button button = new Button();
button.Click += delegate(object sender, EventArgs e)
{
    Console.WriteLine("Button clicked!");
};

C# 예외처리

예외 처리 개요

  • 예외(Exception)는 프로그램 실행 중 발생하는 오류 상황을 나타냄
    • System.Exception을 상속받은 객체들만 예외처리를 할 수 있다.
  • try 문을 사용하여 오류 처리나 정리 코드를 지정할 수 있음
  • try 블록은 하나 이상의 catch 블록이나 finally 블록, 또는 둘 다와 함께 사용됨
  • catch 블록은 try 블록에서 오류가 발생했을 때 실행됨
  • finally 블록은 정리 코드를 수행하며 예외 발생 여부와 관계없이 항상 실행됨

try 문의 기본 구조

try
{
    // 예외가 발생할 수 있는 코드
}
catch (ExceptionA ex)
{
    // ExceptionA 유형의 예외 처리
}
catch (ExceptionB ex)
{
    // ExceptionB 유형의 예외 처리
}
finally
{
    // 정리 코드
}

예제:

int y = Calc(0);
Console.WriteLine(y);

int Calc(int x) => 10 / x;  // x가 0이면 DivideByZeroException 발생

// 예외 처리 추가
try
{
    int y = Calc(0); //예외가 발생하면 반환을 못하고 예외처리로 넘어가게된다.
    Console.WriteLine(y);
}
catch (DivideByZeroException ex) //
{
    Console.WriteLine("x cannot be zero");
}
Console.WriteLine("program completed");

// 출력:
// x cannot be zero
// program completed

catch 절

  • catch 절은 System.Exception 또는 그 하위 클래스 타입을 처리할 수 있음
  • 여러 catch 절을 사용할 때는 구체적인 예외부터 처리해야 함
    • 구체적일수록 자식 클래스에 가깝다.
  • 변수가 필요없는 경우 예외 변수를 생략할 수 있음

예제:

class Test
{
    static void Main(string[] args)
    {
        try
        {
            byte b = byte.Parse(args[0]);
            Console.WriteLine(b);
        }
        catch (IndexOutOfRangeException)  // 변수 생략
        {
            Console.WriteLine("Please provide at least one argument");
        }
        catch (FormatException)  // 변수 생략
        {
            Console.WriteLine("That's not a number!");
        }
        catch (OverflowException)  // 변수 생략
        {
            Console.WriteLine("You've given me more than a byte!");
        }
    }
}

예외 필터링

  • C# 6.0부터 when 절을 사용하여 예외를 필터링할 수 있음
catch (WebException ex) when (ex.Status == WebExceptionStatus.Timeout)
{
    // 타임아웃 예외 처리
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.SendFailure)
{
    // 전송 실패 예외 처리
}

finally 절

  • finally 절은 예외 발생 여부와 관계없이 항상 실행됨
    • 필요한 이유 : 파일 입출력, 통신같은 경우 예외처리가 필수이다.
    • 메모리 삭제 및 정리같은 경우 필요하다.
    • Dispose() : 리소스 정리같은 경우 사용한다.
  • 리소스 해제와 같은 정리 작업에 주로 사용됨

예제:

void ReadFile()
{
    StreamReader reader = null;
    try
    {
        reader = File.OpenText("file.txt");
        if (reader.EndOfStream) return;
        Console.WriteLine(reader.ReadToEnd());
    }
    finally
    {
        if (reader != null) reader.Dispose();
    }
}

using 문

  • IDisposable 인터페이스는 비관리 리소스 해제를 위한 표준 방법을 제공함
public interface IDisposable
{
    void Dispose();  // 리소스 해제를 위한 메서드
}

  • using 문은 IDisposable을 구현하는 객체의 Dispose 호출을 자동화함
  • 기존의 try-finally 패턴을 간단히 작성할 수 있음

수동으로 Dispose를 호출하는 방식:

StreamReader reader = null;
try
{
    reader = File.OpenText("file.txt");
    // 파일 작업 수행
}
finally
{
    if (reader != null)
        reader.Dispose();  // 수동으로 Dispose 호출
}

using 문을 사용한 자동화:

using (StreamReader reader = File.OpenText("file.txt"))
{
    // 파일 작업 수행
} // 블록을 벗어날 때 자동으로 Dispose 호출

  • 컴파일러는 using 문을 try-finally 블록으로 변환함:
{
    StreamReader reader = File.OpenText("file.txt");
    try
    {
        // 파일 작업 수행
    }
    finally
    {
        if (reader != null)
            ((IDisposable)reader).Dispose();
    }
}

  • C# 8.0부터는 더 간단한 using 선언 사용 가능:
if (File.Exists("file.txt"))
{
    using var reader = File.OpenText("file.txt");
    Console.WriteLine(reader.ReadLine());
} // 블록을 벗어날 때 자동으로 Dispose 호출

  • 주요 IDisposable 구현 클래스들:
    • StreamReader, StreamWriter (파일 입출력)
    • SqlConnection (데이터베이스 연결)
    • Graphics (GDI+ 그래픽)
    • Font, Brush (GDI+ 리소스)
    • MemoryStream (메모리 스트림)

using 문의 장점:

  • 코드가 간결해짐
  • Dispose() 호출을 잊을 위험이 없음
  • 예외가 발생해도 안전하게 리소스가 해제됨

예외 발생과 다시 던지기

throw 문

  • throw 문으로 예외를 발생시킬 수 있음
void Display(string name)
{
    if (name == null)
		    //ArgumentNullException : 인자가 null인경우 에러
        throw new ArgumentNullException(nameof(name));
    Console.WriteLine(name);
}

예외 다시 던지기

  • throw를 사용하여 예외를 다시 던질 수 있음
try { ... }
catch (Exception ex)
{
    // 로깅 등의 작업 수행
    throw;  // 같은 예외를 다시 던짐
}

더 구체적인 예외로 다시 던지기:

try
{
    // DateTime 파싱 시도
}
catch (FormatException ex)
{
    throw new XmlException("Invalid DateTime", ex);
}

주요 예외 타입

System.Exception의 주요 속성:

  • StackTrace: 예외 발생부터 catch까지의 메서드 호출 스택
  • Message: 오류 설명 문자열
  • InnerException: 예외를 발생시킨 내부 예외

자주 사용되는 예외 타입들:

  • ArgumentException: 잘못된 인수 전달 시
  • ArgumentNullException: null이 허용되지 않는 인수가 null일 때
  • ArgumentOutOfRangeException: 숫자 인수가 허용 범위를 벗어날 때
  • InvalidOperationException: 객체의 현재 상태가 메서드 실행에 적합하지 않을 때
  • NotSupportedException: 호출된 기능이 지원되지 않을 때
  • NotImplementedException: 메서드가 구현되지 않았을 때
  • ObjectDisposedException: 폐기된 객체를 사용하려 할 때

TryXXX 메서드 패턴

  • 메서드가 실패할 때 예외를 던지는 대신 실패 코드를 반환하는 패턴임
  • int.Parse와 int.TryParse가 대표적인 예시임
// Parse 메서드 - 실패 시 예외 발생
public int Parse(string input);

// TryParse 메서드 - 실패 시 false 반환
public bool TryParse(string input, out int returnValue);

  • TryXXX 패턴 구현 방법:
public return-type XXX(input-type input)
{
    return-type returnValue;
    if (!TryXXX(input, out returnValue))
        throw new YYYException(...);
    return returnValue;
}

열거자와 반복자

열거(Enumeration)의 이해

열거자의 개념

  • 열거자는 시퀀스의 값들을 읽기 전용, 순방향으로 탐색하는 커서임
  • C#은 다음 조건 중 하나를 만족하면 열거자로 취급함:
    • MoveNext 메서드와 Current 속성을 가짐
    • IEnumerator<T> 인터페이스를 구현함
    • IEnumerator 인터페이스를 구현함
  • 즉, 순서를 갖고있는 컨테이너를 의미한다.
    • 순회가 가능한 객체다.

열거 가능한 객체

  • 열거 가능한 객체는 시퀀스의 논리적 표현임
  • 다음 조건 중 하나를 만족하면 열거 가능한 객체로 취급함:
    • GetEnumerator 메서드를 가짐
      • 열거자가 반환된다.
    • IEnumerable\<T> 인터페이스를 구현함
    • IEnumerable 인터페이스를 구현함

기본 사용 예제

// foreach를 사용한 고수준 반복
foreach (char c in "beer")
    Console.WriteLine(c);

// foreach를 사용하지 않은 저수준 반복
using (var enumerator = "beer".GetEnumerator())
    while (enumerator.MoveNext())
    {
        var element = enumerator.Current;
        Console.WriteLine(element);
    }

  • 고수준 : 사람 기준으로 편한 반복
  • 저수준 : 사람 기준으로 불편한 반복
    • GetEnumerator : IEnumerator를 상속한 객체는 전부 호출 가능하다.
      • enumerator 객체, 또는 enumerator 함수가 반환된다.
    • MoveNext : 다음 순회가 가능한지 bool 형으로 반환한다.
    • Current : 현재 순회하는 결과를 반환하는 함수다.
  • 즉 위처럼 고수준으로 작성하면, 컴파일러는 아래처럼 저수준으로 인식한다는 뜻이다.

컬렉션 초기자(Collection Initializer)

컬렉션 초기자 사용법

  • 열거 가능한 객체를 한번에 초기화하고 채울 수 있음
// List 초기화 예제
using System.Collections.Generic;

List<int> list = new List<int> {1, 2, 3};

// 컴파일러는 이를 다음과 같이 변환함
List<int> list = new List<int>();
list.Add(1);
list.Add(2);
list.Add(3);

// Dictionary 초기화 예제
var dict = new Dictionary<int, string>()
{
    { 5, "five" },
    { 10, "ten" }
};

// 더 간단한 Dictionary 초기화 문법
var dict = new Dictionary<int, string>()
{
    [3] = "three",
    [10] = "ten"
};

  • List : 동적 배열
  • Dictionary : 해쉬 테이블을 생성하는 데이터형이다.

반복자(Iterator)

반복자의 개념

  • foreach문은 열거자를 소비(consume)하는 쪽임
  • 반복자는 이 열거자를 만들어내는(produce) 쪽임
  • yield return 문을 사용하여 시퀀스의 각 요소를 하나씩 반환함
    • yield return : 지연 실행한다고 이해해라

예를 들어:

// 이 메서드가 반복자(Iterator)임 - 열거자를 생산함
IEnumerable<int> Generate123()
{
    yield return 1; //일단 여기서 일시정지 후 1 반환 - 1번 순회
    yield return 2; //1호출 후 다시 순회한 다음 정지된 순회 다음부터 순회를 시작
    yield return 3;
}

// foreach는 열거자를 소비함
foreach (int number in Generate123())
{
    Console.WriteLine(number);
}

  • Generate123() 메서드는 반복자로, 1,2,3이라는 시퀀스를 생산함
  • foreach는 이 시퀀스를 소비하는 소비자임
  • yield return을 사용하면 메서드가 반복자가 됨
    • yield return을 하면 첫번째 반환에서 정지하게 된다.
    • 한번 호출된 반복자를 다시 호출하면 일시정지된 반환결과의 다음 반환 결과가 반환된다.
    • 즉, 1이 반환되고 정지한 다음 다시 순회를 시작하면 1 다음인 2가 반환되고 다시 정지한다.

피보나치 수열 예제

using System;
using System.Collections.Generic;

foreach (int fib in Fibs(6))
    Console.Write(fib + " ");

IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
    {
        yield return prevFib;
        int newFib = prevFib+curFib;
        prevFib = curFib;
        curFib = newFib;
    }
}
// 출력: 1 1 2 3 5 8

yield break 사용

  • 반복자를 일찍 종료할 때 사용함
IEnumerable<string> Foo(bool breakEarly)
{
    yield return "One";
    yield return "Two";
    if (breakEarly)
        yield break;
    yield return "Three";
}

try/catch/finally 블록과 반복자

  • yield return은 catch 절에서 사용할 수 없음
  • yield return은 finally 절에서 사용할 수 없음
  • try 블록에서는 finally와 함께 사용 가능함
IEnumerable<string> Foo()
{
    try
    {
        yield return "One"; // OK
    }
    **finally
    {
        // 정리 코드
    }
}**

시퀀스 조합하기

반복자의 조합성

  • 반복자는 매우 조합하기 쉬움
  • LINQ에서 특히 유용함
using System;
using System.Collections.Generic;

foreach (int fib in EvenNumbersOnly(Fibs(6)))
    Console.WriteLine(fib);

IEnumerable<int> Fibs(int fibCount)
{
    for (int i = 0, prevFib = 1, curFib = 1; i < fibCount; i++)
    {
        yield return prevFib;
        int newFib = prevFib+curFib;
        prevFib = curFib;
        curFib = newFib;
    }
}

IEnumerable<int> EvenNumbersOnly(IEnumerable<int> sequence)
{
    foreach (int x in sequence)
        if ((x % 2) == 0)
            yield return x;
}

  • 각 요소는 MoveNext() 호출 시점에 계산됨
  • 시퀀스를 조합할 때 지연 실행(lazy evaluation)이 이루어짐