Operator Overloading 알아보기
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
이 글은 저의 짧은 지식으로 작성 되었구요. 만약 다른 사이트에 배포하실 때에는 출처는 꼭 밝혀 주세요. 안 그러면 미워할꼬야.. 혹시 강좌 내용 중 의심나는 부분이 있으면 주저하지 마시고 좀더 나은 강좌를 위해 바로 위의 메일로 리포트해주세요. 리포트한번 해주시는데에 대해 뽀뽀한번..!!^^
우리가 꿈꾸는 세상..~~^^ by the .NET, of the .NET, for the .NET
=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
안녕하세요. Cache임다. 오늘은 Operator Overloading에 대해 알아보도록 하죠. 아직은 많은 분들께서 요놈에 대해 생소하게 느끼시는 것 같습니다. 이번 기회에 요놈에 대해 확실히 다져보는 시간을 같도록 하겠습니다. 물론 이 강좌를 수행하시기 전에 객체지향의 꽃인 다형성(Polymorphism)에 대해서는 숙지하고 계셔야 합니다.
어떻게 보면 이 강좌도 폴리모피즘을 따르는 개념중에 하나라고 생각하시면 될 듯 합니다. 물론 C#에서 지원하는 연산자에 대해서는 대충 뭐가 있다 정도만 파악하시면 된답니다. 그럼 시작해 볼까요..?
연산자, 연산자를 오버로딩한다...!! 이 말을 어떻게 받아들이시고들 있으신지..? 전혀 감이 안오시나여..? 아님 아.. 그거..!! 하며 필이 오시나여..? 만약 이 글을 읽고 계신분께서 전자에 해당하신다면 꼭 C#관련 책을 한번 읽어 보셔서 예습을 한번 해 보시고 오시면 좀더 이해가 빨리 되시리라 생각되구요. 만약 후자에 해당되신다면 복습한다 치고 가볍게 읽어 보시면 도움이 되리라 생각됩니다.
연산자 오버로딩이라는 의미에 대해서 알아보죠. 이는 클래스에 연산자를 정의함으로써 클래스에 연산 기능을 부여하고, 프로그램을 읽기 쉽게 하는 이점을 제공합니다. 연산자 오버로딩 메소드는 operator 키워드를 사용하구요. 반드시 public static 멤버이어야 합니다.
그럼 통상적으로 우리가 사용하고 있는 + 이항연산자의 경우를 살펴보죠. 피연산자가 숫자형일 경우에는 더하라는 의미이고, 피연산자가 문자열일 경우는 문자열을 합치라는 의미입니다. 그럼 피연산자가 위의 예에서의 Time형 + Time형과 같이 임의 클래스의 타입일 경우에는 어떤 의미일까요..? 바로 이때 연산자 오버로딩이 필요한거죠. 특정 연산자에 대해 새로운 의미를 부여하여 해당 연산자가 사용되면 이 새로 정의한 의미를 부여하는거죠. 만일 위의 예에서 연산자를 재정의 하지 않고 Time형 + Time형을 사용할 경우에는 당연히 에러가 발생하겠죠. 필이 오시지 않나요..?
거두 절미하고 소스를 바로 보시죠. 이 소스를 가지고 기본적인 개념을 설명드리겠습니다. 이 소스는 기억해 두세요. 이번 강좌내내 우려 먹을 것이거든요. ^^
// 파일명 : operatorOverloading.cs
// 코멘트 : 연산자 오버로딩 테스트
namespace operatorOverloading
{
using System;
public class Time
{
// + 연산자 오버로딩(재정의) 부분
public static Time operator+(Time t1, Time t2)
{
Console.WriteLine("Time.+");
return new Time();
}
public static Time operator+(int i, Time t)
{
Console.WriteLine("Time.+(int, Time))");
return new Time();
}
public static Time operator+(Time t, int i)
{
return (i + t);
}
}
public class Class1
{
public static int Main(string[] args)
{
Time t1 = new Time();
Time t2 = new Time();
// 오버로딩된 연산자 사용하는 부분
Time t3 = t1 + t2;
Time t4 = 3 + t1;
Time t5 = t1 + 3;
Centigrade c1 = new Centigrade();
Centigrade c2 = new Centigrade();
If (c1 && c2) { }
If (c1 || c2) { }
Height h1 = new Height();
int i = h1;
Time t6 = (Time) h1;
return 0;
}
}
}
자 그럼 코드를 한줄한줄 분석해 볼까요.
public class Time
{
// + 연산자 오버로딩(재정의) 부분
public static Time operator+(Time t1, Time t2)
{
Console.WriteLine("Time.+");
return new Time();
}
public static Time operator+(int i, Time t)
{
Console.WriteLine("Time.+(int, Time))");
return new Time();
}
public static Time operator+(Time t, int i)
{
return (i + t);
}
}
위의 코드는 + 이항연산자에 대한 연산자 재정의 부분입니다. 연산자 재정의부의 시그너쳐를 살펴보면 기본형식은 아래와 같습니다.
형식 : public static 리턴형 operater OP (인자형1, 인자형2) { 구현부 }
근데 왜 하필 public 일까요..? 이 질문에 대답은 당연하죠 입니다. 연산자 재정의하겠다는 의미는 어디선가에서 호출된다는 대전제가 깔려있지 않을까요..? 그래서리 당연히 어디선가 재정의된 연산자가 호출될 수 있게 해당 연산자 재정의부를 public으로 선언합니다.
그리고 왜 static 일까요..? 그 이유는 인스턴스가 생성될 필요없이 호출되기 때문이죠.
+ 연산자의 인스턴스를 어떻게 생성할까요..? ^^ 또 각각의 인자의 타입에 맞는 연산자를 호출하겠죠...? 당근이죠…!!
Time t1 = new Time();
Time t2 = new Time();
위의 코드는 피연산자로 사용될 Time 클래스형 인스턴스를 생성하는 부분입니다. 인스턴스 t1과 t2가 나중에 연산자 오버로딩에 의한 연산에서 실지 사용되는 놈들입니다.
// 오버로딩된 연산자 사용하는 부분
Time t3 = t1 + t2;
Time t4 = 3 + t1;
Time t5 = t1 + 3;
위의 코드는 이렇게 생성된 Time 클래스의 인스턴스를 + 이항연산자를 사용하여 연산을 수행하는 부분입니다. 좀더 자세히 살펴보죠.
아래의 라인이 수행되면 어떤 연산자 재정의부가 실행 될까요..?
Time t3 = t1 + t2;
당연히 다음 연산자 재정의부가 실행될것입니다.
public static Time operator+(Time t1, Time t2)
{
Console.WriteLine("Time.+");
return new Time();
}
요놈은 두 피연산자가 Time형일때 수행됩니다. 여기서 연산자 재정의 할시 숙지하셔야 할점이 있습니다. 다름이 아니라, 이항연산자의 경우 인자로 넘겨지는 피연산자의 타입의 둘 중 하나는 연산자 재정의 하고자 하는 클래스의 타입이어야 한다는 거죠. 단항연산자를 오버로딩 하실시에는 당연히 재정의 하고자 하는 타입을 인자로 받아야 겠죠. 이해가 되시는지..? 위의 연산자 재정의부는 인자로 둘다 인자로 Time 형을 받고 있습니다. 아무 문제 없겠죠..
그럼 다른 연산자 재정의부들을 살펴보죠. 이번에 살펴볼 놈은 인자로 조금 다른 형태를 취하고 있습니다.
Time t4 = 3 + t1;
위의 라인이 실행될 때는 눈치 체셨겠지만. 아래의 연산자 재정의부가 실행됩니다.
public static Time operator+(int i, Time t)
{
Console.WriteLine("Time.+(int, Time))");
return new Time();
}
여기서 언급해야 할점이 있다. 연산자 재정의부에서 int형과 Time형을 하나씩 인자로 받는다고 해서 아래의 문장도 되겠지 하면 큰 오산이다. 다음은 예외를 발생한다.
Time t5 = t1 + 3;
위의 문장에서 발생한 예외를 없애기 위해서는 당연히 이 형식에 맞는 연산자 재정의부를 추가해 주어야 한다. 피연산자의 순서도 주의해서 살펴보기 바랍니다. 아래의 소스를 추가하여 보면 예외가 사라질 것이다.
public static Time operator+(Time t, int i)
{
return (i + t);
}
간단하죠. 아직도 살펴볼 내용이 많거든요. 위에 언급한 내용은 반드시 숙지하신 후에 이후의 내용으로 들어가십시오. 그럼 이제 부가적인 연산자 오버로딩 부분을 살펴보죠.
먼저 관계형 연산자 오버로딩 부분을 살펴보죠. 관계형 연산자는 반드시 한쌍으로 오버로딩해 주어야 한다. <는 >과, <=는 =>과, ==는 !=과 함께 오버로딩 해주어야 한다. 여기서 주의 할점은 필수적인 사항은 아니지만 권고 사항으로 ==과 !=를 오버로딩 할 때는 Equals() 메소드도 역시 오버라이딩 해주는게 좋다.
그 이유는 ‘같다’라는 의미를 가지는 ==을 오버라이딩 할 때 이와 동일한 의미의 Equals() 메소드도 같이 오버라이딩해서 의미의 모호함을 사전에 방지해 주어야 프로그램의 readability 및 유지,보수가 좀더 편해 진다는 것이다. ==과 Equals() 메소드가 상이한 의미로 사용될 때는 프로그래머가 유지, 보수시 골탕을 먹을수도 있다는 것이다.
다음 논리형 연산자 오버로딩 부분을 살펴보면요. &&과 || 연산자는 직접 오버로딩이 되지 않습니다. 거꾸로 말하면 간접적으로 오버로딩해야 한다는 것이다. 위의 연산자를 오버로딩하기 위해서는 &, |, true, false 이 4개의 연산자를 오버로딩함으로써 트릭을 써서 오버로딩할 수 있다. 다시 말해 &, |, true, false 이 4개의 연산자를 오버로딩하면 자동으로 &&과 || 연사자가 오버로딩 된다는 것이다.
public struct Centigrade
{
public static bool operator false(Centigrade c)
{
Console.WriteLine("Centigrade.false");
return false;
}
public static bool operator true(Centigrade c)
{
Console.WriteLine("Centigrade.true");
return true;
}
public static Centigrade operator|(Centigrade c1, Centigrade c2)
{
Console.WriteLine("Centigrade.|");
return new Centigrade();
}
public static Centigrade operator&(Centigrade c1, Centigrade c2)
{
Console.WriteLine("Centigrade.&");
return new Centigrade();
}
}
}
위에서도 언급했듯이 &&과 ||는 오버로딩할 수 없다고 했었다. 그럼 위의 본문 소스에서 부분을 조금 살펴보자. 아래는 &&과 || 연산시 피연산자로 사용될 Centigrade 클래스의 인스턴스를 생성하는 라인이다.
Centigrade c1 = new Centigrade();
Centigrade c2 = new Centigrade();
아래는 실제 &&과 || 연산자를 사용하는 부분입니다. 이 부분에서 자동으로 재정의된 &&과 || 연산자를 사용하고 있다.
If (c1 && c2) { }
If (c1 || c2) { }
여기서 숙지하고 넘어가셔야 하는 부분이 있다. &&과 || 연산자가 사용될 때의 실제 의미를 파악해야 한다는 것이다. 그 의미를 한번 짚어보면,
X && Y 는 T.false(X) ? X : T.&(X,Y) 이 실제 의미이고, 이렇게만 표시해놓으면 이해하기 어려운 분이 계실까 해서 한번 의미를 풀어보자. 이것은 먼저 오버로딩된 false 연산자를 수행한 후에 & 연산자를 수행한다..
X || Y 는 T.true(X) ? X : T.|(X,Y) 이 실제 의미이다.. 이것은 먼저 오버로딩된 true라는 연산자를 수행한 후에 | 연산자를 수행한다.
다시 말해 && 연산자는 false 연산자 수행후 & 연산자를 수행하고, || 연산자는 true 연산자 수행후 | 연산자를 수행한다. 모르시는 분은 이럴 때 무대포 정신을 발휘하여 그냥 외워 버리세요.
다음 오버로딩의 형변환 부분을 살펴보기로 하죠. 이 부분은 많은(?) 분들이 고개를 갸우뚱 하시는 부분일지도 모르겠다.. 허나 차근 차근 따라해 보시면 금방 이해가 가시리라 생각된다. 먼저 본문 소스에서 이에 연관된 부분을 라인별로 분석해 보면요.
Height h1 = new Height();
int i = h1;
Time t6 = (Time) h1;
위의 소스를 살펴보면요.
먼저 첫머리에 int 형으로 형변환하기 위한 Height 클래스의 인스턴스 h1을 생성한후 다음라인에서 실제로 형변환이 수행되고 있는 부분이다. 그리고 다음 라인에서는 Height 클래스의 인스턴스 h1을 Time 클래스 형으로 형변환을 시도하고 있다.
namespace operatorOverloading
{
using System;
public class Height
{
public static implicit operator int (Height h)
{
Console.WriteLine("implicitly, Height -> int");
return 1;
}
public static explicit operator Time(Height h)
{
Console.WriteLine("explicitly, Height -> Time");
return new Time();
}
}
}
그럼 곰곰히 따져보죠.
int i = h1;
위의 코드는 Height 형을 묵시적으로 System.Int32 형으로 형변환을 시도하고 있다. 여기서 주의 할점은 우리가 이 부분을 형변환 재정의 했다는 것이다. 위의 형변환이 시도되려는 순간 컴파일러는 아래의 코드를 호출할 것이다.
public static implicit operator int (Height h)
{
Console.WriteLine("implicitly, Height -> int");
return 1;
}
여기서 한번 형변환 재정의의 기본 형식을 한번 따져보면,
형식 : public static [implicit 또는 explicit] operater 타겟형 (소스형) { 구현부 }
implicit 또는 explicit 라는 키워드는 이 형변환이 묵시적이냐 명시적이냐 하는 것을 지칭한다. 주의할 점이 있는 형변환 하고자 하는 소스형과 형변환의 결과물이 되는 타겟형의 위치이다. 이 부분은 확실이 눈에 익히도록 하세요. 반대로 인지하여 소스형과 타겟형의 위치를 바꿔 놓으면 예기치 않은 오류또는 의도하지 않은 오류가 나올수도 있을 것이다.
이런 삼천포로 빠졌는데요..^^
다시 본론으로 돌아가서 살펴보자면요 위의 코드가 의미하는 것은 무엇일까요..?
Heght 형에서 int형으로 묵시적으로 형변환을 시도하려는 놈에게는 요놈이 재정의 된다는 것이죠.
그럼 이에 반대되는 명시적 형변환 재정의에 대해 알아보죠. 아래 라인이 그 부분입니다.
Time t6 = (Time) h1;
그리고 아래 부분이 위의 명시적 형변환을 시도하고 있는 라인의 실제 재정의부 입니다.
자세한 내용은 위의 언급된 내용과 중복되기 때문에 생략하겠습니다.
public static explicit operator Time(Height h)
{
Console.WriteLine("explicitly, Height -> Time");
return new Time();
}
마지막으로 연산자도 시그너쳐만 다르다면 다중 오버로딩이 가능합니다. 위의 원본 소스와 같이요. 편의를 위해 아래에 해당 부분을 페이스트 해드립니다.
public static Time operator+(Time t1, Time t2)
{
Console.WriteLine("Time.+");
return new Time();
}
public static Time operator+(int i, Time t)
{
Console.WriteLine("Time.+(int, Time))");
return new Time();
}
public static Time operator+(Time t, int i)
{
return (i + t);
}
참고로, 연산자 오버로딩 메소드와 형변환 메소드가 중복되었을 시에는 연산자 오버로딩 메소드가 우선합니다. 당연하겠죠 연산자 오버로딩의 경우 대부분 r-value쪽에서 일어나고,
형변환 같은 경우는 r-value 형을 l-value 형으로 변환시켜 주는 것이니깐요.
지금까지 연산자 오버로딩에 대해 알아보았습니다. 나름대로는 쉽게 설명한다고 하면서도 어딘가 석연치 않은 부분이 있는 것 같네요. 이 부분은 차츰 컨텐츠를 업데이트 해가면서 차차 수정해 나가도록 하겠습니다. 여러분들께서 이해해 주시기를...
이처럼 연산자 오버로딩이 어렵다면 어려울 것이고 쉽다면 쉽게 느껴질 것이다. 만약 여기까지 읽었는데도 이해가 가질 않는다면 다시한번 처음부터 읽어보시길 바랍니다.