Contents
Chào mừng bạn đến với Fx Studio. Chúng ta sẽ tiếp tục chuyến thăm quan ngôn ngữ lập trình Dart. Bài viết này sẽ nói về Classes & Objects trong Dart. Một trong nhưng phần quan trọng nhất khi bạn bắt tay tìm hiểu về một ngôn ngữ lập trình mới.
Nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Về mặt công cụ editor thì khá đơn giản:
- TextEditor
- Visual Studio Code (nên dùng)
Hoặc bạn vào trang https://dartpad.dev/ để tiến hành code luôn. Khá là giống với Playground của Swift.
Vè mặt lý thuyết, nếu bạn chưa biết gì về Dart, thì có thể theo dõi lại các bài viết trong series Dart Tour nhóe.
Classes
Ngôn ngữ lập trình Dart là một ngôn ngữ lập trình hướng đối tượng. Do đó các Classes là một trong những thành phần chính cấu tạo nên chúng. Điểm đặc biệt trong Dart thì tất cả các đối tượng đều được xây dựng từ các Class. Bao gồm cả các đối tượng cơ bản như: int, double, String …
Nếu bạn nhớ lại, thì với kiểu dữ liệu
Object
là chúa trong ngôn ngữ lập trình Dart này rồi.
Các Classes là thành phần chính tạo nên hướng đối tượng, chúng đều có các thuộc tính & phương thức để cấu hình nên một đối tượng. Và với Class trong Dart cũng như vậy.
Để hiểu một cách đơn giản nhất
- Thuộc tính chính là các biến/hằng mà chúng ta được học
- Phương thức chính là các function của chúng ta
Cả hai đều được bọc trong một khai báo Class. Và thể hiện của Class thì sẽ gọi là các đối tượng.
Chúng ta sẽ bắt đầu tìm hiểu về Classes trong Dart qua các phần dưới đây.
Define class
Sử dụng từ khoá
class
để bắt đầu khai báo một Class trong Dart.
Chúng ta cũng sẽ mượn một ví dụ tạo một lớp có tên là User
trong đó có 2 thuộc tính id
và name
. Xem có ví dụ sau:
class User { int id = 0; String name = ''; }
Trong đó, ta cung cấp luôn giá trị ban đầu cho các thuộc tính. Bạn có thể để chúng nó bằng null
cũng không sao. Và lớp cơ bản này của chúng ta thì chưa có các phương thức nào hết. Nhưng không sao, chúng ta sẽ tìm hiểu nó sau.
Creating an Object
Sau khi, bạn đã có được một lớp rồi. Công việc tiếp theo, chính là sử dụng lớp đó và tạo đối tượng từ lớp.
Dành cho các bạn còn khá mông lung trong các cách gọi tên đối tượng sinh ra từ class. Thì ta có thể gọi bằng các tên sau:
- Tiếng Việt: đối tượng, thể hiện
- Tiếng Anh: object, instance
Cái này vui thôi, chủ yếu là bạn hiểu được là ổn rồi.
Theo ví dụ trên, ta sẽ có cách đơn giản nhất tạo ra một đối tượng từ class thì như sau:
final user = User();
Trong đó:
user
là tên của đối tượngUser
là tên của lớpUser()
là gọi hàm khởi tạo của lớp
Một đối tượng sẽ được sinh ra khi bạn dùng lớp đó & triệu hồi hàm khởi tạo (constructor method) của nó. Kết quả trả về chính là đối tượng từ lớp đó.
Long time ago!
Trước đây, việc tạo đối tượng thì khác một chút, bạn sẽ sử dụng từ khoá new
. Xem ví dụ như sau:
final user = new User();
Bạn cũng không cần quá lo lắng, vì 2 cách đều như nhau. Chỉ là người ta thấy new
dài dòng quá, nên lượt bỏ bớt thôi.
Properties
Bạn đã có được đối tượng rồi và nó sẽ có ý nghĩa khi bạn có thể sử dụng được nó. Mà sử dụng nó thì thông qua các các thuộc tính của nó. Vì
Class là một kiểu dữ liệu custom và cấu trúc dữ liệu phức tạp. Khi bạn không thể dùng các kiểu dữ liệu cơ bản (như: int, double, string …) để diễn dạt hết logic và cấu trúc dữ liệu mong muốn.
Nói về thuộc tính, công việc của nó chính là lưu trữ dữ liệu
cho đối tượng. Do đó, việc thay đổi giá trị sẽ là gán giá trị mới cho thuộc tính. Còn việc thao tác với các thuộc tính, thì bạn sử dụng chính đối tượng và dùng dấu .
để triệu hồi các thuộc tính mà bạn muốn thay đổi.
<object>.<property> = <value>
Xem ví dụ nhoé!
final user = User(); user.id = 22; user.name = 'Fx Studio';
Cũng không có gì là khó hĩ, vẫn là hình bóng quen thuộc qua biết bao ngôn ngữ lập trình.
Printing an object
Bạn thử xem nếu thực thi dòng lệnh này, thì sẽ hiển thị như thế nào.
print(user);
Bạn sẽ nhận được kết quả như sau:
Instance of 'User'
Chỉ đơn giản vậy thôi. Chỉ cho bạn biết đó là một thể hiện của lớp User. Còn nếu bạn kì vọng vào việc hiển thị đầy đủ thông tin thì còn một cách như sau:
@override String toString() { return 'User(id: $id, name: $name)'; }
Trong tất cả các lớp của Dart đều hỗ trợ phương thức toString()
. Công việc của bạn chỉ là định nghĩa lại hàm toString()
và thêm vào cách dữ liệu hay điều gì mà bạn muốn nhắn gửi.
Chúng ta giữ nguyên lệnh print(user);
ở trên và chạy lại chương trình. Kết quả sẽ hiển thị như sau:
User(id: 22, name: Fx Studio)
Nhức nách phải không nào!
Khi gặp print đối tượng, thì tự động triệu hồi phương thức toString()
.
Methods
Trong ví dụ trên, chúng ta thực hiện ghi đè override
một phương thức trong lớp. Chúng ta sẽ thêm các phương thức của riêng mình vào nhoé.
Create a method
Việc thêm một phương thức (method) cũng giống việc định nghĩa một function như trước đây. Có khác là bạn sẽ phải định nghĩa function đó trong phạm vi của lớp.
Xem ví dụ nhoé!
class User { //... void hello() { print('Hello, I am $name.'); } }
Cũng không có gì khó hiểu ở ví dụ, ta sẽ có phương thức hello
cho User. Và cách gọi thực thi phương thức thì như sau.
user.hello();
Vẫn là dấu .
huyền thoại để triệu hồi một phương thức từ đối tượng của Classes.
JSON serialization method
Ở ví dụ print
một đối tượng. Thì về bản chất chúng ta gọi hàm toString()
và chúng được xem là một serialization method hay còn gọi là hàm chuyển đổi kiểu dữ liệu. Ta có từ đối tượng thành String.
Chúng ta sẽ còn rất nhiều kiểu chuyển đổi sang các kiểu dữ liệu khác nữa. Trong đó, một trong những kiểu ta cần quan tâm là JSON.
Vì bạn sẽ cần tới nó rất nhiều sau này.
Xem ví dụ nhoé!
String toJson() { return '{"id":$id,"name":"$name"}'; }
Trong đó:
toJson()
là một phương thức riêng của chúng ta cho User- Không có gì khó trong này. Bạn chỉ cần sử dụng giá trị của các thuộc tính và ném nó vào 1 chuỗi.
- Áp dụng các định dạng của JSON cho chuỗi trả về trong
return
Còn sử dụng thì như thế này thôi.
print(user.toJson());
Ở các bài viết sau, chúng ta sẽ dùng JSON cho Map
trong Dart. Khá hay đó!
Chờ đi nhoé!
Cascade notation
Cascade notation là một cú pháp khá là vui của Dart. Bạn sẽ sử dụng ..
để gọi các thuộc tính sau khi khởi tạo đối tượng. Nó sẽ xâu chuỗi lại các bước thực hiện trên cùng một đối tượng.
Xem ví dụ để dễ hình dùng nào:
- Trước
final user = User(); user.id = 22; user.name = 'Fx Studio';
- Sau
final user = User() ..id = 23 ..name = 'Fx Studio2';
Trong đó:
- đối tượng
user
bị lượt bỏ đi và thay bằng dấu..
- dấu
;
chỉ dùng ở dòng cuối cùng để kết thúc chuỗi các câu lệnh
Bạn đã bắt đầu thấy Dart huyền bí chưa nào. Ahihi!
Constructors
Constructors là một trong nhưng phương thức đặc biệt của Classes trong Dart, hay bất cứ ngôn ngữ lập trình nào. Chúng dùng để tạo nên một đối tượng mới. Đặc điểm chung:
- Có tên chính là tên của lớp
- Kiểu giá trị trả về cũng chính là lớp đó
Ta sẽ có nhiều hàm Constructors trong một class. Bao gồm:
Default Constructors
Đây là hàm Constructors mà hệ thông cung cấp cho bạn. Cho dù, bạn có muốn hay không muốn thì nó vẫn ở đó và vẫn tồn tại.
Xem ví dụ nha:
class Dog { var name = ''; }
Đây là khai báo 1 lớp bình thường và nó sẽ ẩn chứa hàm khởi tạo như sau:
class Dog { Dog(); var name = ''; }
Thì Dog();
là hàm khởi tạo mặc định của lớp Dog. Ta có:
- Không có kiểu giá trị trả về ở khai báo, tuy nhiên nó sẽ trả về chính kiểu dữ liệu của class
- Tên hàm trùng với tên lớp
Custom constructors
Khi bạn muốn khởi tạo một đối tượng và có sẵn các thông tin cho đối tượng mới, thì bạn sẽ phải tạo ra riêng cho mình những hàm khởi tạo cho Classes.
Các hàm khởi tạo kiểu này được gọi là generative constructor.
Về quy tắt viết thì cũng giống với việc bạn viết các hàm khởi tạo mặc định. Tuy nhiên, bạn sẽ có thêm các tham số tuỳ chọn của riêng bạn.
Và Dart cung cấp cho chúng ta 2 kiểu viết hàm khởi tạo mới này:
Long-form
Với kiểu đầy đủ, thì Dart cũng giống như các ngôn ngữ khác và giống như cách viết khác khi bạn khai báo một function bất kì. Xem ví dụ với class User nhoé.
class User { User(int id, String name) { this.id = id; this.name = name; } // ... }
Với từ khoá this
dùng để chỉ ra phạm vi của các biến và các thuộc tính của lớp. Còn lại, bạn chỉ cần gán các giá trị cần thiết cho các thuộc tính mà thôi.
Cách sử dụng như sau:
final user3 = User(24, 'Fx Studio 3'); print(user3);
Khi bạn đã có một hàm khởi tạo của riêng mình rồi, thì hàm khởi tạo mặc định sẽ không còn được sử dụng được nữa. Do đó, bạn sẽ cập nhật lại các đối tượng đã được khai báo ở trên nhoé.
Short-form
Dart cung cấp cho bạn một dạng rút gọn hơn của hàm khởi tạo. Đó là:
- Không có body cho hàm khởi tạo
- Liệt kê các thuộc tính với từ khoá
this
ở trước - Các đối số gởi vào lần lượt sẽ tương ứng với thứ tự các thuộc tính
Viết lại ví dụ trên của hàm khởi tạo lớp User nhoé.
User(this.id, this.name);
Bạn sẽ thay thế cho hàm khởi tạo ở trên nhoé. Nếu không thì lỗi nhiều đó.
Khi bạn đã có các hàm khởi tạo và cung cấp đầy đủ giá trị cho các thuộc tính. Thì bạn hãy xoá đi các giá trị ban đầu gán cho các thuộc tính trong lớp đi nhoé.
Named constructors
Dart còn hỗ trợ cho bạn các phương thức khởi tạo đối tượng (generative constructor) khác. Được gọi là named constructor. Bằng cách bạn thêm tên lớp vào trước. Ví dụ như sau:
ClassName.identifierName()
Để tổng hợp một chút cho bạn dễ nhớ, chúng ta có 2 loại hàm khởi tạo (generative constructor):
- Loại không định danh. Dùng chính tên lớp. Là các hàm constructor truyền thống
- Loại có định danh. Dùng tên lớp làm tiền tố
Nó sinh ra chỉ vì bạn
THÍCH thôi!
Tuỳ thuộc, logic của bạn sẽ triển khai như thế nào nữa. Ví dụ, lớp User của chúng ta sẽ thêm một hàm khởi tạo để tạo ra 1 đối tượng User gọi là ẩn danh. Code ví dụ như sau:
User.anonymous() { id = 0; name = 'anonymous'; }
Các sử dụng thì bạn hãy gọi tới chính hàm khởi tạo định danh này.
final anonymousUser = User.anonymous(); print(anonymousUser);
Nó cũng tượng tự như các Type Methods của các Classes trong các ngôn ngữ lập trình khác. Có khác ở đây là kiểu giá trị trả về chính là kiểu của lớp đó.
Forwarding constructors
Tụi Dart này thích chơi chữ ghê, để dễ hiểu thì chúng nó tương tự với:
Convenience initializers trong Swift.
Đôi khi bạn chỉ cần thay đổi 1 thuộc tính theo ý mình, các phần còn lại thì giữ nguyên. Nhưng bạn phải tạo code lại từ đầu. Dẫn tới khá là dư thừa code.
Ví dụ: với User.anonymous()
thì bạn sẽ phải gán lại id
& name
. Trong khi bạn đã có hàm khởi tạo đầy đủ với id
& name
rồi. Công việc sẽ đơn giản hơn từ hàm khởi tạo định danh, bạn gọi tới hàm khởi tạo không dịnh danh kia.
Dart sẽ gọi nó là forwarding hay redirecting.
Xem ví dụ nhoé!
User.anonymous() : this(0, 'anonymous');
Trong đó:
User.anonymous()
là hàm khởi tạo có định danh- Dấu
:
là báo sự chuyển tiếp this
chính class này gọi tới hàm khởi tạo không định danh- Bỏ qua
body
của hàm khởi tạo
Thực thi lại chương trình và cảm nhận kết quả nhoé!
Optional and named parameters
Phần này, chúng ta đã được tìm hiểu ở bài Functions. Giờ thì, ta sẽ áp dụng nó vào để cho việc khởi tạo rõ nghĩa hơn.
Như các ví dụ trên:
final user3 = User(24, 'Fx Studio 3');
Ta sẽ không thể biết 24
là đối số cho tham số nào, hay Fx Studio 3
cũng vậy. Nếu chúng nó có 1 cái tên hỗ trợ thêm thì nhức nách luôn.
Bắt tay vào việc thôi!
Hồi tưởng lại quá khứ một chút với việc có tên tham số trong lời gọi hàm, thì ta sẽ có 2 cách:
- Đặt vào 2 dấu
[]
. Có thể cho phép tham sốnull
- Đặt vào 2 dấu
{}
. Có thể cho phép tham số códefault value
Từ đó, ta sẽ hợp nhất 2 hàm khởi tạo của lớp User như sau:
- Hàm khởi tạo không định danh sẽ có các giá trị mặc định. Để có thể khởi tạo 1 đối tượng mà không cần nhập các giá trị cho tham số.
- Hợp nhất 2 hàm khởi tạo có định danh và không có định danh
Ví dụ code sẽ như sau:
User({this.id = 0, this.name = 'anonymous'}); User.anonymous() : this();
Trong đó:
- Hàm khởi tạo không định danh sẽ kết hợp
{}
để tạo ra tên cho các tham số & có giá trị mặc định ban đầu. - Hàm khởi tạo có định danh sẽ chuyển tiếp sang hàm khởi tạo không định danh. Và không cần truyền tham số nào hết.
Cách sử dụng mới như sau:
final user = User(id: 22, name: 'Fx');
Bạn đã thấy xuất hiện 2 tên tham số trong lời gọi hàm chưa. Mọi việc sẽ rõ ràng hơn nhiều với nó. Chúng ta cùng nhau xem lại code cho lớp User qua 7749 lần edit nhoé!
class User { // unnamed constructor User({this.id = 0, this.name = 'anonymous'}); // named constructor User.anonymous() : this(); int id; String name; // ... }
Cũng khá là mệt não nhóe, nghỉ ngơi một tí rồi chúng ta tiếp tục nào!
Initializer lists
Mặc dù, chúng ta đã làm cho Classes của chúng ta khá là đẹp rồi. Tuy nhiên, vẫn còn rất nhiều vấn đề cần để tối ưu lớp. Chúng ta sẽ bắt đầu thôi.
Private variables
Đâu tiên, bạn sẽ biến các thuộc tính của lớp từ public
thành private
. Đó sẽ đảm bảo tính bao đóng của hướng đối tượng. Và Dart sẽ hỗ trợ việc này cho bạn bằng từ khoá _
, cho các thuộc tính của chúng ta.
Xem ví dụ nhoé!
User { // ... int _id; String _name; // ... }
Đơn giản như vậy thôi. Bạn không cần tới từ khoá private
. Nhiều bạn dev iOS chắc không biết.
Đây là hồi ức của Objective-C trở về.
Khi bạn có một cái tên mới cho property, thì bạn cũng phải thay đổi lại các tên tham số ở hàm khởi tạo cho đồng bộ.
User({this._id = 0, this._name = 'anonymous'});
Initializer list
Nhưng mà có gì đó sai sai, vì Dart sẽ hiểu tên của tham số là _id
chứ không phải là id
như ban đầu. Ta sẽ chỉnh sửa lại ví dụ trên như sau:
User({id = 0, name = 'anonymous'}) : _id = id, _name = name;
Trong đó:
- Hàm khởi tạo sẽ bỏ đi
this
vì lúc nàyid
&name
không còn là thuộc tính của lớp nữa rồi - Dấu
:
sẽ đặt trước danh sách các thuộc tính private - Việc gán lại giá trị cho các thuộc tính private sẽ thực hiện và chúng cách nhau bởi dấu
,
Công việc làm với dấu
:
đó ở hàm khởi tạo, sẽ được gọi là initializer list.
Và initializer list, sẽ được thực hiện trước body
của hàm, hoặc có thể bỏ qua body
nếu bạn thấy không cần thiết. Ví dụ:
User({int id = 0, String name = 'anonymous'}) : _id = id, _name = name { print('User name is $_name'); }
Như vậy, bạn đã có tiếp một hàm khởi tạo xịn sò và các tham số riêng tư rồi. Ngoài ra có điều cần lưu ý:
Nếu trong nội bộ 1 file Dart, bạn vẫn có thể triệu hồi đc các thuộc tính riêng tư này từ đối tượng. Muốn tối ưu thì hãy tạo một file riêng cho lớp nhoé.
Validation
Chúng ta chỉ hạn chế sai sót ở mặc logic. Tuy nhiên, về mặt dữ liệu thì chúng ta không đoán được. Do đó, việc kiểm tra tiếp giá trị các đối số có phù hợp với yêu cầu bài toán hay không thì cũng rất quan trọng. Giảm tải đi rất nhiều sai sót hay logic để chống và bắt.
Ví dụ kinh điển với phân số, khi bạn nhập mẫu số bằng 0
. Thì phân số đó không có ý nghĩa. Nếu đã không có ý nghĩa rồi thì việc gì chúng ta lại cho đối tượng đó sinh ra. Tốt nhất là triệt tiêu chúng ngay trong trứng nước.
Từ đó, ta sẽ thêm các điều kiện tại initializer list ở hàm khởi tạo. Nhằm để loại trừ các giá trị không cần thiết nhoé. Xem tiếp ví dụ:
User({id = 0, name = 'anonymous'}) : assert(id >= 0), assert(name.isNotEmpty), _id = id, _name = name;
Trong đó:
- Bạn sử dụng thêm câu lệnh
assert()
để kiểm tra điều kiện. Nếu sai thì kết thúc luôn hàm khởi tạo. - Các điều kiện kiểm tra là
id
phải không âm. Vàname
phải không rỗng
Câu lệnh
assert()
dùng để kiểm tra một điều kiện nào đó. Giá trị trả về làbool
.
Ví dụ, bạn khởi tạo một đối tượng sau:
final jb = User(id: -1, name: 'Ahihi');
Khi bấm debug
thì sẽ nhận ngay dòng lỗi này. Và nó sẽ chỉ ra tại vị trí nào lỗi luôn.
Exception has occurred. _AssertionError ('file:...': Failed assertion: line 43 pos 16: 'id >= 0': is not true.)
Constant constructors
Chúng ta lại tiếp tục tối ưu tiếp các Classes của chúng ta. Chúng ta sẽ làm cho cách đối tượng của chúng ta sẽ là bất khả xâm phạm và không thay đổi được giá trị (immutable).
Properties immutable
Bạn có 2 các để biến đổi một thuộc tính bình thường thành một thuộc tính không thể thay đổi được giá trị. Đó là 2 từ khoá const
& final
. Với trường hợp Classes, chúng ta chưa xác định được giá trị của các thuộc tính ban đầu, nó chỉ được cấp giá trị lúc run-time nên sẽ lựa chọn là final
.
Ví dụ nhoé:
final String _name; final int _id;
Như vậy là xong, từ thuộc tính bình thường trở thành thuộc tính không thể thay đổi giá trị được. Chúng sẽ được cấp giá trị khởi tạo lần đầu tiên và sau đó thì sẽ trở thành một hằng số.
Classes immutable
Khi bạn mệt mỏi với biến từng thuộc tính trở thành hằng số, thì tại sao không suy nghĩ việc tạo ra luôn một đối tượng là hằng số. Và đối tượng ta tạo ra sẽ không bị thay đổi được.
Ta sẽ áp dụng từ khoá const
cho 2 hàm khởi tạo của lớp User. Code ví dụ như sau:
const User({int id = 0, String name = 'anonymous'}) : assert(id >= 0), _id = id, _name = name; const User.anonymous() : this();
Ta phải loại bỏ đi name.isNotEmpty
vì chúng thực hiện lúc run-time. Và chúng ta sẽ có các đối tượng được tạo ra như sau:
const user = User(id: 22, name: 'Fx'); const anonymousUser = User.anonymous();
Thực sự thì cũng chưa hiểu ý nghĩa hay mục đích cho công việc này lắm.
Chắc thời gian sẽ dần trả lời chúng ta thôi.
À, mà có người nói khi bạn qua Flutter, thì sẽ dùng nó rất nhiều cho widgets đó à.
Factory constructors
Chúng ta đã tương tác với các loại generative constructors rồi. Bây giờ chúng ta sẽ sang một loại mới với tên là factory constructor. Dễ hiểu thì như sau:
factory constructor = static function + constructor
Với factory constructor, chúng ta sẽ linh hoạt hơn nhiều so với generative constructors. Các generative constructors không chỉ trả vè các đối tưởng của lớp đó, mà còn có thể trả về các đối tượng của lớp con của nó. Hoặc khi bạn có thể tạo từ những kiểu dữ liệu khác nhau.
Syntax
Cú pháp đơn giản là bạn sử dụng từ khoá factory
trước một hàm khởi tạo định danh. Là bạn có một factory constructor rồi.
Ví dụ nhoé:
factory User.fx() { return User(id: 99, name: 'Fx'); }
Trong đó:
User.fx()
là một hàm khởi tạo định danh và kèm theo từ khoáfactory
- Thay vì chuyển tiếp khởi tạo, chúng ta sẽ
return
về đối tượng User. Đây là ý nghĩa chính.
fromJson
Chúng ta sẽ làm 1 ví dụ để có 1 factory constructor làm nhiệm vụ biến đổi JSON thành User. Code tham khảo như sau:
factory User.fromJson(Map<String, Object> json) { final userId = json['id'] as int; final userName = json['name'] as String; return User(id: userId, name: userName); }
Trong đó:
- Kiểu
Map
tương tự Dictionary trong Swift - Bạn sẽ lấy các giá trị ra theo
key
- Tạo một đối tượng User mới từ chúng
Sử dụng như sau:
final map = {'id': 10, 'name': 'Naruto'}; final naruto = User.fromJson(map); print(naruto);
Về Map là gì thì chúng ta sẽ tìm hiểu ở một phần khác nhoé.
Hoặc ta có thể một hàm khởi tạo định danh bình thường như sau mà cũng sinh ra một đối tượng từ JSON.
User.fromJson(Map<String, Object> json) : _id = json['id'] as int, _name = json['name'] as String;
Nhưng mà bạn không nên dùng kiểu này, với factory thì có lợi thế hơn constructor là bạn có thể kiểm tra nhiều giá trị hay nhiều kiểu hơn khi tạo một đối tượng. Ví dụ như tồn tại hay không. Đó là lợi điểm của factory mang lại.
Object
Object là các thể hiện tham chiếu được sinh ra từ lớp và tồn tại trong bộ nhớ. Bạn có thể gán một đối tượng này cho một đối tượng khác. Sẽ tạo thêm một tham chiếu tới vùng nhớ, không tạo thêm một thể hiện mới nào.
Ví dụ:
class MyClass { var myProperty = 1; }
Ta sẽ có 2 đối tượng như sau:
final obj1 = MyClass(); final obj2 = obj1;
Cả 2 đều trỏ tới 1 vùng nhớ, nên một đối tượng này thay đổi giá trị thuộc tính. Thì cũng ảnh hưởng sang đối tượng kia.
Ví dụ:
print(myObject.myProperty); // 1 anotherObject.myProperty = 2; print(myObject.myProperty); // 2
Getters & Setters
Lớp User của chúng ta hiện tại như sau:
class User { // ... final int _id; final String _name; // ... }
Với private cho các thuộc tính, thì bên ngoài sẽ không tài nào truy cập hay thay đổi giá trị của chúng. Tuy nhiên, để lớp chúng ta trở nên thân thiện hơn, thì chúng ta sẽ thêm các Getters & Setters cho Classes. Điều này giúp cho bạn quản lý và xử lý được các giá trị trước khi gán chúng vào thuộc tính.
Ví dụ code như sau:
// properties int _id; String _name; // getter & setter int get id => _id; set id(int value) => _id = value; String get name => _name; set name(String value) => _name = value;
Trong đó:
- Sử dụng
get
cho các Getters &set
cho các Setter =>
dùng để rút gọn function 1 dòng
Ngoài ra, tác dụng lớn nhất của Getters & Setter là xử lý tiền dữ liệu trước khi sử dụng. Bạn sẽ tính toán trước và trả về sau. Ví dụ code sau:
String get name { return _name.toUpperCase(); }
Bạn sẽ trả về name
bằng cách in hoa tất cả kí tự của _name
property trong lớp.
Static members
Chúng ta sẽ tạo nên các thành viên tĩnh với từ khoá là static
cho Classes trong Dart. Các thành viên tĩnh sẽ là:
- Thuộc tính
- Phương thức
Ta có ví dụ như sau:
class SomeClass { static int myProperty = 0; static void myMethod() { print('Hello, Dart!'); } }
Truy cập vào các thuộc tính hay phương thức tĩnh này, thì dùng chính tên lớp để truy cập. Ví dụ cách sử dụng như sau:
final value = SomeClass.myProperty; SomeClass.myMethod();
Với các thành viên tĩnh này, bạn không cần tạo ra các đối tượng của lớp để thực hiện truy cập. Ngoài ra, các biến tĩnh cũng phụ thuộc vào phạm vi của chúng mà sẽ có các tiên gọi khác nhau:
- Biến static thuộc về lớp, gọi là class variables
- Các biến non-static gọi là các instance variables
- Các biến trong một method gọi là local variables
- Các biến ở ngoài class được gọi là global variables
Trong Classes, chúng ta có thể kết hợp cả static
& const
cho các thuộc tính. Ví dụ:
static const _anonymousId = 0;
static const _anonymousName = 'anonymous';
Singleton pattern
Ví dụ và ứng dụng lớn nhất cho hội đồng static
đó chính là Singleton pattern. Một mẫu design pattern được xem là quốc dân trong giới lập trình. Bạn sẽ có một đối tượng dùng cho toàn bộ chương trình và lưu trữ dữ liệu tập trung.
Cách tạo một Singleton cơ bản như sau:
class MySingleton {
MySingleton._();
static final MySingleton instance = MySingleton._();
}
Trong đó, MySingleton._()
là một hàm khởi tạo có định danh ở chế độ riêng tư. Để nhấn mạnh việc nó không thể khởi tạo hay gọi từ bên ngoài. Dấu _
làm cho không thể khởi tạo lớp một cách bình thường. Tuy nhiên, thuộc tính static, chỉ được khởi tạo một lần, cung cấp một tham chiếu đến đối tượng được khởi tạo.
Cách sử dụng thì như sau:
final mySingleton = MySingleton.instance;
mySingleton
sẽ là tham chiếu tới biến static
kia thôi, không có thể hiện nào mới tạo ra.
Hoặc bạn có một cách khác để tạo Singleton theo kiểu factory
. Xem ví dụ nha:
class MySingleton {
MySingleton._();
static final MySingleton _instance = MySingleton._();
factory MySingleton() => _instance;
}
Trong đó, factory
không thực sự tạo ra một đối tượng, nó chỉ trả về _instance
. Hay trả về chính đối tượng Singleton mà thôi. Xem luôn ví dụ cách dùng của nó thì như sau:
final mySingleton = MySingleton();
Bây giờ, nó trông ổn rồi chứ. Ahihi!
Static methods
Cũng có nhiều điều thú vị về các phương thức tĩnh trong các Classes của ngôn ngữ Dart. Cũng tương tự như với các biến, chúng ta vẫn dùng từ khoá static
trước khai báo các function. Và dùng tên của lớp đệ gọi tới các phương thức đó.
Ví dụ code cho một phương thức tĩnh như sau:
static User fromJson(Map<String, Object> json) {
final userId = json['id'] as int;
final userName = json['name'] as String;
return User(id: userId, name: userName);
}
final map = {'id': 10, 'name': 'Sasuke'};
final sasuke = User.fromJson(map);
Bạn chỉ cần chú ý vào static
và User.fromJson(map);
mà thôi. Nó cũng tương tự như với factory
.
Tuy nhiên, về mặt ý nghĩa, thì ta sẽ có 2 công dụng cho các phương thức tĩnh:
- Utility methods : tạo ra các hàm phụ trợ cho lớp. Hoặc nhóm các hàm phụ trợ lại vào 1 trong lớp. Ví dụ, nhiều bạn thích có một class gọi là Helper hay Common để chứa các hàm hay dùng như: trim(), convert ….
- Creating new objects : các phương thức sinh ra các đối tượng mới. Thường sẽ là các dummy data là chính. Vì chúng ta đã có các hàm constructors và factory làm việc này khá tốt rồi. Ví dụ như ở trên kìa.
Static Method vs. Factory Constructor
- Thứ được trả về:
- Factory sẽ trả về một đối tượng của lớp
- Static có thể trả về bất cứ thứ gì. Từ các đối tượng tới các loại dữ liệu khác nhau
- Đặt tên
- Factory thì sẽ không thoái mái trong việc đặc tên cho phương thức. Khi phải có tên Class nhằm tuân thủ quy định này. Và trông giống như một hàm khởi tạo bình thường.
- Static thì bạn cứ vô tư đi nhoé
- Const (có hữu ý với Flutter sau này nhoé)
- Factory có thể là một
const
nếu nó forwarding constructor - Static thì không thể làm được này
- Factory có thể là một
Bài dài và mệt quá xá. Bạn đọc được tới đây cũng là một kì tích rồi. Cảm ơn bạn đã đọc và chúc bạn thành công. Ahuhu!
Tóm tắt
Đúng là toàn tư về Classes trong Dart, tuy nhiên chúng mới là một nữa câu chuyện mà thôi. Bạn sẽ còn gặp lại chúng trong phần hướng đối tượng. Khi chúng ta tiếp tục khám phá thêm các tính chất khác của các Classes trong Dart và lập trình hướng đối tượng. Để chốt lại thì bạn chỉ cần nhớ tới ví dụ này là đủ:
const User(this.id, this.name);
Đó bao gồm:
- non-forwarding (không chuyển tiếp)
- unnamed (không tên tham số)
- generative (khởi tạo bình thường)
- const constructor (khởi tạo hằng số)
Tóm tắt, khi khai báo một Classes nhoé.
- Bắt đầu khai báo
class User {
String name;
}
- Cung cấp giá trị cho thuộc tính
class User {
String name = 'anonymous';
}
- Sử dụng hàm khởi tạo
class User {
User(this.name);
String name;
}
- Sử dụng initializer list cho private properties
class User {
User(String name)
: _name = name;
String _name;
}
- Default value cho hàm khởi tạo theo 2 cách
class User {
User([this.name = 'anonymous']);
String name;
}
// or
class User {
User({this.name = 'anonymous'});
String name;
}
- Yêu cầu có named parameters cho hàm khởi tạo
class User {
User({required this.name});
String name;
}
Tạm kết
- Tìm hiểu về kiểu dữ liệu Classes trong Dart
- Các vấn đề liên quan tới lớp
- Methods & Properties trong lớp
- Các hàm khởi tạo & Factory, Constants Constructor
- Tạo đối tượng của một lớp
- Setter & Getter
- Static members
- …
Okay! Tới đây, mình xin kết thúc bài viết về Classes & Objects trong series Dart Tour. Nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
Cảm ơn bạn đã đọc bài viết này!
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)