What they are (simple)
struct — use when you have one “thing” with properties
enum — use when a value can be ONE of multiple forms
trait — use when multiple types share the same behavior
How Rust allows (multiple instances per enum variant)
How Rust’s enum + match replaces classic inheritance-based polymorphism
Rust enums are much more powerful than Java/Python enums
What they are (simple)
struct: holds data/fields (like a Java class used as a data model).
enum: holds one of many variants (like Java enum, but Rust enums can also store data per variant).
trait: defines shared behavior (like a Java interface).
1)
struct — use when you have one “thing” with properties
When to use (Rust + Java)
Use struct/class when:
You model a real object: User, Hospital, Car
It has fixed fields: id, name, price
You want methods on it
Rust code + output
#[derive(Debug)]
struct User {
id: i32,
name: String,
}
impl User {
fn label(&self) -> String {
format!("{} ({})", self.name, self.id)
}
}
fn main() {
let u = User { id: 1, name: "Ashwani".to_string() };
println!("{:?}", u);
println!("{}", u.label());
}
Output
User { id: 1, name: "Ashwani" }
Ashwani (1)
Java equivalent + output
class User {
int id;
String name;
User(int id, String name) {
this.id = id;
this.name = name;
}
String label() {
return name + " (" + id + ")";
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
}
public class Main {
public static void main(String[] args) {
User u = new User(1, "Ashwani");
System.out.println(u);
System.out.println(u.label());
}
}
Output
User{id=1, name='Ashwani'}
Ashwani (1)
2)
enum — use when a value can be ONE of multiple forms
When to use
Use enum when:
A variable can be different “shapes”: Success/Failure, Card/UPI/Cash
You want compile-time safety (no invalid state)
You want data with variants (Rust is very strong here)
Rust code + output (enum with data + match)
[derive(Debug)]
enum Payment {
Cash,
Card { last4: String },
Upi(String),
}
fn main() {
let p1 = Payment::Cash;
let p2 = Payment::Card { last4: "1234".to_string() };
let p3 = Payment::Upi("ashwani@upi".to_string());
for p in [p1, p2, p3] {
match p {
Payment::Cash => println!("Pay by Cash"),
Payment::Card { last4 } => println!("Pay by Card ending {}", last4),
Payment::Upi(id) => println!("Pay by UPI {}", id),
}
}
}
Output
Pay by Cash
Pay by Card ending 1234
Pay by UPI ashwani@upi
Java equivalent + output (closest simple version)
Java enum can store fields, but it’s not as flexible as Rust’s “variants with different shapes”.
enum PaymentType { CASH, CARD, UPI }
public class Main {
public static void main(String[] args) {
PaymentType p1 = PaymentType.CASH;
PaymentType p2 = PaymentType.CARD;
PaymentType p3 = PaymentType.UPI;
System.out.println("Pay by " + p1);
System.out.println("Pay by " + p2 + " ending 1234");
System.out.println("Pay by " + p3 + " ashwani@upi");
}
}
Output
Pay by CASH
Pay by CARD ending 1234
Pay by UPI ashwani@upi
In Java, to match Rust’s enum power, you usually use sealed classes (Java 17+) or class hierarchies.
3)
trait — use when multiple types share the same behavior
When to use
Use trait/interface when:
Many types do the same action: pay(), draw(), export()
You want polymorphism without inheritance
You want generic code: “anything that can print a summary”
Rust code + output (trait + two structs)
trait Summary {
fn summary(&self) -> String;
}
struct Doctor {
name: String,
}
struct Hospital {
name: String,
}
impl Summary for Doctor {
fn summary(&self) -> String {
format!("Doctor: {}", self.name)
}
}
impl Summary for Hospital {
fn summary(&self) -> String {
format!("Hospital: {}", self.name)
}
}
fn print_summary(item: &impl Summary) {
println!("{}", item.summary());
}
fn main() {
let d = Doctor { name: "Dr. Mehta".to_string() };
let h = Hospital { name: "City Care".to_string() };
print_summary(&d);
print_summary(&h);
}
Output
Doctor: Dr. Mehta
Hospital: City Care
Java equivalent + output (interface)
interface Summary {
String summary();
}
class Doctor implements Summary {
String name;
Doctor(String name) { this.name = name; }
public String summary() { return "Doctor: " + name; }
}
class Hospital implements Summary {
String name;
Hospital(String name) { this.name = name; }
public String summary() { return "Hospital: " + name; }
}
public class Main {
static void printSummary(Summary item) {
System.out.println(item.summary());
}
public static void main(String[] args) {
Doctor d = new Doctor("Dr. Mehta");
Hospital h = new Hospital("City Care");
printSummary(d);
printSummary(h);
}
}
Output
Doctor: Dr. Mehta
Hospital: City Care
Quick comparison table
How Rust allows (multiple instances per enum variant)
enum Payment {
Cash,
Card { last4: String },
Upi(String),
}
fn main() {
let payments = vec![
Payment::Cash,
Payment::Card { last4: "1234".to_string() },
Payment::Card { last4: "5678".to_string() }, // another Card
Payment::Upi("ashwani@upi".to_string()),
Payment::Card { last4: "9999".to_string() }, // another Card
];
for p in payments {
match p {
Payment::Cash => println!("Pay by Cash"),
Payment::Card { last4 } => println!("Pay by Card ending {}", last4),
Payment::Upi(id) => println!("Pay by UPI {}", id),
}
}
}
Output
Pay by Cash
Pay by Card ending 1234
Pay by Card ending 5678
Pay by UPI ashwani@upi
Pay by Card ending 9999
🧠 Why this works (important concept)
Payment::Card { last4: "1234".to_string() }
Payment::Card { last4: "5678".to_string() }
These are two different values, just like:
Circle { r: 2 }
Circle { r: 5 }
Another Example
enum Payment {
Card { last4: String },
}
fn main() {
let p1 = Payment::Card { last4: "1234".to_string() };
let p2 = Payment::Card { last4: "5678".to_string() };
let p3 = Payment::Card { last4: "9999".to_string() };
for p in [p1, p2, p3] {
match p {
Payment::Card { last4 } => println!("Card ending {}", last4),
}
}
}
Output
Card ending 1234
Card ending 5678
Card ending 9999
Java enum = SINGLETONS (this is the key reason)
how Rust’s enum + match replaces classic inheritance-based polymorphism
1️⃣ Polymorphism (what it really means)
Polymorphism = one interface, many behaviors
“Different types respond to the same operation in their own way.”
Polymorphism using Inheritance (Java / Python style)
Java (Inheritance + Method Override)
abstract class Shape {
abstract double area();
}
class Circle extends Shape {
double r;
Circle(double r) { this.r = r; }
double area() {
return Math.PI * r * r;
}
}
class Rectangle extends Shape {
double w, h;
Rectangle(double w, double h) {
this.w = w; this.h = h;
}
double area() {
return w * h;
}
}
public class Main {
public static void main(String[] args) {
Shape s1 = new Circle(2);
Shape s2 = new Rectangle(3, 4);
System.out.println(s1.area());
System.out.println(s2.area());
}
}
Output
12.566370614359172
12.0
✔ Runtime polymorphism
❌ Inheritance hierarchy
❌ Possible invalid states
❌ Runtime dispatch cost
Python (Inheritance)
class Shape:
def area(self):
raise NotImplementedError
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14 * self.r * self.r
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h
shapes = [Circle(2), Rectangle(3, 4)]
for s in shapes:
print(s.area())
Output
12.56
12
✔ Very flexible
❌ No compile-time safety
❌ Errors only at runtime
2️⃣ Polymorphism in Rust using Traits (behavior-based)
Rust avoids inheritance.
Instead → trait = behavior contract
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
r: f64,
}
struct Rectangle {
w: f64,
h: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.14 * self.r * self.r
}
}
impl Shape for Rectangle {
fn area(&self) -> f64 {
self.w * self.h
}
}
fn print_area(s: &dyn Shape) {
println!("{}", s.area());
}
fn main() {
let c = Circle { r: 2.0 };
let r = Rectangle { w: 3.0, h: 4.0 };
print_area(&c);
print_area(&r);
}
Output
12.56
12
✔ Safe polymorphism
✔ No inheritance
✔ Explicit behavior
Rust’s enum + match (NO inheritance, NO traits)
This is the most important Rust concept.
Rust says:
“Instead of many classes, use one enum with variants.”
Polymorphism using enum + match (Rust-native)
enum Shape {
Circle { r: f64 },
Rectangle { w: f64, h: f64 },
}
fn area(s: &Shape) -> f64 {
match s {
Shape::Circle { r } => 3.14 * r * r,
Shape::Rectangle { w, h } => w * h,
}
}
fn main() {
let s1 = Shape::Circle { r: 2.0 };
let s2 = Shape::Rectangle { w: 3.0, h: 4.0 };
println!("{}", area(&s1));
println!("{}", area(&s2));
}
Output
12.56
12
✔ Compile-time exhaustive checking
✔ No inheritance tree
✔ No runtime dispatch
✔ Faster & safer
🔥 Why enum + match is special
Java / Python problem
You can forget to override a method
You can add a subclass and forget to handle it
Errors appear at runtime
Rust solution
enum Status { Open, Closed }
match status {
Status::Open => println!("Open"),
Status::Closed => println!("Closed"),
}
If you add:
Status::Pending
❌ Compiler ERROR until you handle it
Rust enums are much more powerful than Java/Python enums
Java / Python enum (simple labels)
enum Status { SUCCESS, FAILURE }
from enum import Enum
class Status(Enum):
SUCCESS = 1
FAILURE = 2
✔ Just named constants
❌ No attached data per variant
❌ Limited behavior
Rust enum (variants can hold data)
enum Result {
Ok(i32),
Err(String),
}
✔ Each variant can store different data
✔ Variants can have different shapes
✔ Checked at compile time
This single feature alone explains most of Rust’s enum usage.
2️⃣ Enums replace null (BIG reason)
Java / Python
String name = null; // runtime NPE risk
name = None # runtime error risk
Rust (no null)
enum Option<T> {
Some(T),
None,
}
Usage:
let name: Option<String> = Some("Ashwani".to_string());
match name {
Some(n) => println!("{}", n),
None => println!("No name"),
}
✔ Forces you to handle “missing value”
✔ No NullPointerException
✔ Compiler enforces safety
👉 This alone causes enums to appear everywhere
3️⃣ Enums replace exceptions (error handling)
Java
try {
readFile();
} catch (IOException e) {
// runtime path
}
Python
try:
read_file()
except IOError:
pass
✔ Flexible
❌ Errors can be ignored
❌ Hidden control flow
Rust
enum Result<T, E> {
Ok(T),
Err(E),
}
fn read_file() -> Result<String, String> {
Ok("file content".to_string())
}
✔ Errors are values
✔ Must be handled explicitly
✔ No hidden jumps
👉 That’s why you see Result everywhere
4️⃣ Enums replace inheritance & polymorphic class hierarchies
Java style
abstract class Shape {}
class Circle extends Shape {}
class Rectangle extends Shape {}
Rust style
enum Shape {
Circle { r: f64 },
Rectangle { w: f64, h: f64 },
}
Then:
match shape {
Shape::Circle { r } => ...
Shape::Rectangle { w, h } => ...
}
✔ No runtime dispatch
✔ Faster
✔ Exhaustive checking (no missing case)
5️⃣ Enums replace dynamic typing (Python-like behavior)
Python
data = [1, "hello", 3.5, True]
Rust
enum Value {
Int(i32),
Text(String),
Float(f64),
Bool(bool),
}
let data = vec![
Value::Int(1),
Value::Text("hello".to_string()),
Value::Float(3.5),
Value::Bool(true),
];
✔ Python flexibility
✔ Rust safety
✔ No runtime type errors
6️⃣ match + enum = compiler-enforced correctness
Rust forces exhaustive handling:
match status {
Status::Open => ...
Status::Closed => ...
}
If you add:
Status::Pending
👉 Compiler error until you handle it
Java / Python:
❌ No such protection
7️⃣ Why Java & Python don’t use enums this way
Top comments (0)