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. List
và Set
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 keys
và values
, 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
, keys
và values
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 Flutter và this 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ếufalse
có 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
và_endIndex
, sau đó trả vềtrue
để cho biết người dùng vẫn có thể gọi lạimoveNext
.
Đ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.
Kieu Hoa
Khi mình yêu cuộc đời, cuộc đời cũng sẽ yêu mình đắm say
Bài viết liên quan
Tự học Dart: Các Dart Operators (toán tử) bạn cần biết
Sep 02, 2023 • 18 min read
Dart là gì? Giới thiệu cơ bản về ngôn ngữ lập trình Dart
Aug 21, 2023 • 11 min read
Tổng hợp các Shortcuts, Extensions & Settings trong VSCode khi lập trình Flutter
Jun 03, 2022 • 10 min read
[Phần 1] Những extension cần thiết khi làm việc với Flutter trên VS Code
May 27, 2022 • 4 min read
Flutter Design Patterns: 17 — Bridge
Dec 09, 2021 • 11 min read
Navigation 2.0 - Routing On Flutter Web
Nov 28, 2021 • 8 min read