Примечание. Это сообщение было перемещено на мой личный веб-сайт, проверьте его, чтобы прочитать самую последнюю версию и следить за моими последними публикациями: https://liamhammett.com/php -wishlist-operator-overloading-wEQXAr4p

Примечание. С момента написания этого поста я наткнулся на расширение pecl-php-operator на GitHub, которое делает именно то, что я описал в этом посте, это потрясающе!



PHP - отличный язык, который дает разработчикам много возможностей. Базовый язык поддерживает несколько волшебных методов и интерфейсов, которые могут быть реализованы в классах, которые позволяют разработчику выбирать, как объект должен вести себя в определенных ситуациях, например, когда он повторяется или передается через базовые функции, такие как count().

У них есть множество вариантов использования, которые позволяют объектам иметь свободный API и взаимодействовать с ними интуитивно. Основным примером использования этого является класс коллекции Laravel, который реализует многие из них, поэтому с ним можно взаимодействовать, как с любым обычным массивом.

class Collection implements ArrayAccess, Countable, IteratorAggregate, JsonSerializable
{
    // ...
    public function __toString() { /* ... */ }
    public function __get($key) { /* ... */ }
}

Есть веские аргументы в пользу того, что создание таких «волшебных» объектов - это плохой шаблон проектирования, поскольку он отвлекает от разработчика важные функции и может сделать неясным, что на самом деле происходит за кулисами.

Это определенно аргумент, который следует учитывать, но я считаю, что с модерацией и хорошо продуманной реализацией они могут снизить воспринимаемую сложность и сделать наш код более чистым, чтобы мы могли сосредоточиться на бизнес-логике, а не на семантике взаимодействия с объектом. .

Из-за этого я думаю, что PHP может пойти на шаг дальше, чем он уже есть, и позволить объектам решать, как они должны взаимодействовать с различными операторами в языке, например, когда оно сравнивается с другим значением, умноженным на единицу или чем-то еще, что вы может представить.

Как PHP мог это реализовать

Это не новый шаблон для языков программирования; в частности, C ++, C #, Python и Ruby в той или иной форме имеют перегрузку операторов. Давайте посмотрим на… Метаметоды Lua

Основанный на прототипах язык программирования Lua является ярким примером, который позволяет расширять таблицы (аналогичные массивам PHP в том смысле, что они могут быть плоскими или ассоциативными и содержать любой другой тип данных) с помощью функций и дополнительных свойств, точно так же, как объекты PHP.

Lua имеет несколько« метаметодов , которые можно добавить, которые расширяют поведение объекта в различных сценариях. Однако у него есть целый другой набор этих метаметодов, которые PHP не предлагает, могут быть выполнены, когда в объекте используются базовые языковые конструкции, такие как операторы сравнения и изменения.

-- The original table is just a simple list of strings
originalTable = { 'a', 'b', 'c' }
-- Create a "metatable" to let us declare special methods on it
specialTable = setmetatable(originalTable, {
    -- Define a special function that is executed every time
    -- the "add" operator is used with this special table
    __add = function(specialTable, newTable)
        -- Iterate over each item in thew new table
        for i = 1, #newTable do
            -- Insert the values from the 2nd table into the 1st
            table.insert(specialTable, #specialTable + 1, newTable[i])
        end
        return specialTable
    end
})
results = specialTable + { 'd', 'e', 'f' }
-- The "results" variable now includes the following values:
-- { 'a', 'b', 'c', 'd', 'e', 'f' }

PHP Core уже делает это

Ядро PHP уже неявно допускает такое поведение в нескольких местах. Например, оператор + может выполнять сложение между двумя числами, но выполнять объединение между двумя массивами.

5 + 2 === 7;
2 + 5 === 7;
['a' => true] + ['b' => true] === ['a' => true, 'b' => true];

Интересно то, что объединение двух массивов может также привести к двум разным результатам в зависимости от того, какой массив идет первым, что является проблемой, на которую нам пришлось бы обратить внимание, если бы мы реализовали это самостоятельно.

$one = [
    'a' => true,
    'b' => true,
];
$two = [
    'b' => false,
    'c' => true,
];
$one + $two === [
    'a' => true,
    'b' => true,
    'c' => true,
];
$two + $one === [
    'b' => false,
    'c' => true,
    'a' => true,
];

Операторы сравнения также могут использоваться с объектами DateTime для сравнения, если один предшествует другому.

$one = new DateTime('2016-01-01');
$two = new DateTime('2019-04-30');
$one < $two === true;
$one > $two === false;

Реализация в Userland для PHP

Существует множество способов реализации такого рода функций в PHP, позволяющих разработчикам использовать их. Два основных из них:

  • Автономные магические методы. Использование одного оператора не обязательно зависит от другого, поэтому при необходимости они могут быть реализованы по отдельности.
  • Интерфейсы. Это может привести к тому, что методы для одинаковых операторов будут реализованы одновременно, поэтому вы никогда не столкнетесь с ситуацией, когда $a > $b работает, а $a < $b не реализовано.

Левая ассоциативность

Я полагаю, что все эти магические методы оставались бы ассоциативными. То есть они сработают, только если объект, в котором они находятся, находится слева от операции. Единственное значение будет передано в метод как параметр; значение в правой части операции.

Таким образом, обе стороны операции будут доступны в области действия метода: $this для доступа к левой стороне и одному параметру для правой стороны.

Это может сбить с толку, поскольку это означает, что $a + $b может не дать такого же результата, как $b + $a, если две переменные не совпадают. Подобно использованию оператора объединения для двух массивов, это всего лишь деталь реализации, о которой разработчик должен знать.

Типы

Эти магические методы могут принимать в качестве аргумента любой тип. Это означает, что любая проверка типов должна быть проверена и подтверждена разработчиком.

Пример

Давайте посмотрим, как это может работать на практике. Вместо того, чтобы не забывать использовать такие функции, как bcmath() вместо оператора сложения при работе с расширением BCMath, мы могли бы сделать его неявным и естественным, как если бы он работал с обычными числами.

class BCNumber
{
    public $value;
    public function __construct($value)
    {
        $this->value = $value;
    }
    public function __add($toAdd)
    {
        if (is_string($toAdd) && is_numeric($toAdd)) {
            return new BCNumber(bcadd($this->value, $toAdd));
        }
        if (is_integer($toAdd)) {
            return new BCNumber(bcadd($this->value, $toAdd));
        }
        if ($toAdd instanceof BCNumber) {
            return new BCNumber(bcadd($this->value, $toAdd->value));
        }
        throw new InvalidArgumentException('Can not add type ' . gettype($toAdd) . ' to BCNumber.');
    }
}
$exampleOne = new BCNumber(5);
$exampleTwo = new BCNumber(2);
$exampleThree = $exampleOne + $exampleTwo;
echo $exampleThree->value; // 7

Операторы

Теперь, когда у нас есть понимание того, как их можно использовать, давайте посмотрим, какие операторы можно сделать доступными.

Модификация операторов

Это операторы в PHP, которые принимают два разных значения и возвращают что-то совершенно другое.

interface Operatable
{
    /** $this . $value */
    public function __concatenate($value);
    /** $this + $value */
    public function __add($value);
    /** $this - $value */
    public function __subtract($value);
    /** $this * $value */
    public function __multiply($value);
    /** $this / $value */
    public function __divide($value);
    /** $this % $value */
    public function __modulo($value);
    /** $this ** $value */
    public function __exponent($value);
    /** $this & $value */
    public function __bitwiseAnd($value);
    /** $this | $value */
    public function __bitwiseOr($value);
    /** $this ^ $value */
    public function __bitwiseXor($value);
    /** $this << $value */
    public function __shiftLeft($value);
    /** $this >> $value */
    public function __shiftRight($value);
}

Давайте посмотрим на другой пример того, как один из этих методов может быть реализован в реальном объекте коллекции для объединения двух внутренних массивов, аналогично оператору объединения массивов.

class Collection
{
    public $array = [];
    public function __construct(array $array)
    {
        $this->array = $value;
    }
    public function __add($value)
    {
        if (is_array($value)) {
            return new Collection(array_merge($this->array, $value));
        }
        if ($value instanceof Collection) {
            return new Collection(array_merge($this->array, $value->array));
        }
        throw new InvalidArgumentException('Can not add type ' . gettype($toAdd) . ' to Collection.');
    }
}
$collectionOne = new Collection(['a', 'b', 'c']);
$collectionTwo = new Collection(['d', 'e', 'f']);
$newCollection = $collectionOne + $collectionTwo;
$newCollection->value; // ['a', 'b', 'c', 'd', 'e', 'f']

Операторы сравнения

Некоторые операторы в PHP используются для сравнения двух объектов. Они возвращают логическое значение *, чтобы определить, было ли сравнение верным или нет - они являются краеугольным камнем логики любого языка программирования.

Единственная разница с магическими методами для этих операторов состоит в том, что они имеют ожидаемый тип возвращаемого значения.

* Оператор космического корабля возвращает целое число -1, 0 или 1, а не логическое значение

interface Comparable
{
    /** $this == $value */
    public function __equal($value): bool;
    /** $this === $value */
    public function __identical($value): bool;
    /** $this != $value */
    /** $this <> $value */
    public function __notEqual($value): bool;
    /** $this !== $value */
    public function __notIdentical($value): bool;
    /** $this > $value */
    public function __greaterThan($value): bool;
    /** $this < $value */
    public function __lessThan($value): bool;
    /** $this >= $value */
    public function __greaterThanOrEqualTo($value): bool;
    /** $this <= $value */
    public function __lessThanOrEqualTo($value): bool;
    /** $this <=> $value */
    public function __spaceship($value): int;
}

Давайте еще раз рассмотрим пример того, где это можно использовать - приложение, которое работает с деньгами в нескольких валютах. При сравнении одного денежного значения с другим можно неявно конвертировать оба значения в одну и ту же валюту с использованием текущего обменного курса.

class Money
{
    // ...
    public function __greaterThan($value): bool
    {
        if (! $value instanceof Money) {
            throw new Exception('Cannot compare with type other than Money');
        }
        return $this->amount > $value->convertToCurrency($this->currency)->amount;
    }
}
$currentBalance = new Money(50, 'GBP');
$transactionAmount = new Money(25, 'USD');
if ($transactionAmount > $currentBalance) {
    throw new Exception('You do not have enough balance to make this transaction.');
}

Унарные операторы

Унарные операторы уникальны тем, что они принимают только одно значение и что-то с ним делают. Это означает, что любые магические методы для унарных операторов не будут принимать никаких параметров и не ожидать ответа какого-либо одного типа - они могут делать что угодно с текущим объектом и возвращать любое значение.

interface UnaryOperatable
{
    /** !$this */
    public function __unaryNot();
    /** +$this */
    public function __unaryAdd();
    /** -$this */
    public function __unarySubtract();
    /** ~$this */
    public function __unaryBitwiseNot();
    /** ++$this */
    public function __preIncrement();
    /** $this++ */
    public function __postIncrement();
    /** --$this */
    public function __preDecrement();
    /** $this-- */
    public function __postDecrement();
}

Операторы присваивания

Также есть несколько операторов присваивания в PHP, которые нельзя игнорировать. Их также необходимо расширить, чтобы унаследовать некоторые из поведения основных операторов, к которым они относятся.

Меня больше всего беспокоит то, что все может выглядеть странно, если объект спроектирован так, чтобы быть неизменным. Разработчик технически уже может изменить состояние объекта внутри любого магического метода сравнения или модифицируемого оператора, но это не ожидаемый результат, как при использовании оператора присваивания.

В такие классы можно было бы реализовать Immutable интерфейс, который запрещал бы использование этих операторов, выдавая ошибку, если они используются.

class Example implements Immutable
{
    // ...
    public function __add($value)
    {
        // ...
    }
}
$example = new Example();
$example += 'Value'; // PHP Fatal error:  Uncaught Error: Object of type 'Example' is immutable and can not be modified.

Вывод

На протяжении моей разработки с PHP несколько раз я находил некоторые из этих волшебных методов полезными, и я хотел бы увидеть их в будущем.

Я прекрасно понимаю, что при некоторых обстоятельствах могут возникнуть проблемы с таким подходом, и что не все в первую очередь являются поклонниками магических методов, не говоря уже о добавлении новых.

Это не RFC для ядра PHP, хотя я был бы рад, если бы кто-нибудь взялся за него. Это просто то, что доставило бы мне немного больше удовольствия от программирования на PHP, если бы оно когда-либо стало ядром.

Что вы думаете о методах магических операторов?