Operator overloading: Operátor túlterhelés PHP -ben

Ma egy érdekességgel készültem. Mégpedig a címben szereplő operátor túlterhelés bemutatásával. Azért csak érdekességnek és nem "hasznosságnak" hívom, mert a hozzá szükséges pecl kiterjesztés még nagyon is beta. Annyira, hogy csak erőszakkal lehet feltelepíteni. És 5.5-ös PHP-vel még nem kompatibilis.

Aki mondjuk programozott már C++ -ban vagy C#-ban, annak nem idegen a fogalom. A többiek kedvéért annyit, hogy ez egy lehetőség arra, hogy a nyelv beépített operátoraival megismertessük a saját, egyedi típusainkat, osztályainkat is. Így össze lehet adni egy Number típusú objektumot egy egész számmal például. Vagy az összeadás speciális jelentéssel bírhat, és hatására akár egy halmazt is bővíthetünk.

Telepítés

A használatához PHP-ben telepíteni kell az "operator" nevű pecl kiterjesztést.

pecl install --force operator

A "--force" -ra azért van szükség, mert a telepítő látja, hogy ez nem egy stabil kiterjesztés. Így meg kell erősíteni, hogy komolyan gondoljuk.

A "pecl" program a php telepítési könyvtárában van a "bin" mappában. Egyedi telepítésnél, ha a program nincs a PATH változóban, a teljes útvonalát kell beírni.

Működése

Készíteni kell egy saját osztályt, amihez úgynevezett mágikus metódusokat kell definiálni. Ilyenek vannak az objektum tulajdonságainak lekérdezésére és még több más funkcióra is. A pecl kiterjesztés pedig további metódusokat ad.

Minden metódusban adott ugye az aktuális objektum. Az egyoperandusú műveleteknél nem is kell más. A $this változót használva az objektum állapota változtatható.

Kétoperandusú műveleteknél a baloldali operandus az aktuális objektum, a jobboldali pedig paraméterként jön. Ekkor van választási lehetőség, hogy az aktuális objektum állapotát változtatjuk meg a paraméter alapján vagy egy új objektumot hozunk létre és adjuk át visszatérési értékként.

Mivel az aktuális objektum a baloldali operandus, ebből adódik, hogy egy műveletnél baloldalon mindig olyan típusú objektumnak kell állnia, amire az operátorokat definiáljuk. Így, bár lehet konstans számot és objektumot összeadni, a sorrendre figyelni kell.

Bizonyos műveleteknél az egyik, másoknál a másik opció a jobb választás. Valamilyen visszatérési értéket viszont mindig célszerű adni, mivel így a műveletek láncolhatók. Azaz kettőnél több művelet hajtható végre egy utasításban. Erre mutatok majd példát, de a php beépített típusain végzett műveleteknél is ez történik.

Mágikus metódusok

Jelenleg az alábbi mágikus metódusok használhatók egy osztályban:

/* +    */ public function __add($operand) {}
/* -    */ public function __sub($operand) {}
/* *    */ public function __mul($operand) {}
/* /    */ public function __div($operand) {}
/* %    */ public function __mod($operand) {}

/* <<   */ public function __sl($operand) {}
/* >>   */ public function __sr($operand) {}

/* .    */ public function __concat($operand) {}  

/* |    */ public function __bw_or($operand) {}  
/* &    */ public function __bw_and($operand) {}
/* ^    */ public function __bw_xor($operand) {}  
/* ~    */ public function __bw_not($operand) {}
 
/* ===  */ public function __is_identical($operand) {}
/* !==  */ public function __is_not_identical($operand) {}
/* ==   */ public function __is_equal($operand) {}
/* !=   */ public function __is_not_equal($operand) {}
/* <    */ public function __is_smaller($operand) {}
/* <=   */ public function __is_smaller_or_equal($operand) {}
//Igen, a > és >= nem támogatott

/* $i++ */ public function __post_inc($operand) {}
/* $i-- */ public function __post_dec($operand) {}
/* ++$i */ public function __pre_inc($operand) {}
/* --$i */ public function __pre_dec($operand) {}

/* +=   */ public function __assign_add($operand) {}
/* -=   */ public function __assign_sub($operand) {}
/* *=   */ public function __assign_mul($operand) {}
/* /=   */ public function __assign_div($operand) {}
/* %=   */ public function __assign_mod($operand) {}

/* <<=  */ public function __assign_sl($operand) {}
/* >>=  */ public function __assign_sr($operand) {}

/* .=   */ public function __assign_concat($operand) {}  

/* |=   */ public function __assign_bw_or($operand) {}  
/* &=   */ public function __assign_bw_and($operand) {}
/* ^=   */ public function __assign_bw_xor($operand) {}  

Mire jó, mikor használjam?

A bevezetőben említettem, hogy halmazműveleteket is lehet így végezni. Megspórolható pár metódushívás és átláthatóbb lehet a forráskód.

Másik eset, amikor adott egy függvénykönyvtár, mint a gmp, ami eleve nagy számokkal való munkához van, így adja magát a gondolat, hogy miért ne dolgozhatnánk vele operátorokkal.

De kellő fantáziával ennél extrémebb eseteket is ki lehet találni. Még családot is alapíthatunk. Ezt hamarosan kifejtem.

Példák

Egyszerű szám

<?php
class Number {
  private $value = 0;
 
  public function __construct($number) {
    $this->value = self::getOperandValue($number);
  }
 
  public function getValue() {
    return $this->value;
  }
 
  public function __add($number) {
    return new self($this->value + self::getOperandValue($number));
  }
 
  private static function getOperandValue($value) {
    if ($value instanceof self) {
      $value = $value->value;
    }
    return $value;
  }
 
  public function __toString() {
    return strval($this->value);
  }
}

var_dump(new Number(15) + 5);

A getOperandValue metódust azért írtam, hogy tetszőleges típusokra működjön az összeadás. Persze, mint említettem, baloldalon csak Number típusú objektum állhat.

Látszik, hogy megmarad az objektum típus, mert azt adtuk visszatérési értékként, de megnő az értéke 5-tel. És persze a többi operátort is végig lehetne gépelni, de ezt most nem fogom. Inkább nézzük az érdekesebb példákat!

Halmaz

Talán pont halmazműveletekkel gyakoroltuk az egyetemen C++ -ban az operátortúlterhelést. Nézzük ezt PHP-ban:

class Set {
  private $value = array();
 
  public function __construct($value) {
    if ($value instanceof self) {
          $value = $value->getValue(); 
        }
       
        if (is_array($value)) {
                $this->value = array_unique($value);
        }
  }
 
  public function __add($set) {
    return new self(array_merge($this->value, self::getOperandValue($set)));
  }
 
  private static function getOperandValue($value) {
    if ($value instanceof self) {
      $value = $value->value;
    }
    return $value;
  }
}

$A = new Set(array(1, 2, 3));
$B = new Set(array(3, 4, 5));

var_dump($A+$B);

Ezzel összeadtam két halmazt. Kaptam egy harmadikat, aminek a metszetében csak a 3-as van. Így az eredményhalmaz az 1, 2, 3, 4, 5 elemeket tartalmazza egyszer. Most sem fogom a többi halmazműveletet megvalósítani. Mutatom inkább az utolsó és legérdekesebb példát.

Családalapítás

Eresszük el a fantáziánk és modellezzük le a családalapítást. Apu, anyu összebújik és az eredménye egy gyerek. Ennek a gyereknek automatikusan szülei lesznek apu és anyu. Nekik pedig egyre több gyerekük lesz.

class Person { 
  private $children = array();
       
  public function getChildren() {
    return $this->children;
  }
 
  public function addChild(Child $child) {
    $this->children[strval($child)] = $child;
  }
 
  public function __toString() {
    return spl_object_hash($this);
  }
}

class Woman extends Person {
  public function __sl(Man $man) {
    $child = new Child(new Couple($this, $man));
       
        $man->addChild($child);
        $this->addChild($child);
        return $child;
  }
}

class Man extends Person {
  public function __sr(Woman $woman) {
    $child = new Child(new Couple($woman, $this));
        $woman->addChild($child);
        $this->addChild($child);
        return $child;
  }
}

class Child extends Person {
  private $parents = null;
  public function __construct(Couple $parents) {
    $this->parents = $parents;
  }
 
  public function getParents() {
    return $this->parents;
  }
}

class Couple {
  private $man;
  private $woman;
  public function __construct(Woman $woman, Man $man) {
    $this->man = $man;
        $this->woman = $woman;
  }
  public function getMan() {
    return $this->man;
  }
 
  public function getWoman() {
    return $this->woman;
  }
}

$woman = new Woman();
$man = new Man();

echo "Gyerekek száma: ".count($woman->getChildren())."<br />";
$child1 = $woman << $man;
echo "Gyerekek száma: ".count($woman->getChildren())."<br />";
$child2 = $man >> $woman;
echo "Gyerekek száma: ".count($woman->getChildren())."<br />";

echo "Az első gyerek szülei: ";
$parents = $child1->getParents();
echo $parents->getWoman()." és ".$parents->getMan();

Persze a példa nem hibátlan, de aki unatkozik, ennél jobban is kidolgozhatja. Vagy találhat jobbat is.

GMP függvények

Végül pedig a gmp függvénykönyvtár néhány műveletét valósítom meg operátorokkal.

<?php
class GMPNumber  {
  private $resource;
  public function __construct($number) {
    if (!is_resource($number)) {
          $number = gmp_init($number);
        }
    $this->resource = $number;
  }
 
  public function __add($number) {
        return $this->_op('add', $number);
  }
 
  public function __sub($number) {
        return $this->_op('sub', $number);
  }
 
  public function __mul($number) {
    return $this->_op('mul', $number);
  }
 
  public function __div($number) {
        return $this->_op('div', $number);
  }
 
  public function __mod($number) {
        return $this->_op('mod', $number);
  }
   
  private function _op($op, $number) {
        return new self(call_user_func('gmp_'.$op, $this->resource, self::getOperandValue($number)));
  }
 
  private static function getOperandValue($number) {
    return strval($number);
  }
 
  public function __toString() {
        return gmp_strval($this->resource);
  }
}

$a = new GMPNumber('123');
$b = new GMPNumber('110');
?>
<pre>
<?php echo ($a + $b); ?>

<?php echo $a - $b; ?>

<?php echo $a % new GMPNumber('2');?>
</pre>

Vélemény

Meg lehet tehát lenni operátor túlterhelés nélkül, de akadhat olyan eset, amikor nagyon is jól jön, ha van. Éles szerverre azért talán nem telepíteném a kiterjesztést, de egy napon talán stabilabb lesz.

A példákat 5.4-es PHP alatt teszteltem. De elméletileg a korábbiakban is működik.

2013. augusztus 28.:

Az alábbi rfc bejegyzés szerint PHP 5.6-ban már tervezik a beépített megoldás megvalósítását. Az pedig már nincs messze, hiszen 5.5 a legfrissebb verzió.

PHP RFC: Internal operator overloading and GMP improvements

A példát pont a gmp-vel mutatják, de esküszöm, csak ma találtam meg :)

Források

Kategóriák: 
Megosztás/Mentés