공부/C#

연산자 오버로딩, 전처리기

월러비 2025. 8. 25. 19:54

연산자 오버로딩

  • 오버로딩과 오버라이딩의 공통점 :
    • 오버로딩 : 매개변수가 다른 같은이름의 메서드를 정의하는것이다.
    • 오버 라이딩 : 부모클래스의 가상 함수를 자식 클래스에서 재정의하는 것이다.

개념 이해

  • 연산자 오버로딩(Operator Overloading)은 사용자 정의 형식에 대해 연산자의 동작을 정의하는 기능임
  • 주로 기본 데이터 형식과 유사한 사용자 정의 형식에 사용함
  • 예) 복소수, 분수, 행렬 등의 수학적 형식

오버로드 가능한 연산자

오버로드할 수 없는 연산자

연산자 설명

= 할당 연산자
. 멤버 접근 연산자
?: 조건부 연산자
?? null 병합 연산자
?. null 조건부 연산자
-> 포인터 멤버 접근 연산자
[] 인덱서 (대신 인덱서 프로퍼티로 구현)
() 메서드 호출

특수 연산자

  • 형식 변환 연산자: implicit, explicit
  • 진리값 연산자: true, false

자동 오버로드되는 연산자

  • 복합 대입 연산자(+=, -= 등)는 기본 연산자(+, - 등) 오버로드 시 자동으로 오버로드됨
  • 조건부 AND/OR 연산자(&&, ||)는 비트 AND/OR 연산자(&, |) 오버로드 시 자동으로 오버로드됨
  • // 사용 예시 var x = new LogicalValue(true); var y = new LogicalValue(false); // & 연산자 사용 var result1 = x & y; // &&도 자동으로 사용 가능 var result2 = x && y; // & 연산자를 기반으로 동작함 public struct LogicalValue { bool value; public LogicalValue(bool value) { this.value = value; } // & 연산자 오버로드 public static LogicalValue operator &(LogicalValue a, LogicalValue b) { return new LogicalValue(a.value & b.value); } // | 연산자 오버로드 public static LogicalValue operator |(LogicalValue a, LogicalValue b) { return new LogicalValue(a.value | b.value); } }
  • 주의: && 와 || 는 단락 평가(short-circuit evaluation)를 수행함
    • && : 첫 번째 피연산자가 false면 두 번째 피연산자를 평가하지 않음
    • || : 첫 번째 피연산자가 true면 두 번째 피연산자를 평가하지 않음

연산자 함수 정의 규칙

  • operator 키워드를 사용하여 정의함
  • static과 public 한정자가 필요함
  • 매개변수는 연산자의 피연산자를 나타냄
  • 반환 형식은 연산의 결과 형식임
  • 최소한 하나의 매개변수는 해당 형식이어야 함

예제 코드

기본 연산자 오버로딩

public struct Note
{
    int value;

    public Note(int semitonesFromA) { value = semitonesFromA; }

    public static Note operator +(Note x, int semitones)
    {
        return new Note(x.value + semitones);
    }
}

// 사용 예시
Note B = new Note(2);
Note CSharp = B + 2;

암시적/명시적 변환 연산자

public struct Note
{
    // 암시적 변환 - 주파수(Hz)로 변환
    public static implicit operator double(Note x)
        => 440 * Math.Pow(2, (double)x.value / 12);

    // 명시적 변환 - 주파수에서 Note로 변환
    public static explicit operator Note(double x)
        => new Note((int)(0.5 + 12 * (Math.Log(x/440) / Math.Log(2))));
}

// 사용 예시
Note n = (Note)554.37;  // 명시적 변환
double x = n;           // 암시적 변환

  • 메서드를 이용한 형변환 시도시 작성해야할 키워드
    • implicit : 암시적 형변환 키워드
    • explicit : 명시적 형변환 키워드
      • 데이터 손실이 일어날 경우 명시적 형변환을 사용하는게 좋다.

true/false 연산자 오버로딩

// 사용 예시
SqlBoolean a = SqlBoolean.Null;
if (a)
    Console.WriteLine("True");
else if (!a)
    Console.WriteLine("False");
else
    Console.WriteLine("Null");

public struct SqlBoolean
{
    public static bool operator true(SqlBoolean x)
        => x.m_value == True.m_value;

    public static bool operator false(SqlBoolean x)
        => x.m_value == False.m_value;

    public static SqlBoolean operator !(SqlBoolean x)
    {
        if (x.m_value == Null.m_value) return Null;
        if (x.m_value == False.m_value) return True;
        return False;
    }

    public static readonly SqlBoolean Null = new SqlBoolean(0);
    public static readonly SqlBoolean False = new SqlBoolean(1);
    public static readonly SqlBoolean True = new SqlBoolean(2);

    private SqlBoolean(byte value) { m_value = value; }
    private byte m_value;
}

주의사항

  • 연산자 오버로딩은 해당 형식의 자연스러운 의미를 반영해야 함
  • 직관적이지 않은 연산자 오버로딩은 피해야 함
  • 형변환의 경우:
    • implicit: 데이터 손실이 없고 항상 성공하는 경우에만 사용
    • explicit: 데이터 손실 가능성이 있거나 실패할 수 있는 경우 사용

전처리기

전처리기 지시문의 개념

  • 전처리기 지시문은 컴파일러에게 코드 영역에 대한 추가 정보를 제공하는 구문임
  • 모든 전처리기 지시문은 #으로 시작함
  • 컴파일 타임에 일어난다.

기본 지시문

심볼 정의와 해제

  • #define : 심볼을 정의함
  • #undef : 심볼 정의를 제거함
#define DEBUG      // DEBUG 심볼 정의
#undef DEBUG       // DEBUG 심볼 해제

조건부 컴파일

  • 조건부 지시문을 사용해 코드의 특정 영역을 컴파일에 포함하거나 제외할 수 있음
  • #if, #elif, #else, #endif 사용
#define DEBUG
class MyClass
{
    int x;
    void Foo()
    {
        #if DEBUG
        Console.WriteLine("테스트 중: x = {0}", x);
        #endif
    }
}

  • 프로젝트 수준에서 .csproj 파일에 정의할 수도 있음
<PropertyGroup>
    <DefineConstants>DEBUG;ANOTHERSYMBOL</DefineConstants>
</PropertyGroup>

조건부 연산자 사용

  • #if와 #elif 지시문에서 ||, &&, ! 연산자 사용 가능
#if TESTMODE && !DEBUG
    // 이 코드는 TESTMODE가 정의되고 DEBUG가 정의되지 않은 경우에만 컴파일됨
#endif

경고와 오류

  • #warning : 컴파일러 경고를 생성함
  • #error : 컴파일러 오류를 생성함
#if BETA
    #warning "베타 버전에서는 이 기능이 불완전할 수 있음"
#endif

#if !UNDERTESTED
    #error "이 코드는 충분한 테스트가 필요함"
#endif

코드 구역화

  • #region과 #endregion : 코드를 논리적 그룹으로 구성함
  • IDE에서 코드 접기(folding)를 지원하는데 사용됨
class Game
{
    #region 필드선언
    private int score;
    private string playerName;
    private bool isGameOver;
    #endregion

    #region 게임로직
    public void UpdateScore(int points)
    {
        score += points;
        CheckGameOver();
    }

    private void CheckGameOver()
    {
        if (score > 1000)
            isGameOver = true;
    }
    #endregion
}

컴파일러 경고 제어

  • #pragma warning : 특정 경고를 선택적으로 억제할 수 있음
public class Foo
{
    static void Main() { }

    #pragma warning disable 414
    static string Message = "Hello";    // 414 경고 비활성화
    #pragma warning restore 414         // 414 경고 다시 활성화
}

Conditional 어트리뷰트

  • Conditional 어트리뷰트가 적용된 어트리뷰트는 지정된 전처리기 심볼이 있는 경우에만 컴파일됨
  • 어트리뷰트(Attribute)는 코드에 메타데이터를 추가하는 선언적인 태그임
// file1.cs - 어트리뷰트 정의
#define DEBUG
using System;
using System.Diagnostics;

[Conditional("DEBUG")]
public class TestAttribute : Attribute {}

// file2.cs - 어트리뷰트 사용
[Test]  // TestAttribute의 축약형
class Foo
{
    [Test]  // TestAttribute의 축약형
    string s;
}

  • DEBUG 심볼이 정의된 경우:
    • [Test] 어트리뷰트들이 컴파일된 코드에 포함됨
  • DEBUG 심볼이 정의되지 않은 경우:
    • [Test] 어트리뷰트들이 무시되고 컴파일된 코드에서 제외됨
  • 활용:
    • 테스트 관련 어트리뷰트를 개발 빌드에만 포함하고 싶을 때
    • 디버깅용 어트리뷰트를 디버그 빌드에만 포함하고 싶을 때