索引器

当类或结构的实例可以像数组或其他集合一样编制索引时,可以定义 索引器 。 可以设置或检索索引值,而无需显式指定类型或实例成员。 索引器类似于 属性 ,不同之处在于它们的访问器需要参数。

以下示例定义了一个泛型类,其中包含 getset 访问器方法来分配和检索值。

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;
   }
}

前面的示例显示了读/写索引器。 它同时包含 getset 访问器。 可以将只读索引器定义为表达式体成员,如以下示例所示。

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不使用关键字;=>引入表达式正文。

索引器启用 索引 属性:使用一个或多个参数引用的属性。 这些参数为某些值集合提供索引。

  • 索引器使对象能够像数组一样编制索引。
  • get 取值函数返回值。 set 取值函数分配值。
  • 关键字 this 定义索引器。
  • 关键字 value 用作 set 访问器的参数。
  • 索引器不需要整数索引值;它由你决定如何定义特定的查找机制。
  • 索引器可以重载。
  • 索引器可以具有一个或多个正式参数,例如,访问二维数组时。
  • 可以在partial类型中声明partial索引器

你几乎可以将你从处理属性中学到的所有内容应用到索引器中。 该规则的唯一例外是 自动实现属性。 编译器不能始终为索引器生成正确的存储。 只要每个索引器的参数列表是唯一的,就可以在类型上定义多个索引器。

索引器的用法

当类型的 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 类是不属于公共接口的专用嵌套类。 此类的用户无法看到这些详细信息。

字典

另一种常见方案是需要为字典或地图建模。 当类型存储基于键(可能是文本键)的值时出现此情况。 此示例创建一个字典,该字典将命令行参数映射到管理这些选项的 lambda 表达式 。 以下示例演示两个类:一个 ArgsActions 类用于将命令行选项映射到 System.Action 委托,另一个 ArgsProcessor 类在遇到该选项时使用 ArgsActions 执行每个 Action

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 访问器。 相反,设计是使用公共方法来设置选项。

基于日期的索引器

使用基于日期的数据时,可以使用或DateTimeDateOnly用作索引器键。 仅当需要日期部分并且想要避免与时间相关的复杂情况时使用 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;
}

此示例演示DateOnlyDateTime两个索引器。 虽然DateOnly索引器是主要接口,但DateTime重载通过仅提取日期部分提供了便利。 此方法可确保一致地处理给定一天的所有温度数据,而不考虑时间组件。

多维地图

可以创建使用多个参数的索引器。 此外,这些参数未限制为相同的类型。

下面的示例演示一个类,该类生成 Mandelbrot 集的值。 有关集背后的数学的详细信息,请阅读 本文。 索引器使用两个双精度型来定义平面 XY 上的一个点。 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 集合在每个 (x,y) 坐标上为实际数值定义值。 它定义可以包含无限数量的值的字典。 因此,布景后面没有存储空间。 相反,当代码调用 get 访问器时,此类计算每个点的值。 没有使用基础存储。

总结

只要类中有类似于属性的元素,该属性表示的不是单个值,而是一组值,都可以创建索引器。 一个或多个参数标识每个单独的项。 这些参数可以唯一标识应引用的集中的项。 索引器扩展 属性的概念,其中成员被视为类外部的数据项,但类似于内部方法。 索引器允许参数在表示一组项的属性中查找单个项。

可以访问 索引器的示例文件夹。 有关下载说明,请参阅 示例和教程

C# 语言规范

有关详细信息,请参阅 C# 语言规范中的索引器。 语言规范是 C# 语法和用法的明确来源。