PHPのジェネリックとその必要性について


この記事では、PHPの配列に関連するいくつかの一般的な問題を取り上げます。 すべての問題は、PHPにジェネリックを追加するRFCの助けを借りて解決できます。 ジェネリックについて詳しくは説明しませんが、記事の終わりまでに、ジェネリックがどのように役立つか、なぜ多くのジェネリックがPHPに登場するのを待っているのかを理解する必要があります。


ある種のデータソースからダウンロードされた一連のブログ投稿があるとしましょう。


$posts = $blogModel->find(); 

すべての投稿を循環して、それらのデータで何かをする必要があります。 たとえば、 idます。


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

これは一般的なシナリオであり、ジェネリックの役割と、コミュニティがなぜジェネリックをそんなに必要としているのかを説明します。 このシナリオで発生した問題を考慮してください。


データの完全性


PHPでは、配列は...要素のコレクション...


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

投稿のセットを循環させると、結果として重大なエラーが発生します。


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

文字列'foo' ->getId()を呼び出します。 乗り物ではありません。 配列をループするとき、すべての値が特定の型であることを確認したいです。 これを行うことができます:


 foreach ($posts as $post) { if (!$post instanceof Post) { continue; } $id = $post->getId(); // -  } 

これは機能しますが、実稼働用のPHPコードをすでに作成している場合は、このようなチェックが急速に成長し、コードベースを汚染することがあることを知っています。 この例では、メソッド内の各エントリのタイプを確認でき->find() in $blogModel 。 しかし、これは問題をある場所から別の場所に移すだけです。 状況は少し改善されましたが。


データ構造の整合性も複雑です。 ブログ投稿の配列を必要とするメソッドがあるとしましょう:


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

繰り返しますが、ループに追加のチェックを追加できますが、これは$postsがPostsのコレクションのみ$posts含むことを保証するものではありません。


PHP 7.0以降では、 ...演算子を使用してこの問題を解決できます。


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

ただし、このアプローチには裏返しがあります。アンパックされた配列に関して関数を呼び出す必要があります。


 handlePosts(...$posts); 

性能


配列に特定のタイプの要素のみが含まれているかどうかを事前に知る方が、各サイクルで毎回タイプを手動でチェックするよりも良いと想定できます。


ジェネリックはまだ利用できないため、ジェネリックでベンチマークを実行することはできません。したがって、パフォーマンスにどのように影響するかを推測することしかできません。 しかし、Cで記述された最適化されたPHPの動作は、ユーザースペース用の大量のコードを作成するよりも、問題を解決するための優れた方法であると仮定するのはおかしくありません。


コード補完


あなたのことは知りませんが、PHPコードを書くときはIDEに頼ります。 オートコンプリートにより生産性が大幅に向上するため、ここでも使用したいと思います。 投稿をループするとき、各$postPostインスタンスと見なすIDEが必要です。 簡単なPHP実装を見てみましょう。


 # BlogModel public function find() : array { //  ... } 

PHP 7.0以降、戻り値の型が登場し、PHP 7.1ではvoid型とnull許容型で改善されました。 しかし、配列に含まれているものをIDEに伝えることはできません。 したがって、PHPDocに戻ります。


 /** * @return Post[] */ public function find() : array { //  ... } 

モデルクラスなどの一般的な実装を使用する場合、プロンプトメソッド->find()常に使用できる->find()は限りません。 したがって、コードでは、プロンプト変数$の投稿に制限する必要があります。


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

配列の内容に関する不確実性、およびコードの分散がパフォーマンスと保守の容易さに与える影響、および追加のチェックを書くことの不便さにより、私は長い間、より良い解決策を探しました。


* * *


私の意見では、そのようなソリューションはジェネリックです。 私は彼らが何をしているのか詳細に説明しません。あなたはそれについてRFCで読むことができます。 しかし、ジェネリックが上記の問題の解決にどのように役立つかという例を挙げて、コレクションに正しいデータがあることを常に保証します。


重要な注意 :ジェネリックはまだPHPにはありません。 RFCはPHP 7.1用であり、その将来についての詳細情報はありません。 以下のコードは、PHP 5.0に存在するIteratorおよびArrayAccessインターフェースに基づいてます。 最後に、ダミーコードである一般的な例を分析します。


まず、PHP 5.0以降で動作するCollectionクラスを作成します。 このクラスは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"; } 

注: $collectionPostsのみが含まれるという保証はありません。 たとえば、文字列値を追加すると機能しますが、ループが壊れます。


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

うまくいく! ジェネリックなしでも! 問題は1つだけです。ソリューションはスケーラブルではありません。 クラスのタイプのみが異なる場合でも、コレクションのタイプごとに個別の実装が必要です。


おそらく、遅延静的バインディングと再帰PHP APIを悪用することで、より便利なサブクラスを作成できます。 ただし、いずれにしても、使用可能なタイプごとにクラスを作成する必要があります。


素晴らしいジェネリック


これらすべてを踏まえて、ジェネリックがPHPで実装されている場合に記述できるコードを見てみましょう。 すべてのタイプに使用される1つのクラスにすることができます。 便宜上、以前の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>を使用します。 繰り返しになりますが、 GenericCollectionクラスはどのタイプでも使用できます。


私と同じようにジェネリックに感心している場合(そしてこれは氷山の一角にすぎません)、コミュニティを教育してRFCを共有してくださいhttps : //wiki.php.net/rfc/generics



Source: https://habr.com/ru/post/J329494/


All Articles