【C#】ジェネリッククラスの定義とIComparableによる汎用的な範囲判定

C#におけるジェネリック(Generics)は、特定の型に依存しない汎用的なクラスやメソッドを定義するための機能です。これを活用することで、コードの重複を排除し、型安全性を維持したまま再利用性の高いロジックを構築できます。

今回は、数値や日付など「比較可能なあらゆる型」に対して、指定された範囲内に値が含まれるかどうかを判定するRange<T>クラスを題材に、ジェネリッククラスの定義方法と型制約(Constraints)の適用について解説します。


目次

ジェネリッククラスと型制約の概要

クラス定義において<T>のような型パラメータを使用することで、インスタンス化のタイミングで具体的な型を決定できるクラスを作成できます。

しかし、単に<T>としただけでは、その型がどのような機能を持っているかコンパイラは判断できません。そのため、範囲判定のように「大小比較」を行う場合、対象の型が比較可能であることを保証するためにwhere句を用いた型制約が必要となります。

実践的なコード例:汎用的な範囲判定クラス

以下のコードは、IComparable<T>インターフェースを実装している任意の型(int, double, decimal, DateTimeなど)に対して利用可能な範囲管理クラスの実装例です。

ここでは、商品の「価格帯(数値)」と、キャンペーンの「期間(日付)」という異なるデータ型に対して、同一のクラスを使用して判定を行っています。

using System;

namespace GenericImplementation
{
    class Program
    {
        static void Main()
        {
            // シナリオ1: 商品の価格帯(decimal型)での利用
            // 1,000円 ~ 5,000円 の範囲を定義
            var priceRange = new Range<decimal>(1000m, 5000m);

            decimal targetPrice = 3500m;
            
            // 範囲内かどうかの判定
            if (priceRange.Contains(targetPrice))
            {
                Console.WriteLine($"価格 {targetPrice:C} は、適正範囲内です。");
            }

            // シナリオ2: キャンペーン期間(DateTime型)での利用
            // 2025年1月1日 ~ 2025年1月31日 の期間を定義
            var campaignPeriod = new Range<DateTime>(
                new DateTime(2025, 1, 1),
                new DateTime(2025, 1, 31)
            );

            DateTime checkDate = new DateTime(2025, 2, 1); // 期間外の日付

            // 範囲外かどうかの判定
            if (campaignPeriod.IsOutside(checkDate))
            {
                Console.WriteLine($"日付 {checkDate:yyyy/MM/dd} は、キャンペーン期間外です。");
            }
        }
    }

    /// <summary>
    /// 汎用的な範囲を管理するジェネリッククラス
    /// </summary>
    /// <typeparam name="T">比較可能な型 (IComparable&lt;T&gt;を実装している必要がある)</typeparam>
    public class Range<T> where T : IComparable<T>
    {
        // 範囲の開始値(下限)と終了値(上限)
        public T Start { get; }
        public T End { get; }

        public Range(T start, T end)
        {
            // 安全のため、必要に応じて start <= end のチェックを入れることも検討可能です
            Start = start;
            End = end;
        }

        /// <summary>
        /// 指定された値が範囲内(Start以上、End以下)に含まれるか判定します。
        /// </summary>
        public bool Contains(T value)
        {
            // CompareToメソッドの戻り値:
            // 0未満: 自身より小さい
            // 0    : 等しい
            // 0より大: 自身より大きい
            
            // value >= Start && value <= End
            return value.CompareTo(Start) >= 0 && value.CompareTo(End) <= 0;
        }

        /// <summary>
        /// 指定された値が範囲外であるか判定します。
        /// </summary>
        public bool IsOutside(T value)
        {
            return !Contains(value);
        }
    }
}

実行結果

価格 ¥3,500 は、適正範囲内です。
日付 2025/02/01 は、キャンペーン期間外です。

技術的なポイント

1. 型制約(where T : IComparable<T>)

範囲判定を行うには、値の大小比較が必要です。しかし、ジェネリック型Tに対して直接演算子(<, >)を使用することはできません(数値型とは限らないため)。 そこで、where T : IComparable<T>という制約を付与します。これにより、型Tは必ずCompareToメソッドを持っていることが保証され、コンパイルが可能になります。

2. CompareToメソッドの挙動

IComparable<T>.CompareToメソッドは、比較対象との関係を整数で返します。

  • a.CompareTo(b) < 0 : a は b より小さい
  • a.CompareTo(b) == 0 : a と b は等しい
  • a.CompareTo(b) > 0 : a は b より大きい

上記のコードでは、value.CompareTo(Start) >= 0 で「valueがStart以上であること」を判定しています。

3. コードの再利用性

このRange<T>クラス一つで、int, long, double, decimal, DateTime, TimeSpan、さらにはstring(辞書順比較)など、IComparable<T>を実装するすべての型に対応できます。型ごとに個別の範囲クラスを作成する必要がなくなり、保守性が向上します。

まとめ

ジェネリッククラスを定義することで、特定のデータ型に縛られない柔軟な設計が可能になります。特に、型に対して特定の機能(比較や引数なしコンストラクタなど)を期待する場合は、where句による制約を適切に設定することが重要です。共通ロジックの抽象化において、ジェネリックは非常に強力な手段となります。

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

この記事を書いた人

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

目次