Generics in C# allow for the definition of versatile classes and methods that do not depend on specific types. By utilizing this feature, code duplication can be eliminated, and highly reusable logic can be built while maintaining type safety.
This article explains how to define generic classes and apply type constraints (where clause) using a Range<T> class as an example. This class determines whether a value is included within a specified range for any “comparable type” such as numbers or dates.
Overview of Generic Classes and Type Constraints
By using type parameters like <T> in a class definition, a class can be created where the specific type is determined at the time of instantiation.
However, simply using <T> does not allow the compiler to determine what functionality that type possesses. Therefore, when performing operations such as “size comparison” for range checking, a type constraint using the where clause is required to guarantee that the target type is comparable.
Practical Code Example: Universal Range Check Class
The following code is an implementation example of a range management class that can be used for any type implementing the IComparable<T> interface (such as int, double, decimal, DateTime, etc.).
Here, the same class is used to check different data types: a product’s “price range (numeric)” and a campaign’s “period (date).”
using System;
namespace GenericImplementation
{
class Program
{
static void Main()
{
// Scenario 1: Usage with product price range (decimal type)
// Define range from 1,000 to 5,000
var priceRange = new Range<decimal>(1000m, 5000m);
decimal targetPrice = 3500m;
// Check if within range
if (priceRange.Contains(targetPrice))
{
Console.WriteLine($"Price {targetPrice:C} is within the valid range.");
}
// Scenario 2: Usage with campaign period (DateTime type)
// Define period from Jan 1, 2025 to Jan 31, 2025
var campaignPeriod = new Range<DateTime>(
new DateTime(2025, 1, 1),
new DateTime(2025, 1, 31)
);
DateTime checkDate = new DateTime(2025, 2, 1); // Date outside the period
// Check if outside range
if (campaignPeriod.IsOutside(checkDate))
{
Console.WriteLine($"Date {checkDate:yyyy/MM/dd} is outside the campaign period.");
}
}
}
/// <summary>
/// Generic class for managing ranges
/// </summary>
/// <typeparam name="T">Comparable type (must implement IComparable<T>)</typeparam>
public class Range<T> where T : IComparable<T>
{
// Start (lower bound) and End (upper bound) of the range
public T Start { get; }
public T End { get; }
public Range(T start, T end)
{
// For safety, consider adding a check for start <= end here
Start = start;
End = end;
}
/// <summary>
/// Determines if the specified value is within the range (Start <= value <= End).
/// </summary>
public bool Contains(T value)
{
// CompareTo method return values:
// Less than 0: Smaller than the instance
// 0 : Equal
// Greater than 0: Larger than the instance
// Equivalent to: value >= Start && value <= End
return value.CompareTo(Start) >= 0 && value.CompareTo(End) <= 0;
}
/// <summary>
/// Determines if the specified value is outside the range.
/// </summary>
public bool IsOutside(T value)
{
return !Contains(value);
}
}
}
Execution Result
Price ¥3,500 is within the valid range.
Date 2025/02/01 is outside the campaign period.
(Note: Currency symbols depend on system locale.)
Technical Points
1. Type Constraints (where T : IComparable<T>)
To perform range checking, value comparison is necessary. However, operators (<, >) cannot be directly used on a generic type T because it is not necessarily a numeric type.
Therefore, the constraint where T : IComparable<T> is applied. This guarantees that type T possesses the CompareTo method, allowing compilation.
2. Behavior of the CompareTo Method
The IComparable<T>.CompareTo method returns an integer indicating the relationship with the comparison target.
a.CompareTo(b) < 0: a is smaller than ba.CompareTo(b) == 0: a is equal to ba.CompareTo(b) > 0: a is larger than b
In the code above, value.CompareTo(Start) >= 0 is used to determine that “value is greater than or equal to Start.”
3. Code Reusability
This single Range<T> class can handle all types that implement IComparable<T>, such as int, long, double, decimal, DateTime, TimeSpan, and even string (lexicographical comparison). This eliminates the need to create individual range classes for each type, improving maintainability.
Summary
Defining generic classes enables flexible design that is not tied to specific data types. When expecting specific functionality (such as comparison or parameterless constructors) from a type, it is important to properly set constraints using the where clause. Generics are a powerful tool for abstracting common logic.
