本文内容
- 索引器语法
- 方案
- 总结
索引器类似于属性。 很多时候,创建索引器与创建属性所使用的编程语言特性是一样的。 索引器使属性可以被索引:使用一个或多个参数引用的属性。 这些参数为某些值集合提供索引。
1、索引器语法
可以通过变量名和方括号访问索引器。 将索引器参数放在方括号内:
var item = someObject["key"];
someObject["AnotherKey"] = item;
使用 this
关键字作为属性名声明索引器,并在方括号内声明参数。 此声明与前一段中所示的用法相匹配:
public int this[string key]
{
get { return storage.Find(key); }
set { storage.SetAt(key, value); }
}
从最初的示例中,可以看到属性语法和索引器语法之间的关系。 此类比在索引器的大部分语法规则中进行。 索引器可以使用任何有效的访问修饰符(public、protected internal、protected、internal、private 或 private protected)。 它们可能是密封、虚拟或抽象的。 与属性一样,可以在索引器中为 get 和 set 访问器指定不同的访问修饰符。 你还可以指定只读索引器(忽略 set 访问器)或只写索引器(忽略 get 访问器)。
属性的各种用法同样适用于索引器。 此规则的唯一例外是“自动实现属性”。 编译器无法始终为索引器生成正确的存储。
用于引用项的集合中的某个项的参数可区分索引器和属性。 只要每个索引器的参数列表是唯一的,就可以对一个类型定义多个索引器。 让我们来探讨可能在类定义中使用一个或多个索引器的不同场景。
2、方案
如果类型的 API 对集合进行建模,并且为集合定义了参数,则需要在此类型中定义索引器。 索引器可能直接映射到属于 .NET Core 框架一部分的集合类型,也可能不。 除了对集合进行建模,类型还有其他职责。 通过索引器可提供与类型的抽象化匹配的 API,而无需公开如何存储或计算此抽象化的值的内部细节。如下我们演练一些使用索引器的常见场景。
2.1 数组和矢量
创建索引器的一个最常见的场景是当类型对数组或矢量进行建模时。 可以创建一个索引器用于对已排序的数据列表进行建模。
创建自己的索引器的优点是你可以为集合定义存储以满足你的需求。 假设以下场景:类型对历史数据进行建模,并且此历史数据太大而无法立即加载到内存中。 需要根据使用情况加载和卸载集合的某些部分。 以下示例对此行为进行建模。 此示例报告存在多少数据点。 此示例按需创建页以存储部分数据。 此示例从内存中删除页,以便为较新的请求所需的页腾出空间。
public class DataSamples
{
private class Page
{
private readonly List<Measurements> pageData = new List<Measurements>();
private readonly int startingIndex;
private readonly int length;
private bool dirty;
private DateTime lastAccess;
public Page(int startingIndex, int length)
{
this.startingIndex = startingIndex;
this.length = length;
lastAccess = DateTime.Now;
// 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 => dirty;
public DateTime LastAccess => lastAccess;
}
private readonly int totalSize;
private readonly List<Page> pagesInMemory = new List<Page>();
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
类是私有嵌套类,不是公共接口的一部分。 向此类的任何用户隐藏这些详细信息。
2.2 字典
另一个常见场景是需要对字典或映射进行建模时。 当类型存储基于键(通常是文本键)的值时出现此情况。 本示例创建的字典将命令行参数映射到管理这些选项的 Lambda 表达式。 以下示例演示了两个类:ArgsActions
类将命令行选项映射到 Action
委托;ArgsProcessor
类在遇到此选项时使用 ArgsActions
执行每个 Action
。
public class ArgsProcessor
{
private readonly ArgsActions actions;
public ArgsProcessor(ArgsActions actions)
{
this.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 Dictionary<string, Action>();
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
访问器。 相反,设计使用公共方法来设置选项。
2.3 多维映射
可以创建使用多个参数的索引器。 此外,这些参数未限制为相同的类型。 请看以下两个示例。
第一个示例演示为 Mandelbrot 集合生成值的类。 索引器使用两个双精度型来定义平面 XY 上的一个点。 Get 访问器计算迭代的次数,直到确定某个点不在集合中。 如果达到最大迭代数,并且点在集合中,则返回类的 maxIterations 值。 (Mandelbrot 集合常用的计算机生成的图像定义迭代数量的颜色,以便确定一个点是否在集合外部。)
public class Mandelbrot
{
readonly private int maxIterations;
public Mandelbrot(int maxIterations)
{
this.maxIterations = 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))
{
var newX = x * x - y * y + x0;
y = 2 * x * y + y0;
x = newX;
iterations++;
}
return iterations;
}
}
}
Mandelbrot 集合在每个 (x,y) 坐标上为实际数值定义值。 这将定义一个字典,其中可能包含无限数目的值。 因此,集合后面没有任何存储。 相反,当代码调用 get
访问器时,此类计算每个点的值。 未使用任何基础存储。
请查看上一次索引器的使用,其中索引器采用多个不同类型的参数。 请考虑一个管理历史温度数据的程序。 此索引器使用一个城市和一个日期来设置或获取位置的高温和低温:
using DateMeasurements =
System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>;
using CityDataMeasurements =
System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>>;
public class HistoricalWeatherData
{
readonly CityDataMeasurements storage = new CityDataMeasurements();
public Measurements this[string city, DateTime date]
{
get
{
var cityData = default(DateMeasurements);
if (!storage.TryGetValue(city, out cityData))
throw new ArgumentOutOfRangeException(nameof(city), "City not found");
// strip out any time portion:
var index = date.Date;
var measure = default(Measurements);
if (cityData.TryGetValue(index, out measure))
return measure;
throw new ArgumentOutOfRangeException(nameof(date), "Date not found");
}
set
{
var cityData = default(DateMeasurements);
if (!storage.TryGetValue(city, out cityData))
{
cityData = new DateMeasurements();
storage.Add(city, cityData);
}
// Strip out any time portion:
var index = date.Date;
cityData[index] = value;
}
}
}
此示例创建的索引器将天气数据映射到两个不同的参数:城市(由 string
表示)和日期(由 DateTime
表示)。 内部存储使用两个 Dictionary
类来表示此二维字典。 公共 API 不再表示基础存储。 相反地,凭借索引器的语言特性可以创建表示抽象化的一个公共接口,即使基础存储必须使用不同的核心集合类型也是如此。
一些开发人员可能不熟悉此代码的两部分。 这两个 using
指令:
using DateMeasurements = System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>;
using CityDataMeasurements = System.Collections.Generic.Dictionary<string, System.Collections.Generic.Dictionary<System.DateTime, IndexersSamples.Common.Measurements>>;
为构造泛型类型创建别名。 通过这些语句,稍后代码可以使用更具描述性的 DateMeasurements
和 CityDataMeasurements
名称,而不是 Dictionary<DateTime, Measurements>
和 Dictionary<string, Dictionary<DateTime, Measurements> >
的泛型构造。 此构造要求在 =
符号右侧使用完全限定的类型名称。
另一项技术是对任何用于集合的索引的 DateTime
对象剥离时间部分。 .NET 不包含仅日期类型。 开发人员使用 DateTime
类型,但使用 Date
属性来确保这一天的任何 DateTime
对象是对等的。
3、总结
只要类中有类似于属性的元素就应创建索引器,此属性代表的不是一个值,而是值的集合,其中每一个项由一组参数标识。 这些参数可以唯一标识应引用的集合中的项。 索引器延伸了属性的概念,索引器中的一个成员被视为类外部的一个数据项,但又类似于内部的一个方法。 索引器允许参数在代表项的集合的属性中查找单个项。