, December 04, 2022

0 kết quả được tìm thấy

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

  • Đăng bởi  Kieu Hoa
  •  Jul 24, 2021

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

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:

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.

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:

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

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

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:

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:

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:

(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ó:

print(evenNumbers.toList());

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

[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:

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 ở:

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.

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:

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:

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:

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:

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:

@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:

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:

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:

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.

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

Flutter Tutorial 2022: Giới thiệu Flutter Travel App

Trong series này, chúng ta sẽ cùng nhau thực hiện ứng dụng Travel App với một bản UI Design vô cùng đẹp mắt, thu hút có sẵn....

Flutter Tutorial 2022: Giới thiệu Flutter Travel App
Tổng hợp các Shortcuts, Extensions & Settings trong VSCode khi lập trình Flutter

Trong bài viết này, mình sẽ liệt kê cho bạn các shortcuts, extensions, setting mà bạn nên sử dụng để lập trình Flutter hàng ngày....

Tổng hợp các Shortcuts, Extensions & Settings trong VSCode khi lập trình Flutter
[Phần 1] Những extension cần thiết khi làm việc với Flutter trên VS Code

VS Code là 1 text editor tuyệt vời được phát triển bởi Mircosoft. Đây là một trong các công cụ được developer trên toàn thế giới yêu thích vì sự tiện lợi, dễ dùng và nó chứa hàng ngàn, thậm chí trăm ngàn các extension phục vụ cho bạn trong quá trình bạn phát triển phần mềm....

[Phần 1] Những extension cần thiết khi làm việc với Flutter trên VS Code
Dart 2.15 có gì mới?

Ở bài viết này, chúng ta sẽ cùng nhau tìm hiểu chi tiết những tính năng vừa mới được cập nhật thêm ở phiên bản Dart 2.15. Cùng xem thử để xem có tính năng nào mà bạn đang "mong chờ" từ lâu đối với ngôn ngữ Dart hay không 😄....

Dart 2.15 có gì mới?
Flutter Design Patterns: 17 — Bridge

Tổng quan về Bridge design pattern và việc thực hiện nó trong Dart và Flutter...

Flutter Design Patterns: 17 — Bridge
You've successfully subscribed to 200Lab Blog
Great! Next, complete checkout for full access to 200Lab Blog
Xin chào mừng bạn đã quay trở lại
OK! Tài khoản của bạn đã kích hoạt thành công.
Success! Your billing info is updated.
Billing info update failed.
Your link has expired.