次の方法で共有


インデクサー

クラスまたは構造体のインスタンスを配列やその他のコレクションのようにインデックス付けできる場合は、 インデクサー を定義します。 インデックス付き値は、型またはインスタンス メンバーを明示的に指定せずに設定または取得できます。 インデクサーは、アクセサーがパラメーターを受け取ることを除き、 プロパティ に似ています。

次の例では、値を割り当てて取得する get および set アクセサー メソッドを持つジェネリック クラスを定義します。

namespace Indexers;

public class SampleCollection<T>
{
   // Declare an array to store the data elements.
   private T[] arr = new T[100];

   // Define the indexer to allow client code to use [] notation.
   public T this[int i]
   {
      get => arr[i];
      set => arr[i] = value;
   }
}

前の例は、読み取り/書き込みインデクサーを示しています。 getアクセサーとsetアクセサーの両方が含まれています。 次の例に示すように、読み取り専用インデクサーを式形式のメンバーとして定義できます。

namespace Indexers;

public class ReadOnlySampleCollection<T>(params IEnumerable<T> items)
{
   // Declare an array to store the data elements.
   private T[] arr = [.. items];

   public T this[int i] => arr[i];

}

get キーワードは使用されません。=> では式の本文が導入されます。

インデクサーでは、 インデックス付き プロパティ (1 つ以上の引数を使用して参照されるプロパティ) が有効になります。 これらの引数は、値のコレクションにインデックスを提供します。

  • インデクサーを使用すると、オブジェクトのインデックスを配列と同様に作成できます。
  • get アクセサーは値を返します。 set アクセサーは値を割り当てます。
  • this キーワードはインデクサーを定義します。
  • value キーワードは、set アクセサーの引数です。
  • インデクサーでは、整数のインデックス値は必要ありません。特定の検索メカニズムを定義する方法はユーザー次第です。
  • インデクサーはオーバーロードできます。
  • インデクサーは、2 次元配列にアクセスする場合など、1 つ以上の仮パラメーターを持つことができます。
  • インデクサーpartialpartialで宣言できます。

プロパティの操作から学んだほとんどすべてのものをインデクサーに適用できます。 そのルールの唯一の例外は、 プロパティを自動的に実装します。 コンパイラは、インデクサーの適切なストレージを常に生成できるわけではありません。 各インデクサーの引数リストが一意である限り、1 つの型に対して複数のインデクサーを定義できます。

インデクサーの使用

型の インデクサー は、その API がコレクションをモデル化するときに定義します。 インデクサーは、.NET Core フレームワークの一部であるコレクション型に直接マップする必要はありません。 インデクサーを使用すると、その抽象化の値がどのように格納または計算されるかの内部の詳細を公開することなく、型の抽象化に一致する API を提供できます。

配列とベクター

型は、配列またはベクターをモデル化する場合があります。 独自のインデクサーを作成する利点は、ニーズに合わせてそのコレクションのストレージを定義できることです。 型で、一度にメモリに読み込むには大きすぎる履歴データをモデル化するシナリオを想像してください。 使用状況に基づいて、コレクションのセクションを読み込んでアンロードする必要があります。 次の例では、この動作をモデル化しています。 存在するデータ ポイントの数が報告されます。 必要に応じてデータのセクションを保持するページが作成されます。 メモリからページを削除して、より新しい要求で必要なページのスペースを作ります。

namespace Indexers;

public record Measurements(double HiTemp, double LoTemp, double AirPressure);

public class DataSamples
{
    private class Page
    {
        private readonly List<Measurements> pageData = new ();
        private readonly int _startingIndex;
        private readonly int _length;

        public Page(int startingIndex, int length)
        {
            _startingIndex = startingIndex;
            _length = length;

            // This stays as random stuff:
            var generator = new Random();
            for (int i = 0; i < length; i++)
            {
                var m = new Measurements(HiTemp: generator.Next(50, 95),
                    LoTemp: generator.Next(12, 49),
                    AirPressure: 28.0 + generator.NextDouble() * 4
                );
                pageData.Add(m);
            }
        }
        public bool HasItem(int index) =>
            ((index >= _startingIndex) &&
            (index < _startingIndex + _length));

        public Measurements this[int index]
        {
            get
            {
                LastAccess = DateTime.Now;
                return pageData[index - _startingIndex];
            }
            set
            {
                pageData[index - _startingIndex] = value;
                Dirty = true;
                LastAccess = DateTime.Now;
            }
        }

        public bool Dirty { get; private set; } = false;
        public DateTime LastAccess { get; set; } = DateTime.Now;
    }

    private readonly int _totalSize;
    private readonly List<Page> pagesInMemory = new ();

    public DataSamples(int totalSize)
    {
        this._totalSize = totalSize;
    }

    public Measurements this[int index]
    {
        get
        {
            if (index < 0) throw new IndexOutOfRangeException("Cannot index less than 0");
            if (index >= _totalSize) throw new IndexOutOfRangeException("Cannot index past the end of storage");

            var page = updateCachedPagesForAccess(index);
            return page[index];
        }
        set
        {
            if (index < 0) throw new IndexOutOfRangeException("Cannot index less than 0");
            if (index >= _totalSize) throw new IndexOutOfRangeException("Cannot index past the end of storage");
            var page = updateCachedPagesForAccess(index);

            page[index] = value;
        }
    }

    private Page updateCachedPagesForAccess(int index)
    {
        foreach (var p in pagesInMemory)
        {
            if (p.HasItem(index))
            {
                return p;
            }
        }
        var startingIndex = (index / 1000) * 1000;
        var newPage = new Page(startingIndex, 1000);
        addPageToCache(newPage);
        return newPage;
    }

    private void addPageToCache(Page p)
    {
        if (pagesInMemory.Count > 4)
        {
            // remove oldest non-dirty page:
            var oldest = pagesInMemory
                .Where(page => !page.Dirty)
                .OrderBy(page => page.LastAccess)
                .FirstOrDefault();
            // Note that this may keep more than 5 pages in memory
            // if too much is dirty
            if (oldest != null)
                pagesInMemory.Remove(oldest);
        }
        pagesInMemory.Add(p);
    }
}

この設計のイディオムに従って、メモリ内コレクションにデータのセット全体を読み込まない正当な理由がある任意の種類のコレクションをモデル化できます。 Page クラスは、パブリック インターフェイスの一部ではないプライベートの入れ子になったクラスであることに注意してください。 これらの詳細は、このクラスのユーザーには表示されません。

ディクショナリ

もう 1 つの一般的なシナリオは、ディクショナリまたはマップをモデル化する必要がある場合です。 このシナリオは、型がキー (テキスト キーの場合もあります) に基づいて値を格納する場合です。 この例では、コマンド ライン引数を、それらのオプションを管理する ラムダ式 にマップするディクショナリを作成します。 次の例は、コマンド ライン オプションを ArgsActions デリゲートにマップする System.Action クラスと、ArgsProcessorを使用してそのオプションが発生したときに各ArgsActionsを実行するActionの 2 つのクラスを示しています。

namespace Indexers;
public class ArgsProcessor
{
    private readonly ArgsActions _actions;

    public ArgsProcessor(ArgsActions actions)
    {
        _actions = actions;
    }

    public void Process(string[] args)
    {
        foreach (var arg in args)
        {
            _actions[arg]?.Invoke();
        }
    }

}
public class ArgsActions
{
    readonly private Dictionary<string, Action> _argsActions = new();

    public Action this[string s]
    {
        get
        {
            Action? action;
            Action defaultAction = () => { };
            return _argsActions.TryGetValue(s, out action) ? action : defaultAction;
        }
    }

    public void SetOption(string s, Action a)
    {
        _argsActions[s] = a;
    }
}

この例では、 ArgsAction コレクションは基になるコレクションに密接にマップされます。 getは、特定のオプションが構成されているかどうかを判断します。 その場合は、そのオプションに関連付けられている Action が返されます。 そうでない場合は、何も行わない Action が返されます。 パブリック アクセサーには、 set アクセサーは含まれません。 むしろ、この設計では、オプションを設定するためのパブリック メソッドが使用されています。

日付ベースのインデクサー

日付ベースのデータを操作する場合は、 DateTime または DateOnly をインデクサー キーとして使用できます。 日付部分のみが必要で、時間関連の複雑さを避けたい場合は、 DateOnly を使用します。 次の例は、 DateOnly を主インデクサー キーとして使用する温度追跡システムを示しています。

using System;
using System.Collections.Generic;

namespace Indexers;

public class DailyTemperatureData
{
    private readonly Dictionary<DateOnly, (double High, double Low)> _temperatureData = new();

    // Indexer using DateOnly for date-only scenarios
    public (double High, double Low) this[DateOnly date]
    {
        get
        {
            if (_temperatureData.TryGetValue(date, out var temp))
            {
                return temp;
            }
            throw new KeyNotFoundException($"No temperature data available for {date:yyyy-MM-dd}");
        }
        set
        {
            _temperatureData[date] = value;
        }
    }

    // Overload using DateTime for convenience, but only uses the date part
    public (double High, double Low) this[DateTime dateTime]
    {
        get => this[DateOnly.FromDateTime(dateTime)];
        set => this[DateOnly.FromDateTime(dateTime)] = value;
    }

    public bool HasDataFor(DateOnly date) => _temperatureData.ContainsKey(date);

    public IEnumerable<DateOnly> AvailableDates => _temperatureData.Keys;
}

この例では、 DateOnly インデクサーと DateTime インデクサーの両方を示します。 DateOnly インデクサーはプライマリ インターフェイスですが、DateTime オーバーロードでは、日付部分のみを抽出することで便利です。 この方法により、特定の日のすべての温度データが、時間コンポーネントに関係なく一貫して処理されます。

多次元マップ

複数の引数を使用するインデクサーを作成できます。 さらに、これらの引数は同じ型に制限されません。

次の例は、Mandelbrot セットの値を生成するクラスを示しています。 セットの背後にある数学の詳細については、 この記事を参照してください。 インデクサーは、2 つの倍精度浮動小数点数を使用して、X および Y 平面上の点を定義します。 get アクセサーは、ポイントがセットに含まれていないと判断されるまでの反復回数を計算します。 イテレーションの最大数に達すると、ポイントがセット内にあり、クラスの maxIterations 値が返されます。 (Mandelbrot セット用に普及しているコンピューターで生成されたイメージは、ポイントがセットの外側にあることを判断するために必要な反復回数の色を定義します)。

namespace Indexers;
public class Mandelbrot(int maxIterations)
{

    public int this[double x, double y]
    {
        get
        {
            var iterations = 0;
            var x0 = x;
            var y0 = y;

            while ((x * x + y * y < 4) &&
                (iterations < maxIterations))
            { 
                (x, y) = (x * x - y * y + x0, 2 * x * y + y0);
                iterations++;
            }
            return iterations;
        }
    }
}

Mandelbrot Set は、実数値のすべての (x,y) 座標で値を定義します。 これは、無限の数の値を含むことができるディクショナリを定義します。 そのため、セットの背後にストレージはありません。 代わりに、このクラスは、コードが get アクセサーを呼び出すときに、各ポイントの値を計算します。 基になるストレージは使用されません。

総括

インデクサーは、そのプロパティが単一の値ではなく値のセットを表す、プロパティに似た要素がクラス内に存在する場合は、いつでも作成します。 1 つ以上の引数は、個々の項目を識別します。 これらの引数は、セット内のどの項目を参照すべきかを一意に識別できます。 インデクサーはプロパティの概念を拡張します。メンバーはクラス外部からデータ項目のように扱われますが、内部ではメソッドとして扱われます。 インデクサーを使用すると、引数は、一連の項目を表すプロパティ内の 1 つの項目を検索できます。

インデクサーのサンプル フォルダーにアクセスできます。 ダウンロード手順については、サンプルとチュートリアルを参照してください。

C# 言語仕様

詳細については、C# 言語仕様インデクサーを参照してください。 言語仕様は、C# の構文と使用法の決定的なソースです。