Sunday, 16 August 2026

Rust - Introduction to lambda style closures

Rust Language Primer - FirstOrDefault with Lambda style closures

If you're a C# developer exploring Rust, you’ll quickly notice that many concepts feel familiar — especially when working with collections, generics, and filtering values using lambda‑like syntax. In C#, you might write:

v.FirstOrDefault(x => x % 2 == 0);

I Rust, this is similar, note that we do not use extension methods like in C# and LINQ.

|x| x % 2 == 0

This article walks through how to build Rust equivalents of FirstOrDefault, including default values, nullable handling, and predicate‑based filtering. Along the way, you’ll see how Rust’s Option<T>, generics, and closures map nicely to concepts you already know from C#.

🧠 Rust Closures: The |x| Syntax

Rust closures use vertical bars to define parameters: C#: x => x % 2 == 0 Rust: |x| x % 2 == 0 |x| which for many who are familiar to some entry level Calculus reads like 'absolute' value - this is instead the 'x goes to' operator in Rust! Closures in Rust are strongly typed, safe, and have zero runtime overhead thanks to monomorphization.

🛠️ Implementing FirstOrDefault in Rust

The code shown in this article will show three variants
  • 1. A default‑based version (T::default())
  • 2. A nullable version using Option<T>
  • 3. A predicate‑based version similar to C# lambdas
  • Information: Each method is shown first, followed by usage examples.
📌 First variant - 1. first_or_default — Using T::default() This version returns the first element or a default value (like 0 for i32). It maps good to types like non-nullable integers and other numeric numbers as mentioned i32 but many other scenarios too.

/// Returns the first element of the slice, or T::default() if the slice is empty.
fn first_or_default<T: Default + Clone>(slice: &[T]) -> T {
	slice.first().cloned().unwrap_or_default()
}

📌 Second variant - 2. first_or_defaultv2 — Nullable Version Using Option<T> Rust does not have nulls — instead, it uses Option<T>. (Similar to Monad Option seen in Functional Programming)

/// Returns the first element wrapped in Option<T>, or None if empty.
fn first_or_defaultv2<T: Clone>(slice: &[T]) -> Option<T> {
	slice.first().cloned()   //notice the simplified syntax where T does not have to include Default trait and the omitting of unwrap_or_default()
}

This is the closest Rust equivalent to nullable types in C#. 📌 Third variant 3. first_or_default_where — Predicate‑Based Filtering Let's look at adding a predicate (filter) capability too for FirstOrDefault method. This is the Rust equivalent of: v.FirstOrDefault(x => x % 2 == 0);

/// Returns the first element matching the predicate, or None if none match.
fn first_or_default_where<T, F>(slice: &[T], predicate: F) -> Option<T>
where
T: Clone,
F: Fn(&T) -> bool,
{
	slice.iter().find(|x| predicate(x)).cloned()   // |x| is to be read as 'x goes to' similar to C# 'x => '
}

Notice the closure syntax: |x| predicate(x) — just like C# lambdas, but with pipes instead of parentheses.

🧪 Example Usage

Below is a complete example showing how these functions behave with vectors, empty slices, nullable values, and predicate filtering.

fn main() {

// A normal vector of i32 values
let v = vec![1, 2, 3];

// Uses T::default() (0 for i32) when the slice is empty
let first_number_nonempty_vector = first_or_default(&v);
println!("First number: {:?}", first_number_nonempty_vector);

// Explicitly typed empty vector
let empty: Vec<i32> = vec![];
let first_number_empty_vector = first_or_default(&empty);
println!("First number of empty vector: {:?} {:?}", empty, first_number_empty_vector);

// Nullable version: returns Option<T>
let first_number_empty_vector_nullable = first_or_defaultv2(&empty);
println!(
"First number (nullable) of empty vector {:?}: {:?}  . - Is the result None? : {}",
empty,
first_number_empty_vector_nullable,
first_number_empty_vector_nullable == None
);

// Another vector for predicate-based filtering
let s = vec![11, 3, 7, 13, 128, 5];

// Finds the first even number using a closure (lambda)
let first_even_number_nonempty_vector = first_or_default_where(&s, |x| x % 2 == 0);
println!("First event number of non-empty vector {:?}: {:?}", s, first_even_number_nonempty_vector);

// unwrap_or_else provides a safe fallback (0) if None
println!(
"First event number of non-empty vector {:?}: {:?}",
s,
first_even_number_nonempty_vector.unwrap_or_else(|| 0)
);
}

📤 Output

First number: 1
First number of empty vector: [] 0
First number (nullable) of empty vector []: None  . - Is the result None? : true
First event number of non-empty vector [11, 3, 7, 13, 128, 5]: Some(128)
First event number of non-empty vector [11, 3, 7, 13, 128, 5]: 128



🎯 Final Thoughts

Rust may look different at first, but if you're coming from C#, you’ll quickly recognize familiar patterns:
  • Option<T> behaves like nullable types
  • |x| closures behave like C# lambdas
  • Generics are powerful and zero‑cost
  • unwrap_or_default() and unwrap_or_else() feel like C#’s ?? operator or GetValueOrDefault()
  • Rust gives you the same expressive power — but with stronger safety guarantees and no runtime overhead.
If you're a C# developer exploring Rust, trying to implement C# Linq methods is a good start to familiarize yourself with the syntax. Happy (Rust) coding! 🦀

No comments:

Post a Comment