Rust: Compare u16 to #[repr(u16)] Enum Variant
Learn how to compare a u16 value to a #[repr(u16)] Rust enum variant like packet types from devices. Use casting with 'as u16', pattern matching, or TryFrom for idiomatic, safe comparisons without PartialEq errors.
How can I compare a u16 value to a #[repr(u16)] enum variant in Rust?
I’m reading data from a device and I’ve defined an enum to represent packet types:
#[derive(Copy, Clone)]
#[repr(u16)]
pub enum Packets {
StartCode = 0xEF01,
CommandPacket = 0x01,
DataPacket = 0x02,
AckPacket = 0x07,
EndDataPacket = 0x08,
}
Later I read a value from the device and try to check if it’s StartCode:
let start: u16 = <response from device>;
if start != Packets::StartCode {
// ...
}
But the compiler reports:
438 | if start != Packets::StartCode {
| ^^ no implementation for `u16 == Packets`
|
= help: the trait `PartialEq<Packets>` is not implemented for `u16`
= help: the following other types implement trait `PartialEq<Rhs>`:
`u16` implements `PartialEq<fixed::FixedI128<Frac>>`
`u16` implements `PartialEq<fixed::FixedI16<Frac>>`
`u16` implements `PartialEq<fixed::FixedI32<Frac>>`
`u16` implements `PartialEq<fixed::FixedI64<Frac>>`
`u16` implements `PartialEq<fixed::FixedI8<Frac>>`
`u16` implements `PartialEq<fixed::FixedU128<Frac>>`
`u16` implements `PartialEq<fixed::FixedU16<Frac>>`
`u16` implements `PartialEq<fixed::FixedU32<Frac>>`
and 3 others
How should I properly compare the u16 value to the enum variant? Should I cast the enum (e.g., Packets::StartCode as u16), implement PartialEq between u16 and Packets, or use another approach such as match, From/TryFrom conversions, or pattern matching? What’s the idiomatic Rust way to handle this kind of comparison and why?
To compare a u16 value from your device to a #[repr(u16)] rust enum variant like Packets::StartCode, simply cast the enum to u16 with as u16 for a direct equality check: if start == Packets::StartCode as u16. This works safely because #[repr(u16)] guarantees the variant’s discriminant matches its underlying u16 representation, avoiding the PartialEq error you’re seeing. It’s the most idiomatic rust enum handling for low-level data like packet types—no extra traits or conversions needed unless you’re parsing into the full enum.
Contents
- Understanding #[repr(u16)] Enums in Rust
- Why Direct Comparison Fails Without Casting
- Casting Enums to u16: The Go-To Fix
- Pattern Matching for More Complex Checks
- Converting u16 Back to Rust Enum Variants
- Advanced Options and Edge Cases
- Sources
- Conclusion
Understanding #[repr(u16)] Enums in Rust
Rust enums aren’t just fancy switch statements—they’re powerful for modeling states like your packet types. With #[repr(u16)], you’re telling the compiler to lay out the enum like a C-style integer enum, where each variant’s discriminant (that unique ID) gets stored as a raw u16. Your Packets enum nails this: StartCode sits at 0xEF01, CommandPacket at 0x01, and so on.
Why bother? For device I/O, like reading raw bytes, this repr lets you map hardware values directly without padding surprises or alignment issues. The Rust Reference on Enumerations spells it out: custom discriminants become castable integers, perfect for as u16. Add Copy/Clone like you did, and variants behave like lightweight primitives.
But here’s the catch—Rust’s type system won’t let you compare u16 == Packets out of the box. It’s protective, ensuring you don’t accidentally mismatch types. You’ll see that PartialEq list in the error; u16 plays nice with fixed-point stuff, but not raw enums.
Why Direct Comparison Fails Without Casting
That compiler error? It’s Rust saying, “Hey, prove you know what you’re doing.” Enums are their own type, even with repr(u16). PartialEq
Think about it: without casting, Rust can’t assume your enum’s guts match u16 exactly (though repr guarantees it). Transmute? Tempting for perf junkies, but unsafe and overkill. The std::mem::discriminant docs warn against hacking discriminants to primitives—it’s opaque and transmute risks UB.
Short version: Don’t fight the borrow checker. Cast instead. Simple fix, zero runtime cost.
Casting Enums to u16: The Go-To Fix
Cast the enum variant. Done.
let start: u16 = /* device read */;
if start == Packets::StartCode as u16 {
println!("Got start code!");
} else if start == Packets::DataPacket as u16 {
// Handle data
}
Boom—no traits, no match arms bloating your code. Since #[repr(u16)], the cast extracts the exact discriminant: 0xEF01 for StartCode. The Rust By Example on C-like enums shows this pattern: variant as i32, but u16 fits your case perfectly.
For inequality, flip it: if start != Packets::AckPacket as u16. Or chain: match start { v if v == Packets::StartCode as u16 => ..., _ => ... }. Efficient? Absolutely—compiles to a single cmp instruction.
Your enum’s explicit values shine here. Implicit ones (starting at 0) work too, but custom like 0xEF01? Casting preserves them exactly.
What if it’s a ref? *packet_ref as u16, assuming Copy. Safe, idiomatic.
Pattern Matching for More Complex Checks
Casting rocks for equality, but need exhaustive handling? Match on the u16 against casts.
match start {
x if (x == Packets::StartCode as u16) => {
// Start logic
}
x if (x == Packets::CommandPacket as u16) => {
// Command
}
_ => {
// Unknown packet
}
}
Cleaner than if-else chains. Or, if parsing full enums later, combine with TryFrom (next section). Stack Overflow threads like matching enums with integers push this—avoids PartialEq boilerplate.
Pro tip: For hot loops (device polling?), casting wins on brevity. Match adds a jump table, negligible on modern CPUs but measurable in tight code.
Ever wonder why no built-in u16 == Enum? Safety. Forces intent.
Converting u16 Back to Rust Enum Variants
Just checking? Cast. But to parse into Packets?
Derive TryFrom
use std::convert::TryFrom;
impl TryFrom<u16> for Packets {
type Error = &'static str;
fn try_from(value: u16) -> Result<Self, Self::Error> {
match value {
0xEF01 => Ok(Packets::StartCode),
0x01 => Ok(Packets::CommandPacket),
0x02 => Ok(Packets::DataPacket),
0x07 => Ok(Packets::AckPacket),
0x08 => Ok(Packets::EndDataPacket),
_ => Err("Unknown packet type"),
}
}
}
// Usage
let packet = Packets::try_from(start).ok();
if let Some(Packets::StartCode) = packet {
// Handle
}
The Reference recommends this for raw-to-enum. Exhaustive, handles invalids gracefully. For your device, parse once, then match on the enum—no repeated casts.
PartialEq
impl PartialEq<u16> for Packets {
fn eq(&self, other: &u16) -> bool {
*other == *self as u16
}
}
But why? Casts are shorter. Use if APIs demand it.
Advanced Options and Edge Cases
Discriminant function? std::mem::discriminant(&variant) compares variants ignoring data, but it’s opaque—not u16. For primitives, stick to as. Discriminant docs confirm: cast unit variants directly.
Refs to enums? From Stack Overflow on ref casting: *ref as u16 if Copy.
Non-unit variants? Your Packets are unit—good. Data-carrying? Cast only works on discriminant, loses fields.
UB risks? None with repr(u16) + as. Future-proof.
Perf: Benchmarks? Casting is zero-cost abstraction. In 2026 Rust (stable channel), no changes here.
Multi-byte? Read as [u8;2], transmute to u16 if endianness matches—safer than raw u16 from device sometimes.
Idiomatic winner: Cast for checks, TryFrom for parsing. Scales from embedded to servers.
Sources
- Enumerations - The Rust Reference
- C-like - Rust By Example
- discriminant in std::mem - Rust
- How do I match enum values with an integer? - Stack Overflow
- How one can cast ref to enum value as u16 (base enum type) in Rust? - Stack Overflow
Conclusion
Casting #[repr(u16)] rust enum variants to u16 with as u16 is your fastest, safest bet for comparing device bytes to packets—direct, zero-overhead, and what the docs endorse. Scale up with TryFrom for full parsing or match for exhaustiveness, but skip custom PartialEq unless forced. This pattern handles real-world I/O like yours without hacks, keeping code readable and bug-free. Next time a device spits bytes, you’ll dispatch them flawlessly.