Type system trong ngôn ngữ Dart
I. Lời mở đầu
Ngôn ngữ Dart kết hợp kết hợp việc kiểm tra kiểu dữ liệu static
và kiểu dữ liệu runtime
để đảm bảo giá trị của một biến luôn luôn phù hợp với kiểu dữ liệu static
của biến đó, điều này đôi khi được gọi bằng thuật ngữ sound typing
. Trong ngôn ngữ Dart, về bản chất một biến bắt buộc phải có kiểu dữ liệu, nhưng lập trình viên đôi khi không nhất thiết phải dùng type annotations
để chỉ ra kiểu dữ liệu của biến đó vì Dart có thể tự suy luận ra kiểu dữ liệu. Ta gọi kiểu dữ liệu được suy ra đó là type inference
.
Một trong những lợi ích của việc kiểm tra kiểu dữ liệu đó là khả năng phát hiển bug lúc compile time
sử dụng static analyzer
.
void printInts(List<int> a) => print(a);
void main() {
var list = [];
list.add(1);
list.add("2");
printInts(list);
}
// Unhandled exception: type 'List<dynamic>' is not a subtype of type 'List<int>'
Ở đoạn code phía trên, ta thấy biến list
không được chỉ ra kiểu dữ liệu cụ thể nên type inference
của nó sẽ là List<dynamic>
. Việc add các giá trị 1
và "2"
vào là hoàn toàn hợp lệ. Nhưng đến khi truyền list
vào hàm printInts
thì sẽ bị lỗi vì hàm printInts
trong muốn nhận vào tham số có kiểu dữ liệu là List<int>
. Nhưng biến list
lại có kiểu dữ liệu là List<dynamic>
tức không phải là List<int>
và cũng không phải là subtype
của List<int>
.
Để sửa lỗi, ta sẽ sử code lại thành như sau:
void printInts(List<int> a) => print(a);
void main() {
var list = <int>[];
list.add(1);
list.add(2);
printInts(list);
}
II. Soundness là gì?
Soundness
là 1 khái niệm nhằm thể hiện rằng mong muốn đảm bảo có chương trình không thể rơi vào trạng thái chứa các giá trị không hợp lệ. Một sound type system
có nghĩa là chương trình không bao giờ có trạng thái mà chứ một giá trị không phù hợp với static type
của nó. Ví dụ như một expression
thể hiện static type
của nó là String
thì tại thời điểm runtime
ta đảm bảo nó chỉ có thể thể vào giá trị là String
.
Type system
của Dart cũng giống như type system
trong các ngôn ngữ như Java, C#, .. chúng là sound
. Điều này đảm bảo rằng ta kiểm tra cả dữ liệu kiểu static
và kiểu runtime
. Ví dụ như việc gán một String
cho một int
là một compile-time error
.
III. Lợi ích của soundness là gì?
- Phát hiện bug liên quan đến kiểu dữ liệu ở thời điểm
compile time
. Mộtsound type system
bắt buộc code không được mơ hồ về kiểu dữ liệu của nó. Nên một số bug mà có phát hiện ởruntime
có thể được phát hiện từ sớm về dễ dàng ở thời điểmcompile time
. - Code dễ đọc. Code sẽ trở nên dễ đọc vì ta có thể nhìn thấy kiểu dữ liệu của nó là gì.
- Code dễ được bảo trì. Khi ta sửa 1 phần code nhỏ.
type system
cá thể cảnh báo rằng việc thay đổi đó có thể làm ảnh hưởng hoặc hư hại để những phần code khác. - Thuận lợi hơn cho
ahead of time (AOT) compilation
. Mặc dùAOT compilation
vẫn khả thi khi không có kiểu dữ liệu, nhưng phần code đượcgenerate
ra sẽ không tối ưu bằng code đượcgenerate
ra khi có kiểu dữ liệu.
IV. Một số tips để không bị lỗi ở bước static analysis
- Sử dụng
sound return type
khi override phương thức:
Kiểu dữ liệu trả về của phương thức trong subclass
phải cùng kiểu hoặc là subtype
của phương thức tương ứng trong superclass
. Ta xét phương thức getter
trong Animal
class bên dưới:
class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}
Phương thức getter cha trả về một Animal
. Trong lớp con HoneyBadger
, bạn có thể thay thế kiểu trả về getter bằng HoneyBadger
(hoặc bất kỳ kiểu con nào khác của Animal
), nhưng không được phép sử dụng loại không liên quan.
// static analysic: success
class HoneyBadger extends Animal {
void chase(Animal a) { ... }
HoneyBadger get parent => ...
}
// static analysic: error/warning
class HoneyBadger extends Animal {
void chase(Animal a) { ... }
Root get parent => ...
}
- Sử dụng
sound parameter type
khi override phương thức:
Tham số của phương thức được bị overridden
phải có cùng loại hoặc là supertype
của tham số tương ứng trong superclass
.
Xét phương thức chase (Animal)
bên dưới:
class Animal {
void chase(Animal a) { ... }
Animal get parent => ...
}
- Không sử dụng
dynamic list
dưới dạngtyped list
:
Dynamic list
hữu dùng khi bạn muốn có một danh sách với các loại khác nhau trong đó. Tuy nhiên ta không thể sử dụng dynamic list
như một typed list
Đoạn code sau tạo một Dynamic list
của Dog
và gán nó vào danh sách kiểu Cat
.
// static analysic: error/warning
class Cat extends Animal { ... }
class Dog extends Animal { ... }
void main() {
List<Cat> foo = <dynamic>[Dog()]; // Error
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // OK
}
V. Runtime checks
Các công cụ kiểm tra kiểu dữ liệu tại thời điểm runtime
như Dart VM
và dartdevc
có thể bắt được lỗi mà analyzer
không bắt được.
// runtime: error
void main() {
List<Animal> animals = [Dog()];
List<Cat> cats = animals;
}
VI. Type inference
Analyzer
có thể suy ra các kiểu cho các trường, phương thức, biến cục bộ và hầu hết các generic type argument
. Khi analyzer
không có đủ thông tin để suy ra một loại cụ thể, nó sẽ sử dụng kiểu dynamic
.
Sau đây là một ví dụ thể hiện cách type inference
làm việc với generic
. Trong ví dụ này, biến arguments
có giá trị là map
với key kiểu String
và với value
có nhiều kiểu khác nhau. Nếu ta ghi ra kiểu dữ liệu, code được viết như bên dưới:
Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};
Trường hợp ta không muốn phải ghi rõ kiểu dữ liệu mà muốn Dart hỗ trợ ta tự suy ra kiểu dữ liệu thì code sẽ như bên dưới:
var arguments = {'argA': 'hello', 'argB': 42}; // Map<String, Object>
- Field and method inference
Một trường hoặc phương thức không có kiểu dữ liệu được chỉ định cụ thể và override
trường hoặc phương thức của class cha sẽ kế thừa kiểu dữ liệu tương ứng từ phương thức hoặc trường của class cha
Một trường không có loại khai báo hoặc kế thừa nhưng được khai báo với giá trị ban đầu, sẽ có loại được suy ra dựa trên giá trị ban đầu đó.
- Static field inference
Static field
và biến sẽ nhận kiểu dữ liệu từ initializer
của chúng.
- Local variable inference
Kiểu dữ liệu của biến cục bộ được suy ra từ initializer
(nếu có). Các phép gán sau đó sẽ không được tính tới. Điều này có nghĩa là nếu như ta biết trước kiểu dữ liệu được suy ra không phù hợp, thì ngay từ đầu ta chỉ định rõ kiểu dữ liệu chứ không phó thác cho Dart làm.
// static analysic: error/warning
var x = 3; // x is inferred as an int
x = 4.0;
// static analysic: success
num y = 3; // a num can be double or int
y = 4.0;
- Type argument inference
// Inferred as if you wrote <int>[].
List<int> listOfInt = [];
// Inferred as if you wrote <double>[3.0].
var listOfDouble = [3.0];
// Inferred as Iterable<int>
var ints = listOfDouble.map((x) => x.toInt());
Trong ví dụ trên x
có kiểu được suy ra là double
và ints
có kiểu dữ liệu được suy ra là Iterable<int>
VII. Substituting types
Khi override
một phương thức, ta thay thế một số thứ (trong phương thức cũ) thành một thứ khác (trong phương thức mới), tương tự như vậy khi ta truyền tham số cho một hàm, ta thay một thứ có kiểu dữ liệu được khai báo trong định nghĩa hàm bằng 1 thứ có kiểu dữ liệu (có thể khác) tương ứng với giá trị của tham số mà ta truyền vào. Câu hỏi là đặt ra là việc thay thế như vậy khi nào đúng khi nào sai? Kiểu dự liệu nào có thể thay thế cho kiểu dữ liệu nào?
Khi bàn về vấn đề trên, ta thường sẽ sử dụng 2 thuật ngữ là consumer
và producer
. Consumer
là cái mà nhận kiểu dữ liệu và producer
là cái mà tạo ra kiểu dữ liệu. Nguyên tắc là ta có thể thay kiểu dữ liệu của comsumer
bằng supertype
của nó, và có thể thay kiểu dữ liệu của producer
bằng subtype
của nó.
Xét sơ đồ kế thừa bên trên, ta gán 1 phép gán đơn giản như sau:
Cat c = Cat();
c
sẽ đóng vai trò là consumer
và Cat()
đóng vai trò là producer
.
// static analysic: success
Animal c = Cat();
// static analysic: error/warning
MaineCoon c = Cat();
Tương tự ta có ví dụ như bên dưới:
// static analysic: success
List<Cat> myCats = List<MaineCoon>();
Chuyện gì sẽ xảy ra nên ta gán List<Animal>
cho List<Cat>
?
// static analysic: success
List<Cat> myCats = List<MaineCoon>();
Ta thấy vẫn pass được static analysic
vì nó được dịch thành code như bên dưới:
List<Cat> myCats = List<Animal>() as List<Cat>;
Tuy nhiên đoạn code trên có thể bị fail tại thời điểm runtime
. Để chặn việc ép kiểu ngầm ta có thể setting bằng implicit-casts