PHPジェネリックとそれが必要な理由

in Vlog

(jp) =

今日のブログ投稿では、PHP の配列に関する一般的な問題について説明します。 リストされているすべての問題は、PHP にジェネリックを追加する保留中の RFC で解決できます。 ジェネリックとは何かについては詳しく説明しませんが、この記事を読み終えた時点で、ジェネリックがなぜ有用なのか、なぜ PHP でジェネリックが本当に必要なのかを理解できるはずです。 それでは、これ以上苦労することなく、本題に飛び込みましょう。

データ ソースから読み込まれたブログ投稿のコレクションがあるとします。

$posts = $blogModel->find();

ここで、すべての投稿をループして、次のようにします。 なにか そのデータとともに; たとえば、 id.

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

これはよくあるシナリオです。 そして、ジェネリックが優れている理由と、PHP コミュニティがジェネリックを切実に必要としている理由について説明するために、このシナリオを検討します。

上記のアプローチの問題点を見てみましょう。

# データの整合性

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 コードを書いたことがある場合は、これらのチェックが急速に拡大し、コードベースを汚染する可能性があることを知っています。 この例では、各エントリのタイプを確認できます ->find() メソッド $blogModel. ただし、それは問題をある場所から別の場所に移動しているだけです。 それは少し良いですが。

データの整合性には別の問題があります。 の配列を必要とするメソッドがあるとします。 Post秒。

function handlePosts(array $posts) 
    foreach ($posts as $post) 
        
    

繰り返しますが、このループに追加のチェックを追加することはできますが、それを保証することはできませんでした $posts のコレクションのみを保持します Post オブジェクト。

PHP 7.0 以降では、 ... オペレーターがこの問題を回避します。

function handlePosts(Post ...$posts) 
    foreach ($posts as $post) 
        
    

ただし、このアプローチの欠点は、アンパックされた配列で関数を呼び出す必要があることです。

handlePosts(...$posts);

tpyoに気づきましたか? PR を送信して修正することができます。 このブログの最新情報を知りたい場合は、私をフォローしてください。 ツイッター または私のニュースレターを購読してください:

# パフォーマンス

ループ内の型を毎回手動でチェックするよりも、配列に特定の型の要素のみが含まれているかどうかを事前に知っておく方がよいと想像できます。

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

# コード補完

あなたのことはわかりませんが、私は PHP コードを書くときに IDE を使用しています。 コード補完は生産性を大幅に向上させるので、ここでも使用したいと思います。 投稿をループするとき、IDE にそれぞれを認識させたい $post のインスタンスです Post. 単純な PHP 実装を見てみましょう。



public function find(): array 
    

PHP 7.0 以降、戻り値の型が追加され、PHP 7.1 では nullable と void で洗練されました。 しかし、IDE が配列の中身を知る方法はありません。 そのため、PHPDoc に戻ります。


public function find(): array 
    

モデル クラスなどの「一般的な」実装を使用する場合は、 ->find() 方法ができない場合があります。 そのため、型のヒントにこだわっています $posts 私たちのコードでは変数です。


$posts = $blogModel->find();

配列の正確な内容の不確実性、散在するコードによるパフォーマンスとメンテナンスへの影響、およびこれらの余分なチェックを記述する際の不便さの両方により、より良い解決策が切望されています。

私の意見では、その解決策はジェネリックです。 ジェネリックが何をするかについては詳しく説明しませんが、RFC を読んでそれを知ることができます。 しかし、ジェネリックがこれらの問題を解決する方法の例を示し、開発者がコレクション内に常に正しいデータを保持できるようにします。

大きな注意: ジェネリックは PHP にはまだ存在しません。 RFC は PHP 7.1 を対象としており、将来に関する詳細情報はありません。 次のコードは、PHP 5.0 の時点で存在する Iterator インターフェイスと ArrayAccess インターフェイスに基づいています。 最後に、ダミー コードであるジェネリックの例に飛び込みます。

まず、 Collection PHP 5.0 以降で動作するクラス。 このクラスは実装します Iterator そのアイテムをループできるようにする ArrayAccess 配列のような構文を使用して、コレクション内のアイテムを追加およびアクセスできるようにします。

class Collection implements Iterator, ArrayAccess

    private int $position;

    private array $array = [];

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

    public function current(): mixed 
        return $this->array[$this->position];
    

    public function next(): void 
        ++$this->position;
    

    public function key(): int 
        return $this->position;
    

    public function valid(): bool 
        return array_key_exists($this->position, $this->array);
    

    public function rewind(): void 
        $this->position = 0;
    

    public function offsetExists($offset): bool 
        return array_key_exists($offset, $this->array);
    

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

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

    public function offsetUnset($offset): void 
        unset($this->array[$offset]);
    

これで、このようなクラスを使用できます。

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

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

この単純な実装では、次のことが保証されないことに注意してください。 $collection のみ保持 Post 物体。 たとえば、文字列を追加すると問題なく動作しますが、ループが壊れてしまいます。

$collection[] = 'abc';

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

現在の PHP では、この問題を解決するには、 PostCollection クラス。

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

今だけ Post オブジェクトをコレクションに追加できます。

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

$collection[] = 'abc';

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

できます! ジェネリック無しでも! 問題は 1 つだけです。推測できるかもしれません。 これはスケーラブルではありません。 これらのクラスの唯一の違いは型であっても、コレクションの型ごとに個別の実装が必要です。 また、IDE と静的アナライザーは、戻り値の型に基づいて型を正しく判断できることに注意してください。 offsetGetPostCollection.

レイト スタティック バインディングと 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 $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;
        
    

    
    
    
    
    
    
$collection = new GenericCollection<Post>();
$collection[] = new Post(1);


$collection[] = 'abc';

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

以上です! 使用しています T 実行前にチェックできる動的タイプとして。 そして再び、 GenericCollection
class は、常にすべてのタイプで使用できます。


//platform.twitter.com/widgets.js

関連記事

前の投稿
Hochsteins の離婚により、キャストは彼らの結婚を再検討しました
次の投稿
最もシンプルなプラグインのサポート – Stitcher.io