연산자도 메서드다
앞에서 점 이라는 값 타입을 만들었습니다. 두 점을 더하려면 지금은 이렇게 씁니다.
점 c = new 점(a.X + b.X, a.Y + b.Y);
Unity 에서는 이렇게 씁니다.
Vector3 c = a + b;
Vector3 도 그냥 struct 입니다. 우리가 만든 점 과 다를 것이 없습니다.
그런데 + 가 됩니다. 뭔가 특별해서가 아니라, 누가 그 + 를 만들어 뒀기 때문입니다.
a + b는 컴파일할 때점.op_Addition(a, b)라는 메서드 호출이 됩니다. 그 메서드를 우리가 적으면 됩니다.
연산자 오버로딩은 이렇게 생겼다
메서드인데 이름 자리에 operator 와 기호가 들어갑니다.
struct 점
{
public int X;
public int Y;
public 점(int x, int y) { X = x; Y = y; }
public static 점 operator +(점 a, 점 b)
{
return new 점(a.X + b.X, a.Y + b.Y);
}
}
이제 됩니다.
점 a = new 점(1, 2);
점 b = new 점(3, 4);
점 c = a + b; // (4, 6)
static 이어야 하는 이유가 있습니다. a + b 에서 + 는 a 의 것도 b 의 것도 아닙니다.
둘 사이의 일이라 어느 한쪽에 붙이면 이상해집니다. 그래서 타입에 붙입니다.
a 나 b 를 고치지 않고 새 값을 만들어 돌려주는 것도 중요합니다.
3 + 5 를 했다고 3 이 바뀌지 않는 것과 같습니다.
== 를 만들면 세 개를 더 만들어야 한다
여기서부터 함정입니다. == 를 하나 만들면 컴파일러가 화를 냅니다.
public static bool operator ==(점 a, 점 b)
{
return a.X == b.X && a.Y == b.Y;
}
이것만 쓰면 이런 에러가 납니다.
'점.operator ==' 를 정의하려면 'operator !=' 도 정의해야 합니다
== 와 !=, < 와 >, <= 와 >= 는 반드시 짝으로 만들어야 합니다.
한쪽만 있으면 a == b 는 되는데 a != b 는 안 되는 이상한 타입이 되기 때문입니다.
그리고 경고가 두 개 더 붙습니다.
'점' 이 'operator ==' 를 정의하지만 'Equals' 를 재정의하지 않습니다
'점' 이 'operator ==' 를 정의하지만 'GetHashCode' 를 재정의하지 않습니다
이건 에러가 아니라 경고라 그냥 넘어갈 수 있습니다. 넘어가면 나중에 크게 당합니다. 그 이야기는 이 글 끝에서 다시 합니다.
만들 수 있는 것과 없는 것
만들 수 있는 것은 정해져 있습니다.
| 종류 | 기호 |
|---|---|
| 이항 | + - * / % |
| 비교 | == != < > <= >= |
| 단항 | + - ! ~ ++ -- |
| 비트 | & | ^ << >> |
| 형변환 | explicit implicit |
만들 수 없는 것도 알아 둘 만합니다. =, &&, ||, ?:, . 은 못 만듭니다.
a += b 는 따로 만드는 것이 아니라 + 를 만들면 저절로 됩니다. a = a + b 로 풀리기 때문입니다.
형변환도 만들 수 있습니다.
public static implicit operator 점(int n) => new 점(n, n);
public static explicit operator int(점 p) => p.X;
implicit 은 캐스팅 없이 되고 explicit 은 (int)p 처럼 적어야 합니다.
값이 손실되거나 실패할 수 있으면 explicit 으로 둡니다. 앞에서 본 캐스팅 규칙과 같은 기준입니다.
인덱서는 대괄호를 만드는 것
같은 이야기가 [] 에도 있습니다. list[0] 이나 dict["키"] 의 대괄호도 우리가 만들 수 있습니다.
class 격자
{
private int[] 칸 = new int[9];
public int this[int x, int y]
{
get { return 칸[y * 3 + x]; }
set { 칸[y * 3 + x] = value; }
}
}
this[...] 라고 씁니다. 프로퍼티와 생김새가 같습니다. get 과 set 이 있고 value 로 받습니다.
프로퍼티에 이름 대신 대괄호가 붙었다고 보면 정확합니다.
격자 g = new 격자();
g[1, 2] = 7;
Console.WriteLine(g[1, 2]); // 7
2차원 배열처럼 보이는데 안은 1차원 배열입니다. 바깥에서 쓰는 모양과 안의 구조를 따로 정할 수 있다는 것이 인덱서의 쓸모입니다. 행과 열을 하나짜리 번호로 바꾸는 계산은 앞에서 2차원 배열을 볼 때 나왔던 그 식입니다.
하면 안 되는 것
기술적으로는 아무 기호에 아무 뜻이나 넣을 수 있습니다. 그래서 규칙이 필요합니다.
기호가 이미 가진 뜻을 지킵니다. + 는 더하기입니다.
플레이어 + 아이템 이 “아이템을 장착한다” 를 뜻하게 만들면 코드를 읽는 사람이 매번 정의를 찾아봐야 합니다.
그건 메서드 이름으로 적으면 됩니다. 플레이어.장착(아이템) 이 훨씬 낫습니다.
원래 값을 안 고칩니다. a + b 를 했는데 a 가 바뀌면 아무도 예상하지 못합니다.
비싼 일을 넣지 않습니다. 연산자는 짧고 싸 보입니다. 그 안에서 파일을 읽거나 네트워크를 부르면 읽는 사람이 비용을 못 봅니다.
그래서 실제로 쓸 만한 자리는 좁습니다. 좌표, 색, 시간, 돈, 분수처럼 원래 수학에서 더하고 빼던 것입니다.
Vector3 나 Quaternion 이 딱 그 자리입니다.
정리
a + b는 static 메서드 호출로 바뀝니다. 그 메서드를operator +로 적으면 됩니다static이어야 하는 이유는 어느 한쪽의 것이 아니기 때문입니다==와!=는 짝으로 만들어야 합니다. 한쪽만은 컴파일 에러입니다this[...]로 인덱서를 만들면 바깥에서 대괄호로 쓸 수 있습니다- 기호가 원래 가진 뜻을 벗어나면 만들지 않는 편이 낫습니다