枚举(Enums)
想象你正在搭建一个电商平台,需要跟踪每个订单的状态。订单会经历多个状态:已下单(placed)、已确认(confirmed)、已发货(shipped)、已送达(delivered)或已取消(cancelled)。你该如何在代码中表示这些状态?
你可以使用字符串:"PLACED"、"SHIPPED"、"DELIVERED"。但如果有人把 "SHIPPED" 误写成 "Shiped",会发生什么?
编译器不会捕获这个错误,你的代码会在运行时悄然失败。
你也可以使用整数:1 表示已下单、2 表示已发货、3 表示已送达。但这样一来,你的代码里就会充斥着「魔法数字」。if (status == 2) 究竟是什么意思?
而这正是 枚举(enum) 要解决的问题。
java
编程资源
https://pan.quark.cn/s/7f7c83756948
更多资源
https://pan.quark.cn/s/bda57957c548
1. 什么是枚举(Enum)?
枚举(enum,enumeration 的缩写) 是一种特殊的数据类型,用于定义一组固定的命名常量。与字符串或整数不同,枚举是**类型安全(type-safe)**的------编译器会确保你只能使用所定义集合中实际存在的值。
它们确保一个变量只能取预定义有效选项集合中的某一个值。
如果一个值只能是预定义选项集合中的某一个,就考虑使用枚举。
为什么要使用枚举?
为了体会枚举带来的好处,不妨看看不用枚举的替代方案。如果没有枚举,你会把订单状态表示成散落在代码库各处的字符串:
java
String status = "PENDING";
// Somewhere else in the codebase...
if (status.equals("PNDING")) { // Typo! This condition is never true
processOrder();
}
这段代码可以毫无警告地通过编译。在编译器看来,拼写错误 "PNDING" 是一个完全合法的字符串。这个 bug 只有在客户投诉订单始终未被处理时才会暴露出来。
枚举消除了这一类 bug。当你把 OrderStatus 定义为一个包含 PENDING、CONFIRMED、SHIPPED、DELIVERED 等值的枚举时,编译器就能确切知道哪些值是合法的。
与普通的常量或字符串相比,枚举具有以下几个关键优势:
- 避免「魔法值」: 代码中不再散落
"PENDING"或3这样的字符串或整数。 - 提升可读性: 枚举让意图一目了然------
OrderStatus.SHIPPED远比3更具描述性。 - 启用编译器检查: 编译器会校验枚举的使用,尽早发现拼写错误和非法赋值。
- 支持 IDE 功能: 大多数 IDE 都能为枚举值提供自动补全和重构工具。
- 减少 bug: 你无法意外地赋一个不属于该枚举的随机字符串或数字。
枚举示例
枚举非常适合用来定义那些很少会发生变化的类别或状态。
- 订单状态(例如:
PENDING、IN_PROGRESS、COMPLETED) - 用户角色(例如:
ADMIN、CUSTOMER、DRIVER) - 车辆类型(例如:
CAR、BIKE、TRUCK) - 方向(例如:
NORTH、SOUTH、EAST、WEST)
用枚举代替原始字符串,可以让你的系统更易于理解,也更难被误用。
2. 枚举示例
简单枚举
枚举最基本的形式,是在一个类型下定义一组命名常量。让我们在电商系统中为订单状态建模。
java
public enum OrderStatus {
PLACED,
CONFIRMED,
SHIPPED,
DELIVERED,
CANCELLED
}
python
from enum import Enum
# Simple Enum
class OrderStatus(Enum):
PLACED = "PLACED"
CONFIRMED = "CONFIRMED"
SHIPPED = "SHIPPED"
DELIVERED = "DELIVERED"
CANCELLED = "CANCELLED"
cpp
enum class OrderStatus {
PLACED,
CONFIRMED,
SHIPPED,
DELIVERED,
CANCELLED
};
csharp
public enum OrderStatus
{
PLACED,
CONFIRMED,
SHIPPED,
DELIVERED,
CANCELLED
}
go
type OrderStatus int
const (
Placed OrderStatus = iota
Confirmed
Shipped
Delivered
Cancelled
)
func (s OrderStatus) String() string {
switch s {
case Placed:
return "PLACED"
case Confirmed:
return "CONFIRMED"
case Shipped:
return "SHIPPED"
case Delivered:
return "DELIVERED"
case Cancelled:
return "CANCELLED"
default:
return "UNKNOWN"
}
}
// Go doesn't have a built-in enum keyword, so you use iota with typed constants to achieve the same result.
typescript
enum OrderStatus {
PLACED = "PLACED",
CONFIRMED = "CONFIRMED",
SHIPPED = "SHIPPED",
DELIVERED = "DELIVERED",
CANCELLED = "CANCELLED"
}
这个枚举定义了订单所能拥有的有限个合法状态,除此之外都不被允许。
在代码中使用
java
OrderStatus status = OrderStatus.SHIPPED;
if (status == OrderStatus.SHIPPED) {
System.out.println("Your package is on the way!");
}
python
status = OrderStatus.SHIPPED
if status == OrderStatus.SHIPPED:
print("Your package is on the way!")
cpp
OrderStatus status = OrderStatus::SHIPPED;
if (status == OrderStatus::SHIPPED) {
cout << "Your package is on the way!" << endl;
}
csharp
package main
import "fmt"
type OrderStatus int
const (
SHIPPED OrderStatus = iota
)
func main() {
status := SHIPPED
if status == SHIPPED {
fmt.Println("Your package is on the way!")
}
}
go
OrderStatus status = OrderStatus.SHIPPED;
if (status == OrderStatus.SHIPPED)
{
Console.WriteLine("Your package is on the way!");
}
typescript
const status: OrderStatus = OrderStatus.SHIPPED;
if (status === OrderStatus.SHIPPED) {
console.log("Your package is on the way!");
}
当你只需要一组命名常量时,简单枚举非常好用。但如果每个常量还需要携带额外数据呢?
带属性和方法的枚举
枚举的作用不止是给常量命名。在许多语言中,每个枚举值都可以携带额外数据,甚至定义行为。这让枚举在建模领域概念时出奇地强大。
我们来考虑一个表示美国硬币及其面值的 Coin 枚举。每种硬币都有一个名称(PENNY、NICKEL、DIME、QUARTER)和一个以「分」为单位的面值(1、5、10、25)。与其维护一张独立的面值查找表,不如把面值直接内嵌到枚举中。
java
public enum Coin {
PENNY(1),
NICKEL(5),
DIME(10),
QUARTER(25);
private final int value;
Coin(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
python
from enum import Enum
class Coin(Enum):
PENNY = 1
NICKEL = 5
DIME = 10
QUARTER = 25
def __init__(self, value):
self.coin_value = value
def get_value(self):
return self.coin_value
cpp
enum class Coin {
PENNY,
NICKEL,
DIME,
QUARTER
};
// C++ enums can't hold fields, so use a helper function
int getCoinValue(Coin coin) {
switch (coin) {
case Coin::PENNY: return 1;
case Coin::NICKEL: return 5;
case Coin::DIME: return 10;
case Coin::QUARTER: return 25;
default: return 0;
}
}
csharp
// C# enums can have integer values but no methods.
// For richer behavior, use a class with static readonly fields.
public class Coin
{
public static readonly Coin PENNY = new("PENNY", 1);
public static readonly Coin NICKEL = new("NICKEL", 5);
public static readonly Coin DIME = new("DIME", 10);
public static readonly Coin QUARTER = new("QUARTER", 25);
public string Name { get; }
public int Value { get; }
private Coin(string name, int value)
{
Name = name;
Value = value;
}
public int GetValue() => Value;
}
go
type Coin struct {
Name string
Value int
}
var (
Penny = Coin{"PENNY", 1}
Nickel = Coin{"NICKEL", 5}
Dime = Coin{"DIME", 10}
Quarter = Coin{"QUARTER", 25}
)
func (c Coin) GetValue() int {
return c.Value
}
typescript
class Coin {
static readonly PENNY = new Coin("PENNY", 1);
static readonly NICKEL = new Coin("NICKEL", 5);
static readonly DIME = new Coin("DIME", 10);
static readonly QUARTER = new Coin("QUARTER", 25);
private constructor(
public readonly name: string,
private readonly value: number
) {}
getValue(): number {
return this.value;
}
}
在代码中使用
java
int total = Coin.DIME.getValue() + Coin.QUARTER.getValue(); // 35
python
total = Coin.DIME.get_value() + Coin.QUARTER.get_value() # 35
cpp
int total = getCoinValue(Coin::DIME) + getCoinValue(Coin::QUARTER); // 35
csharp
package main
type Coin int
const (
PENNY Coin = iota + 1
NICKEL
DIME
QUARTER
)
func getCoinValue(c Coin) int {
switch c {
case PENNY:
return 1
case NICKEL:
return 5
case DIME:
return 10
case QUARTER:
return 25
default:
return 0
}
}
func main() {
total := getCoinValue(DIME) + getCoinValue(QUARTER) // 35
_ = total
}
go
int total = Coin.DIME.GetValue() + Coin.QUARTER.GetValue(); // 35
typescript
const total: number = Coin.DIME.getValue() + Coin.QUARTER.getValue(); // 35
这比维护独立的数组或查找表要优雅、安全得多。数据与它所归属的常量紧挨在一起,因此不存在面值与名称脱节的风险。
3. 实战示例:订单处理系统
我们来构建一个小型订单处理系统,它用到两个枚举:已经见过的 OrderStatus,以及 PaymentMethod。它们共同展示了枚举如何为一个真实的领域模型带来结构性与安全性。
Order 类跟踪订单的状态、支付方式和总金额。它提供了在生命周期中推进状态、取消订单以及展示订单信息的方法。
关键在于:枚举控制了合法的转换------订单只能沿状态链向前推进(PLACED → CONFIRMED → SHIPPED → DELIVERED),并且只有在发货前才允许取消。
java
public enum OrderStatus {
PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
}
public enum PaymentMethod {
CREDIT_CARD("Credit Card", 2.5),
DEBIT_CARD("Debit Card", 1.0),
UPI("UPI", 0.0),
NET_BANKING("Net Banking", 1.5);
private final String displayName;
private final double feePercent;
PaymentMethod(String displayName, double feePercent) {
this.displayName = displayName;
this.feePercent = feePercent;
}
public String getDisplayName() { return displayName; }
public double getFeePercent() { return feePercent; }
}
public class Order {
private final String orderId;
private OrderStatus status;
private final PaymentMethod paymentMethod;
private final double amount;
public Order(String orderId, PaymentMethod paymentMethod, double amount) {
this.orderId = orderId;
this.paymentMethod = paymentMethod;
this.amount = amount;
this.status = OrderStatus.PLACED;
}
public boolean advanceStatus() {
switch (status) {
case PLACED:
status = OrderStatus.CONFIRMED;
return true;
case CONFIRMED:
status = OrderStatus.SHIPPED;
return true;
case SHIPPED:
status = OrderStatus.DELIVERED;
return true;
default:
return false;
}
}
public boolean cancel() {
if (status == OrderStatus.PLACED || status == OrderStatus.CONFIRMED) {
status = OrderStatus.CANCELLED;
return true;
}
return false; // Can't cancel after shipping
}
public double getTotalWithFees() {
return amount + (amount * paymentMethod.getFeePercent() / 100);
}
public void displayInfo() {
System.out.printf("Order %s | Status: %s | Payment: %s | Amount: $%.2f (with fees: $%.2f)%n",
orderId, status, paymentMethod.getDisplayName(), amount, getTotalWithFees());
}
}
// Usage
public class Main {
public static void main(String[] args) {
Order order = new Order("ORD-001", PaymentMethod.CREDIT_CARD, 99.99);
order.displayInfo();
order.advanceStatus(); // PLACED -> CONFIRMED
order.advanceStatus(); // CONFIRMED -> SHIPPED
order.displayInfo();
System.out.println("Cancel after shipping: " + order.cancel()); // false
}
}
python
from enum import Enum
class OrderStatus(Enum):
PLACED = "PLACED"
CONFIRMED = "CONFIRMED"
SHIPPED = "SHIPPED"
DELIVERED = "DELIVERED"
CANCELLED = "CANCELLED"
class PaymentMethod(Enum):
CREDIT_CARD = ("Credit Card", 2.5)
DEBIT_CARD = ("Debit Card", 1.0)
UPI = ("UPI", 0.0)
NET_BANKING = ("Net Banking", 1.5)
def __init__(self, display_name: str, fee_percent: float):
self.display_name = display_name
self.fee_percent = fee_percent
class Order:
_status_transitions = {
OrderStatus.PLACED: OrderStatus.CONFIRMED,
OrderStatus.CONFIRMED: OrderStatus.SHIPPED,
OrderStatus.SHIPPED: OrderStatus.DELIVERED,
}
def __init__(self, order_id: str, payment_method: PaymentMethod, amount: float):
self._order_id = order_id
self._status = OrderStatus.PLACED
self._payment_method = payment_method
self._amount = amount
def advance_status(self) -> bool:
next_status = self._status_transitions.get(self._status)
if next_status:
self._status = next_status
return True
return False
def cancel(self) -> bool:
if self._status in (OrderStatus.PLACED, OrderStatus.CONFIRMED):
self._status = OrderStatus.CANCELLED
return True
return False
def get_total_with_fees(self) -> float:
return self._amount + (self._amount * self._payment_method.fee_percent / 100)
def display_info(self) -> None:
print(f"Order {self._order_id} | Status: {self._status.value} | "
f"Payment: {self._payment_method.display_name} | "
f"Amount: ${self._amount:.2f} (with fees: ${self.get_total_with_fees():.2f})")
if __name__ == "__main__":
order = Order("ORD-001", PaymentMethod.CREDIT_CARD, 99.99)
order.display_info()
order.advance_status() # PLACED -> CONFIRMED
order.advance_status() # CONFIRMED -> SHIPPED
order.display_info()
print(f"Cancel after shipping: {order.cancel()}") # False
cpp
#include <iostream>
#include <string>
enum class OrderStatus {
PLACED, CONFIRMED, SHIPPED, DELIVERED, CANCELLED
};
std::string orderStatusToString(OrderStatus s) {
switch (s) {
case OrderStatus::PLACED: return "PLACED";
case OrderStatus::CONFIRMED: return "CONFIRMED";
case OrderStatus::SHIPPED: return "SHIPPED";
case OrderStatus::DELIVERED: return "DELIVERED";
case OrderStatus::CANCELLED: return "CANCELLED";
default: return "UNKNOWN";
}
}
struct PaymentMethod {
std::string displayName;
double feePercent;
static const PaymentMethod CREDIT_CARD;
static const PaymentMethod DEBIT_CARD;
static const PaymentMethod UPI;
static const PaymentMethod NET_BANKING;
};
const PaymentMethod PaymentMethod::CREDIT_CARD{"Credit Card", 2.5};
const PaymentMethod PaymentMethod::DEBIT_CARD{"Debit Card", 1.0};
const PaymentMethod PaymentMethod::UPI{"UPI", 0.0};
const PaymentMethod PaymentMethod::NET_BANKING{"Net Banking", 1.5};
class Order {
private:
std::string orderId;
OrderStatus status;
PaymentMethod paymentMethod;
double amount;
public:
Order(const std::string& orderId, const PaymentMethod& paymentMethod, double amount)
: orderId(orderId), status(OrderStatus::PLACED),
paymentMethod(paymentMethod), amount(amount) {}
bool advanceStatus() {
switch (status) {
case OrderStatus::PLACED:
status = OrderStatus::CONFIRMED; return true;
case OrderStatus::CONFIRMED:
status = OrderStatus::SHIPPED; return true;
case OrderStatus::SHIPPED:
status = OrderStatus::DELIVERED; return true;
default:
return false;
}
}
bool cancel() {
if (status == OrderStatus::PLACED || status == OrderStatus::CONFIRMED) {
status = OrderStatus::CANCELLED;
return true;
}
return false;
}
double getTotalWithFees() const {
return amount + (amount * paymentMethod.feePercent / 100);
}
void displayInfo() const {
printf("Order %s | Status: %s | Payment: %s | Amount: $%.2f (with fees: $%.2f)\
",
orderId.c_str(), orderStatusToString(status).c_str(),
paymentMethod.displayName.c_str(), amount, getTotalWithFees());
}
};
int main() {
Order order("ORD-001", PaymentMethod::CREDIT_CARD, 99.99);
order.displayInfo();
order.advanceStatus(); // PLACED -> CONFIRMED
order.advanceStatus(); // CONFIRMED -> SHIPPED
order.displayInfo();
std::cout << "Cancel after shipping: " << (order.cancel() ? "true" : "false") << std::endl;
return 0;
}
csharp
using System;
public enum OrderStatus
{
Placed, Confirmed, Shipped, Delivered, Cancelled
}
public class PaymentMethod
{
public static readonly PaymentMethod CreditCard = new PaymentMethod("Credit Card", 2.5);
public static readonly PaymentMethod DebitCard = new PaymentMethod("Debit Card", 1.0);
public static readonly PaymentMethod Upi = new PaymentMethod("UPI", 0.0);
public static readonly PaymentMethod NetBanking = new PaymentMethod("Net Banking", 1.5);
public string DisplayName { get; }
public double FeePercent { get; }
private PaymentMethod(string displayName, double feePercent)
{
DisplayName = displayName;
FeePercent = feePercent;
}
}
public class Order
{
private readonly string _orderId;
private OrderStatus _status;
private readonly PaymentMethod _paymentMethod;
private readonly double _amount;
public Order(string orderId, PaymentMethod paymentMethod, double amount)
{
_orderId = orderId;
_paymentMethod = paymentMethod;
_amount = amount;
_status = OrderStatus.Placed;
}
public bool AdvanceStatus()
{
switch (_status)
{
case OrderStatus.Placed:
_status = OrderStatus.Confirmed; return true;
case OrderStatus.Confirmed:
_status = OrderStatus.Shipped; return true;
case OrderStatus.Shipped:
_status = OrderStatus.Delivered; return true;
default:
return false;
}
}
public bool Cancel()
{
if (_status == OrderStatus.Placed || _status == OrderStatus.Confirmed)
{
_status = OrderStatus.Cancelled;
return true;
}
return false;
}
public double GetTotalWithFees()
{
return _amount + (_amount * _paymentMethod.FeePercent / 100);
}
public void DisplayInfo()
{
Console.WriteLine(
$"Order {_orderId} | Status: {_status} | Payment: {_paymentMethod.DisplayName} " +
$"| Amount: ${_amount:F2} (with fees: ${GetTotalWithFees():F2})");
}
}
// Usage
public class Program
{
public static void Main()
{
var order = new Order("ORD-001", PaymentMethod.CreditCard, 99.99);
order.DisplayInfo();
order.AdvanceStatus(); // Placed -> Confirmed
order.AdvanceStatus(); // Confirmed -> Shipped
order.DisplayInfo();
Console.WriteLine($"Cancel after shipping: {order.Cancel()}"); // False
}
}
go
package main
import "fmt"
type OrderStatus int
const (
PLACED OrderStatus = iota
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
)
func (s OrderStatus) String() string {
names := [...]string{"PLACED", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED"}
if int(s) >= 0 && int(s) < len(names) {
return names[s]
}
return "UNKNOWN"
}
type PaymentMethod struct {
displayName string
feePercent float64
}
var (
CREDIT_CARD = PaymentMethod{"Credit Card", 2.5}
DEBIT_CARD = PaymentMethod{"Debit Card", 1.0}
UPI = PaymentMethod{"UPI", 0.0}
NET_BANKING = PaymentMethod{"Net Banking", 1.5}
)
func (p PaymentMethod) GetDisplayName() string { return p.displayName }
func (p PaymentMethod) GetFeePercent() float64 { return p.feePercent }
type Order struct {
orderId string
status OrderStatus
paymentMethod PaymentMethod
amount float64
}
func NewOrder(orderId string, paymentMethod PaymentMethod, amount float64) *Order {
return &Order{orderId: orderId, paymentMethod: paymentMethod, amount: amount, status: PLACED}
}
func (o *Order) AdvanceStatus() bool {
switch o.status {
case PLACED:
o.status = CONFIRMED
return true
case CONFIRMED:
o.status = SHIPPED
return true
case SHIPPED:
o.status = DELIVERED
return true
default:
return false
}
}
func (o *Order) Cancel() bool {
if o.status == PLACED || o.status == CONFIRMED {
o.status = CANCELLED
return true
}
return false
}
func (o *Order) GetTotalWithFees() float64 {
return o.amount + (o.amount * o.paymentMethod.GetFeePercent() / 100)
}
func (o *Order) DisplayInfo() {
fmt.Printf("Order %s | Status: %s | Payment: %s | Amount: $%.2f (with fees: $%.2f)\
",
o.orderId, o.status, o.paymentMethod.GetDisplayName(), o.amount, o.GetTotalWithFees())
}
// Usage
func main() {
order := NewOrder("ORD-001", CREDIT_CARD, 99.99)
order.DisplayInfo()
order.AdvanceStatus() // PLACED -> CONFIRMED
order.AdvanceStatus() // CONFIRMED -> SHIPPED
order.DisplayInfo()
fmt.Println("Cancel after shipping:", order.Cancel()) // false
}
typescript
enum OrderStatus {
PLACED = "PLACED",
CONFIRMED = "CONFIRMED",
SHIPPED = "SHIPPED",
DELIVERED = "DELIVERED",
CANCELLED = "CANCELLED"
}
class PaymentMethod {
static readonly CREDIT_CARD = new PaymentMethod("Credit Card", 2.5);
static readonly DEBIT_CARD = new PaymentMethod("Debit Card", 1.0);
static readonly UPI = new PaymentMethod("UPI", 0.0);
static readonly NET_BANKING = new PaymentMethod("Net Banking", 1.5);
private constructor(
public readonly displayName: string,
public readonly feePercent: number
) {}
}
class Order {
private status: OrderStatus;
constructor(
private readonly orderId: string,
private readonly paymentMethod: PaymentMethod,
private readonly amount: number
) {
this.status = OrderStatus.PLACED;
}
advanceStatus(): boolean {
const transitions: Partial<Record<OrderStatus, OrderStatus>> = {
[OrderStatus.PLACED]: OrderStatus.CONFIRMED,
[OrderStatus.CONFIRMED]: OrderStatus.SHIPPED,
[OrderStatus.SHIPPED]: OrderStatus.DELIVERED,
};
const next = transitions[this.status];
if (next) {
this.status = next;
return true;
}
return false;
}
cancel(): boolean {
if (this.status === OrderStatus.PLACED || this.status === OrderStatus.CONFIRMED) {
this.status = OrderStatus.CANCELLED;
return true;
}
return false;
}
getTotalWithFees(): number {
return this.amount + (this.amount * this.paymentMethod.feePercent / 100);
}
displayInfo(): void {
console.log(
`Order ${this.orderId} | Status: ${this.status} | ` +
`Payment: ${this.paymentMethod.displayName} | ` +
`Amount: $${this.amount.toFixed(2)} (with fees: $${this.getTotalWithFees().toFixed(2)})`
);
}
}
// Usage
const order = new Order("ORD-001", PaymentMethod.CREDIT_CARD, 99.99);
order.displayInfo();
order.advanceStatus(); // PLACED -> CONFIRMED
order.advanceStatus(); // CONFIRMED -> SHIPPED
order.displayInfo();
console.log(`Cancel after shipping: ${order.cancel()}`); // false
为什么这样设计是可行的
- 状态转换受控:
advanceStatus()方法强制订单按照合法的顺序流转。你不能从 PLACED 直接跳到 DELIVERED,也不能从 SHIPPED 回退到 CONFIRMED。枚举配合 switch 语句,让合法的转换变得一目了然。 - 支付费率自包含: 每个
PaymentMethod都携带自己的费率百分比。不需要额外的查找表或配置文件来保持同步。新增一种支付方式,只需在枚举中加一个带费率的值,其余代码即可自动生效。 - 取消规则清晰:
cancel()方法通过枚举比较来执行业务规则。只有发货前才能取消。如果有人试图取消已发货的订单,方法会返回 false。没有歧义,也无需进行字符串匹配。 - 易于扩展: 需要新增
RETURNED状态?把它加进枚举并更新 switch 语句即可。如果你漏掉了某个分支的处理,编译器会提醒你。需要新增像WALLET这样的支付方式?在枚举里加一行(含显示名称和费率)就行。
java
编程资源
https://pan.quark.cn/s/7f7c83756948
更多资源
https://pan.quark.cn/s/bda57957c548