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! π¦
Share this article on LinkedIn.
No comments:
Post a Comment