Kiểu liệt kê (enum)

####Chào các bạn đang theo dõi khóa học lập trình trực tuyến ngôn ngữ C++.

Trong bài học ngày hôm nay, chúng ta sẽ cùng tìm hiểu về từ khóa enum, cách sử dụng, và quan trọng nhất là tại sao chúng ta sử dụng enum trong ngôn ngữ C++.

###Enumarated types

Kiểu liệt kê là một trong số các kiểu dữ liệu do người lập trình tự định nghĩa. Tại sao chúng ta cần định nghĩa kiểu dữ liệu mới? Trong quá trình lập trình, những kiểu dữ liệu được định nghĩa sẵn trong ngôn ngữ lập trình có thể không mang lại ý nghĩa phù hợp. Ví dụ mình muốn sử dụng các giá trị từ 1 đến 7 để đại diện cho 7 ngày trong tuần (1 đại diện cho ngày chủ nhật, 7 đại diện cho thứ 7), như vậy mình cần ít nhất là 7 biến để lưu trữ các giá trị này:

const int SUNDAY = 1;
const int MONDAY = 2;
const int TUESDAY = 3;
const int WEDNESDAY = 4;
const int THURSDAY = 5;
const int FRIDAY = 6;
const int SATURDAY = 7;

Mình không sử dụng mảng một chiều trong trường hợp này vì:

int DAYS_OF_WEEK[7] = { 1, 2, 3, 4, 5, 6, 7 };

Những con số cụ thể không mang lại ý nghĩa cho người đọc mã nguồn chương trình. Việc sử dụng tên của các biến hằng số sẽ giúp chương trình chúng ta rõ ràng hơn.

Nhưng việc khai báo các hằng số như trên vẫn có một số nhược điểm:

  • Có thể khai báo thiếu sót một vài giá trị khi danh sách các hằng số là quá nhiều.

  • Có thể khai báo không theo một quy luật (hay thứ tự) nhất định khiến chúng ta khó tìm trong chương trình. Ví dụ:

const int WEDNESDAY = 4;
const int SUNDAY = 1;
const int TUESDAY = 3;
const int FRIDAY = 6;
const int MONDAY = 2;
const int SATURDAY = 7;
const int THURSDAY = 5;
  • Có một số hằng số không liên quan đến nhau nhưng được khai báo gần nhau khiến chúng ta dễ rối. Ví dụ:
const float PI = 3.14;
const float ACCELERATION_OF_GRAVITY = 9.8;
const int MAX_SIZE_OF_ARRAY = 255;
//..............

Như vậy, muốn khắc phục một số nhược điểm trên, chúng ta cần tìm cách để tập hợp các hằng số có ý nghĩa tương đương nhau thành những nhóm hằng số riêng biệt. Kiểu liệt kê sẽ giúp chúng ta thực hiện điều này.

#####Công dụng của kiểu liệt kê

Như mình đã trình bày ở trên, kiểu liệt kê có tác dụng giúp thay thế các con số (giá trị cụ thể) bằng những cái tên có ý nghĩa, và nó còn giúp chúng ta tập hợp các giá trị có ý nghĩa liên quan với nhau thành từng nhóm. Mỗi nhóm hằng số này khi đưa vào kiểu liệt kê sẽ trở thành một kiểu dữ liệu (người ta thường gọi enumeration là một kiểu dữ liệu trong C++ vì nó có cách khai báo tương tự như khai báo biến, chứ mình thấy nó giống một group của các giá trị hơn).

#####Cú pháp khai báo kiểu liệt kê

Để định nghĩa một kiểu liệt kê mới, chúng ta sử dụng từ khóa enum theo cấu trúc sau:

enum <name_of_enumeration>
{
	//list all of values inside this block
	//each enumerator is separated by a comma, not a semicolon
};

Việc khai báo kiểu dữ liệu mới (như kiểu enum) không yêu cầu chương trình cấp phát bộ nhớ, lúc nào chúng ta sử dụng kiểu enum vừa đã được định nghĩa để tạo ra biến kiểu enum thì chương trình mới cấp phát bộ nhớ.

Mỗi giá trị trong block của kiểu enum cách nhau bởi một dấu phẩy (đối với giá trị cuối cùng thì không cần sử dụng dấu phẩy).

#####Khai báo kiểu liệt kê

Mình đã nói về lý thuyết của kiểu enum (kiểu liệt kê) xong, chắc bây giờ các bạn cũng đang tò mò muốn biết cuối cùng thì khai báo và sử dụng nó như thế nào. Dưới đây là một ví dụ:

enum DaysOfWeek
{
	SUNDAY,
	MONDAY,
	TUESDAY,
	WEDNESDAY,
	THURSDAY,
	FRIDAY,
	SATURDAY
};

Như các bạn thấy, sau khi định nghĩa một kiểu enum xong thì kết thúc nó là một dấu chấm phẩy, vì đây cũng là một câu lệnh. Về mặt cơ bản, chúng ta phải đặt toàn bộ câu lệnh trên cùng một dòng:

enum DaysOfWeek { SUNDAY,	MONDAY,	TUESDAY, WEDNESDAY,	THURSDAY, FRIDAY, SATURDAY };

Nhưng compiler vẫn hiểu được một câu lệnh nằm trên nhiều dòng nên mình chọn cách viết ở trên (tách thành nhiều dòng) để phần định nghĩa của mình rõ ràng hơn.

Như vậy là chúng ta đã có một kiểu dữ liệu mới cho chương trình. Các bạn có thể gọi DaysOfWeek là một kiểu dữ liệu (kiểu enum hay kiểu liệt kê) hoặc có thể gọi là tên của một nhóm các giá trị cũng như chúng ta hay đi chơi với bạn bè theo nhóm nhỏ rồi đặt tên cho nhóm vậy.

Trong một chương trình, chúng ta có thể có nhiều khai báo kiểu enum khác nhau. Ví dụ mình khai báo thêm vài kiểu enum khác:

enum DaysOfWeek
{
	SUNDAY,
	MONDAY,
	TUESDAY,
	WEDNESDAY,
	THURSDAY,
	FRIDAY,
	SATURDAY
};

enum Color
{
	RED,
	GREEN,
	BLUE,
	WHITE
};

enum Animal
{
	CAT,
	DOG,
	HORSE,
	MONKEY,
	CHICKEN
};

Như vậy là chương trình của chúng ta có 3 kiểu dữ liệu mới (3 nhóm giá trị mới), mỗi kiểu enum này hoàn toàn không liên quan gì đến nhau, chỉ có các giá trị bên trong mỗi kiểu enum mới có liên quan đến nhau về mặt ý nghĩa.

Nhưng có thấy giá trị nào đâu?

Khi nhìn vào bên trong khối lệnh định nghĩa của kiểu enum có tên Color, chúng ta chỉ thấy những những danh từ như RED, GREEN, BLUE… mà không hề thấy những con số. Thực chất, những danh từ này đã được gắn cho một giá trị cụ thể, và những cái danh từ mà chúng ta nhìn thấy sẽ đại diện cho những giá trị đó. Sử dụng những danh từ để thay thế cho những con số sẽ giúp người đọc chương trình dễ hiểu hơn (chứ không giúp chương trình chạy nhanh hơn).

#####Enumerator values

Bây giờ mình sẽ làm một chương trình mẫu để show cho các bạn xem những giá trị được đặt trong block của một kiểu enum mình tự định nghĩa:

int main()	{

	enum Alphabet
	{
		LETTER_A,
		LETTER_B,
		LETTER_C,
		LETTER_D,
		LETTER_E
	};

	cout << LETTER_A << endl;
	cout << LETTER_B << endl;
	cout << LETTER_C << endl;
	cout << LETTER_D << endl;
	cout << LETTER_E << endl;

	return 0;
}

Khi mình chạy chương trình, kết quả xuất hiện trên console là:

Như vậy, không cần chúng ta trực tiếp gán giá trị cho các tên hằng số, compiler đã tự động khởi tạo giá trị cho chúng, bắt đầu với giá trị 0 và tăng dần. Các bạn cũng đã thấy rằng, sau khi định nghĩa xong 1 kiểu enum thì chúng ta có thể sử dụng các tên gọi bên trong enum như những hằng số. Vì những giá trị hằng số này là giá trị kiểu integer (int), nên chúng ta cũng có thể gán chúng cho những biến kiểu int khác. Ví dụ:

enum Alphabet
{
	LETTER_A,
	LETTER_B,
	LETTER_C,
	LETTER_D,
	LETTER_E
};

int iValue = LETTER_A;

Bên cạnh việc tự động gán giá trị cho từng phần tử được liệt kê, chúng ta cũng có thể chủ động thay đổi giá trị cho chúng (nhưng chỉ có thể thay đổi giá trị trong phần khai báo), một enum sau khi đã định nghĩa xong thì không thể thay đổi những giá trị của danh sách các phần tử nữa.

enum Direction
{
	UP = 1,   //assigned 1 by programmer
	DOWN = 3, //assigned 3 by programmer
	LEFT,     //assigned 4 by compiler
	RIGHT     //assigned 5 by compiler
};

cout << UP << " " << DOWN << " " << LEFT << " " << RIGHT << endl;

Đoạn chương trình này sẽ in ra:

1 3 4 5

Như vậy, compiler sẽ tự động gán giá trị cho các phần tử không được khởi tạo giá trị. Ngoại trừ phần tử đầu tiên trong enum, những hằng số khác sẽ được gán giá trị bằng phần tử trước nó cộng thêm 1.

Lưu ý: những hằng số trong cùng một enum có thể có cùng giá trị với nhau.

Best practice: Don’t assign specific values to your enumerators.

Rule: Don’t assign the same value to two enumerators in the same enumeration unless there’s a very good reason.

#####Sử dụng kiểu enum đã định nghĩa như một kiểu dữ liệu thông thường

Như mình trình bày ở trên, từ khóa enum trong C++ giúp chúng ta định nghĩa một kiểu dữ liệu mới cho chương trình. Tuy nó chỉ là tập hợp danh sách các hằng số có ý nghĩa tương quan với nhau, nhưng bản chất nó vẫn là một kiểu dữ liệu (kiểu liệt kê) nên chúng ta có thể sử dụng chúng để tạo ra các biến. Ví dụ:

enum Color
{
    COLOR_BLACK,
    COLOR_RED, 
    COLOR_BLUE, 
    COLOR_GREEN, 
    COLOR_WHITE,
    COLOR_CYAN,
    COLOR_YELLOW
};

Color backgroundColor;

Bây giờ, chúng ta đã có một biến kiểu Color. Biến backgroundColor chỉ có tác dụng lưu trữ giá trị của một trong số tất cả các hằng số đã được liệt kê bên trong kiểu Color. Việc thực hiện gán các giá trị khác kiểu Color sẽ gây ra lỗi về mặt cú pháp.

Color backgroundColor = 5; //error

Bây giờ mình sẽ chọn ra bất kì một hằng số thuộc kiểu Color để gán cho biến backgroundColor.

Color backgroundColor = COLOR_GREEN; 

Các bạn cần lưu ý rằng, biến kiểu enum chỉ có thể được gán giá trị là một trong số các hằng đã khai báo bên trong kiểu dữ liệu của chính nó, không thể sử dụng hằng của kiểu enum khác. Ví dụ:

enum Test
{
	TEST1,
	TEST2,
	TEST3
};

enum Color
{
    COLOR_BLACK,
    COLOR_RED, 
    COLOR_BLUE, 
    COLOR_GREEN, 
    COLOR_WHITE,
    COLOR_CYAN,
    COLOR_YELLOW
};

Color backgroundColor = TEST1; //this line makes an error

Compiler sẽ thông báo lỗi: “a value of type Test cannot be used to initialize an entity of type Color”.

#####Những ví dụ về việc sử dụng enum

Sau khi đã gán giá trị cho biến kiểu enum, biến này sẽ mang giá trị là một số nguyên, và chúng ta có thể sử dụng biến này để in ra, tính toán, so sánh, truyền vào hàm theo kiểu giá trị, … và còn nhiều mục đích khác.

Trên thực tế, chúng ta thường sử dụng kiểu enum để đưa ra lựa chọn hàm hoặc phương thức để thực thi. Ví dụ:

enum ItemTypes
{
	LAPTOP,
	DESKTOP,
	MOBILE,
	NETWORK
};

void showAllLaptop() {

}

void showAllDesktop() {

}

void showAllMobile() {

}

void showAllNetworkItem() {

}

void showProducts(ItemTypes type)	{

	switch (type)
	{
	case LAPTOP:
		showAllLaptop();
		break;

	case DESKTOP:
		showAllDesktop();
		break;

	case MOBILE:
		showAllMobile();
		break;

	case NETWORK:
		showAllNetworkItem();
		break;

	default:
		break;
	}
}

int main()	{

	ItemTypes type = LAPTOP;
	showProducts(type);

	return 0;
}

Vì biến kiểu enum lưu trữ giá trị số nguyên, nên mình có thể đưa vào làm biểu thức mệnh đề cho câu lệnh switch-case. Dựa trên loại Item mà người dùng chọn, ứng dụng của chúng ta sẽ trả về thông tin của toàn bộ sản phẩm hiện có trong kho hàng.

Thông thường, sau mỗi nhãn case chúng ta sẽ đặt một giá trị số nguyên ứng với mỗi trường hợp, nhưng bây giờ chúng ta có thể thay thế những con số bằng các định danh của enum. Trước đây khi làm việc với câu lệnh switch-case, chúng ta có thể bỏ sót một số trường hợp cần xem xét nếu số lượng các trường hợp là quá nhiều. Nhưng khi sử dụng Visual studio 2015 và kiểu enum, chúng ta sẽ tránh được sự thiếu sót này. Dưới đây là cách mà Visual studio 2015 hổ trợ cho kiểu enum:

Trong hàm showProducts, mình gõ câu lệnh switch nhưng sử dụng gợi ý của Visual studio.

Sau khi chọn vào gợi ý của lệnh switch, IDE phát sinh code cho chúng ta như sau:

Lúc này, các bạn chỉ cần gõ thay thế từ switch_on bằng tên của biến enum rồi nhấn phím mũi tên sang phải, IDE sẽ liệt kê tất cả các case ứng với tất cả giá trị được định nghĩa bên trong kiểu của biến enum đó.

Đây cũng là một ưu điểm của Visual studio 2015. Bây giờ chúng ta trở lại với bài học.

Thêm một ví dụ khác cũng có thể có ích. Xét đoạn chương trình dưới đây:

bool initialize() {

	//init all component
	//if something wrong, return false
	return false;
}

bool loadResource() {

	//load data from file
	//if something wrong, return false
	return false;
}

int main()	{

	if (initialize() == false) {
		return -1;
	}
	
	if (loadResource() == false) {
		return -2;
	}

	bool isRunning = true;
	while (isRunning) {

		//Application event loop
		return -3;
	}

	return 0;
}

Đoạn chương trình này sử dụng những giá trị âm để biểu diễn các lỗi có thể xảy ra. Mỗi chức năng trong chương trình gây ra lỗi thì chương trình sẽ trả về một giá trị khác nhau. Tuy nhiên, những con số cụ thể thường không mang nhiều ý nghĩa, do đó chúng ta thay thế chúng bằng kiểu enum để code của chúng ta rõ ràng hơn.

enum ReturnValue
{
	SUCCESS = 0,
	INITIALIZE_ERROR = -1,
	LOAD_RESOURCE_ERROR = -2,
	RUN_TIME_ERROR = -3
};

int main()	{

	if (initialize() == false) {
		return INITIALIZE_ERROR;
	}
	
	if (loadResource() == false) {
		return LOAD_RESOURCE_ERROR;
	}

	bool isRunning = true;
	while (isRunning) {

		//Application event loop
		return RUN_TIME_ERROR;
	}

	return SUCCESS;
}

Khi chúng ta viết một game với nhân vật có nhiều trạng thái khác nhau, mỗi trạng thái sẽ khiến nhân vật phản ứng bằng một hành động tương ứng, chúng ta có thể làm như sau:

enum BossState
{
	IDLING,
	RUNNING,
	JUMPING,
	DYING
};

BossState state;

void initBoss()
{
	//init something
	state = IDLING;
}

void attack()
{
	//............
}

void activated()	
{
	//............
}

void updateAnimation(BossState state)	
{
	switch(state)
	{
	case IDLING:
		standStill();
		break;
		
	case RUNNING:
		setRunningAnimation();
		break;
		
	case JUMPING:
		setJumpingAnimation();
		break;
		
	case DYING:
		setDyingAnimation();
		break;
		
	default;
		break;
	}
}

Như các bạn thấy, kiểu enum được áp dụng khá thường xuyên trong thực tế. Trên đây chỉ là một vài ví dụ minh họa cho việc sử dụng kiểu enum thường gặp.

#####Phạm vi sử dụng kiểu enum

Trong một file chương trình, phạm vi sử dụng của một khai báo enum cũng tương tự như phạm vi sử dụng khi khai báo biến. Nếu chúng ta muốn sử dụng kiểu enum tại tất cả các khối lệnh trong chương trình, chúng ta nên khai báo kiểu enum phía trên cùng của các khối lệnh (giống như khai báo biến toàn cục). Ví dụ:

enum ItemTypes
{
	LAPTOP,
	DESKTOP,
	MOBILE,
	NETWORK
};

void foo()
{
	cout << MOBILE << endl;	
}

int main()	{
	
	cout << LAPTOP << endl;
	cout << DESKTOP << endl;

	return 0;
}

Vì kiểu ItemTypes khai báo bên ngoài các khối lệnh, nên chúng ta có thể sử dụng tại các khối lệnh của các hàm bên dưới. Một trường hợp khác, khi mà enum chỉ được định nghĩa trong một khối lệnh của hàm nào đó:

void foo()
{
	enum ItemTypes
	{
		LAPTOP,
		DESKTOP,
		MOBILE,
		NETWORK
	};

	cout << MOBILE << endl;	
}

int main()	{
	
	cout << DESKTOP << endl; //error

	return 0;
}

Compiler sẽ thông báo lỗi DESKTOP trong hàm main chưa được định nghĩa, vì chúng ta không thể truy xuất các giá trị của kiểu ItemTypes trong hàm foo từ hàm main được.

Thông thường, chúng ta nên định nghĩa kiểu dữ liệu enum bên ngoài các khối lệnh, vì việc khai báo kiểu enum mới không yêu cầu cấp phát bộ nhớ nên không hề ảnh hưởng đến tài nguyên của hệ thống.

Việc định nghĩa kiểu enum bên ngoài các khối lệnh không những có thể sử dụng bất cứ đâu trong chương trình mà còn có thể sử dụng tại file chương trình khác của dự án. Chúng ta sẽ tìm hiểu về cách quản lý dự án với nhiều file C++ trong các bài học sau.

###Enum class

Việc sử dụng kiểu enum tự định nghĩa đã giúp chúng ta tổ chức chương trình rõ ràng, dễ đọc hơn. Nhưng khi một chương trình có nhiều enum được định nghĩa, sẽ có nhiều giá trị trùng nhau giữa các enum khác nhau, và nó có thể làm cho chương trình không có ý nghĩa gì mặc dù đã sử dụng kiểu enum.

enum Color
{
	RED,
	GREEN,
	BLUE
};

enum Fruit
{
	APPLE,
	BANANA
};

int main() {

	Color color = GREEN;
	Fruit fruit = BANANA;

	if (color == fruit)
		cout << "It's the same" << endl;
	else
		cout << "It's not the same" << endl;

	return 0;
}

Trong hàm main, compiler sẽ so sánh color và fruit như 2 giá trị số nguyên, khi Color::GREEN và Fruit::BANANA đều được gán giá trị là 2 thì color và fruit được compiler cho là bằng nhau. Đây là một trường hợp ngoài ý muốn khi sử dụng kiểu enum. Điều này xảy ra vì enum Color và enum Fruit có thể truy cập đồng thời trong cùng một khối lệnh.

Điều chúng ta mong muốn lúc này là biến của kiểu Color chỉ được so sánh với giá trị trong enum Color, và biến kiểu Fruit chỉ có thể so sánh với giá trị trong enum Fruit. Chuẩn C++11 đã hổ trợ cho chúng ta một khái niệm enum mới, đó là enum class (có thể gọi là scoped enumeration). Mình sử dụng lại ví dụ trên nhưng thay thế enum bằng enum class:

Như các bạn thấy, compiler đã thông báo lỗi ở câu lệnh if, đồng thời cũng thông báo lỗi ở 2 câu lệnh gán giá trị cho 2 biến color và fruit. Vì việc sử dụng enum class cần phải cung cấp thêm cho compiler biết là giá trị của enum đó được định nghĩa bên trong enum nào, phải cung cấp cho compiler một cái tên của kiểu enum đứng trước giá trị chúng ta muốn sử dụng (ví dụ Color::RED).

Các bạn đã được biết toán tử “::” là toán tử chỉ phạm vi truy cập. Khi mình sử dụng Color::RED có nghĩa là RED được định nghĩa bên trong khối lệnh của Color (không cần biết Color là gì, chúng ta chỉ biết RED nằm trong Color). Việc sử dụng enum class đòi hỏi lập trình viên phải chỉ ra đích danh của kiểu enum chứa giá trị cần sử dụng. Mình sửa lại đoạn chương trình trên như sau:

int main() {
	Color color = Color::GREEN;
	Fruit fruit = Fruit::BANANA;

	if (color == fruit)
		cout << "It's the same" << endl;
	else
		cout << "It's not the same" << endl;

	return 0;
}

Đến đây, 2 phép gán đã có thể thực thi được nhưng câu lệnh if thì còn lỗi. Đó là do compiler đã phát hiện ra 2 biến này có 2 kiểu dữ liệu khác nhau, 1 cái là của kiểu Color trong khi cái kia là kiểu Fruit. Compiler không chấp nhận điều này nên đưa ra thông báo lỗi.

Thậm chí khi các bạn sử dụng biến kiểu enum class để so sánh với một số nguyên cũng không được cho phép.

if (color == 2) // error
{
	
}	

Chỉ có một cách duy nhất để sử dụng enum class là sử dụng giá trị trong chính enum của nó.

if(color == Color::GREEN)
{
	//OK
}

Nếu các bạn sử dụng compiler C++11 trở lên, không có lý do nào mà các bạn sử dụng kiểu enum thông thường thay vì sử dụng enum class.


###Tổng kết

Trong bài học này, chúng ta đã tìm hiểu một số khái niệm về kiểu dữ liệu tự định nghĩa bằng từ khóa enum:

  • Cú pháp khai báo, giá trị khởi tạo cho các thành phần của kiểu enum.
  • Một số cách sử dụng enum thường gặp.
  • Phân biệt enum và enum class trong chuẩn C++11.
  • Visual studio hổ trợ cho chúng ta liệt kê tất cả các giá trị cần so sánh trong mệnh đề switch-case. Điều này có nghĩa chúng ta nên sử dụng switch-case thay vì if-else khi cần phân loại biến kiểu enum.

Sử dụng kiểu enum không làm cho chương trình của các bạn chạy nhanh hơn, cũng không làm cho chương trình của các bạn ngắn gọn hơn, nó chỉ có tác dụng duy nhất là làm cho chương trình của các bạn rõ ràng hơn.


Hẹn gặp lại các bạn trong bài học tiếp theo trong khóa học lập trình C++ hướng thực hành.

Mọi ý kiến đóng góp hoặc thắc mắc có thể đặt câu hỏi trực tiếp tại diễn đàn.

www.daynhauhoc.com

6 Likes
83% thành viên diễn đàn không hỏi bài tập, còn bạn thì sao?