익명 메서드
개요
- 익명 메서드(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 인터페이스를 구현함
- GetEnumerator 메서드를 가짐
기본 사용 예제
// 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 : 현재 순회하는 결과를 반환하는 함수다.
- GetEnumerator : IEnumerator를 상속한 객체는 전부 호출 가능하다.
- 즉 위처럼 고수준으로 작성하면, 컴파일러는 아래처럼 저수준으로 인식한다는 뜻이다.
컬렉션 초기자(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)이 이루어짐
'공부 > C#' 카테고리의 다른 글
| 익명 타입, 튜플, 레코드, 패턴, 어트리뷰트 (4) | 2025.08.25 |
|---|---|
| Nullable, 확장 메서드, 메서드 체이닝 (0) | 2025.08.19 |
| 델리게이트, 멀티캐스트 델리게이트, Func / Action, Predicate, 이벤트, 람다식, 외부변수 캡처 (1) | 2025.08.18 |
| 제네릭, 제네릭 메서드, 타입 매개변수 제약조건 (0) | 2025.08.18 |
| 인터페이스, 인터페이스 상속, 열거형, 플래그 열거형, 중첩 타입 (6) | 2025.08.13 |