[C#] Defining Generic Classes and Universal Range Checking with IComparable

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&lt;T&gt;)</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 b
  • a.CompareTo(b) == 0: a is equal to b
  • a.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.

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次