2.7.3 연산자 오버로딩(Operator Overloading)

2.7.3 연산자 오버로딩(Operator Overloading)

추상은 클래스 필드에 @:op 메타데이터를 추가해 단항·이항 연산자를 오버로드할 수 있어요:

abstract MyAbstract(String) {
  public inline function new(s:String) {
    this = s;
  }

  @:op(A * B)
  public function repeat(rhs:Int):MyAbstract {
    var s:StringBuf = new StringBuf();
    for (i in 0...rhs)
      s.add(this);
    return new MyAbstract(s.toString());
  }
}

class Main {
  static public function main() {
    var a = new MyAbstract("foo");
    trace(a * 3); // foofoofoo
  }
}

@:op(A * B)를 정의함으로써, 왼쪽 값의 타입이 MyAbstract이고 오른쪽 값의 타입이 Int일 때 곱셈 * 연산자의 연산자 메서드로 repeat 함수가 동작해요. 사용은 17행에 나와 있고, JavaScript로 컴파일하면 다음 코드로 바뀝니다:

console.log(_AbstractOperatorOverload.
  MyAbstract_Impl_.repeat(a,3));

클래스 필드가 있는 암시적 캐스팅과 비슷하게, 필요할 때 오버로드 메서드 호출이 삽입됩니다.

예제의 repeat 함수는 교환 법칙이 성립하지 않아요. MyAbstract * Int는 되지만 Int * MyAbstract는 되지 않습니다. @:commutative 메타데이터를 함수에 붙이면 어느 순서든 타입을 받도록 강제할 수 있어요.

함수가 Int * MyAbstract에만, 즉 MyAbstract * Int에는 동작하지 않아야 한다면, 오버로드 메서드를 static으로 만들어 IntMyAbstract를 각각 첫·두 번째 타입으로 받게 할 수 있어요.

단항 연산자 오버로딩도 비슷합니다:

abstract MyAbstract(String) {
  public inline function new(s:String) {
    this = s;
  }

  @:op(++A) public function pre() return "pre" + this;

  @:op(A++) public function post() return this + "post";
}

class Main {
  static public function main() {
    var a = new MyAbstract("foo");
    trace(++a); // prefoo
    trace(a++); // foopost
  }
}

이항·단항 연산자 오버로드는 모두 어떤 타입이든 반환할 수 있어요.

Haxe 4.0.0 이후@:op 문법으로 추상의 필드 접근과 배열 접근을 오버로드할 수 있습니다:

  • 인자 하나를 가진 함수의 @:op([])는 배열 읽기 접근을 오버로드해요.
  • 인자 두 개를 가진 함수의 @:op([])는 배열 쓰기 접근을 오버로드하며, 첫 인자는 인덱스, 둘째 인자는 쓰여질 값입니다.
  • 인자 하나를 가진 함수의 @:op(a.b)는 필드 읽기 접근을 오버로드해요.
  • 인자 두 개를 가진 함수의 @:op(a.b)는 필드 쓰기 접근을 오버로드합니다.
abstract MyAbstract(String) from String {
  @:op([]) public function arrayRead(n:Int)
    return this.charAt(n);

  @:op([]) public function arrayWrite(n:Int, char:String)
    return this.substr(0, n) + char + this.substr(n + 1);

  @:op(a.b) public function fieldRead(name:String)
    return this.indexOf(name);

  @:op(a.b) public function fieldWrite(name:String, value:String)
    return this.split(name).join(value);
}

class Main {
  static public function main() {
    var s:MyAbstract = "example string";
    trace(s[1]); // "x"
    trace(s[2] = "*"); // "ex*mple string"
    trace(s.string); // 8
    trace(s.string = "code"); // "example code"
  }
}

기반 타입 연산 노출(Exposing underlying type operations)@:op 함수의 메서드 본문은 생략할 수 있어요. 단 추상의 기반 타입이 해당 연산을 허용하고, 결과 타입이 추상에 다시 할당 가능할 때만 가능합니다.

abstract MyAbstractInt(Int) from Int to Int {
  // The following line exposes the (A > B) operation from the underlying Int
  // type. Note that no function body is used:
  @:op(A > B) static function gt(a:MyAbstractInt, b:MyAbstractInt):Bool;
}

class Main {
  static function main() {
    var a:MyAbstractInt = 42;
    if (a > 0) ...
  }
}

출처: Operator Overloading

본문

@:op 연산자 오버로딩

@:op(연산자) 메타데이터를 메서드에 붙여 연산자를 오버로드할 수 있어요. @:op(A * B)는 왼쪽이 추상 타입, 오른쪽이 Int일 때의 곱셈을 정의합니다. 표기에서 A는 추상 타입, B는 다른 피연산자 타입을 나타내요.

교환 법칙과 방향

오버로드된 연산자는 기본적으로 교환 법칙이 성립하지 않습니다. @:commutative를 붙이면 어느 순서든 허용하고, static 메서드로 만들면 방향을 고정할 수 있어요.

단항 연산자

@:op(++A)@:op(A++) 같은 단항 연산자도 오버로드할 수 있어요. 전자는 전위, 후자는 후위 연산을 정의합니다.

필드·배열 접근 오버로드

Haxe 4.0.0부터 @:op([])로 배열 읽기·쓰기, @:op(a.b)로 필드 읽기·쓰기 접근을 오버로드할 수 있습니다.

기반 타입 연산 노출

@:op 함수의 본문을 생략하면 기반 타입의 연산을 그대로 노출할 수 있어요. 예를 들어 @:op(A > B) static function gt(...)는 기반 Int 타입의 > 연산을 노출합니다.

더 알아보기