В этой статье мы рассмотрим некоторые распространённые проблемы, связанные с массивами в 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

Поделиться с друзьями
-->

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


  1. vtvz_ru
    26.05.2017 18:38

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

    P.S. Я читаю теги!


    1. AxisPod
      27.05.2017 14:36
      +3

      Дженерики это фигня по сравнению с шаблонами C++.


      1. Fesor
        27.05.2017 22:32

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


  1. Caravus
    26.05.2017 19:26
    +8

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

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


    1. vchik
      26.05.2017 19:50
      +2

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


      1. Fesor
        27.05.2017 02:14

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


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

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


        type ArticlesCollection = Collection<Article>;

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


  1. cjbars
    26.05.2017 19:48
    -1

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

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

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


    1. Fesor
      27.05.2017 02:31
      +3

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

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


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


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


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


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


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


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


      1. cjbars
        27.05.2017 09:02

        А ведь вы меня убедили, пора пересматривать мои старческие подходы :-) Спасибо!


      1. herr_kaizer
        27.05.2017 18:06

        А что не так с системой типов в Java?


    1. magazovski
      27.05.2017 08:55
      +2

      Дженерики как раз и привносят статическую типизацию.


  1. CrazyNiger
    26.05.2017 20:03
    +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. Fesor
      27.05.2017 02:32
      +1

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


      1. CrazyNiger
        27.05.2017 11:45
        -1

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

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

        Обе строчки «равнозначны».


        1. oxidmod
          27.05.2017 13:48

          Не равнозначны. Во втором варианте клиентский код никак не отличит содержимое

          $collection = new Collection('Post');
          

          от
          $collection = new Collection('User');
          


          1. CrazyNiger
            27.05.2017 14:10
            -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. oxidmod
              27.05.2017 14:21
              +1

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


              1. CrazyNiger
                27.05.2017 14:37
                -1

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


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

                от
                $collection = new Collection<User>();
                


                1. oxidmod
                  27.05.2017 14:46

                  Когда дженерики нативные у вас нет необходимости проверять тип элемента в коллекции. Просто нет нужды в методах isInstanceOf или типа того. Кроме того, IDE сможет вам показать методы элемента коллекции без костылей с аннотациями. Статические анализаторы легко могут проверить что вы не вызываете несуществующий метод.

                  Дело вот в чем:

                  abstract public mixed offsetGet ( mixed $offset )
                  


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


                  1. CrazyNiger
                    27.05.2017 15:13
                    -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, то увидите, что в коллекцию могут быть добавлены только объекты того класса, который был указан при создании коллекции, так что необходимости проверять каждый элемент коллекции перед использованием нет.


                    1. oxidmod
                      27.05.2017 15:57

                      Ну вот эта проверка

                      if (!$posts->isInstanceOf(Post::class)) {
                              throw new InvalidArgumentException();
                          }
                      

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


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

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


                      1. CrazyNiger
                        27.05.2017 17:05
                        -1

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

                        Т.е. делает тоже самое, что и указание джинерика в сигнатуре метода.


                        1. oxidmod
                          27.05.2017 17:18

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


                          1. CrazyNiger
                            27.05.2017 18:45

                            В пыхе достаточно способов напихать в вашу коллекцию что попало

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


                            1. Lailore
                              27.05.2017 18:57

                              А почему собственно без?


                              1. Fortop
                                27.05.2017 22:01

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


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


                                1. SerafimArts
                                  28.05.2017 04:26

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


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


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


                                  1. TheShock
                                    28.05.2017 05:45

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


                                  1. Fortop
                                    28.05.2017 09:21

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


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


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


                              1. CrazyNiger
                                28.05.2017 09:36

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


                                1. Fesor
                                  28.05.2017 16:17
                                  +1

                                  1. Я точно знаю как «сломать» коллекцию через рефликсию, хочется увидеть что-то новое, чего я не знаю.

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


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


                            1. oxidmod
                              28.05.2017 13:48

                              Ну как то так без рефлексии

                              $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. CrazyNiger
                                28.05.2017 19:01
                                -1

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


                            1. oxidmod
                              28.05.2017 14:03

                              Ну или как то так.

                              $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);
                              }
                              


                              1. CrazyNiger
                                28.05.2017 19:01

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


                        1. Fesor
                          27.05.2017 22:46

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


                          1. CrazyNiger
                            28.05.2017 09:45

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

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


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


                            1. NeLexa
                              28.05.2017 10:21

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

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

                              разные типы не стоит.


                              1. CrazyNiger
                                28.05.2017 10:25

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


                              1. Fesor
                                28.05.2017 12:27

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


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


                            1. Fesor
                              28.05.2017 11:41

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

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

                              и вуаля.


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


                              1. CrazyNiger
                                28.05.2017 19:02

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


                                1. Fesor
                                  28.05.2017 19:13

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


                                1. oxidmod
                                  28.05.2017 20:11

                                  Аннотации? ничего не гарантируют, к сожалению


                1. Fesor
                  27.05.2017 22:44
                  +2

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

                  public function doSomethingWithUsers(Collection<User> $user)
                  {
                      //     
                  }


                  1. CrazyNiger
                    28.05.2017 07:21
                    -1

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


                    1. Fesor
                      28.05.2017 11:43
                      +1

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


            1. Fesor
              27.05.2017 22:42
              +1

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

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


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


  1. vollossy
    26.05.2017 23:08
    -1

    Может тогда уже сразу переходить на java? Не, ну серьезно, скоро пых станет гораздо мудреннее, чем java


    1. arvitaly
      26.05.2017 23:41

      В PHP, как и в Java, гораздо важнее виртуальная машина, нежели синтаксис языка. Поэтому и Hack, и Kotlin, и Scala, а машинки все те же.


      1. vollossy
        27.05.2017 12:08

        Java-да. Даже не сама виртуальная машина, а ее архитектура, которая описывается, насколько я помню в java blueprints(могу ошибаться) и доступна разработчикам сторонних виртуальных машин.

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

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


        1. Grox
          27.05.2017 23:48

          Он и сейчас имеет низкий порог входа.


        1. Fesor
          28.05.2017 11:55

          Даже не сама виртуальная машина, а ее архитектура

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


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

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


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


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

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


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

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


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

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


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

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


      1. http2
        27.05.2017 13:59

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


  1. workmi
    26.05.2017 23:59

    По поводу Итераторов. Бывает полезно применять 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. necromant2005
    27.05.2017 05:33
    +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. Fesor
      27.05.2017 11:49
      +1

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


      1. necromant2005
        28.05.2017 12:37

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


        1. Fesor
          28.05.2017 12:53

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

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


  1. drch
    27.05.2017 08:55
    +1

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


    На мой взгляд «Неуверенность в содержимом массива» — основной признак говнокода…


    1. Lailore
      27.05.2017 17:52
      -1

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


  1. e1nste1n
    27.05.2017 08:55

    есть еще такое https://github.com/ircmaxell/PhpGenerics


  1. http2
    27.05.2017 12:56
    -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. Fesor
      28.05.2017 12:12
      +1

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

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


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

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


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

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


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

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


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

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


      2. языки умирают не из-за "оттока" молодой крови, а из-за отсутствия спроса. Когда язык не развивается — спрос падает.


  1. profesor08
    27.05.2017 13:00
    -3

    Вместо создания проверки и выброса исключения, куча кода, которая приводит к той-же проверке и выбросу исключения. А точнее это просто фильтрация данных, которая должна быть где-то до foreach ($posts as $post)… Пример из статьи напоминает:

    query("SELECT * FROM posts WHERE id = " . $_GET['id']);
    


  1. http2
    27.05.2017 13:02
    -4

    Кто все эти люди, хотящие странного?
    Неудачники из других, непопулярных языков?


  1. http2
    27.05.2017 14:11

    А может просто добавить

    PostCollection: array of Posts
    
    ?


    1. TheShock
      28.05.2017 01:34
      +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. yurybykov
    27.05.2017 14:22
    -1

    Очень нужная вещь в языке. Стараемся использовать максимум из языка, type hint первый важный шаг. Теперь нужны генерики и структуры как в С. Поддержание нескольких подходов (например, динамическая/статическая типизация) и парадигм залог использования языка в будущем.


  1. Akuma
    27.05.2017 20:49

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

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


  1. customtema
    28.05.2017 08:12

    Прошу прощения за отвлеченный вопрос…

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


    1. Fesor
      28.05.2017 12:15

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


  1. AlexLeonov
    28.05.2017 21:22

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

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

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


    1. VolCh
      29.05.2017 08:33
      +1

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

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


      1. VolCh
        29.05.2017 14:34

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


      1. SerafimArts
        29.05.2017 14:51

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


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


        1. Fesor
          29.05.2017 14:54

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


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

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

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


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

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


          1. SerafimArts
            29.05.2017 15:09

            Ну "клиентам" это не мешает видеть 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;


            1. Fesor
              29.05.2017 15:54

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

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


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

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


              1. SerafimArts
                30.05.2017 00:13

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

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


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

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


            1. michael_vostrikov
              29.05.2017 16:30

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


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


            1. TheShock
              29.05.2017 16:56

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

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


        1. oxidmod
          29.05.2017 15:39
          +1

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


          1. SerafimArts
            29.05.2017 23:58

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


  1. mafia8
    30.05.2017 07:45

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


  1. ACPrikh
    31.05.2017 13:14

    Дженерики — это заметание сора под ковер. Тип проверять не надо, но надо быть уверенным в типобезопасности уже системы типов.
    То есть если при ошибке в типе вы получите конкретную ошибку, то при ошибке системы типов вы получите «что-то пошло не так».


    1. TheShock
      31.05.2017 13:54
      +1

      Дженерики — это заметание сора под ковер. Тип проверять не надо, но надо быть уверенным в типобезопасности уже системы типов.

      Что? Вы пользовались когда-то языками с мощной типизацией? Например, C#. Они не оставляют никаких сомнений, там все надежно и прекрасно


      1. ACPrikh
        01.06.2017 10:37

        Я рад, что Вы не испытываете никаких сомнений. Вам хорошо на свете.


  1. maxzuber
    31.05.2017 14:23
    +1

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