PHP Generics và lý do chúng ta cần chúng

  • Post category:lập trình


Trong bài đăng blog hôm nay, chúng ta sẽ khám phá một số vấn đề phổ biến với mảng trong PHP. Tất cả các vấn đề và vấn đề được liệt kê có thể được giải quyết bằng RFC đang chờ xử lý bổ sung các khái niệm chung cho PHP. Chúng ta sẽ không khám phá quá chi tiết generics là gì, nhưng khi kết thúc bài viết này, bạn sẽ có ý tưởng hay về lý do tại sao chúng hữu ích và tại sao chúng ta thực sự muốn chúng trong PHP. Vì vậy, không dài dòng nữa, hãy đi sâu vào chủ đề.

Hãy tưởng tượng bạn có một bộ sưu tập các bài đăng trên blog, được tải từ nguồn dữ liệu.

$posts = $blogModel->find();

Bây giờ bạn muốn lặp lại mọi bài đăng và làm thứ gì đó với dữ liệu của nó; giả sử, id.

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

Đây là một kịch bản thường xuyên xảy ra. Và chính kịch bản này chúng ta sẽ khám phá để thảo luận tại sao generics lại tuyệt vời và tại sao cộng đồng PHP lại rất cần chúng.

Chúng ta hãy xem xét các vấn đề của phương pháp trên.

# Toàn vẹn dữ liệu

Trong PHP, mảng là tập hợp các… thứ.

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

Lặp lại mảng bài đăng này sẽ dẫn đến một lỗi nghiêm trọng.

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

Chúng tôi đang gọi ->getId() trên chuỗi 'foo'. Không được thực hiện. Khi lặp qua một mảng, chúng ta muốn chắc chắn rằng mọi giá trị đều thuộc một loại nhất định. Chúng ta có thể làm điều gì đó như thế này.

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

    $id = $post->getId();
    
    
}

Điều này sẽ hiệu quả, nhưng nếu bạn đã viết một số mã PHP sản xuất, bạn biết rằng những bước kiểm tra này có thể phát triển nhanh chóng và làm ô nhiễm cơ sở mã. Trong ví dụ của chúng tôi, chúng tôi có thể xác minh loại của từng mục trong ->find() phương pháp trên $blogModel. Tuy nhiên, đó chỉ là chuyển vấn đề từ nơi này sang nơi khác. Tuy nhiên nó tốt hơn một chút.

Có một vấn đề khác với tính toàn vẹn dữ liệu. Giả sử bạn có một phương thức yêu cầu một mảng PostS.

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

Một lần nữa, chúng tôi có thể thêm các bước kiểm tra bổ sung vào vòng lặp này, nhưng chúng tôi không thể đảm bảo rằng $posts chỉ chứa một bộ sưu tập Post các đối tượng.

Kể từ PHP 7.0, bạn có thể sử dụng ... nhà điều hành để giải quyết vấn đề này.

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

Nhưng nhược điểm của phương pháp này: bạn sẽ phải gọi hàm bằng một mảng chưa được đóng gói.

handlePosts(...$posts);

Nhận thấy một tpyo? Bạn có thể gửi PR để sửa nó. Nếu bạn muốn cập nhật về những gì đang diễn ra trên blog này, bạn có thể Theo dõi danh sách gửi thư của tôi: gửi email đến [email protected] và tôi sẽ thêm bạn vào danh sách.

# Hiệu suất

Bạn có thể tưởng tượng rằng sẽ tốt hơn nếu biết trước liệu một mảng có chứa chỉ các phần tử của một kiểu nhất định hay không, thay vì kiểm tra thủ công các kiểu trong một vòng lặp, mọi lúc, một lần.

Chúng tôi không thể đánh giá các thuốc gốc vì chúng chưa tồn tại nên chỉ có thể đoán xem chúng sẽ tác động như thế nào đến hiệu suất. Tuy nhiên, không có gì quá xa vời khi cho rằng hành vi được tối ưu hóa của PHP, được viết bằng C; là cách tốt hơn để giải quyết vấn đề hơn là viết nhiều mã vùng người dùng.

# Hoàn thành mã

Không biết bạn thế nào chứ tôi sử dụng IDE khi viết mã PHP. Việc hoàn thành mã giúp tăng năng suất rất nhiều, vì vậy tôi cũng muốn sử dụng nó ở đây. Khi lặp qua các bài đăng, chúng tôi muốn IDE của mình biết từng bài đăng $post là một ví dụ của Post. Chúng ta hãy xem cách triển khai PHP đơn giản.



public function find(): array {
    
}

Kể từ PHP 7.0, các kiểu trả về đã được thêm vào và trong PHP 7.1, chúng đã được tinh chỉnh với các giá trị rỗng và khoảng trống. Nhưng không có cách nào IDE của chúng ta có thể biết bên trong mảng có gì. Vì vậy, chúng tôi quay trở lại PHPDoc.


public function find(): array {
    
}

Khi sử dụng cách triển khai “chung” chẳng hạn như một lớp mô hình, hãy nhập gợi ý ->find() phương pháp có thể không thực hiện được. Vì vậy, chúng tôi bị mắc kẹt với kiểu gợi ý $posts biến, trong mã của chúng tôi.


$posts = $blogModel->find();

Cả sự không chắc chắn về những gì chính xác trong một mảng, tác động đến hiệu suất và bảo trì do mã phân tán và sự bất tiện khi viết những kiểm tra bổ sung đó, khiến tôi khao khát một giải pháp tốt hơn.

Giải pháp đó, theo tôi là thuốc generic. Tôi sẽ không giải thích chi tiết generics làm gì, bạn có thể đọc RFC để biết điều đó. Nhưng tôi sẽ cho bạn một ví dụ về cách generics có thể giải quyết những vấn đề này, đảm bảo rằng nhà phát triển sẽ luôn có dữ liệu chính xác trong bộ sưu tập.

Ghi chú lớn: generics chưa tồn tại trong PHP. RFC nhắm mục tiêu PHP 7.1 và không có thêm thông tin nào về tương lai. Đoạn mã sau dựa trên giao diện Iterator và giao diện ArrayAccess, cả hai đều tồn tại kể từ PHP 5.0. Cuối cùng, chúng ta sẽ đi sâu vào một ví dụ tổng quát, đó là mã giả.

Đầu tiên chúng ta sẽ tạo một Collection class hoạt động trong PHP 5.0+. Lớp này thực hiện Iterator để có thể lặp qua các mục của nó và ArrayAccess có thể sử dụng cú pháp giống mảng để thêm và truy cập các mục trong bộ sưu tập.

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

Bây giờ chúng ta có thể sử dụng lớp như thế này.

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

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

Lưu ý rằng với cách thực hiện đơn giản này, không có gì đảm bảo rằng $collection chỉ giữ Post sự vật. Ví dụ: việc thêm một chuỗi sẽ hoạt động tốt nhưng sẽ làm hỏng vòng lặp của chúng ta.

$collection() = 'abc';

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

Với PHP như hiện tại, chúng ta có thể khắc phục vấn đề này bằng cách tạo một PostCollection lớp học.

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

Bây giờ chỉ Post các đối tượng có thể được thêm vào bộ sưu tập của chúng tôi.

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

$collection() = 'abc';

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

Nó hoạt động! Ngay cả khi không có thuốc generic! Chỉ có một vấn đề, bạn có thể đoán được. Đây không phải là khả năng mở rộng. Bạn cần triển khai riêng cho từng loại bộ sưu tập, mặc dù sự khác biệt duy nhất giữa các lớp đó là loại. Cũng lưu ý rằng IDE và máy phân tích tĩnh sẽ có thể xác định chính xác loại, dựa trên kiểu trả về của offsetGet TRONG PostCollection.

Bạn có thể làm cho việc tạo các lớp con trở nên thuận tiện hơn bằng cách “lạm dụng” liên kết tĩnh muộn và API phản chiếu của PHP. Nhưng bạn vẫn cần tạo một lớp cho mọi loại có sẵn.

# Generic vinh quang

Với tất cả những điều đó, chúng ta hãy xem đoạn mã mà chúng ta có thể viết nếu Generics được triển khai trong PHP. Điều này sẽ một lớp có thể được sử dụng cho mọi loại. Để thuận tiện cho bạn, tôi sẽ chỉ viết những thay đổi so với trước đó Collection lớp, vì vậy hãy ghi nhớ điều đó.

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

Và thế là xong! Đang sử dụng T dưới dạng kiểu động, có thể được kiểm tra trước khi chạy. Và một lần nữa, GenericCollection
class sẽ luôn có thể sử dụng được cho mọi loại.




Trả lời