Generics

Generics in C# provide a way to create definitions for types and methods that can be parameterized over other types. This improves code reuse, type-safety and performance (e.g. avoid run-time casts). Consider the following example of a generic type that adds a timestamp to any value:

using System;

sealed record Timestamped<T>(DateTime Timestamp, T Value)
{
    public Timestamped(T value) : this(DateTime.UtcNow, value) { }
}

Rust also has generics as shown by the equivalent of the above:

use std::time::*;

struct Timestamped<T> { value: T, timestamp: SystemTime }

impl<T> Timestamped<T> {
    fn new(value: T) -> Self {
        Self { value, timestamp: SystemTime::now() }
    }
}

See also:

Generic type constraints

In C#, generic types can be constrained using the where clause. The following example shows such constraints in C#:

using System;

// Note: records automatically implement `IEquatable`. The following
// implementation shows this explicitly for a comparison to Rust.
sealed record Timestamped<T>(DateTime Timestamp, T Value) :
    IEquatable<Timestamped<T>>
    where T : IEquatable<T>
{
    public Timestamped(T value) : this(DateTime.UtcNow, value) { }

    public bool Equals(Timestamped<T>? other) =>
        other is { } someOther
        && Timestamp == someOther.Timestamp
        && Value.Equals(someOther.Value);

    public override int GetHashCode() => HashCode.Combine(Timestamp, Value);
}

The same can be achieved in Rust:

use std::time::*;

struct Timestamped<T> { value: T, timestamp: SystemTime }

impl<T> Timestamped<T> {
    fn new(value: T) -> Self {
        Self { value, timestamp: SystemTime::now() }
    }
}

impl<T> PartialEq for Timestamped<T>
    where T: PartialEq {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value && self.timestamp == other.timestamp
    }
}

Generic type constraints are called bounds in Rust.

In C# version, Timestamped<T> instances can only be created for T which implement IEquatable<T> themselves, but note that the Rust version is more flexible because it Timestamped<T> conditionally implements PartialEq. This means that Timestamped<T> instances can still be created for some non-equatable T, but then Timestamped<T> will not implement equality via PartialEq for such a T.

See also: