65.9K
CodeProject 正在变化。 阅读更多。
Home

IIndexable:一个只读列表接口

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (15投票s)

2011 年 9 月 30 日

CDDL

4分钟阅读

viewsIcon

33338

IIndexable 接口在 .NET BCL 中并不存在,它提供了一种迭代和索引只读集合的方法。

注意:从 .NET 4.5 开始,本文已过时。 .NET 4.5 版本引入了 `IReadOnlyList` 接口,其成员与本文中描述的完全相同,因此本文已过时。此外,泛型类的协变和逆变也应用于基接口,例如 `IEnumerable`,这极大地简化了一些使用模式。如果您面向 .NET 4.5 或更高版本,并希望创建自己的只读列表包装器和扩展,则应该使用官方接口,因为集合 `List` 本身就实现了只读接口。我也正在将我的新项目迁移到使用它。

介绍 

在许多情况下,将集合设为只读非常方便。可以将只读项目列表传递到应用程序的其他部分,从而确保不会意外或恶意地修改它。`IIndexable` 是一个简单的接口,它恰好可以做到这一点:它只公开迭代器(`IEnumerable`)、只读索引器(只有 getter)和长度信息。但是,这种简单性赋予它一些有趣的特性,以及未来类似 LINQ 扩展的各种可能性。

背景

仅使用 .NET BCL 类型,将列表设为只读通常是通过将其包装到 `ReadOnlyCollection` 中来实现的。由于它只是围绕列表的轻量级包装器(不会复制源数据),因此它效率很高并且很好地完成了它的目的。不幸的是,它只是通过对每个会修改源列表的方法抛出 `NotSupportedException` 来实现只读语义,这意味着此类错误在编译时不会被发现。此外,它可以自由地传递给接受 `IList` 的任何代码(显然是因为它实现了该契约),这使得调用代码错误地认为它可以修改其内容。

另一方面,`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` 或数组(`T[]`)包装到 `IIndexable` 中,使用名为 `AsIndexable()` 的扩展方法

// 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` 的解析器接口允许我将二进制 FIFO 队列(或任何其他数据源)包装到 `IIndexable` 中,并通过在馈送解析器之前简单地偏移和限制数据来重用流中的各种解析器。这可能是另一篇文章的主题。

历史

  • 2011-09-30:初始版本。添加了一些实现细节。
© . All rights reserved.