[C# 기초강좌] 14. C# 연산자 오버로딩

Posted in SW개발 // Posted at 2023. 11. 20. 10:37
728x90
이 글은 제가 과거에 운영했던 사이트인 http://dotnet.mkexdev.net 의 글을 옮겨온 것입니다. 원본 글은 2010년에 작성되었습니다.

그 전에 운영했었던 사이트(mkex.pe.kr)은 흔적도 없이 사라 졌습니다. 그속의 글들도 모두... 그래서 이 사이트도 사라지기 전에 옮기고 싶은 글을 조금씩 이 블로그로 이동시키려 합니다.
(원본글) http://dotnet.mkexdev.net/Article/Content.aspx?parentCategoryID=1&categoryID=5&ID=678

이 글은 닷넷 기초 지식을 전파하기 위해 2010경에 작성되었으며, 당시 윤성우의 프로그래밍 스터디그룹 네이버 카페에도 필진으로 참여하여 연재했던 글이기도 합니다. 현재 시점에서 조금 달라진 부분이 있을 수 있으나 기본 원리와 언어 기초에 해당하는 부분은 크게 변하지 않았을 것으로 생각하며 이런 부분들을 감안해서 봐 주시기 바랍니다.

 

연산자 오버로딩, 이것 역시 메서드 입니다 

안녕하세요. 박종명입니다. 닷넷 열네 번째 강좌를 진행하도록 하겠습니다.
이번 강좌는 C#에서 연산자를 새로이 정의할 수 있는 기법인 연산자 오버로딩에 대해 알아 보겠습니다

연산자 오버로딩은 사칙연산과 같이 기본 제공되는 +, -, *, / 연산자는 물론 비교 연산자, 형 변환 연산자 등을 오버로드 하여 새로운 연산 논리를 구현하는 기법으로써 오버로드 된 연산자 역시 결국에는 메서드입니다.


숫자 형식 데이터를 다루는데 있어 사칙연산은 프로그램의 생명 주기에 상당히 많이 차지하는 기본 연산입니다.

int a = 1; int b = 2;
int sum = a + b;

 숫자 형식은 그렇다 치더라도 사용자 정의 자료형인 객체나 구조체끼리의 사칙연산이 가능할까요?

Pserson 이라는 클래스의 객체 person1  person2에 대해 다음과 같이 더하기(+) 연산은 가능할까요?

Person p1 = new Person ();
Person p2 = new Person ();
Person sumPerson = p1 + p2;

코드를 실행해 볼 필요도 없이 이 연산은 컴파일 오류를 발생시킵니다.

 현실적으로 사람을 추상화 한 Person 객체에 더하기를 수행할 이유는 없어 보입니다. 그러나 만일 정말 만일에, 두 사람을 더하면(+) 두 사람의 나이(age)가 더해져서 반환되도록 해야 한다면 어떻게 하는 것이 좋을까요?

언뜻 AddPerson(Person p1, Person p2) 이라는 메서드를 생각해 볼 수 있습니다.
좋은 선택입니다. 그럼 두 사람이 아닌 셋, , 다섯 사람을 더해야 한다면 아래와 같은 코드가 예상됩니다

Person sumPerson = Person.AddPerson(Person.AddPerson(Person.AddPerson(p1, p2), p3),p4); 

코드는 총 4 사람(Person)에 대해 더하기를 수행하고 있습니다. 
뭔가 복잡해 보입니다. 숫자를 더하듯 할 수 있다면 간편하지 않을까요? 아래와 같이요.

Person sumPerson = p1 + p2 + p3 + p4;

훨씬 간단해 보입니다. 이것이 가능할까요?

연산자 오버로딩은 바로 이것입니다. 객체와 같은 사용자 정의 타입에 연산자를 새롭게 정의 할 수 있도록 해 주는 기법입니다.

이때 연산자는 사칙연산과 같이 기본 제공되는 연산자 키워드를 기반으로 다시 정의하는 것이기 때문에 연산자 오버로딩이라고 합니다. 메서드 오버로딩이 그러하듯 이요.

 

복소수 클래스의 연산자 오버로딩

수학에서 복소수라 함은 실수와 허수의 합으로 이루어진 수 인데요. 다음과 같이 표현합니다.

a + bi (a, b는 실수, i2 = -1을 만족하는 허수단위)

복소수의 수학적 개념보다도 우리는 이 복소수를 표현하고 연산하기 위해 프로그램을 작성한다고 가정합니다.

다음과 같이 실수부와 허수부에 해당하는 멤버 변수 2개를 가진 복소수 클래스를 정의하고 생성자를 통해 값을 초기화 합니다. 그리고 ToString() 메서드에서 복소수 표현식인 a + bi 형태로 반환하도록 오버라이딩 해 둡니다.

class Complex{
    public int real;     //실수부
    public int imaginary; //허수부 

    public Complex(int real, int imaginary) {
            this.real = real;
            this.imaginary = imaginary;
    }               

    public override string ToString(){
        return (System.String.Format("{0} + {1}i", real, imaginary));
    }
}

 

복소수의 사칙 연산

복소수에 사칙연산을 수행할 수 있는데요.
두 복소수 (a + bi)  (c + di)의 덧넷, 뺄셈, 곱셈은 다음과 같이 계산할 수 있습니다

덧셈: (a + bi) + (c + di) = (a + c) + (b + d)i

뺄셈: (a + bi) - (c + di) = (a - c) + (b - d)i

곱셈: (a + bi) * (c + di) = (ac - bd) + (bc + ad)i

우리가 정의한 복소수 클래스(Complex)에 이러한 사칙연산을 미리 정의해 두면 이 클래스를 사용하는 입장에서 편리하겠죠. 그리고 클래스 구조적으로도 사칙연산의 행위를 미리 정의해 두는 것이 좋습니다.

 물론 메서드를 통해 각각의 연산 메서드를 정의할 수 있겠지만 우리는 연산자 오버로딩을 통해 구현하도록 합니다.

 operator X 키워드

연산자 오버로딩을 위해서 사용되는 키워드는 operator 입니다. 그리고 X 부분은 오버로딩할 연산자입니다. 
우리는 덧넷,뺄셈,곱셈을 정의할 것이기에 X  +, - , * 이 되겠네요

아래 세 연산자를 오버로드 한 코드입니다.

public static Complex operator + (Complex c1, Complex c2){
    return new Complex(c1.real + c2.real, c1.imaginary + c2.imaginary);
}

public static Complex operator - (Complex c1, Complex c2){
    return new Complex(c1.real - c2.real, c1.imaginary - c2.imaginary);
}

 

public static Complex operator * (Complex c1, Complex c2){
    return new Complex((c1.real * c2.real) - (c1.imaginary * c2.imaginary),
                                (c1.imaginary * c2.real) + (c1.real * c2.imaginary));
}

 세가지 연산이 정의되었기 때문에 다음과 같이 복소수 연산을 일반 연산처럼 쉽게 이용할 수 있습니다.

static void Main(string[] args){           
    Complex num1 = new Complex(4, 3);
    Complex num2 = new Complex(3, 2);           

    Complex add = num1 + num2;

    System.Console.WriteLine("복소수 합: {0}", add); 

    Complex minus = num1 - num2;

    System.Console.WriteLine("복소수 차: {0}", minus); 

    Complex multi = num1 * num2;

    System.Console.WriteLine("복소수 곱: {0}", multi);   
}

 

마치 일반 숫자 데이터의 사칙연산을 하듯이 객체의 사칙연산이 가능해 졌습니다. 물론 메서드로 이러한 연산을 제공해 줄 수도 있지만 연산자 오버로딩을 구현하면 메서드로 처리하는 것보다 직관적이며 명확해져 편리성과 잠재적 버그 유발성을 줄일 수 있는 장점이 있습니다.

  

연산자 오버로딩 규칙

연산자를 오버로딩하는 언어적 규칙이 몇 가지 있습니다. 이를 소개합니다.

- 연산자 오버로딩은 operator X 키워드로 구현한다.
- 반드시 public 로 정의되어야 한다.
- 반드시 static 로 정의되어야 한다.
- 입력 매개변수 중 하나 이상은 반드시 그 클래스의 형식과 동일해야 한다

public static Complex operator + (Complex c1, int i){ .. }  //가능
public static Complex operator + (int i, int j) { .. }      //불가능

- 연산자 오버로딩을 다시 오버로딩 할 수 있다.

연산자 오버로딩도 결과적으로는 메서드이다. 따라서 시그너처를 달리 하면 이미 정의된 연산 오버로딩을 다시 오버로딩 할 수 있다아래 코드는 더하기(+) 연산자를 시그너처를 달리 하여 두 개 오버로딩 한 예시이다.

public static Complex operator + (Complex c1, Complex c2){
      return new Complex(c1.real + c2.real, c1.imaginary + c2.imaginary);
  }

  public static Complex operator + (Complex c1, int i){
      return new Complex(c1.real + i, c1.imaginary + i);
  }

 

연산자 오버로딩도 결국 메서드라 하였는데요.
 IL 코드를 보면 덧넷,뺄셈,곱셈에 대한 각각의 연산자 오버로딩이 op_Addition, op_Subtraction, op_Multiply 메서드로 자동 생성된 것을 확인할 수 있습니다.

이렇듯 연산자 오버로딩을 하게 되면 op_예약된심벌 형태의 메서드로 치환되는 것입니다.

비교연산자 오버로딩

지금까지는 사칙연산에 대한 연산자 오버로딩을 알아 보았는데요. 연산자 오버로딩은 사칙연산뿐만 아니라 다른 연산자에 대한 오버로딩도 가능합니다.

비교연산자의 오버로딩에 대해 알아 보겠습니다. C#의 비교 연산자는 다음과 같습니다.

1) 동일성 비교
== : 두 값의 일치 여부, != : 두 값의 불일치 여부

2) 크기 비교
< : ‘보다 작음 비교, > :  보다 큼 비교
<=: ‘작거나 같음 비교, >= 크거나 같음 비교

비교연산자를 자세히 보면 모두 쌍으로 이루어져 있습니다.  ==  !=  <  > 와 쌍을 이루고 있지요.

비교연산자를 오버로딩 할 경우 반드시 이 쌍을 모두 오버로딩 해야 합니다(필수 사항입니다)

예를 들어 == 연산자를 오버로딩 할 경우 반드시 그 쌍을 이루는 != 를 같이 오버로딩 해야 하는 것이죠.

닷넷 프레임워크에서 제공하는 System.String 에서 동일성 비교 연산자를 오버로딩 한 예를 볼 수 있는데요. Msdn System.String 클래스의 설명을 보면 아래와 같이 ==  !=를 오버로딩 한 것을 확인할 수 있습니다.

public static bool operator == (
             string a,
             string b
)

 

public static bool operator != (
             string a,
             string b
)

 

참고로 동일성 비교 연산자를 오버로딩 할 경우 Object 로부터 상속받는 Equals 메서드도 같은 연산으로 오버라이딩 하는 것이 좋습니다(프로그램 일관성을 위한 권장사항 입니다)
(나아가 Equals 메서드를 오버라이딩 할 경우 GetHashCode 메서드도 오버라이딩 하는 것이 좋습니다)

 

 

연산자 오버로딩 가능한 연산자 목록

앞서 살펴본 것과 같이 사칙연산과 비교 연산자는 연산자 오버로딩이 가능합니다. 닷넷의 연산자 중 오버로딩이 가능한 연산자와 일부 제한이 있는 연산자가 있습니다.

다음 표는 msdn 의 설명입니다

 

형 변환 연산자 오버로딩

마지막으로 형식 변환과 관련된 연산자 오버로딩에 대해 알아 보겠습니다.
연산자 오버로딩은 산술연산자나 비교연산자 외에도 형식(Type) 변환에도 적용할 수 있습니다

특정 자료형을 다른 자료형으로 변환하는 것을 형 변환 이라고 하는데요. 예를 들어 문자열 정수인 int 형의 데이터를 실수인 double 형으로의 형 변환은 다음과 같이 자연스럽게 이루어 집니다.

int iValue = 10;
double dValue = iValue; //묵시적 형변환

특별히 어떤 키워드 없이도 정수형 자료가 실수 형 자료로 할당되었습니다. 이것은 더 작은 자료형에서 큰 자료형으로의 변환은 자동으로 이루어 지는 즉 묵시적 형 변환 사례입니다.

반면 실수형 자료를 정수형 자료로 할당하고자 할 경우에는 다음 코드와 같이 반드시 명시적 형 변환을 해 줘야 합니다.

double dValue = 10d;
int iValue = (int) dValue; //명시적 형변환

명시적 형 변환 위한 (형식)  이라는 연산자가 이용된 것입니다.

이것은 클래스의 상속 관계에서도 적용되는데요. ‘부모 ? 자식 클래스 구조에서자식객체 -> 부모 객체로의 변환은 묵시적으로 이루어 지고 부모객체 -> 자식 객체로의 변환은 명시적으로 형 변환을 해 줘야 합니다.

이러한 묵시적, 명시적 형 변환을 새롭게 정의할 수 있는 것이 형 변환 연산자 오버로딩 입니다.

형 변환 오버로딩의 키워드는 operator X 와 더불어 다음 두 키워드가 사용됩니다.

implicit: 묵시적 형 변환 오버로딩
explicit: 명시적 형 변환 오버로딩

그럼. 형 변환 오버로딩 예를 보겠습니다.

Person이라는 클래스를 정의하고 이름(name)과 나이(age)를 속성으로 가지도록 합니다. 그리고 정수타입(int)에서 Person 타입으로 명시적 형 변환이 가능하도록 구현하고 Person 타입에서 문자열(string)타입으로 묵시적 형 변환이 가능하도록 오버로딩 해 보겠습니다.

class Person{
    public string name;
    public int age;

    public Person(string name, int age){
        this.name = name;
        this.age = age;
    }

    //명시적 형변환 오버로딩
    public static explicit operator Person(int age){
        return new Person("아무개", age);
    }

    //묵시적 형변환 오버로딩
    public static implicit operator string(Person person){
        return "제 이름은 " + person.name + "입니다";
    }
}

이렇게 클래스가 정의되고 클래스에서 각각의 형 변환을 위한 오버로딩 메서드가 구현되었으면 다음과 같이 사용할 수 있습니다.

int age = 20;
Person person1 = (Person) age; //명시적 형변환 수행(int -> Person 으로 변환)

Person person2 = new Person("홍길동", 20);
string s = person2; //묵시적 형변환 수행(Person -> string 으로 변환)

참고로 string 형 변환 오버로딩을 구현 한 경우 ToString 메서드도 같이 오버라이딩 해 주는 것이 좋습니다.

이상으로 연산자 오버로딩 강좌를 마치도록 하겠습니다.

행복한 한 주 되세요 ~~~