연산자도 메서드다
Esc

    연산자도 메서드다

    –C#

    앞에서 점 이라는 값 타입을 만들었습니다. 두 점을 더하려면 지금은 이렇게 씁니다.

    점 c = new 점(a.X + b.X, a.Y + b.Y);

    Unity 에서는 이렇게 씁니다.

    Vector3 c = a + b;

    Vector3 도 그냥 struct 입니다. 우리가 만든 점 과 다를 것이 없습니다. 그런데 + 가 됩니다. 뭔가 특별해서가 아니라, 누가 그 + 를 만들어 뒀기 때문입니다.

    a + b 는 컴파일할 때 점.op_Addition(a, b) 라는 메서드 호출이 됩니다. 그 메서드를 우리가 적으면 됩니다.

    연산자 오버로딩은 이렇게 생겼다

    메서드인데 이름 자리에 operator 와 기호가 들어갑니다.

    public static 점 operator +(점 a, 점 b)반드시 static. 객체가 아니라 타입에 붙습니다돌려주는 타입. 새 값을 만들어 돌려줍니다기호 자리. 이항 연산자는 매개변수 둘
    이름이 기호일 뿐 평범한 static 메서드입니다.
    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[...] 로 인덱서를 만들면 바깥에서 대괄호로 쓸 수 있습니다
    • 기호가 원래 가진 뜻을 벗어나면 만들지 않는 편이 낫습니다