Verification: a143cc29221c9be0

Php array keys with objects

Краткое описание класса WeakMap

final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traversable {
public function offsetGet(object $object);
public function offsetSet(object $object, mixed $value): void;
public function offsetExists(object $object): bool;
public function offsetUnset(object $object): void;
public function count(): int;
}

Подробнее о Weak Maps

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

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


$a = new Foo();
$b = $a;

//$b and $a теперь отдельные переменные
//оба указывают на объект Foo где-то в памяти.

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

Слабая ссылка же или слабая карта - это способ создания переменной, которая действует как и любая другая, но когда PHP проверяет, указывают ли какие-либо переменные на объект,

эти «слабые» переменные не учитываются. Поэтому, если есть еще три слабые ссылки, указывающие на объект, но нет обычных переменных, PHP с радостью удалит объект и вместо этого установит для остальных переменных значение null.

$map = new WeakMap;
$obj = new stdClass;
$map[$obj] = 42;
var_dump($map);

// object(WeakMap)#1 (1) {
// [0]=>
// array(2) {
// ["key"]=>
// object(stdClass)#2 (0) {
// }
// ["value"]=>
// int(42)
// }
// }

// Здесь объект уничтожается, а ключ автоматически удаляется со слабой карты.
unset($obj);
var_dump($map);
// object(WeakMap)#1 (0) {
// }

  Т.е. по сути Слабые ссылки это способ сказать: 

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

Будет больше смысла увидеть это в более-менее рабочем действии. Предположим, у вас есть серия объектов Product, написанных кем-то там еще в какой-то там библиотеке. Вы не можете их изменить, но вы собираетесь их использовать.

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

Вместо этого мы создадим отдельный объект ReviewList, содержащий слабую карту объектов Review, лениво загружая их по мере необходимости и отслеживая их Product. Как только Product удаляется из памяти, и соотвественно все переменные, которые на него ссылаются, выходят из области видимости, нам не нужно хранить все эти объекты Review. WeakMap в этом случае действует как самоочищающийся кеш и работает аналогично ArrayObject.


class ReviewList
{
private WeakMap $cache;

public function __construct()
{
$this->cache = new WeakMap();
}

public function getReviews(Product $prod): string
{
return $this->cache[$prod] ??= $this->findReviews($prod->id());
}

protected function findReviews(int $prodId): array
{
// получение обзоров продукта
}
}

$reviewList = new ReviewList();
$prod1 = getProduct(1);
$prod2 = getProduct(2);

$reviewsP1 = $reviewList->getReviews($prod1);
$reviewsP1 = $reviewList->getReviews($prod2);

// ...

$reviewsP1Again = $reviewList->getReviews($prod1);

unset($prod1);

 В этом примере ReviewList есть внутренний «слабый кеш», с ключом объектов Product. Когда вызывается getReviews(), если желаемое значение уже находится в кеше, оно будет возвращено. Если нет, он будет загружен в память, сохранен в кеше WeakMap и затем возвращен. (Здесь есть ??=  - "Оператор присваивания значения NULL", представленный в PHP 7.4, и является исключительно четким именно для такого рода случаев.) Позже, когда мы создадим $reviewsP1Again, значение будет вместо этого искаться в кэше.

Однако в какой-то момент в будущем мы уничтожим $prod1 - unset($prod1). Обычно это не выполняется вручную, но переменная выходит за пределы области видимости и собирает мусор. Поскольку обычных ссылок на объект product 1 больше нет, ссылка на этот объект в $cache Weak Map будет удалена автоматически. Это также приведет Review к автоматической очистке соответствующего списка объектов. Память сохранена, и дальнейшая работа не требуется. Это «Just Works!».

Если вы попытаетесь сделать то же самое с обычным массивом, возникнут две проблемы:

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

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

Замечания по WeakMap

Ключи WeakMap должны быть объектами

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

$map = new WeakMap();
$map['Foo'] = 'Bar';

// Fatal error: Uncaught TypeError: WeakMap key must be an object in ...:...

Добавление к WeakMap также не допускается. 

$map = new WeakMap();
$map[] = 'Baz';

// Fatal error: Uncaught Error: Cannot append to WeakMap in ...:...

Error на несуществующих ключах 

Если запрошенный объект не существует в объекте WeakMap, создается исключение \Error.

$map = new WeakMap();
$map[new stdClass()];

// Fatal error: Uncaught Error: Object stdClass#2 not contained in WeakMap in ...:...

Этого можно избежать, проверив индекс на существование.

$map = new WeakMap();
isset($map[new stdClass()]); // false

WeakMap не разрешает свойства 

$map = new WeakMap();
$map->foo = 'Bar';

// Fatal error: Uncaught Error: Cannot create dynamic property WeakMap::$foo in ...:...

WeakMap не поддерживает сериализацию/десериализацию 

Слабые карты не могут быть сериализованы или десериализованы. Если WeakMap класс объявлен final, это тоже можно изменить.

$map = new WeakMap();
serialize($map);

// Fatal error: Uncaught Exception: Serialization of 'WeakMap' is not allowed in ...:...

$serialized_str = 'C:7:"WeakMap":0:{}';
unserialize($serialized_str);

// Fatal error: Uncaught Exception: Unserialization of 'WeakMap' is not allowed in ...:...

Итерация слабых карт  

 Класс WeakMap реализует интерфейс Traversable, его можно использовать итеративно с помощью foreach. Далее WeakMap имплементирует IteratorAggregate, приносящий метод WeakMap::getIterator.

map = new WeakMap();

$obj1 = new stdClass();
$map[$obj1] = 'Object 1';

foreach ($map as $key => $value) {
var_dump($key); // var_dump($obj1)
var_dump($value); // var_dump('Object 1');
}

Сам итератор можно извлечь с помощью метода getIterator, а возвращаемое значение - это iterable.

$map = new WeakMap();

$obj1 = new stdClass();
$map[$obj1] = 'Object 1';

$iterator = $map->getIterator();

foreach ($iterator as $key => $value) {
var_dump($key); // var_dump($obj1)
var_dump($value); // var_dump('Object 1');
}

Задача

Необходимо перебрать по очереди и обработать все или некоторые элементы массива.

Решение

Используйте оператор foreach:

foreach ($array as $value) {
    // Действие с $value
}

Или для получения ключей и значений массива:

foreach ($array as $key => $value) {
    // Действие II
}

Другим способом является применение оператора for:

for ($key = 0, $size = count($array); $key     // Действие III
}

И наконец, можно использовать функцию each() в комбинации с функцией list() и оператором while:

reset($array) // сброс внутреннего указателя в начало массива
while (list($key, $value) = each ($array)) {
    // Окончательное действие
}

Цикл each (jQuery.each). Примеры использования

Синтаксис функции each:

// array или object - массив или объект, элементы или свойства которого необходимо перебрать
// callback - функция, которая будет выполнена для каждого элемента массива или свойства объекта
$.each(array или object,callback);

Работу с функцией each разберём на примерах.

Пример №1. В нём выполним переберор всех элементов массива (array).

// массив, состоящий из 3 строк
var arr = ['Автомобиль','Грузовик','Автобус'];

// переберём массив arr
$.each(arr,function(index,value){

  // действия, которые будут выполняться для каждого элемента массива
  // index - это текущий индекс элемента массива (число)
  // value - это значение текущего элемента массива
  
  //выведем индекс и значение массива в консоль
  console.log('Индекс: ' + index + '; Значение: ' + value);

});

/*
Результат (в консоли):
Индекс: 0; Значение: Автомобиль
Индекс: 1; Значение: Грузовик
Индекс: 2; Значение: Автобус
*/

В вышеприведённом коде функция each используется для перебора массива. Функция имеет 2 обязательных параметра. Первый параметр - это сущность (массив или объект), элементы (свойства) которой необходимо перебрать. В данном случае - это массив arr. Второй параметр - это функция обратного вызова, которая будет выполнена для каждого элемента (в данном случае) массива. Она имеет 2 параметра, которые доступны внутри неё посредством соответствующих переменных. Первый параметр - это порядковый номер элемента (отсчёт выполняется с 0). Второй параметр - это значение текущего элемента массива.

Пример №2. В этом примере осуществим перебор всех свойств объекта.

// объект smartphone, имеющий 5 свойств
var smartphone = {
  "name": "LG G5 se",
  "year": "2016",
  "screen-size": "5.3",
  "screen-resolution": "2560 x 1440",
  "os" : "Android 6.0 (Marshmallow)"
};

// переберём объект smartphone
$.each(smartphone, function( key, value ) {

  // действия, которые будут выполняться для каждого свойства объекта
  // key - текущее имя свойства массива
  // value - значение текущего свойства объекта
 
  // выведем имя свойства и его значение в консоль
  console.log( 'Свойство: ' +key + '; Значение: ' + value );

});

/*
Результат (в консоли):
Свойство: name; Значение: LG G5 se
Свойство: year; Значение: 2016
Свойство: screen-size; Значение: 5.3
Свойство: screen-resolution; Значение: 2560 x 1440
Свойство: os; Значение: Android 6.0 (Marshmallow)
*/

Функция each может использоваться для перебора JavaScript объектов. Отличие её использования заключается только в том, что параметры функции обратного вызова имеют другие значения. Первый параметр хранит название свойства объекта, а второй - значение этого свойства.

Пример №3. В нём осуществим перебор более сложной структуры (рассмотрим, как использовать вложенные each).

// объект, состоящий из 2 свойств. Каждое свойство этого объект имеет в качестве значения массив, элементами которого являются тоже объекты
var articles = {
  "Bootstrap": [
    {"id":"1", "title":"Введение"},
    {"id":"2", "title":"Как установить"},
    {"id":"3", "title":"Сетка"}
  ],
  "JavaScript": [
    {"id":"4", "title":"Основы"},
    {"id":"5", "title":"Выборка элементов"}
  ]  
};

$.each(articles,function(key,data) {
  console.log('Раздел: ' + key);
  $.each(data, function(index,value) {
    console.log('Статья: id = ' + value['id'] + '; Название = '+ value['title']);
  });
});

/*
Результат:
Раздел: Bootstrap
Статья: id = 1; Название = Введение
Статья: id = 2; Название = Как установить
Статья: id = 3; Название = Сетка
Раздел: JavaScript
Статья: id = 4; Название = Основы
Статья: id = 5; Название = Выборка элементов
*/

Как прервать each (выйти из цикла)

Прерывание (break) цикла each осуществляется с помощью оператора return, который должен возвращать значение false.

Например, прервём выполнение цикла each после того как найдём в массиве arr число 7:

// массив, состоящий из 5 чисел
var arr = [5, 4, 7, 17, 19];

// число, которое необходимо найти
var find = 7;

// переберём массив arr
$.each(arr, function (index, value) {
  // если необходимое число найдено, то..
  if (value === find) {
    // вывести его в консоль
    console.log('Ура! Число ' + find + ' найдено! Данное число имеет индекс: ' + index);
    // прервать выполнение цикла
    return false;
  } else {
  // иначе вывести в консоль текущее число
  console.log('Текущее число: ' + value);
  }
});

/* Результат (в консоли):
Текущее число: 5
Текущее число: 4
Ура! Число 7 найдено! Данное число имеет индекс: 2
*/

Как перейти к следующей итерации (each continue)

В each прерывание выполнения текущей итерации и переход к следующей осуществляется с помощью оператора return, который должен иметь значение отличное от false.

// массив, состоящий из чисел
var arr = [3, 5, 4, 9, 17, 19, 30, 35, 40];

// массив, который должен содержать все элементы массива arr, кроме чётных чисел
var newarr = [];

// переберём массив arr
$.each(arr, function (index, value) {

  // если элемент чётный, то пропустим его
  if (value % 2 === 0) {
    // прервём выполнение текущей итерации и перейдём к следующей
    return;
  }
  // добавить в массив newarr значение value
  newarr.push(value);

});

console.log('Исходный массив (arr): ' + arr.join());
console.log('Результирующий массив (newarr): ' + newarr.join());

/* Результат (в консоли):
Исходный массив (arr): 3,5,4,9,17,19,30,35,40
Результирующий массив (newarr): 3,5,9,17,19,35
*/