Facebook Pixel

Tìm hiểu Iterables và Iterators trong ngôn ngữ Dart

24 Jul, 2021

Kieu Hoa

Author

Tôi sẽ giải thích các iterable là gì, khác với iterator như nào và chỉ cho bạn ví dụ thực tế về cách tạo ra iterable của riêng mình.

Tìm hiểu Iterables và Iterators trong ngôn ngữ Dart

Mục Lục

Trước khi thực hiện một số nghiên cứu và thực hành, các iterable khá khó hiểu đối với tôi. Nếu bạn giống như tôi, thì bài viết này là dành cho bạn. Thực tế thì chúng không khó đến vậy. Tôi sẽ giải thích các iterable là gì và chúng khác với iterator như thế nào. Tôi cũng sẽ chỉ cho bạn một ví dụ thực tế về cách tạo ra iterable của riêng mình.

1. Iterable là gì?

iterable là một loại collection trong Dart. Đó là một collection mà bạn có thể duyệt qua từng element một. ListSet là hai ví dụ phổ biến về iterable collection. Queue là một  iterable collection khác ít phổ biến hơn.

Nếu bạn nhìn vào source code của List, bạn sẽ thấy như sau:

Dart
abstract class List<E> implements EfficientLengthIterable<E> { ... }

Bản thân EfficientLengthIterable là một subclass của Iterable. Vì vậy, theo định nghĩa của nó, bạn có thể thấy rằng lists là các iterable.

Tiếp theo, bạn sẽ thấy một số lợi ích của việc trở thành một iterable collection là như thế nào.

1.1 Iterating over the elements of a collection (Iterating các element của một collection)

Có thể duyệt tuần tự qua tất cả các element của một collection là điều kiện tiên quyết để sử dụng vòng lặp for-in.

Dart
final myList = [2, 4, 6];
for (var number in myList) {
  print(number);
}

Vì  List là iterable, bạn có thể sử dụng vòng lặp qua nó.

Tuy nhiên, không phải tất cả các Dart collection đều là iterable. Đáng chú ý nhất là Map. Đó là lý do tại sao bạn không thể sử dụng trực tiếp vòng lặp for-in với các element của Map collection.

Nếu bạn cố gắng làm như sau:

Dart
final myMap = {'a': 1, 'b':2, 'c':3};
for (var element in myMap) {
  print(element);
}

Bạn sẽ gặp lỗi:

Dart
The type 'Map<String, int>' used in the 'for' loop must implement Iterable.

Tuy nhiên, maps có các thuộc tính keysvalues, thuộc loại Iterable. Điều đó có nghĩa là bạn có thể sử dụng vòng lặp qua một trong hai thuộc tính đó. Dưới đây là một ví dụ về việc sử dụng vòng lặp qua các keys:

Dart
final myMap = {'a': 1, 'b':2, 'c':3};
for (var key in myMap.keys) {
  print('key: $key, value: ${myMap[key]}');
}

1.2 Các lợi ích khác của iterables

Iterable cung cấp cho bạn quyền truy cập vào nhiều tính năng khác ngoài việc sử dụng chúng với vòng lặp for-in. Ví dụ: có một số phương pháp thứ tự cao hơn (higher order methods) có sẵn, chẳng hạn như map, where, fold, và expand.
Dưới đây là một ví dụ về phương pháp sử dụng where, rất hữu ích để lọc ra các element nhất định của một collection:

Dart
const myList = [1, 2, 3, 4, 5, 6, 7, 8];
final evenNumbers = myList.where((element) => element.isEven);
print(evenNumbers);

Kết quả in ra như sau:

Dart
(2, 4, 6, 8)

Có các dấu ngoặc bao quanh collection thay vì dấu ngoặc vuông vì where trả về một object kiểu Iterable chứ không phải List. Nếu bạn thực sự muốn một List cụ thể, bạn có thể sử dụng phương thức toList mà các iterable có:

Dart
print(evenNumbers.toList());

Điều này cung cấp cho các dấu ngoặc vuông như mong đợi:

Dart
[2, 4, 6, 8]

Lưu ý: Một iterable đại diện cho một collection các element tiềm năng, nhưng nó không cung cấp cho bạn các element đó cho đến khi bạn yêu cầu. Điều đó có thể hữu ích khi phải thực hiện một số công việc nặng nhọc để tính toán các element là gì. Bạn không muốn làm công việc đó trừ khi bạn thực sự cần các element. Tuy nhiên, khi bạn gọi toList, bạn đang buộc iterable phải lặp lại tất cả các element để tạo danh sách.

2. Cách tạo iterable của riêng bạn

Như bạn đã học ở trên, List, Set, Queue, keysvalues của Map đều là các iterable, nhưng nếu bạn muốn tạo kiểu iterable của riêng mình thì sao?

Để tạo một class iterable với tất cả các lợi ích được mô tả ở trên, bạn phải tạo một iterator. Lý do là, iterable không thực sự biết cách lặp lại trên các element của nó. Tuy nhiên, tất cả các iterable đều có một iterator và nhiệm vụ của iterator là di chuyển tuần tự qua tất cả các element của iterable.

Trong ví dụ dưới đây, tôi sẽ hướng dẫn bạn cách tạo class iterable của riêng bạn cùng với iterator của nó.

2.1 Mô tả vấn đề

Trong Flutter, bạn có thể hiển thị hầu hết các string một cách dễ dàng bằng cách sử dụng Text widget. Tuy nhiên, nếu bạn muốn render văn bản ở cấp độ thấp, mọi thứ sẽ khó khăn hơn một chút. Thật không may, Flutter ẩn API cho bộ ngắt dòng cần thiết để biết vị trí cần để ngắt các string dài xuống dòng tiếp theo. (Xem My First Disappointment with Flutterthis GitHub issue để biết chi tiết.)

Dấu ngắt dòng (line breaker) lấy một string dài và cho bạn biết các vị trí trên string có thể bắt đầu dòng mới mà không cần cắt đôi một từ. Nơi ngắt tự nhiên nhất là tại các khoảng trắng, nhưng Unicode mô tả nhiều hơn thế nữa.

Trong ví dụ sau, bạn sẽ tạo một iterable đơn giản có các element là các dòng văn bản giữa các điểm có thể ngắt dòng. Vì đây chỉ là một minh chứng cơ bản, bạn chỉ sử dụng một ký tự khoảng trắng làm điểm ngắt.

Ví dụ, cho string sau:

Dart
This is a long string that I want to iterate over.

Các ký tự | hiển thị các vị trí mà bạn có thể xếp hàng ở:

Dart
This |is |a |long |string |that |I |want |to |iterate |over.

Các substring giữa các ký tự | đại diện cho các element của iterable.

2.2 Tạo một class extends từ Iterable

Điều đầu tiên bạn nên làm khi tạo một iterable là extends class Iterable.

Dart
class TextRuns extends Iterable<String> {
  TextRuns(this.text);
  final String text;
  @override
  Iterator<String> get iterator => TextRunIterator(text);
}

Tôi có thể gọi nó là LineBreaks, nhưng tôi quyết định chọn TextRuns để nhấn mạnh rằng các element của collection là các string.

Lưu ý rằng yêu cầu duy nhất đối với iterable là nó có một getter tên là iterator của kiểu Iterator. Giống như tôi đã nói trước đó, các iterable không biết cách lặp lại các element của chúng. Đó là công việc của iterator.

Khi bạn tạo iterable của riêng mình, bạn cũng phải tạo Iterator của riêng mình. Trong đoạn code trên, bạn có thể thấy rằng tôi đã gọi iterator TextRunIterator. Vì bạn chưa làm được điều đó, bạn sẽ làm điều đó ở phần tiếp theo.

2.3 Tạo một class implements class Iterator

Iterator là nơi tất cả công việc được thực hiện. Các Iterator cơ bản chỉ phải implement 1 class trừu tượng đơn giản sau:

Dart
abstract class Iterator<E> {
  E get current;
  bool moveNext();
}

Chữ E đại diện cho một kiểu generic và là viết tắt của element. Điều đó có nghĩa là bạn có thể có một collection có các element thuộc bất kỳ loại nào.

Trong khi có các iterable hai chiều (ví dụ: thuộc tính runes của String), thì một iterator đơn giản chỉ di chuyển một hướng qua collection. Bất cứ khi nào, moveNext được gọi là iterator chọn element tiếp theo của collection. Nó gọi element này là current.

2.4 Khởi tạo 1 class cơ bản

Đây là phần bắt đầu đối với TextRunIterator:

Dart
class TextRunIterator implements Iterator<String> {
  TextRunIterator(this.text);
  final String text;
}

Bạn sẽ truyền vào text string trong hàm dựng, đến từ iterable mà bạn đã tạo.

2.5 Thêm các thuộc tính private cho việc substring

Bạn chưa triển khai current hoặc moveNext, nhưng trước tiên, hãy nghĩ về cách bạn sẽ đi qua các iterator qua các dấu ngắt trong một string. Để văn bản chạy giữa các vị trí ngắt, bạn sẽ sử dụng phương thức substring của String, phương thức này có chỉ mục (index) bắt đầu và kết thúc. Vì vậy, hãy thêm các thuộc tính private sau vào TextRunIterator:

Dart
int _startIndex = 0;
int _endIndex = 0;

Mặc dù không bắt buộc, nhưng bạn sẽ bắt đầu từ đầu string, vì vậy bạn có thể khởi tạo các chỉ mục bằng 0.

2.6 Adding the current getter

Tiếp theo, bạn sẽ triển khai phương thức getter để lấy ra current. Thêm các dòng sau vào class của bạn:

Dart
String _currentTextRun;
@override
String get current => _currentTextRun;

Hiện tại, bạn chưa thực sự làm được gì cả. Bạn sẽ đặt _currentTextRun trong phương thức moveNext chỉ trong một quy tắc nhỏ. Nếu mọi người cố gắng cập nhật trước khi gọi moveNext, sẽ nhận được giá trị null.

2.7 Thêm phương thức moveNext

Cuối cùng, triển khai moveNext bằng cách thêm đoạn code sau:

Dart
@override
bool moveNext() {
  _startIndex = _endIndex;
  if (_startIndex == text.length) {
    _currentTextRun = null;
    return false;
  }
  final next = text.indexOf(breakChar, _startIndex);
  _endIndex = (next != -1) ? next + 1 : text.length;
  _currentTextRun = text.substring(_startIndex, _endIndex);
  return true;
}
final breakChar = RegExp(' ');

Đây là những gì đang xảy ra:

  • Khi tính toán substring, _startIndex là bao hàm trong khi _endIndex là loại trừ. Khi nỗ lực tìm substring tiếp theo, bạn sẽ di chuyển index bắt đầu đến bất kỳ nơi nào mà substring cuối cùng kết thúc.
  • Phương thức moveNext trả về một Boolean. Nếu falsecó nghĩa là iterator không thể di chuyển đến element tiếp theo vì không còn element nữa. Do đó, bạn bắt đầu bằng cách kiểm tra xem _startIndex đã đến cuối văn bản chưa. Trả về false nếu đúng như vậy.
  • Sau đó, bạn tìm index của vị trí tiếp theo của ký tự ngắt dòng. Pattern matcher breakChar là một biểu thức chính quy khớp với một ký tự khoảng trắng, nhưng bạn cũng có thể làm cho nó phức tạp hơn để khớp với các ký tự bổ sung.
  • indexOf của String trả về -1 nếu không có kết quả phù hợp. Trong trường hợp đó, bạn sẽ đặt _endIndex ở cuối string. Nếu không, hãy đặt _endIndex một ký tự sau ký tự ngắt (vì bạn bao gồm ký tự ngắt trong lần chạy văn bản trước đó).
  • Cuối cùng, đặt _currentTextRun thành substring được đại diện bởi _startIndex_endIndex, sau đó trả về true để cho biết người dùng vẫn có thể gọi lại moveNext.


Điều đó hoàn thành iterator của bạn và cũng làm iterable của bạn có thể sử dụng được.

2.8 Sử dụng iterable của bạn

Bây giờ bạn có thể sử dụng iterable của mình như cách bạn sử dụng bất kỳ iterable khác. Đây là đối với một vòng lặp for-in:

Dart
const myString = 'This is a long string that I want to iterate over.';
final myIterable = TextRuns(myString);
for (var textRun in myIterable) {
  print(textRun);
}

Run nó và bạn sẽ thấy như thế này:

Dart
This 
is 
a 
long 
string 
that 
I 
want 
to 
iterate 
over.

Xin chúc mừng! Bạn đã làm được rồi đấy!

3. Tiếp tục

Nếu bạn muốn tạo một iterator có thể quay ngược lại cũng như chuyển tiếp, hãy kiểm tra class BidirectionalIterator. Nó giống như Iterator với việc bổ sung phương thức movePrevious:

Dart
abstract class BidirectionalIterator<E> implements Iterator<E> {
  bool movePrevious();
}

Rune sử dụng iterator hai chiều.

4. Full code

Đây là full source code. Bạn cũng có thể sửa đổi code trong DartPad.

Dart
void main() {
  const myString = 'This is a long string that I want to iterate over.';
  final myIterable = TextRuns(myString);
  for (var textRun in myIterable) {
    print(textRun);
  }
}

class TextRuns extends Iterable<String> {
  TextRuns(this.text);
  final String text;

  @override
  Iterator<String> get iterator => TextRunIterator(text);
}

class TextRunIterator implements Iterator<String> {
  TextRunIterator(this.text);
  final String text;

  String _currentTextRun;
  int _startIndex = 0;
  int _endIndex = 0;

  final breakChar = RegExp(' ');

  @override
  String get current => _currentTextRun;

  @override
  bool moveNext() {
    _startIndex = _endIndex;
    if (_startIndex == text.length) {
      _currentTextRun = null;
      return false;
    }
    final next = text.indexOf(breakChar, _startIndex);
    _endIndex = (next != -1) ? next + 1 : text.length;
    _currentTextRun = text.substring(_startIndex, _endIndex);
    return true;
  }
}

Bài viết này được dịch từ đây.

Bài viết liên quan

Lập trình backend expressjs

xây dựng hệ thống microservices
  • Kiến trúc Hexagonal và ứng dụngal font-
  • TypeScript: OOP và nguyên lý SOLIDal font-
  • Event-Driven Architecture, Queue & PubSubal font-
  • Basic scalable System Designal font-

Đăng ký nhận thông báo

Đừng bỏ lỡ những bài viết thú vị từ 200Lab