C# 마법 구문 | Ricardo Peres


C# 마법같은 문법 (Magical Syntax)

개요

C# 언어에는 특정 API가 아닌 규칙(convention)에 기반한 마법같은 문법들이 존재합니다. 이러한 문법들은 잘 정의된 이름을 가진 메서드들을 기반으로 하지만, 기본 클래스나 인터페이스에 정의되어 있지 않음에도 불구하고 정상적으로 작동합니다.

1. 컬렉션 열거 (Enumerating Collections)

핵심 개념

foreach 문은 실제로 GetEnumerator 메서드 호출을 감싸는 구문입니다.

중요한 특징

  • IEnumerable 또는 IEnumerable<T> 구현이 필수가 아님
  • GetEnumerator 메서드만 있으면 foreach 사용 가능
  • 반환 타입은 IEnumerator 또는 IEnumerator<T>이어야 함

코드 예제

public class Enumerable
{
    public IEnumerator GetEnumerator()
    {
        yield return 1;
        yield return 2;
        yield return 3;
    }
}

var e = new Enumerable();
foreach (int i in e) { /* 1, 2, 3 */ }

주의사항

  • 클래스가 IEnumerable을 구현하지 않아도 foreach 사용 가능
  • 컴파일러가 GetEnumerator 메서드의 존재만 확인

2. 튜플 분해 (Deconstruction to Tuples)

핵심 개념

C# 7에서 도입된 튜플을 사용하여 메서드에서 여러 값을 반환할 수 있으며, 클래스를 튜플로 분해할 수 있습니다.

중요한 특징

  • Deconstruct 메서드를 통해 클래스를 튜플로 분해 가능
  • 매개변수는 반드시 out 키워드 사용
  • 여러 개의 Deconstruct 메서드 오버로드 가능

기본 예제

public class Rectangle
{
    public int Height { get; set; }
    public int Width { get; set; }
    
    public void Deconstruct(out int h, out int w)
    {
        h = this.Height;
        w = this.Width;
    }
}

var rectangle = new Rectangle { Height = 10, Width = 20 };
var (h, w) = rectangle;

고급 분해 예제

public void Deconstruct(out int perimeter, out int area, out bool square)
{
    perimeter = this.Width * 2 + this.Height * 2;
    area = this.Width * this.Height;
    square = this.Width == this.Height;
}

var (perimeter, area, square) = rectangle;

주의사항

  • 튜플 선언과 일치하는 Deconstruct 메서드를 찾지 못하면 예외 발생
  • 모든 매개변수는 out 키워드 필수

3. 컬렉션 초기화 (Collection Initialization)

핵심 개념

C# 6부터 컬렉션을 간결한 문법으로 초기화할 수 있습니다.

중요한 특징

  • Add 메서드의 존재가 핵심
  • IEnumerable 또는 IEnumerable<T> 구현 필수
  • 중괄호 안의 각 항목에 대해 Add 메서드가 여러 번 호출됨

기본 예제

var strings = new List<string> { "A", "B", "C" };

사용자 정의 컬렉션 예제

public class Collection : IEnumerable
{
    public IEnumerator GetEnumerator() => /* ... */
    public void Add(string s) { /* ... */ }
}

var col = new Collection { "A", "B", "C" };

딕셔너리 초기화

방법 1: 중괄호 구문

var dict = new Dictionary<string, int> { { "A", 1 }, { "B", 2 }, { "C", 3 } };

방법 2: 인덱서 구문

var dict = new Dictionary<string, int>
{
    ["A"] = 1,
    ["B"] = 2,
    ["C"] = 3
};

다중 오버로드 지원

public void Add(int i) { /* ... */ }
public void Add(string s) { /* ... */ }

var col = new Collection { 1, 2, 3, "a", "b", "c" };

주의사항

  • Add 메서드가 없으면 컬렉션 초기화 구문 사용 불가
  • IEnumerable 구현은 필수이지만 Add 메서드는 정의하지 않음

4. 소멸자 (Finalizers)

핵심 개념

.NET의 모든 클래스는 Object에서 상속받으므로 Finalize 메서드를 가집니다.

중요한 특징

  • ~ClassName 문법 사용
  • 클래스에서만 사용 가능 (구조체 불가)
  • 기본 Finalize 메서드는 자동으로 호출됨

코드 예제

public class MyClass
{
    ~MyClass()
    {
        //do cleanup
    }
}

주의사항

  • 기본 Finalize 메서드를 명시적으로 호출할 필요 없음
  • 리소스 정리용으로만 사용하는 것이 권장됨

5. 연산자 오버로딩 (Operator Overloading)

핵심 개념

C#에서는 연산자를 오버로드하고 캐스트 연산자를 정의할 수 있습니다.

캐스트 연산자

명시적 캐스트

public class MyClass
{
    private bool _someInternalField;

    public static explicit operator bool(MyClass mc)
    {
        return mc._someInternalField;
    }
}

MyClass c = new MyClass();
bool b = (bool) c; // 명시적 캐스트 필요

암시적 캐스트

MyClass c = new MyClass();
bool b = c; // 암시적 캐스트 (implicit 키워드 사용 시)

오버로드 가능한 연산자

연산자 그룹 연산자 특별 규칙
단항 연산자 +x, -x, !x, ~x, ++, --, true, false truefalse는 함께 오버로드 필요
이항 연산자 `x + y, x - y, x * y, x / y, x % y, x & y, x y, x ^ y, x << y, x >> y, x >>> y`
비교 연산자 x == y, x != y, x < y, x > y, x <= y, x >= y 쌍으로 오버로드 필요: ==!=, <>, <=>=

덧셈 연산자 오버로드 예제

public class Complex(int Real, int Imaginary)
{
    public static Complex operator + (Complex c1, Complex c2) => 
        new(c1.Real + c2.Real, c1.Imaginary + c2.Imaginary);
}

Complex c1 = new(10, 0);
Complex c2 = new(5, 5);
Complex sum = c1 + c2;

주의사항

  • 연산자의 원래 의미를 존중해야 함
  • 예상치 못한 동작을 방지하기 위해 신중하게 사용
  • 관련 연산자들은 쌍으로 오버로드해야 함

6. 사용자 정의 Await (Custom Await)

핵심 개념

Task 객체가 아닌 사용자 정의 클래스에서도 await 사용이 가능합니다.

필요한 조건

  1. GetAwaiter 메서드 제공
  2. INotifyCompletion 인터페이스 구현
  3. IsCompleted 불린 속성 제공
  4. GetResult 메서드 제공 (매개변수 없음)

void 반환 타입 예제

public class CustomVoidAwaitable
{
    public CustomVoidAwaiter GetAwaiter() => throw new NotImplementedException();
}

public class CustomVoidAwaiter : INotifyCompletion
{
    public void OnCompleted(Action continuation) => throw new NotImplementedException();
    public bool IsCompleted => throw new NotImplementedException();
    public void GetResult() => throw new NotImplementedException();
}

제네릭 반환 타입 예제

public class CustomAwaitable<T>
{
    public CustomAwaiter<T> GetAwaiter() => throw new NotImplementedException();
}

public class CustomAwaiter<T> : INotifyCompletion
{
    public void OnCompleted(Action continuation) => throw new NotImplementedException();
    public bool IsCompleted => throw new NotImplementedException();
    public T GetResult() => throw new NotImplementedException();
}

사용 방법

CustomVoidAwaitable ca = ...;
await ca;

CustomAwaitable<string> csa = ...;
var result = await csa;

주의사항

  • ValueTask<T>도 이 패턴으로 구현됨
  • 모든 필수 구성 요소를 정확히 구현해야 함

7. 사용자 정의 쿼리 패턴 (Custom Query Pattern)

핵심 개념

C# 쿼리 구문을 사용자 정의 클래스에서 사용할 수 있습니다.

필요한 표준 연산자들

  • Cast
  • GroupBy
  • GroupJoin
  • Join
  • Order
  • OrderBy
  • OrderByDescending
  • Select
  • SelectMany
  • ThenBy
  • ThenByDescending
  • Where

기본 구조 예제

class CustomQueryable
{
    public CustomQueryable<T> Cast<T>() => throw new NotImplementedException();
}

class CustomQueryable<T> : CustomQueryable
{
    public CustomQueryable<T> Where(Func<T,bool> predicate) => throw new NotImplementedException();
    public CustomQueryable<U> Select<U>(Func<T,U> selector) => throw new NotImplementedException();
    public CustomQueryable<V> SelectMany<U,V>(Func<T,CustomQueryable<U>> selector, Func<T,U,V> resultSelector) => throw new NotImplementedException();
    // ... 기타 연산자들
}

정렬 가능한 쿼리 클래스

class CustomOrderedQueryable<T> : CustomQueryable<T>
{
    public CustomOrderedQueryable<T> ThenBy<K>(Func<T,K> keySelector) => throw new NotImplementedException();
    public CustomOrderedQueryable<T> ThenByDescending<K>(Func<T,K> keySelector) => throw new NotImplementedException();
}

그룹화 클래스

class CustomGroup<K,T> : CustomQueryable<T>
{
    public K Key { get; } => throw new NotImplementedException();
}

쿼리 사용 예제

var query = from c in myObj.Get<string>()
            where c.Length > 0
            orderby c
            select c;

주의사항

  • .NET 9/C# 13에서 추가 연산자들이 도입됨
  • 모든 필요한 연산자 메서드를 구현해야 함
  • 쿼리 구문의 각 절은 해당하는 메서드로 변환됨

참고 자료

본 문서에서 언급된 추가 정보를 위한 링크들:

실용적인 팁

개발 시 고려사항

  1. 규칙 기반 문법의 이해: C#의 많은 기능들이 특정 메서드 이름에 의존한다는 점을 인식
  2. 신중한 연산자 오버로딩: 예상치 못한 동작을 방지하기 위해 연산자의 원래 의미 존중
  3. 성능 고려: 사용자 정의 구현 시 성능 영향 검토
  4. 명명 규칙 준수: GetEnumerator, Deconstruct, Add 등의 정확한 메서드 이름 사용

주의사항

  1. 기괴한 코드 방지: 마법같은 문법의 남용으로 인한 가독성 저하 주의
  2. 최신 언어 변화 추적: C# 언어의 지속적인 변화에 대한 업데이트 필요
  3. 문서화의 중요성: 사용자 정의 구현 시 충분한 문서화 제공