company_banner

О дженериках в PHP и о том, зачем они нам нужны

Автор оригинала: stitcher.io blog
  • Перевод


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


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


$posts = $blogModel->find();

Вам нужно циклически пройти по всем постам и что-то сделать с их данными. Например, с id.


foreach ($posts as $post) {
    $id = $post->getId();

    // Что-то делаем
}

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


Целостность структуры данных (Data integrity)


В PHP массив представляет собой коллекцию ... элементов.


$posts = [
    'foo',
    null,
    self::BAR,
    new Post('Lorem'),
];

Если циклически пройти по нашему набору постов, то в результате получим критическую ошибку.


PHP Fatal error:  Uncaught Error: Call to a member function getId() on string

Вызываем ->getId() применительно к строке 'foo'. Не прокатило. При циклическом проходе по массиву мы хотим быть уверены, что все значения принадлежат к определённому типу. Можно сделать так:


foreach ($posts as $post) {
    if (!$post instanceof Post) {
        continue;
    }

    $id = $post->getId();

    // Что-то делаем
}

Это будет работать, но если вы уже писали PHP-код для production, то знаете, что такие проверки иногда быстро разрастаются и загрязняют кодовую базу. В нашем примере можно проверять тип каждой записи в методе ->find() в $blogModel. Но это лишь переносит проблему из одного места в другое. Хотя ситуация чуть улучшилась.


С целостностью структуры данных есть ещё одна сложность. Допустим, у вас есть метод, которому нужен массив блог-постов:


function handlePosts(array $posts) {
    foreach ($posts as $post) {
        // ...
    }
}

Мы опять можем добавить в цикл дополнительные проверки, но это не гарантирует, что $posts содержит только коллекцию постов Posts.


Начиная с PHP 7.0 для решения этой проблемы вы можете использовать оператор ...:


function handlePosts(Post ...$posts) {
    foreach ($posts as $post) {
        // ...
    }
}

Но у этого подхода есть обратная сторона: вам придётся вызывать функцию применительно к распакованному массиву.


handlePosts(...$posts);

Производительность


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


Мы не можем прогнать на дженериках бенчмарки, потому что их пока нет, так что остаётся лишь гадать, как они повлияют на производительность. Но не будет безумием предположить, что оптимизированное поведение PHP, написанное на С, — это лучший способ решения проблемы по сравнению с созданием кучи кода для пользовательского пространства.


Автозавершение (Code completion)


Не знаю, как вы, а я при написании PHP-кода прибегаю к IDE. Автозавершение чрезвычайно повышает продуктивность, так что я хотел бы использовать его и здесь. При циклическом проходе по постам нам нужно, чтобы IDE считал каждый $post экземпляром Post. Давайте посмотрим на простую PHP-реализацию:


# BlogModel

public function find() : array {
    // возвращает ...
}

Начиная с PHP 7.0 появились типы возвращаемых значений, а в PHP 7.1 они были улучшены с помощью void и типов, допускающих значение null. Но мы никак не можем сообщить IDE, что содержится в массиве. Поэтому мы возвращаемся к PHPDoc.


/**
 * @return Post[]
 */
public function find() : array {
    // возвращает ...
}

При использовании реализации «дженерика», например класса моделей (model class), не всегда возможен подсказывающий метод ->find(). Так что в нашем коде придётся ограничиться подсказывающей переменной $posts.


/** @var Blog[] $posts */
$posts = $blogModel->find();

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


* * *


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


Важное замечание: дженериков пока что нет в PHP. RFC предназначен для PHP 7.1, о его будущем нет никакой дополнительной информации. Нижеприведённый код основан на интерфейсах Iterator и ArrayAccess, которые существуют с PHP 5.0. В конце мы разберём пример с дженериками, представляющий собой фиктивный код.


Для начала создадим класс Collection, который работает в PHP 5.0+. Этот класс реализует Iterator, чтобы можно было циклически проходить по его элементам, а также ArrayAccess, чтобы можно было использовать «массивоподобный» синтаксис для добавления элементов коллекции и обращения к ним.


class Collection implements Iterator, ArrayAccess
{
    private $position;

    private $array = [];

    public function __construct() {
        $this->position = 0;
    }

    public function current() {
        return $this->array[$this->position];
    }

    public function next() {
        ++$this->position;
    }

    public function key() {
        return $this->position;
    }

    public function valid() {
        return isset($this->array[$this->position]);
    }

    public function rewind() {
        $this->position = 0;
    }

    public function offsetExists($offset) {
        return isset($this->array[$offset]);
    }

    public function offsetGet($offset) {
        return isset($this->array[$offset]) ? $this->array[$offset] : null;
    }

    public function offsetSet($offset, $value) {
        if (is_null($offset)) {
            $this->array[] = $value;
        } else {
            $this->array[$offset] = $value;
        }
    }

    public function offsetUnset($offset) {
        unset($this->array[$offset]);
    }
}

Теперь можем воспользоваться подобным классом:


$collection = new Collection();
$collection[] = new Post(1);

foreach ($collection as $item) {
    echo "{$item->getId()}\n";
}

Обратите внимание: нет никакой гарантии, что $collection содержит только Posts. Если добавить, к примеру, строковое значение, то работать будет, но наш цикл сломается.


$collection[] = 'abc';

foreach ($collection as $item) {
    // This fails
    echo "{$item->getId()}\n";
}

При текущем уровне развития PHP мы можем решить эту проблему с помощь создания класса PostCollection. Обратите внимание: типы возвращаемых данных, допускающие использование null, доступны лишь с PHP 7.1.


class PostCollection extends Collection
{
    public function current() : ?Post {
        return parent::current();
    }

    public function offsetGet($offset) : ?Post {
        return parent::offsetGet($offset);
    }

    public function offsetSet($offset, $value) {
        if (!$value instanceof Post) {
            throw new InvalidArgumentException("value must be instance of Post.");
        }

        parent::offsetSet($offset, $value);
    }
}

Теперь в нашу коллекцию могут добавляться только Posts.


$collection = new PostCollection();
$collection[] = new Post(1);

// This would throw the InvalidArgumentException.
$collection[] = 'abc';

foreach ($collection as $item) {
    echo "{$item->getId()}\n";
}

Работает! Даже без дженериков! Есть только одна проблема: решение немасштабируемое. Вам нужны отдельные реализации для каждого типа коллекции, даже если классы будут различаться только типом.


Вероятно, создавать подклассы можно с бо́льшим удобством, «злоупотребив» поздним статическим связыванием и рефлексивным API PHP. Но вам в любом случае понадобится создавать классы для каждого доступного типа.


Великолепные дженерики


Учитывая всё это, давайте рассмотрим код, который мы могли бы написать, будь дженерики реализованы в PHP. Это может быть один класс, используемый для всех типов. Ради удобства я приведу лишь изменения по сравнению с предыдущим классом Collection, имейте это в виду.


class GenericCollection<T> implements Iterator, ArrayAccess
{
    public function current() : ?T {
        return $this->array[$this->position];
    }

    public function offsetGet($offset) : ?T {
        return isset($this->array[$offset]) ? $this->array[$offset] : null;
    }

    public function offsetSet($offset, $value) {
        if (!$value instanceof T) {
            throw new InvalidArgumentException("value must be instance of {T}.");
        }

        if (is_null($offset)) {
            $this->array[] = $value;
        } else {
            $this->array[$offset] = $value;
        }
    }

    // public function __construct() ...
    // public function next() ...
    // public function key() ...
    // public function valid() ...
    // public function rewind() ...
    // public function offsetExists($offset) ...
}
$collection = new GenericCollection<Post>();
$collection[] = new Post(1);

// This would throw the InvalidArgumentException.
$collection[] = 'abc';

foreach ($collection as $item) {
    echo "{$item->getId()}\n";
}

И всё! Мы используем <T> в качестве динамического типа, который можно проверять перед runtime. И опять же, класс GenericCollection можно было бы брать для любых типов.


Если вы так же впечатлены дженериками, как и я (а это лишь верхушка айсберга), то ведите просветительскую работу в сообществе и делитесь RFC: https://wiki.php.net/rfc/generics

Mail.ru Group
1295,00
Строим Интернет
Поделиться публикацией

Комментарии 89

    0
    Впервые узнал про дженерики из Java. Абсолютно согласен с тем, что это была бы крайне полезная фича. Особенно для различных библиотек.

    P.S. Я читаю теги!
      +3
      Дженерики это фигня по сравнению с шаблонами C++.
        0

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

      +8
      Если циклически пройти по нашему набору постов, то в результате получим критическую ошибку.

      PHP Fatal error: Uncaught Error: Call to a member function getId() on string
      Так мы работаем с массивом постов, или с массивом рандомных элементов?
        +2

        Согласен, статья рассматривает какую-то гипотетическую проблему, а не практическую. Если контролировать формирование массива, то описанная ситуация невозможна.

          0

          Но это означает что нам нужно делать специализированные коллекции на каждый случай. А так можно было бы делать что-то типа:


          interface ArticlesProvider
          {
              public function getArticles(): Collection<Article>;
          }

          Еще если добавить к этому элиасы типов как в Hack то вообще удобно:


          type ArticlesCollection = Collection<Article>;

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

        –1
        Чот странное ощущение от этих дженнриков. С одной стороны ругают php за динамическую типизацию, с другой стороны тащат в пых динамическое все, следить за которым еще то удовольствие.

        Полочается «Возьми то, не знаю что, и верни его точно существующее свойство или метод.

        Сорян, хрень какая то, в масштабах 10 — 20 сущностей.
        Если больше то может и имеет смысл
          +3
          с другой стороны тащат в пых динамическое все, следить за которым еще то удовольствие.

          Давайте разберемся с терминологией. "Статическое" — это когда информация о чем-то доступна из кода. "Динамическое" — то что происходит в рантайме.


          Почему статическое хорошо — потому что можно не выполняя код проверить корректность определенных вещей. К примеру возможность декларировать типы и указывать их в сигнатурах методов позволяет нам при помощи статического анализа сильно уменьшить количество потенциальных багов.


          Почему динамическое хорошо — потому что когда все статическое, чтобы что-то поменять нужно менять код, что не слишком удобно.


          К примеру у нас есть объект представляющий некую посылку. Эдакий контейнер. Нам без разницы что там, нам нужно просто дать ему поведение "хранения" какой-то вещи, возможность задать адресата и узнать пустая посылка или нет. Если мы будем жестко (статически) декларировать тип хранимого объекта, нам придется множить количество реализаций посылок на каждый возможный тип объектов которые мы хотим переслать. А еще — мы хотим сделать это дело библиотекой которую можно реюзать между проектами. Или у нас вайтлейблы и для каждого могут быть свои типы объектов. И на каждый чих надо множить подтипы.


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


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


          Теперь немного про "PHP ругают за динамическую..." Ругают его в основном за то что там типизация не только динамическая но еще и слабая. Скажем python со своей сильной динамической типизацией допускает меньший класс ошибок. В Java скажем система типов тоже не сахар, хоть и статическая.

            0
            А ведь вы меня убедили, пора пересматривать мои старческие подходы :-) Спасибо!
              0
              А что не так с системой типов в Java?
              +2
              Дженерики как раз и привносят статическую типизацию.
              +7
              Реализовать дженерики в PHP можно проще простого, достаточно чу-чуть изменить конструктор и метод offsetSet:
              class Collection implements Iterator, ArrayAccess {
              //    ...
              
                  /** @var string */
                  private $className = null;
              
                  /**
                   * @param string $T
                   */
                  public function __construct($T) {
                      if (is_string($T) && class_exists($T)) {
                          $this->className = $T;
                      } else {
                          throw new \InvalidArgumentException("T must be name of class.");
                      }
                      $this->position = 0;
                  }
              
              //...
                  public function offsetSet($offset, $value) {
                      if (!($value instanceof $this->className)) {
                          throw new \InvalidArgumentException("value must be instance of {$this->className}.");
                      }
                      if (is_null($offset)) {
                          $this->array[] = $value;
                      } else {
                          $this->array[$offset] = $value;
                      }
                  }
              }
              
              
              $postCollection = new Collection(Post::class);
              
              $postCollection[] = new Post(); //ok
              $postCollection[] = 'Post'; //throw Exception
              


              Единственное чего не хватает, так это дополнения в IDE. Но и как выше говорили, достаточно нормально следить за заполнением массива.

              UPD: Если указать тип для $postCollection через PHPDoc как массив постов, то будет подсказывать
              /** @var Post[] $postCollection */
              $postCollection = new Collection(Post::class);
              
                +1

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

                  –1
                  Плодить подтипы не нужно. В клиентском коде достаточно указать имя класса в конструкторе класса-коллекции.

                  $collection = new GenericCollection<Post>(); //Вариант из статьи
                  $collection = new Collection('Post'); //Мой вариант
                  

                  Обе строчки «равнозначны».
                    0
                    Не равнозначны. Во втором варианте клиентский код никак не отличит содержимое
                    $collection = new Collection('Post');
                    

                    от
                    $collection = new Collection('User');
                    
                      –1
                      Для различия содержимого достаточно добавить в класс коллекции пару методов:
                          /**
                           * Замена для instanceOf
                           * @param $class
                           * @return bool
                           */
                          public function isInstanceOf($class)
                          {
                              return is_a($this->className, $class, true);
                          }
                      
                          /**
                           * @return string
                           */
                          public function getClassName()
                          {
                              return $this->className;
                          }
                      
                        +1
                        Тогда везде в клиентском коде нужно проверять тип элементов в коллекции. Это лишь немногим удобней обычного массива в пхп
                          –1
                          Тогда везде в клиентском коде нужно проверять тип элементов в коллекции. Это лишь немногим удобней обычного массива в пхп


                          Похоже мы с вами друг-друга не понимаем. Будьте добры, приведите пример, как вы будите использовать джинерики, для отличия
                          $collection = new Collection<Post>();
                          

                          от
                          $collection = new Collection<User>();
                          
                            0
                            Когда дженерики нативные у вас нет необходимости проверять тип элемента в коллекции. Просто нет нужды в методах isInstanceOf или типа того. Кроме того, IDE сможет вам показать методы элемента коллекции без костылей с аннотациями. Статические анализаторы легко могут проверить что вы не вызываете несуществующий метод.

                            Дело вот в чем:
                            abstract public mixed offsetGet ( mixed $offset )
                            


                            Никакие решения на базе mixed не позволят избавить клиентский код от необходимости проверок типа елементов колекции.
                              –1
                              Пока в ПХП такая типизация, какая она есть, без костылей с аннотациями, к сожалению не обойтись, но кажется я вас понял. Если бы дженерики были нативные, то приведенный ниже код гарантировал бы, что внутри функции useCollection все элементы $posts являются экземплярами класса Post (если это не так, то будет эксешпшен при вызове).
                              function useCollection(Collection<Post> $posts) {
                                  // ....
                              }
                              


                              Мой код позволяет добиться такого поведения, без особых заморочек:
                              function useCollection(Collection $posts) {
                                  if (!$posts->isInstanceOf(Post::class)) {
                                      throw new InvalidArgumentException();
                                  }
                                  /** @var Post[] $posts */
                                  // ....
                              }
                              

                              Это 100% гарантирует, что каждый элемент коллекции является экземпляром Post

                              Дело вот в чем:
                              abstract public mixed offsetGet ( mixed $offset )
                              


                              Никакие решения на базе mixed не позволят избавить клиентский код от необходимости проверок типа элементов коллекции.


                              Если вы внимательно посмотрите в мою реализацию метода offsetGet, то увидите, что в коллекцию могут быть добавлены только объекты того класса, который был указан при создании коллекции, так что необходимости проверять каждый элемент коллекции перед использованием нет.
                                0
                                Ну вот эта проверка
                                if (!$posts->isInstanceOf(Post::class)) {
                                        throw new InvalidArgumentException();
                                    }
                                

                                по сути ничем не отличается от
                                if (!$post instanceof Post) {...}
                                


                                Единственное преимущество у вас, это то, что не нужно проверять каждый елемент.

                                Я внимательно смотрел вашу реализацию. Но дело в том, что код который наполняет коллекцию и код который коллекцию использует — это два разных кода. Код который использует коллекцию знает что Collection это что-то к чему можно обращаться как к массиву и при этом метод получения элемента возвращает mixed. Именно поэтому используют аннотации, чтобы хоть IDE подсветила методы
                                  –1
                                  Единственное преимущество у вас, это то, что не нужно проверять каждый элемент.

                                  Т.е. делает тоже самое, что и указание джинерика в сигнатуре метода.
                                    0
                                    Нет. В пыхе достаточно способов напихать в вашу коллекцию что попало. Дженерики не позволяют этого сделать на уровне языка
                                      0
                                      В пыхе достаточно способов напихать в вашу коллекцию что попало

                                      Прошу, приведите пример кода, но без использования рефлексии.
                                        0
                                        А почему собственно без?
                                          0

                                          Потому что в общем случае это нарушение инкапсуляции.


                                          И за регулярное использование рефлексии в обычных проектах лично я буду отбивать руки

                                            0

                                            Рефлексия — это одно из самых мощных средств для реализации метапрограммирования. А учитывая то, что в PHP оно ещё и невероятно быстрое, по сравнению, например, с Java — непонятно почему от неё стоит отказываться.


                                            С другой стороны и помимо рефлексии есть множество способов реализовать задуманное, но думаю проксирование сквозь Stream API, токенизация и построение минимального базового AST чуть более накладно и на некоторый порядок сложнее в осознании.


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

                                              0
                                              Рефлексия — магия, которая ведет к неожиданному поведению, непонятному стек-трейсу и плохому автодополнению
                                                0

                                                Повторюсь.
                                                В общем случае использование рефлексии в вашем коде это признак проблем архитектуры.


                                                Не такой большой объем задач действительно требует её. И эти задачи не пишутся ежедневно.


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

                                              0
                                              Три причины:
                                              1. Использование рефликсия является самым очевидным способом, обойти интерфейс и запихать в приватное поле все что вздумается, а в исходном комменте заявлено несколько способов.
                                              2. Я точно знаю как «сломать» коллекцию через рефликсию, хочется увидеть что-то новое, чего я не знаю.
                                              3. Я не очень хорошо знаком с возможностями рефлексии в других языках, но у верен, что в C# можно с ее помощью сломать даже местные дженерики.
                                                +1
                                                1. Я точно знаю как «сломать» коллекцию через рефликсию, хочется увидеть что-то новое, чего я не знаю.

                                                можно создать замыкание в контексте интересующего вас класса (Closure::bind) и таким образом "надломить инкапсуляцию". Но идея не в этом, это как рассматривать только самый негативный кейс.


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

                                              0
                                              Ну как то так без рефлексии
                                              $collection = new Collection(Item::class);
                                              $collection[] = new Item();
                                              
                                              $collection->__construct(FakeItem::class);
                                              $collection[] = new FakeItem();
                                              
                                              foreach ($collection as $item) {
                                                  var_dump(get_class($item));
                                              }
                                              
                                              

                                                –1
                                                От этого можно защитится установив в конструкторе защиту от переопределения.
                                                0
                                                Ну или как то так.

                                                $collection = new Collection(Item::class);
                                                $collection[] = new Item();
                                                
                                                $addFakeItems = function (array $fakeItems) {
                                                    $this->array = array_merge($this->array, $fakeItems);
                                                };
                                                
                                                $addFakeItems = $addFakeItems->bindTo($collection, $collection);
                                                
                                                $addFakeItems(['azazaz', 42, 6.66, new FakeItem(), false, null]);
                                                
                                                foreach ($collection as $item) {
                                                    var_dump($item);
                                                }
                                                

                                                  0
                                                  А вот это зачет, возьму себе в копилку.
                                              0

                                              Вот только статические анализаторы эту запись не поймут. А значит смысла не так много.

                                                0
                                                К сожалению, из-за особенностей типизации PHP, статистически анализаторы много-где не могут помочь. Например, отсутствие возможности перегрузки методов вынуждает отказываться даже от typehintig-га.

                                                public void method (ClassA a) {
                                                // ...
                                                } 
                                                public void method (ClassB a) {
                                                // ...
                                                } 
                                                
                                                


                                                в PHP превращается в
                                                public method ($a) {
                                                // работаем без поддержки анализатора =(
                                                } 
                                                

                                                  0
                                                  Думаю здесь нужен был интерфейс в качестве параметра или метод с другим именем, раз вы передаёте другой класс. Перезагрузки действительно нет и перезагружать такими костылями
                                                  public method ($a) {
                                                  // работаем без поддержки анализатора =(
                                                  }
                                                  

                                                  разные типы не стоит.
                                                    0
                                                    Интерфейс это вариант, при условии что оба класса его реализуют. А разделение на методы — это вынос проблемы на уровень выше, вызывающему коду теперь нужно принимать решение о том, какой метод вызвать.
                                                      0

                                                      Только если при этом вы не нарушите LSP. Чаще же проще сделать два метода (в некоторых языках, например в Objective-C перегрузка именно так и работает, неявно создаются просто разные методы, а синтаксис позволяет это завуалировать).


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

                                                      0
                                                      отсутствие возможности перегрузки методов

                                                      /**
                                                       * @param ClassA | ClassB $a
                                                       */
                                                      public function method($a)
                                                      {
                                                      }

                                                      и вуаля.


                                                      С другой стороны — вам часто это нужно?

                                                        0
                                                        Нет, нужно не часто, рассматривался гипотетический случай. А выше на меня «наехали», сказав что указание типа через нотации — это выкрутасы =)
                                                          0

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

                                                            0
                                                            Аннотации​ ничего не гарантируют, к сожалению
                                              +2
                                              приведите пример, как вы будите использовать джинерики, для отличия

                                              public function doSomethingWithUsers(Collection<User> $user)
                                              {
                                                  //     
                                              }
                                                –1
                                                Собственно, выше я писал вариант для этого. Но суть «спора» в том, что я рассуждаю с позиции того, что сейчас есть в языке (собственно, я так и написал в моей первом комменте).
                                                  +1

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

                                            +1
                                            Для различия содержимого достаточно добавить в класс коллекции пару методов:

                                            тем самым вы перекладываете проверки не только на рантайм но еще и на пользователя ваших коллекций.


                                            В этом нет никакого практического смысла — проще использовать просто массивы.

                                    –1
                                    Может тогда уже сразу переходить на java? Не, ну серьезно, скоро пых станет гораздо мудреннее, чем java
                                      0
                                      В PHP, как и в Java, гораздо важнее виртуальная машина, нежели синтаксис языка. Поэтому и Hack, и Kotlin, и Scala, а машинки все те же.
                                        0
                                        Java-да. Даже не сама виртуальная машина, а ее архитектура, которая описывается, насколько я помню в java blueprints(могу ошибаться) и доступна разработчикам сторонних виртуальных машин.

                                        Насчет PHP — не согласен. Во-первых, потому что изначально php имел довольно низкий порог входа и быстрое получение результата, обусловленные в том числе и простотой синтаксиса. Во-вторых, вм php все же проигрывает jvm по производительности. Ну и в третьих, большинство споров, улучшений и прочего в мире php связаны не с расширением функционала стандартной библиотеки, ни с повышением производительности, а именно с добавлением различного синтаксического сахара и применения лучших паттернов проектирования(порой кажется, что мы пытаемся играть в серьезных ынтырпрайз разработчиков со всеми этими фабриками, добавлением дженериков, сложными архитектурами кода и прочим).

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

                                            Все же нет, как раз таки виртуальная машина. Никто не хочет писать свои виртуальные машины.


                                            изначально php имел довольно низкий порог входа и быстрое получение результата

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


                                            Можно ли так делать сегодня? Все еще да, так что с порогом входа в PHP ничего не изменилось.


                                            Во-вторых, вм php все же проигрывает jvm по производительности.

                                            Причем тут это? Да, он проигрывает и будет проигрывать так как все же динамическая система типов добавляет некислый оверхэд. И да, даже если у нас появится AOT/JIT, всеравно PHP будет медленнее хотя бы в силу модели выполнения.


                                            Ну и в третьих, большинство споров, улучшений и прочего в мире php связаны не с расширением функционала стандартной библиотеки, ни с повышением производительности, а именно с добавлением различного синтаксического сахара

                                            а теперь посмотрите изменения за последние 5 лет. В основном — изменения в плане производительности, расширение стандартной библиотеки PHP (password api вспомните, сейчас еще libsodium запилили), генераторы, минимум сахара.


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

                                            Попробуйте раскрыть мысль. Ибо звучит это все так как будто вы предлагаете два варианта:


                                            • не пытаться писать серьезных приложений на PHP
                                            • писать все строго в процедурном стиле.

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

                                            0
                                            Да? А я-то думаю, почему я пользуюсь пыхом.
                                            Из-за виртуальной машины, вот оно что.
                                          0
                                          По поводу Итераторов. Бывает полезно применять json_decode/code

                                          Объект = json_decode(json_encode($Дерево));
                                          $Итератор = new \RecursiveIteratorIterator( new \RecursiveArrayIterator($Объект), \RecursiveIteratorIterator::SELF_FIRST );
                                          


                                          И Классы для фильтрации внутри Итераторов

                                          class Фильтрация
                                          {
                                            public function Фильтрация($Трансформер)
                                            {
                                              echo "Фильтрация\n";
                                          
                                              $array = array("test1", array("taste2", "test3", "test4"), "test5");
                                              $iterator = New \RecursiveArrayIterator($array);
                                              $filter = New StartsWithFilter($iterator, "test3");
                                          
                                              foreach(New \RecursiveIteratorIterator($filter) as $key => $value)
                                              {
                                                echo $value . "\n";
                                              }
                                            }
                                          }
                                          
                                          
                                          class StartsWithFilter extends \RecursiveFilterIterator
                                          {
                                            protected $word;
                                          
                                            public function __construct(\RecursiveIterator $rit, $word)
                                            {
                                              $this->word = $word;
                                              parent::__construct($rit);
                                            }
                                          
                                            public function accept()
                                            {
                                              return $this->hasChildren() OR strpos($this->current(), $this->word) === 0;
                                            }
                                          
                                            public function getChildren()
                                            {
                                              return New self($this->getInnerIterator()->getChildren(), $this->word);
                                            }
                                          }
                                          
                                            +1

                                            php 7.1 как бы вышел уже, generics там нет http://php.net/manual/en/migration71.new-features.php
                                            и в 7.2 их тоже (барабанная дробь) не будет https://wiki.php.net/rfc#php_next_72

                                              +1

                                              Цель статьи — показать полезность такой вещи в PHP. Почему дженериков не было в 7.2 — потому что для того чтобы их добавить без необходимости жертвовать производительностью нужно было чуть поменять некоторые внутренние структуры данных, что уже не так просто. Но скажем к 8-ой версии это вполне возможно.

                                                0

                                                Проблема в том что generics, висят в Under Discussion с Created 2015/04/28 и как бы стол и ныне там.
                                                Так увидеть их в 8 тоже маловероятно. Для того чтоб куда-то что-то двигалось нужно чтоб прошло голосование (In voting phase). А пока это one-person-opinion-feature.

                                                  0
                                                  А пока это one-person-opinion-feature.

                                                  RFC обсуждалась достаточно долго, была реализация (и не одна) и все упирались в производительность. Вот тут больше подробностей почему все плохо. Проблема не с идеей а с имплементацией.

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


                                              На мой взгляд «Неуверенность в содержимом массива» — основной признак говнокода…
                                                –1
                                                Ага, и лучше что бы компилятор не давал говнокод писать.
                                                0
                                                есть еще такое https://github.com/ircmaxell/PhpGenerics
                                                  –3
                                                  PHP Fatal error: Uncaught Error: Call to a member function getId() on string

                                                  Кроме говнокодеров такое у кого-то может возникнуть? :)

                                                  Допустим, у вас есть метод, которому нужен массив блог-постов:

                                                  Так а выше о чем говорилось?.. :)

                                                  RFC предназначен для PHP 7.1, о его будущем нет никакой дополнительной информации.

                                                  А ниче, что он давно вышел? :)

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

                                                  Да, а параметры придумала сотона мохнатая.

                                                  class GenericCollection<T> implements Iterator, ArrayAccess
                                                  
                                                  О, нет.
                                                  Не превращайте PHP в С++.

                                                  Благодаря дженерикам, наш сайт упадет не момент получения неправильного элемента, а в момент записи? :)

                                                  Все это повышает сложность языка. Прилив молодой крови может поуменьшиться и язык умрет.
                                                    +1
                                                    Кроме говнокодеров такое у кого-то может возникнуть? :)

                                                    Да. Просто не так красочно как в статье, но схлопотать null в середине коллекции вы запросто можете.


                                                    А ниче, что он давно вышел? :)

                                                    А ниче что этот абзац именно об этом? В частности фраза "о его (RFC) будущем ничего не известно".


                                                    Не превращайте PHP в С++.

                                                    Просто не используйте их если вам и так все ок.


                                                    Благодаря дженерикам, наш сайт упадет не момент получения неправильного элемента, а в момент записи? :)

                                                    Если штука которая пишет в коллекцию обязалась соблюдать контракт что мол "я буду всегда записывать коллекции только с такими-то типами" то да. Тогда это будет равносильно "падай когда нарушил контракт". Это намного лучше чем ловить непонятные сайд эффекты дальше по приложению. Не говоря уже о том что статические анализаторы будут неплохо это трекать.


                                                    Все это повышает сложность языка. Прилив молодой крови может поуменьшиться и язык умрет.

                                                    1. это увеличивает возможности языка. Вас никто этим пользоваться не заставляет. PHP как не требовал что бы все типы выводились так и не требует.


                                                    2. языки умирают не из-за "оттока" молодой крови, а из-за отсутствия спроса. Когда язык не развивается — спрос падает.
                                                    –3
                                                    Вместо создания проверки и выброса исключения, куча кода, которая приводит к той-же проверке и выбросу исключения. А точнее это просто фильтрация данных, которая должна быть где-то до foreach ($posts as $post)… Пример из статьи напоминает:
                                                    query("SELECT * FROM posts WHERE id = " . $_GET['id']);
                                                    
                                                      –4
                                                      Кто все эти люди, хотящие странного?
                                                      Неудачники из других, непопулярных языков?
                                                        0
                                                        А может просто добавить
                                                        PostCollection: array of Posts
                                                        
                                                        ?
                                                          +2
                                                          Дженерики, на самом деле, гараздо мощнее, чем только «массив типов».
                                                          Вот к примеру на C# можно написать так:

                                                          tank.GetAbility<AttackAbility>().
                                                          


                                                          И дальше, после точки, вы будете уверены в типе переменной, а ваша IDE корректно автодополнит такой код.

                                                          Ну или у вас, скажем, с сервера по API часто получается такая структура:

                                                          {
                                                            pageCount: 10,
                                                            currentPage: 3,
                                                            items: []
                                                          }
                                                          


                                                          И у вас все приходит в таком формате — список новостей постранично, список пользователей постранично, список комментариев постранично. Можно на каждую структуру создавать свой класс, а можно что-то вроде такого:

                                                          class ItemsPageOf<TItem> {
                                                            int pageCount;
                                                            int currentPage;
                                                            TItem[] items;
                                                          }
                                                          


                                                          А потом просто использовать Дженерики:
                                                          ItemsPageOf<MessageItem> messages;
                                                          ItemsPageOf<ArticleItem> articles;
                                                          ItemsPageOf<CommentItem> comments;
                                                          


                                                          messages.items[0]. // угадайте, какой тут тип?
                                                          
                                                          –1
                                                          Очень нужная вещь в языке. Стараемся использовать максимум из языка, type hint первый важный шаг. Теперь нужны генерики и структуры как в С. Поддержание нескольких подходов (например, динамическая/статическая типизация) и парадигм залог использования языка в будущем.
                                                            0
                                                            В целом идея отличная, хоть и усложняет язык. Я о дженериках узнал из Java и они мне очень понравились как раз ключе удобства. Но в остальном оно только замедлит PHP.

                                                            Но вот пример из поста какой-то надуманный, набыдлокоженный и изначально неправильный. С какого перепуга мы перебираем массив случайных элементов? У меня за несколько лет практики небыло ни одного случая, когда в массив попадали настолько неоднородные элементы: такого небывает даже у совсем-новичков.
                                                              0
                                                              Прошу прощения за отвлеченный вопрос…

                                                              А разве применение разных типов данных в одной коллекции (элементов перебираемого массива) не является косяком?
                                                                0

                                                                В контексте самой коллекции (а именно к ним применяются дженерики) — нет. Коллекции нет дела что она хранит. Дженерики как раз таки позволяют клиентскому коду (коду который собирается что-то там перебирать) жестко декларировать что он хочет перебирать.

                                                                0
                                                                Честно говоря, единственный приведенный пример якобы пользы шаблонов — типизированная коллекция — выглядит надуманным.

                                                                То есть всё понятно в плане что это за языковая фича. Непонятно — зачем она? Какие еще есть внятные применения?

                                                                Имхо это тупик какой-то. Гораздо интереснее было бы форсить RFC про кастомные типы. Тогда бы никакие дженерики для типизированных массивов (коллекций) не потребовались бы.
                                                                  +1

                                                                  Полно применений для инфраструктурных вещей. Например, с классом, реализующим клиент HTTP Rest API на дженериках можно будет писать что-то вроде $client = new RestClient('example.com/api/v1/users'); $user = $client->get(123); гарантированно получая инстанс User. Да, есть и другие способы, сделать это, например, передавая имя класса как строковый параметр и используя new или ReflectionClass::newInstance с ним или просто на каждый чих создавая новый класс, но это, как минимум, менее удобно, а, главное, никаких гарантий на уровне языка ни разработчику, ни рантайму, ни статическим анализаторам.

                                                                  Но главное, по-моему, именно обычные структуры данных: коллекции, карты, списки, стэки, множества и т. п.

                                                                    0

                                                                    RestClient<User>('example.com/api/v1/users')

                                                                      0

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


                                                                      Так что хочется задаться вопросом: Да, это всё удобно и круто, но насколько этот RFC покрывает проблемы, с которыми сталкиваются разработчики при написании своего ПО? От каких ошибок оно избавляет? Кажется, что эти плюшки нужны только для автокомплита и не более. Разве нет?

                                                                        0

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


                                                                        От каких ошибок оно избавляет?

                                                                        function onlyPremiumUsers(array<User> $users): array<User>
                                                                        {
                                                                            // ..
                                                                        }

                                                                        А если еще и сделать элиасы для типов… ух


                                                                        type Users = array<User>;
                                                                        type PremiumUsers = array<User>;
                                                                        // ...
                                                                        function onlyPremiumUsers(Users $users): PremiumUsers
                                                                        {
                                                                            // ..
                                                                        }

                                                                        выразительность, дешевая проверка простеньких бизнес правил…

                                                                          0

                                                                          Ну "клиентам" это не мешает видеть return докблок:


                                                                          /**
                                                                           * @return iterable|User[]
                                                                           */
                                                                          public function premium(): iterable
                                                                          {
                                                                              // ...
                                                                          }

                                                                          А на счёт тайпалиасов — это вообще огонь. Но, кажется, без наличия деклараций, аналогичных инвариантам DbC — получится ещё один костыль. Нужно что-то вроде:


                                                                          // Декларация
                                                                          type invokable($value): bool 
                                                                          {
                                                                              return is_object($value) && method_exists($value, '__invoke');
                                                                          }
                                                                          
                                                                          // Алиас
                                                                          typealias callable = invokable | array | string | \Closure;
                                                                            0
                                                                            Ну "клиентам" это не мешает видеть return докблок:

                                                                            Принимающая сторона ничего не знает о том кто продьюснул коллекцию. Она знает только о том что ей нужна коллекция с определенными элементами.


                                                                            без наличия деклараций, аналогичных инвариантам DbC — получится ещё один костыль

                                                                            с инвариантами неплохо было бы, но суть то в том что бы обеспечить проверки на уровне системы типов. А контракты можно юнит тестами описать. Мне такой подход больше нравится.

                                                                              0
                                                                              Принимающая сторона ничего не знает о том кто продьюснул коллекцию. Она знает только о том что ей нужна коллекция с определенными элементами.

                                                                              Имеется ввиду вызов от интерфейса, а не от реализации N метода? Тогда да, не отрицаю, возможно дженерики что-то и решат…


                                                                              с инвариантами неплохо было бы, но суть то в том что бы обеспечить проверки на уровне системы типов. А контракты можно юнит тестами описать. Мне такой подход больше нравится.

                                                                              А как тайпалиасами описать тип "callable", например? Кажется, что без императивщины, хотя бы в минимальном её проявлении это сделать невозможно. Если даже посмотреть на любой язык, где есть подобные штуки (ака Swift, Haskell (вот тут не уверен, могу ошибаться), TypeScript, FlowType и проч.) — их типы всё равно не полноценные. Они покрывают лишь набор булевых условий на соответствие встроенным типам. А более сложные вещи реализуются лишь в языках, вроде Irdis, о которых вообще никто не знает и не слышал =) И прошу заметить, там всё тоже вполне императивно.

                                                                              0

                                                                              Тоже думал о похожей штуке, чтобы была функция, которая возвращает соответствие типу.


                                                                              Ну и до кучи: http://marc.info/?l=php-internals&m=148921424807417
                                                                              (ничего не решили, обсуждение заглохло).

                                                                                0
                                                                                Ну «клиентам» это не мешает видеть return докблок:

                                                                                1. У нас уже есть механизм типизации? Зачем для какого-то одного исключения использовать другой, менее удобный механизм типизации?
                                                                                2. Я уже говорил выше, что Дженерики — не только массив. Другие механизмы использования Дженериков так не закостылишь.

                                                                              +1
                                                                              Уже 100500 раз натыкался на забытые аннотации. Код поменяли, коммент забыли. Аннотация врет и вводит в заблуждение. Я за более выразительные и явные конструкции
                                                                                0

                                                                                У меня уже давно не бывало такого. Нынче современные IDE светятся как новогодние ёлки (ну шторм, по крайней мере), если аннотация не соответствует типам или что-то идёт не так. А в случае наличие EA расширения — так ещё и чуть ли не код исправляет за тебя. Добавить к этому всякие CI-штуки, вроде скрутинизера… В результате, любую подобную ошибку ну просто невозможно не заметить. Всё окружение будет говорить о том, что есть несоответствие типов.

                                                                          0
                                                                          Когда что-то идёт не так, надо редактировать «плохой» код, а не огораживаться от него. Кроме данных не того типа, могут быть неправильные данные того типа (например, взял элемент массива, отредактировал его и вместо замены добавил на новое место).
                                                                            0
                                                                            Дженерики — это заметание сора под ковер. Тип проверять не надо, но надо быть уверенным в типобезопасности уже системы типов.
                                                                            То есть если при ошибке в типе вы получите конкретную ошибку, то при ошибке системы типов вы получите «что-то пошло не так».
                                                                              +1
                                                                              Дженерики — это заметание сора под ковер. Тип проверять не надо, но надо быть уверенным в типобезопасности уже системы типов.

                                                                              Что? Вы пользовались когда-то языками с мощной типизацией? Например, C#. Они не оставляют никаких сомнений, там все надежно и прекрасно
                                                                                0
                                                                                Я рад, что Вы не испытываете никаких сомнений. Вам хорошо на свете.
                                                                              +1
                                                                              Оператор… не подходит для передачи коллекций с большим количеством элементов. Он представляет собой синтаксический сахар для сворачивания-разворачивания ряда аргументов в объявлении функции или метода. В действительности, аргументы передаются по-отдельности, с соответствующим расходованием памяти и времени. Если бы свёрнутые аргументы реально передавались массивом, в сочетании со строгой типизацией можно было бы реализовать типизированные коллекции, но увы.

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

                                                                              Самое читаемое