Menu

Enum trong C#: giá trị, ToString, Parse, Flags và duyệt enum

Cách enum hoạt động trong C#: khai báo các hằng số có tên, giá trị số nguyên bên dưới và ép kiểu, chuyển enum sang chuỗi và chuỗi sang enum bằng Parse và TryParse, liệt kê mọi giá trị, [Flags] với toán tử bit và HasFlag, dùng switch với enum và xử lý giá trị không xác định.

Trang này có trình soạn thảo chạy trực tiếp. Sửa, chạy và xem đầu ra ngay lập tức.

Enum (enumeration, kiểu liệt kê) là một kiểu dữ liệu có giá trị thuộc một tập cố định các hằng số có tên: trạng thái đơn hàng, các ngày trong tuần, mức độ log. Bên dưới, mỗi tên là một số nguyên, nhưng hệ thống kiểu không cho một OrderStatus bị lẫn với một int thông thường hay với một enum khác.

Khai báo và dùng enum

Liệt kê tên các thành viên trong cặp ngoặc nhọn. Mặc định thành viên đầu tiên là 0 và mỗi thành viên sau lớn hơn một đơn vị:

Đầu ra:

Paid
On its way
True
2

Enum là một kiểu thực sự: một phương thức nhận OrderStatus không thể bị gọi nhầm với 3 hay với một LogLevel. Enum là kiểu giá trị (value type), nên không bao giờ là null và được so sánh theo giá trị bằng ==.

Giá trị tường minh và kiểu bên dưới

Bạn có thể tự gán số. Việc này quan trọng mỗi khi con số đi ra ngoài chương trình (một cột trong cơ sở dữ liệu, một mã trạng thái HTTP, một định dạng tệp), vì khi đó đánh số lại sẽ làm hỏng dữ liệu đã lưu:

Đầu ra:

404
Created
418
1
Byte

Có hai điều cần chú ý. Ép một int sang enum không bao giờ thất bại: (HttpStatus)418 là một giá trị hợp lệ, chỉ là không có tên, và được in ra dưới dạng con số. Ngoài ra, kiểu bên dưới có thể là bất kỳ kiểu số nguyên nào (byte, short, long, ...), điều này chỉ quan trọng với code nhạy cảm về dung lượng lưu trữ; int là mặc định và gần như luôn là lựa chọn đúng.

Khi thêm thành viên về sau, hãy thêm vào cuối hoặc gán giá trị tường minh. Chèn Refunded vào giữa Paid và Shipped sẽ âm thầm thay đổi số của mọi thành viên đứng sau.

Chuyển enum sang chuỗi

ToString() trả về tên thành viên, cũng là thứ mà Console.WriteLine và nội suy chuỗi dùng. Chuỗi định dạng sẽ thay đổi kết quả:

Đầu ra:

Warning
2
00000002
[Warning]
Error
Error
Needs attention

Tên thành viên là định danh, nên không chứa được dấu cách và không được dịch. Với văn bản hiển thị cho người dùng, hãy tự ánh xạ giá trị như Label đã làm, hoặc dùng một Dictionary<LogLevel, string>. Một số codebase gắn thuộc tính [Description("Needs attention")] lên từng thành viên và đọc nó bằng reflection; trang reflection và attribute cho thấy cách tra cứu đó hoạt động.

Chuyển chuỗi sang enum: Parse và TryParse

Enum.Parse chuyển một tên trở lại thành giá trị và ném ArgumentException nếu không có gì khớp. Enum.TryParse thì trả về false, đúng thứ bạn cần với mọi đầu vào bạn không kiểm soát được:

Đầu ra:

Large
Medium
Parse threw ArgumentException for Huge
small  parsed=True  value=Small  defined=True
XL     parsed=False value=Small  defined=True
2      parsed=True  value=Large  defined=True
7      parsed=True  value=7      defined=False

Hai dòng cuối là cái bẫy. Cả hai phương thức đều chấp nhận chuỗi số, nên "7" được parse thành công thành một Size không có tên. Còn một lần TryParse thất bại sẽ đặt kết quả về 0, ở đây là Small trông hoàn toàn hợp lệ. Khi văn bản đến từ query string, tệp cấu hình hay một biểu mẫu, hãy luôn kiểm tra cả giá trị trả về lẫn Enum.IsDefined:

if (Enum.TryParse(input, true, out Size size) && Enum.IsDefined(typeof(Size), size))
{
    // safe to use size
}

.NET Core 2.0 trở lên có thêm phiên bản generic Enum.Parse<Size>("Large"), không cần ép kiểu.

Liệt kê mọi giá trị

Enum.GetValues trả về mọi thành viên, sắp xếp theo giá trị số (so sánh như số không dấu, nên các thành viên âm đứng cuối); Enum.GetNames trả về tên của chúng. Đây là cách bạn đổ dữ liệu vào một dropdown hoặc kiểm tra với mọi lựa chọn:

Đầu ra:

Free       0 EUR/month
Starter    9 EUR/month
Pro       29 EUR/month
Team      99 EUR/month
Free | Starter | Pro | Team
3 paid plans

Enum.GetValues(typeof(Plan)) trả về một Array thông thường, vì vậy cần Cast<Plan>() trước khi dùng LINQ. Trên .NET 5 trở lên, Enum.GetValues<Plan>() trả về thẳng một Plan[] có kiểu.

Flags: kết hợp các giá trị

Một số enum mô tả một tập lựa chọn thay vì một lựa chọn duy nhất: quyền truy cập tệp, những ngày cửa hàng mở cửa, các kênh thông báo. Hãy cho mỗi thành viên một bit riêng (1, 2, 4, 8, ...), thêm None = 0 và đánh dấu enum bằng [Flags]. Khi đó các giá trị kết hợp được bằng |:

Đầu ra:

Read, Share
Editor, Share
True
False
Editor
3
Read, Delete
True

Tác dụng của từng toán tử: | bật bit, & ~X tắt bit, ^ đảo bit, còn (value & X) != 0 hoặc value.HasFlag(X) kiểm tra bit. HasFlag(X) nghĩa là "mọi bit của X đều được bật", nên HasFlag(None) đúng với mọi giá trị, và HasFlag(Editor) đòi hỏi cả Read lẫn Write.

Hãy để ý dòng thứ hai: khi một tổ hợp có tên bao phủ một phần các bit đang bật, ToString sẽ dùng tên đó, nên Read | Write | Share được in ra là Editor, Share. Nhớ điều này trước khi parse kết quả của ToString bằng bất cứ thứ gì khác ngoài Enum.Parse.

Thuộc tính này không thay đổi phép tính. Nó thay đổi cách định dạng: không có [Flags], Read | Share được in ra là 9, vì không có thành viên nào mang đúng giá trị đó. Có nó, cả ToString và Parse đều làm việc với dạng phân tách bằng dấu phẩy. Các thành viên vẫn phải là lũy thừa của 2; viết Read, Write, Delete với cách đánh số mặc định (0, 1, 2) sẽ khiến Write | Delete bằng 3, một giá trị vô nghĩa.

Dùng switch với enum

switch là cách tự nhiên để xử lý theo từng giá trị enum. Hãy có nhánh default, vì một biến enum có thể chứa giá trị không có tên:

switch (status)
{
    case OrderStatus.Pending:
    case OrderStatus.Paid:
        return "Preparing";
    case OrderStatus.Shipped:
        return "On the way";
    case OrderStatus.Delivered:
        return "Delivered";
    default:
        return "Unknown";
}

Từ C# 8, biểu thức switch (switch expression) ngắn gọn hơn. Nếu thiếu nhánh _, trình biên dịch sẽ cảnh báo: CS8509 khi thiếu một thành viên có tên, và CS8524 khi mọi tên đã được xử lý nhưng các giá trị không tên như (OrderStatus)7 thì chưa:

string text = status switch
{
    OrderStatus.Pending or OrderStatus.Paid => "Preparing",   // 'or' pattern: C# 9
    OrderStatus.Shipped => "On the way",
    OrderStatus.Delivered => "Delivered",
    OrderStatus.Cancelled => "Cancelled",
    _ => throw new ArgumentOutOfRangeException(nameof(status)),
};

Giá trị mặc định và giá trị không xác định

Giá trị mặc định của mọi enum là 0, dù có thành viên mang giá trị đó hay không. Trường, phần tử mảng và một lần TryParse thất bại đều tạo ra giá trị này. Hãy thiết kế với điều đó trong đầu:

  • Biến 0 thành một thành viên "chưa đặt" có ý nghĩa (None, Unknown) thay vì một lựa chọn thật. Nếu không, một trường chưa khởi tạo sẽ âm thầm được đọc thành lựa chọn thật đầu tiên.
  • Kiểm tra các con số từ bên ngoài bằng Enum.IsDefined. Với enum [Flags], IsDefined trả về false cho các tổ hợp không có tên (Read | Share), nên hãy kiểm tra các bit: (value & ~Permissions.All) == 0, với một thành viên All bao phủ mọi bit.

Lỗi thường gặp

  • Chỉ tin vào TryParse. Chuỗi số vẫn parse được, và một lần parse thất bại cho ra 0. Hãy thêm Enum.IsDefined.
  • Dựa vào cách đánh số ngầm định cho giá trị được lưu trữ. Chèn một thành viên sẽ đánh số lại các thành viên phía sau. Hãy gán giá trị tường minh cho mọi enum được lưu lại.
  • Flags không dùng lũy thừa của 2. Cách đánh số mặc định (0, 1, 2, 3) làm các bit chồng lên nhau. Hãy dùng 1, 2, 4, 8 hoặc 1 << n.
  • Hiển thị ToString() cho người dùng. Tên thành viên là định danh trong code. Hãy ánh xạ giá trị sang văn bản hiển thị.
  • Không có default trong switch. Một enum có thể chứa giá trị nằm ngoài các thành viên có tên.

Câu hỏi thường gặp

Làm sao chuyển enum sang chuỗi trong C#?

Gọi ToString(): OrderStatus.Shipped.ToString() trả về "Shipped", và nội suy chuỗi (string interpolation) cũng cho kết quả như vậy. ToString("D") cho ra con số thay vì tên. Với tên đã biết lúc biên dịch, nameof(OrderStatus.Shipped) là một hằng số. Với văn bản hiển thị cho người dùng, có dấu cách hoặc cần dịch, hãy tự ánh xạ giá trị sang chuỗi (bằng switch hoặc một từ điển) thay vì dựa vào tên thành viên.

Làm sao chuyển chuỗi sang enum trong C#?

Dùng Enum.TryParse<OrderStatus>(text, true, out var status): phương thức này trả về false thay vì ném ngoại lệ khi văn bản không khớp thành viên nào (true giúp so sánh không phân biệt hoa thường). Enum.Parse(typeof(OrderStatus), text) ném ArgumentException khi đầu vào sai. Cả hai đều chấp nhận chuỗi số như "42", nên hãy kiểm tra kết quả bằng Enum.IsDefined khi đầu vào đến từ người dùng.

Làm sao chuyển qua lại giữa enum và int trong C#?

Ép kiểu theo chiều nào cũng được: int code = (int)OrderStatus.Paid; và var status = (OrderStatus)2;. Phép ép từ int không bao giờ thất bại, kể cả với số không có thành viên tương ứng; kết quả là một giá trị enum được in ra dưới dạng con số. Hãy kiểm tra bằng Enum.IsDefined(typeof(OrderStatus), value) khi con số đến từ bên ngoài.

Làm sao duyệt qua mọi giá trị của enum trong C#?

foreach (OrderStatus s in Enum.GetValues(typeof(OrderStatus))) đi qua từng thành viên theo thứ tự giá trị số. Từ .NET 5 có phiên bản generic Enum.GetValues<OrderStatus>(), không cần ép kiểu. Enum.GetNames(typeof(OrderStatus)) trả về các tên dưới dạng chuỗi.

[Flags] có tác dụng gì với enum trong C#?

Thuộc tính này đánh dấu một enum có các giá trị là bit, dùng để kết hợp bằng |, ví dụ Read | Write. Hãy gán cho mỗi thành viên một lũy thừa của 2 (1, 2, 4, 8) và thêm None = 0. Nhờ thuộc tính này, ToString() in tổ hợp thành "Read, Write" và Enum.Parse đọc lại được định dạng đó. Kiểm tra một bit bằng HasFlag hoặc (value & Permissions.Write) != 0.

Hình minh họa các ngôn ngữ lập trình của Coddy

Học lập trình cùng Coddy

Bắt đầu