IIndexable:一个只读列表接口
IIndexable 接口在 .NET BCL 中并不存在,它提供了一种迭代和索引只读集合的方法。
注意:从 .NET 4.5 开始,本文已过时。 .NET 4.5 版本引入了 `IReadOnlyList
介绍
在许多情况下,将集合设为只读非常方便。可以将只读项目列表传递到应用程序的其他部分,从而确保不会意外或恶意地修改它。`IIndexable
背景
仅使用 .NET BCL 类型,将列表设为只读通常是通过将其包装到 `ReadOnlyCollection
另一方面,`IIndexable` 代表了集合索引的实际只读契约。由于数据没有被复制,而只是被包装,因此它也可以高效地进行投影(如 LINQ `Select` 方法)、偏移(类似于 LINQ `Skip`)或限制(类似于 `Take` 扩展方法)。此外,由于其简单性,如果提供的实现不足,则很容易创建自定义实现。
使用代码
首先,接口本身
如引言中所述,该接口相当简单
public interface IIndexable<T> : IEnumerable<T>
{
/// <summary>
/// Gets the value of the item at the specified index.
/// </summary>
/// <value></value>
T this[int index] { get; }
/// <summary>
/// Gets the length of this collection.
/// </summary>
/// <value>The length.</value>
int Length { get; }
}
创建只读列表
我们首先将 `IList
// source data is an array
var data = new int[] { 1, 2, 3, 4, 5 };
// using the extension method, a ListIndexable
// wrapper is created around the supplied array
IIndexable<int> readonlyData = data.AsIndexable();
// underlying data can be accessed
Console.WriteLine(readonlyData.Length); // prints 5
Console.WriteLine(readonlyData[3]); // prints 4
// new object is only a *wrapper*, which means
// changes to the source list are reflected in the
// read-only list
data[3] = 100;
Console.WriteLine(readonlyData[3]); // prints 100
// but the read-only collection cannot be changed
readonlyData[0] = 5; // this will not compile
公开列表的受限只读窗口
有时在将数据传递到代码的其他部分之前对其进行一些修剪并仅公开部分“窗口”非常有用。为此也有一些扩展方法,它们的好处在于数据**不会被复制**,而是索引偏移量在访问时(动态地)计算。
// we have a range of numbers in IIndexable
var list = Enumerable.Range(0, 100).ToList();
var items = list.AsIndexable();
// using the extension method, we can limit the
// read-only list to contain only items from 20th to 30th
var window = items.Offset(20, 10);
Console.WriteLine(window.Length); // prints 10
Console.WriteLine(window[5]); // prints 25
// we can do this as many times we need
// (each call to Offset creates a new wrapper)
var evenSmaller = window.Offset(3); // length can be omitted
Console.WriteLine(evenSmaller.Length); // prints 7
Console.WriteLine(evenSmaller[0]); // prints 23
// again, these are merely light wrappers;
// changing the original list will change all
// derived windows
list[23] = 200;
Console.WriteLine(evenSmaller[0]); // prints 200
// note that data cannot be changed by callers, and that all
// data outside the specified window is invisible to them
投影数据
LINQ 提供的最重要功能之一是能够在迭代项目时对其进行投影(使用 `IEnumerable.Select` 方法)。这里使用了相同的原理,它允许我们根据调用代码的需要延迟实例化对象,并且仍然提供 LINQ(`IEnumerable`)中缺少的长度信息和列表索引。
// start by defining a read only collection
var words = new ListIndexable<string>("the", "quick", "brown", "fox", "jumps");
// skip 2 items, then project
var uppercaseWords = words
.Offset(2)
.Select(w => w.ToUpper());
// note that the result of this projection is NOT
// an IEnumerable, but an IIndexable, which means
// we still have the length information and the
// posibility to index the collection
Console.WriteLine(uppercaseWords[0]); // prints "BROWN" in uppercase
实现细节
对于大多数操作,静态类 `IIndexableExt` 提供扩展方法,这些方法主要只是实例化适当的包装器。例如,要将 `List` 包装到 `IIndexable` 中,可以使用前面提到的 `AsIndexable` 方法。
public static class IIndexableExt
{
/// <summary>
/// Wraps this list into an IIndexable of same generic type.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">The list to wrap.</param>
/// <returns></returns>
public static IIndexable<T> AsIndexable<T>(this IList<T> list)
{
// simply instantiates the readonly wrapper
return new ListIndexable<T>(list);
}
}
这个基本包装器(`ListIndexable
/// <summary>
/// IIndexable wrapper for a IList object.
/// </summary>
/// <typeparam name="T"></typeparam>
public class ListIndexable<T> : IIndexable<T>
{
private readonly IList<T> _src;
#region Constructor overloads
// default constructor accepts an IList instance,
// which is referenced in the private _src field.
public ListIndexable(IList<T> src)
{ _src = src; }
// for convenience, there is also the "params"
// constructor, which allows creating an array
// of items by comma separating them
public ListIndexable(params T[] src)
{ _src = src; }
#endregion
#region IIndexed<T> Members
// indexer only exposes the "get" accessor
public T this[int index]
{ get { return _src[index]; } }
// length is taken from the source list
public int Length
{ get { return _src.Count; } }
#endregion
#region IEnumerable<T> Members
// this is where IEnumerable is implemented,
// by creating a dedicated IndexableEnumerator
// for each caller
public IEnumerator<T> GetEnumerator()
{
return new IndexableEnumerator<T>(this);
}
// non-generic interface needs to be implemented also
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return new IndexableEnumerator<T>(this);
}
#endregion
}
类似地,前面提到的 `Offset` 扩展方法创建 `OffsetIndexable` 的实例,它只不过是在访问列表时偏移索引。
// Creates a wrapper around an IIndexable, with
// a fixed offset.
public class OffsetIndexable<T> : IIndexable<T>
{
private readonly IIndexable<T> _src;
private readonly int _offset;
private readonly int _length;
#region Constructor overloads
// when creating the offset, length does not have to
// be explicitly set (if not, remainder of the list is
// used as the final length)
public OffsetIndexable(IIndexable<T> source, int offset)
{
source.ThrowIfNull("source");
if (offset < 0)
throw new ArgumentOutOfRangeException(
"offset", "offset cannot be negative");
_src = source;
_offset = offset;
_length = source.Length - offset;
if (_length < 0)
throw new ArgumentOutOfRangeException(
"length", "length cannot be negative");
}
public OffsetIndexable(IIndexable<T> source, int offset, int length)
{
source.ThrowIfNull("source");
if (length < 0)
throw new ArgumentOutOfRangeException(
"length", "length cannot be negative");
_src = source;
_offset = offset;
_length = length;
}
#endregion
#region IIndexer<T> Members
// this is where all the work gets done:
// - index is checked to see whether it's within bounds
// - source array is indexed by offseting the specified index
public T this[int index]
{
get
{
if (index < 0 || index >= _length)
throw new ArgumentOutOfRangeException(
"index",
"index must be a positive integer smaller than length");
return _src[_offset + index];
}
}
// length is precalculated inside constructor
public int Length
{
get { return _length; }
}
#endregion
#region IEnumerable<T> Members
// this part is taken from ListIndexable
#endregion
}
结论
我自己也遇到过几次需要真正只读集合的情况,并在几个 .NET 论坛上遇到了类似的问题。这个主题对于一篇完整的文章来说感觉有点琐碎,但是这个简单的解决方案对我来说很有用,我觉得我应该分享一下(它看起来像是一个轻松发布我的第一篇文章的好方法)。
这个集合在解析二进制数据时被证明特别方便。仅接受 `IIndexable
历史
- 2011-09-30:初始版本。添加了一些实现细节。