+1

Co-operative PHP Multitasking

Co-operative PHP Multitasking (part I)

Khi một array giống như một chuyến phiêu lưu!

Bài viết được dịch từ Co-operative PHP Multitasking

Đó chính là tiêu điểm của mọi thứ mà chúng ta sẽ xem xét. Nhưng chúng ta sẽ bắt đầu ở một vài chỗ đơn giản và quen thuộc hơn.

Tất cả bắt đầu với mảng

Chúng ta có thể sử dụng mảng cho các vòng lặp đơn giản

<?php
$array = ["foo", "bar", "baz"];

foreach ($array as $key => $value) {
    print "item: " . $key . "|" . $value . "\n";
}

for ($i = 0; $i < count($array); $i++) {
    print "item: " . $i . "|" . $array[$i] . "\n";
}

Đây là một loại mà chúng ta thường sử dụng khi code mỗi ngày. Đó là duyệt qua mảng và sử dụng các key và value của mỗi item. Tự nhiên là nếu bạn muốn biết khi nào chúng ta làm việc với một mảng, có một function thực hiện chức năng này:

<?php
print is_array($array) ? "yes" : "no"; // yes

Mảng giống như đồ đạc vậy?

Đôi khi chúng ta nắm giữ những thứ hoạt động theo cách này, nhưng không phải mảng. Xem xét làm việc với DOMDocument class:

<?php
$document = new DOMDocument();
$document->loadXML("<div></div>");

$elements = $document->getElementsByTagName("div");

print_r($elements); // DOMNodeList Object ( [length] => 1 )

điều này rõ ràng không phải là một mảng, nhưng nó lại có length property. Chúng ta có thể nghiên cứu kỹ điều này, theo cách tương tự chúng ta có thể nghiên cứu kỹ lưỡng về mảng. Chúng ta có thể tìm ra bằng cách kiểm tra xem nó thực hiện một interface đặc biệt:

<?php
print ($elements instanceof Traversable) ? "yes" : "no"; // yes

Điều đó thật là hữu ích. Chúng ta không phải kích hoạt các lỗi nếu điều mà chúng ta muốn đi qua không thể đi qua được. Chúng ta chỉ có thể kiểm tra trước bằng tay. Điều nãy dẫn đến một câu hỏi khác: chúng ta có thể làm cho các class có cùng cách hành xử? Câu trả lời là có. Cách tiếp cận đầu tiên như sau:

<?php
class MyTraversable implements Traversable
{
    // our nonsense here...
}

Nếu chúng ta chạy đoạn code trên thì nó sẽ xuất hiện thông báo lỗi:

PHP Fatal error: Class MyTraversable must implement interface Traversable as part of either Iterator or IteratorAggregate…

Iterator

Theo cách trên chúng ta không thể triển khai được Traversable, nhưng chúng ta có thể thử cách thứ hai:

class MyIterator implements Iterator
{
    // our nonsense here...
}

Interface này yêu cầu triển khai 5 methods.

<?php
class MyIterator implements Iterator
{
    protected $data;

    protected $index = 0;

    public function __construct(array $data)
    {
        $this->data = $data;
    }

    public function current()
    {
        return $this->data[$this->index];
    }

    public function next()
    {
        return $this->data[$this->index++];
    }

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

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

    public function valid()
    {
        return $this->index < count($this->data);
    }
}

Một số điều quan trọng cần lưu ý:

  1. Chúng ta đang lưu mảng constructor $data, bởi vậy chúng ta có thể trả về các items từ nó sau đó.
  2. Chúng ta cũng cần một index của vòng lặp (hoặc con trỏ) để theo dõi những thứ như currentnext
  3. rewind() chỉ reset index, bởi vậy current() và next() sẽ hoạt động như kỳ vọng
  4. Keys không phải là numeric! Đó chỉ là những gì tôi đang sử dụng ở đây để giữ mọi thứ đơn giản.

Chúng ta có thể chạy với đoạn code sau:

<?php
$iterator = new MyIterator(["foo", "bar", "baz"]);
 
foreach ($iterator as $key => $value) {
    print "item: " . $key . "|" . $value . "\n";
}

trông thì có vẻ như đoạn trên có nhiều xử lý, nhưng thực chất nó chỉ là cách đơn giản để vòng lặp foreach/for kiếm tra mảng có những gì.

IteratorAggregate

Nhớ rằng interface thứ hai Traversable exception được gợi ý chúng ta implement? Hóa ra có một cách nhanh hơn để implement Iterator:

<?php
class MyIteratorAggregate implements IteratorAggregate
{
    protected $data;

    public function __construct(array $data)
    {
        $this->data = $data;
    }

    public function getIterator()
    {
        return new ArrayIterator($this->data);
    }
}

Thay vì implement một Iterator hoàn toàn mới, chúng ta thêm một ArrayIterator. Việc này tốn ít công sức hơn việc triển khai một Iterator mới hoàn toàn.

Hãy so sánh code nào. Đầu tiên chúng ta sẽ đọc mỗi dòng trong một file không có generator:

<?php
$content = file_get_contents(__FILE__);

$lines = explode("\n", $content);

foreach ($lines as $i => $line) {
    print $i . ". " . $line . "\n";
}

đoạn code trên sẽ in ra tất cả các dòng với số dòng và dữ liệu của dòng đó. Vậy tạo thêm một generator, bởi vì tại sao lại không!

<?php
function lines($file) {
    $handle = fopen($file, "r");
 
    while (!feof($handle)) {
        yield trim(fgets($handle));
    }
 
    fclose($handle);
}
 
foreach (lines(__FILE__) as $i => $line) {
    print $i . ". " . $line . "\n";
}

Trông đoạn code vừa rồi trông phức tạp hơn. Tất nhiên, chủ yếu vì chúng ta không sử dụng file_get_contents(). Một generator trông giống như một function, nhưng nó dừng lại mỗi lần nó nhận được từ khóa yield. Generators trông nhỏ gọn hơn giống iterators:

<?php
print_r(lines(__FILE__)); // Generator Object ( )

Bởi vì nó không phải là iterator, nó là một Generator. Nó có những methods nào?

<?php
print_r(get_class_methods(lines(__FILE__)));

// Array
// (
//     [0] => rewind
//     [1] => valid
//     [2] => current
//     [3] => key
//     [4] => next
//     [5] => send
//     [6] => throw
//     [7] => __wakeup
// )

Nếu bạn đọc một file đủ lớn, và sử dụng memory_get_peak_usage(), bạn sẽ chú ý rằng generator code sử dụng một lượng memory cố định, không quan trọng việc file đó lớn thế nào. Nó chỉ việc đọc một dòng đơn tại một thời điểm. Việc đọc file với file_get_contents() sử dụng bộ nhớ nhiều hơn file nhận được. Đây là một trong những lợi ích trong việc sử dụng iterators cho những việc như thế này.

Send

Nó có khả thi trong việc gửi data vào trong một generator. Xem xét generator dưới đây:

<?php
$generator = call_user_func(function() {
    yield "foo";
});
 
print $generator->current() . "\n"; // foo

Chú ý làm thế nào mà chúng ta gói một generator function ở trong call_user_func()? Điều đó chỉ là một shortcut cho định nghĩa function và ngay sau đó gọi nó để nhận một generator instance mới...

Chúng ta đã thấy cách sử dụng yeild trong trường hợp này. Chúng ta có thể mở rộng generator để chấp nhận data:

<?php
$generator = call_user_func(function() {
    $input = (yield "foo");
    print "inside: " . $input . "\n";
});
 
print $generator->current() . "\n";
 
$generator->send("bar");

Data được nhập vào và rời khỏi thông qua từ khóa yeild. Để bắt đầu với current() execute code cho tới khi nó thấy yield và trả về foo. send() đẩy nó qua yield tới nơi mà generator in ra dữ liệu đầu vào. Phải mất một thời gian mới quen với việc sử dụng điều này được.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí