Exploring Rust’s Structs, Traits, Enums, and Collections
A Node.js Developer’s Guide about Rust
In our previous article, we discussed Rust powerful type system from a Node.js developer’s perspective. You can find the article here:
Building upon our previous article, which introduced Rust’s powerful type system, in this article, we’ll dive into Rust’s core features: structs, traits, enums, and collections, with examples and comparisons to Node.js.
Structs
In Rust, structs are custom data types that allow you to group together related pieces of data. They’re similar to JavaScript objects in terms of functionality but have a different syntax.
Rust Struct Example:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("user@example.com"),
username: String::from("username123"),
active: true,
sign_in_count: 1,
};
}
Node.js Object Example:
class User {
constructor(email, username, active, signInCount) {
this.email = email;
this.username = username;
this.active = active;
this.signInCount = signInCount;
}
}
const user1 = new User("user@example.com", "username123", true, 1);
In the Rust example, we define a User
struct with fields and their respective types. In the main function, we create an instance of the User
struct by providing values for each field. In the Node.js example, we define a User
class with a constructor and create an instance using the new
keyword.
Traits
Traits in Rust are similar to interfaces in other languages, allowing you to define shared behavior across different types. They’re useful for implementing polymorphism and can be compared to JavaScript’s prototype-based inheritance.
Rust Trait Example:
trait Speak {
fn speak(&self);
}
struct Human {
name: String,
}
struct Dog {
name: String,
}
impl Speak for Human {
fn speak(&self) {
println!("Hello, my name is {}.", self.name);
}
}
impl Speak for Dog {
fn speak(&self) {
println!("Woof! My name is {}.", self.name);
}
}
fn main() {
let human = Human { name: String::from("Alice") };
let dog = Dog { name: String::from("Buddy") };
human.speak();
dog.speak();
}
Node.js Interface Example:
class Speakable {
speak() {
throw new Error("speak method not implemented");
}
}
class Human extends Speakable {
constructor(name) {
super();
this.name = name;
}
speak() {
console.log(`Hello, my name is ${this.name}.`);
}
}
class Dog extends Speakable {
constructor(name) {
super();
this.name = name;
}
speak() {
console.log(`Woof! My name is ${this.name}.`);
}
}
const human = new Human("Alice");
const dog = new Dog("Buddy");
human.speak();
dog.speak();
In the Rust example, we define a Speak
trait with a single method, speak
. We then define two structs, Human
and Dog
, and implement the Speak
trait for each of them. In the Node.js example, we achieve a similar result using class inheritance.
Enums
Enums in Rust allow you to define a type that can represent one of several variants. This is useful for creating data structures that can hold different types of data. In JavaScript, you might achieve similar functionality using tagged unions or discriminated unions.
Rust Enum Example:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn process_message(message: Message) {
match message {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to x: {}, y: {}", x, y),
Message::Write(text) => println!("Write: {}", text),
Message::ChangeColor(r, g, b) => println!("Change color to red: {}, green: {}, blue: {}", r, g, b),
}
}
fn main() {
let message = Message::Write(String::from("Hello, Rust!"));
process_message(message);
}
Node.js Discriminated Union Example:
class Message {
constructor(type) {
this.type = type;
}
}
class Quit extends Message {
constructor() {
super("Quit");
}
}
class Move extends Message {
constructor(x, y) {
super("Move");
this.x = x;
this.y = y;
}
}
class Write extends Message {
constructor(text) {
super("Write");
this.text = text;
}
}
class ChangeColor extends Message {
constructor(r, g, b) {
super("ChangeColor");
this.r = r;
this.g = g;
this.b = b;
}
}
function processMessage(message) {
switch (message.type) {
case "Quit":
console.log("Quit");
break;
case "Move":
console.log(`Move to x: ${message.x}, y: ${message.y}`);
break;
case "Write":
console.log(`Write: ${message.text}`);
break;
case "ChangeColor":
console.log(`Change color to red: ${message.r}, green: ${message.g}, blue: ${message.b}`);
break;
}
}
const message = new Write("Hello, Node.js!");
processMessage(message);
In the Rust example, we define an enum Message
with four variants, each representing a different kind of message. We then create a process_message
function that uses pattern matching to handle each variant. In the Node.js example, we use a base class Message
and create subclasses for each variant, using the type
property for discriminated union functionality.
Collections
Rust offers several built-in collection types for storing and organizing data, such as Vec
(a growable array), HashMap
(a key-value store), and HashSet
(a set of unique values). In Node.js, you can use native data structures like Array
, Map
, and Set
for similar purposes.
Rust Collections Example:
use std::collections::{HashMap, HashSet};
fn main() {
let mut vec = Vec::new();
vec.push(1);
vec.push(2);
vec.push(3);
let mut hashmap = HashMap::new();
hashmap.insert(String::from("Blue"), 10);
hashmap.insert(String::from("Yellow"), 20);
let mut hashset = HashSet::new();
hashset.insert(1);
hashset.insert(2);
hashset.insert(3);
}
Node.js Collections Example:
const array = [1, 2, 3];
const map = new Map();
map.set("Blue", 10);
map.set("Yellow", 20);
const set = new Set([1, 2, 3]);
In the Rust example, we import and create a Vec
, a HashMap
, and a HashSet
from the standard library. Afterward, we insert values into each of these collections. Meanwhile, the Node.js example demonstrates how to achieve similar functionality using native data structures like Array
, Map
, and Set
.
Conclusion
Throughout this article, we have explored some of Rust’s essential features, including structs, traits, enums, and collections, by drawing comparisons to Node.js concepts. Our aim was to provide you with a deeper understanding of Rust’s inner workings and how you can harness its features effectively. As a Node.js developer, learning Rust can broaden your perspective, improve your problem-solving skills, enhance performance, and enable you to write safer and more concurrent code. Happy coding!