CodeGym /행동 /C# SELF /C#에서 연산자 오버로딩

C#에서 연산자 오버로딩

C# SELF
레벨 34, 레슨 2
사용 가능

1. 소개

너도 아마 +로 숫자를 더하거나 ==로 비교하는 연산자에 익숙할 거야. 이런 연산자들은 내장 타입에서는 잘 동작하지. 근데 만약 네가 Vector(벡터) 같은 클래스를 직접 만들고, 벡터 두 개를 vector1 + vector2처럼 자연스럽게 더하고 싶다면? 아니면 Money(돈) 객체 두 개를 moneyAmount1 == moneyAmount2로 비교하고 싶다면? 이런 상황에서 C#의 연산자 오버로딩이 필요한 거지.

연산자 오버로딩을 하면, +, -, *, /, ==, !=, >, <, ++, -- 등등의 표준 연산자들이 네가 만든 클래스나 구조체 인스턴스에서 어떻게 동작할지 직접 정의할 수 있어. 이걸로 네 타입도 내장 타입처럼 직관적이고 읽기 쉬운 코드로 쓸 수 있지.

연산자 오버로딩의 기본

연산자를 오버로딩하려면, 클래스나 구조체 안에 operator 키워드와 오버로딩할 연산자 기호를 써서 static 메서드를 선언해야 해.

  • static 메서드: 연산자 오버로딩 메서드는 항상 public static이어야 해.
  • 메서드 이름: 메서드 이름은 operator 키워드 뒤에 연산자 기호가 붙는 형태야 (예: operator +, operator ==).
  • 파라미터: 파라미터 개수는 연산자 종류(단항/이항)에 따라 달라.
    • 단항 연산자 (+, -, !, ++, -- 등): 하나의 파라미터(해당 클래스/구조체 타입)를 받아.
    • 이항 연산자 (+, -, *, /, ==, != 등): 두 개의 파라미터를 받아. 둘 중 하나는 반드시 해당 타입이어야 해.
  • 리턴 타입: 연산 결과로 반환할 타입.

2. 이항 연산자 오버로딩 (Binary Operators)

이항 연산자는 피연산자 두 개를 받아. 대표적으로 산술 연산자(+, -, *, /)랑 비교 연산자(==, !=, >, <, >=, <=)가 있어.

예시: 덧셈 연산자(+) 오버로딩

public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }

    public Point(int x, int y) => (X, Y) = (x, y);

    // + 연산자 오버로딩
    public static Point operator +(Point p1, Point p2)
    {
        return new Point(p1.X + p2.X, p1.Y + p2.Y);
    }

    public override string ToString() => $"({X}, {Y})";
}

// 사용 예시:
Point point1 = new Point(1, 2);
Point point2 = new Point(3, 4);
Point sumPoint = point1 + point2; // 여기서 오버로딩된 + 연산자 호출됨

Console.WriteLine($"점의 합: {sumPoint}"); // 출력: 점의 합: (4, 6)

여기서는 Point 두 개를 더하면, 각 좌표를 더한 새로운 Point 객체가 반환되게 했어.

예시: 스칼라(숫자) 곱셈 연산자(*) 오버로딩

public struct Vector
{
    public double X { get; set; }
    public double Y { get; set; }

    public Vector(double x, double y) => (X, Y) = (x, y);

    // * 연산자 오버로딩 (벡터 * 숫자)
    public static Vector operator *(Vector vec, double scalar)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    // * 연산자 오버로딩 (숫자 * 벡터) - 대칭성 위해
    public static Vector operator *(double scalar, Vector vec)
    {
        return new Vector(vec.X * scalar, vec.Y * scalar);
    }

    public override string ToString() => $"<{X}, {Y}>";
}

// 사용 예시:
Vector vec1 = new Vector(2, 3);
Vector scaledVec1 = vec1 * 5;      // Vector * double 호출됨
Vector scaledVec2 = 5 * vec1;      // double * Vector 호출됨

Console.WriteLine($"스케일된 벡터 1: {scaledVec1}"); // 출력: 스케일된 벡터 1: <10, 15>
Console.WriteLine($"스케일된 벡터 2: {scaledVec2}"); // 출력: 스케일된 벡터 2: <10, 15>

곱셈 연산자는 Vector * double과 double * Vector 둘 다 오버로딩해서 대칭적으로 동작하게 했어.

예시: 비교 연산자(== 와 !=) 오버로딩

==와 != 연산자를 오버로딩할 때는 중요한 규칙이 있어: 하나만 오버로딩하면 안 되고 둘 다 꼭 오버로딩해야 해. 그리고 Equals()랑 GetHashCode()도 같이 맞춰주는 게 좋아.

public class Money
{
    public decimal Amount { get; set; }
    public string Currency { get; set; }

    public Money(decimal amount, string currency)
    {
        Amount = amount;
        Currency = currency;
    }

    // == / != 오버로딩할 땐 Equals, GetHashCode도 꼭 오버라이드
    public override bool Equals(object? obj)
    {
        if (obj is not Money other) return false;
        return Amount == other.Amount && Currency == other.Currency;
    }

    public override int GetHashCode() => HashCode.Combine(Amount, Currency);

    // == 연산자 오버로딩
    public static bool operator ==(Money? m1, Money? m2)
    {
        // 참조 타입은 null 체크 필요
        if (ReferenceEquals(m1, null)) return ReferenceEquals(m2, null);
        return m1.Equals(m2); // 오버라이드한 Equals 사용
    }

    // != 연산자 오버로딩 (== 오버로딩하면 필수)
    public static bool operator !=(Money? m1, Money? m2)
    {
        return !(m1 == m2);
    }
}

// 사용 예시:
Money cash1 = new Money(100, "USD");
Money cash2 = new Money(100, "USD");
Money cash3 = new Money(50, "USD");
Money cash4 = new Money(100, "EUR");

Console.WriteLine($"cash1 == cash2: {cash1 == cash2}"); // True
Console.WriteLine($"cash1 == cash3: {cash1 == cash3}"); // False
Console.WriteLine($"cash1 == cash4: {cash1 == cash4}"); // False

값 타입(구조체)에서는 ==와 !=이 기본적으로 비트 단위 비교를 해. 근데 클래스(class)에서는 ==이 참조 비교라서, 값 비교를 원하면 오버로딩이 꼭 필요해.

3. 단항 연산자 오버로딩 (Unary Operators)

단항 연산자는 피연산자 하나만 받아. 예시로 +(단항 플러스), -(단항 마이너스), !(논리 NOT), ~(비트 NOT), ++(증가), --(감소) 등이 있어.

예시: 단항 마이너스(-) 오버로딩

public struct Vector3D
{
    public double X, Y, Z;

    public Vector3D(double x, double y, double z) => (X, Y, Z) = (x, y, z);

    // 단항 - 연산자 오버로딩
    public static Vector3D operator -(Vector3D vec)
    {
        return new Vector3D(-vec.X, -vec.Y, -vec.Z);
    }

    public override string ToString() => $"<{X}, {Y}, {Z}>";
}

// 사용 예시:
Vector3D originalVec = new Vector3D(1, -2, 3);
Vector3D invertedVec = -originalVec; // 오버로딩된 단항 - 연산자 호출

Console.WriteLine($"원본 벡터: {originalVec}"); // 출력: 원본 벡터: <1, -2, 3>
Console.WriteLine($"반전된 벡터: {invertedVec}"); // 출력: 반전된 벡터: <-1, 2, -3>

예시: 증가(++)와 감소(--) 연산자 오버로딩

이 연산자들은 피연산자를 바꾸고, 바뀐 값을 반환해.

public struct Counter
{
    public int Value { get; set; }

    public Counter(int value) => Value = value;

    // ++ 연산자 오버로딩
    public static Counter operator ++(Counter c)
    {
        // 구조체가 불변이면 새 객체 반환,
        // 클래스면 현재 객체 수정 후 반환
        return new Counter(c.Value + 1);
    }

    // -- 연산자 오버로딩
    public static Counter operator --(Counter c)
    {
        return new Counter(c.Value - 1);
    }

    public override string ToString() => $"[카운터: {Value}]";
}

// 사용 예시:
Counter myCounter = new Counter(5);
myCounter++; // 후위 증가
Console.WriteLine(myCounter); // 출력: [카운터: 6]

++myCounter; // 전위 증가
Console.WriteLine(myCounter); // 출력: [카운터: 7]

myCounter--;
Console.WriteLine(myCounter); // 출력: [카운터: 6]

오버로딩된 ++와 --는 항상 전위(먼저 바꾸고 반환)로 동작해. 컴파일러가 후위 형태도 자동으로 처리해줘.

4. 타입 변환 연산자 오버로딩

네 타입 인스턴스를 다른 타입으로 명시적/암시적으로 변환하는 방법도 정의할 수 있어.

  • implicit (암시적 변환): 항상 안전하고 데이터 손실이 없을 때 사용 (예: int에서 long).
  • explicit (명시적 변환): 데이터 손실이나 오류 가능성이 있을 때, 명시적으로 변환해야 함 ((Type)obj).

예시: Score를 int로 암시적 변환

public struct Score
{
    public int Points { get; set; }
    public Score(int points) => Points = points;

    // Score를 int로 암시적 변환
    public static implicit operator int(Score s)
    {
        return s.Points;
    }
}

// 사용 예시:
Score examScore = new Score(95);
int scoreValue = examScore; // 암시적 변환
Console.WriteLine($"점수: {scoreValue}"); // 출력: 점수: 95

예시: Celsius를 Fahrenheit로 명시적 변환

public struct Celsius
{
    public double Degrees { get; set; }
    public Celsius(double degrees) => Degrees = degrees;
}

public struct Fahrenheit
{
    public double Degrees { get; set; }
    public Fahrenheit(double degrees) => Degrees = degrees;

    // Fahrenheit를 Celsius로 명시적 변환
    public static explicit operator Celsius(Fahrenheit f)
    {
        return new Celsius((f.Degrees - 32) * 5 / 9);
    }

    // Celsius를 Fahrenheit로 명시적 변환
    public static explicit operator Fahrenheit(Celsius c)
    {
        return new Fahrenheit(c.Degrees * 9 / 5 + 32);
    }
}

// 사용 예시:
Celsius c = new Celsius(25);
Fahrenheit f = (Fahrenheit)c; // 명시적 변환
Console.WriteLine($"25°C = {f.Degrees}°F"); // 출력: 25°C = 77°F

Fahrenheit f2 = new Fahrenheit(212);
Celsius c2 = (Celsius)f2; // 명시적 변환
Console.WriteLine($"212°F = {c2.Degrees}°C"); // 출력: 212°F = 100°C

5. 제한사항과 팁

모든 연산자를 오버로딩할 수 있는 건 아냐: &&, ||, ?., new, typeof, is, as, == (string은 이미 오버로딩되어 있고 특별한 동작을 함), ()(메서드 호출), =(할당) 등은 오버로딩 불가야.

대칭성: 이항 연산자를 오버로딩할 땐, 피연산자 순서 바꿔서도 동작하게 오버로딩하는 게 좋을 때가 많아 (예: Vector * double).

규칙 준수: 특히 비교 연산자(==, !=, >, <, >=, <=)와 Equals(), GetHashCode()의 관계는 꼭 맞춰줘야 해. 안 그러면 컬렉션 등에서 이상한 버그 날 수 있어.

가독성과 직관성: 연산자 오버로딩이 코드를 더 직관적이고 읽기 쉽게 만들 때만 써. 동작이 헷갈릴 수 있으면 Add() 같은 명확한 메서드 쓰는 게 나아.

변경 가능한(Mutable) 객체: 변경 가능한 타입에 연산자 오버로딩할 땐 조심해야 해. 예를 들어 Vector v1 = v2 + v3;는 보통 새 객체를 만들지 v2를 바꾸지 않아. 만약 기존 객체를 바꾼다면, 그건 직관적이지 않을 수 있어.

구조체 vs 클래스: 연산자 오버로딩은 보통 값 의미가 강한 구조체(예: Point, ComplexNumber)에서 많이 써. 이런 타입은 수학 연산이나 값 비교가 자연스러우니까.

2
과제
C# SELF, 레벨 34, 레슨 2
잠금
이항 덧셈 연산자 오버로딩
이항 덧셈 연산자 오버로딩
코멘트
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION