将编译时安全性添加到C#列表索引器中是可能的

本文关键字:索引 列表 编译 安全性 添加 | 更新日期: 2023-09-27 17:59:37

[注意:这个问题吸引了运行时性能较低的解决方案,所以这里值得强调的是,除了类型安全之外,性能也是关键。有关更多性能信息,请参阅末尾的注意。]

我在一个相当复杂的算法中使用了List<int>的几个不同实例。

其中一些列表包含彼此的索引,即它们提供了一定程度的间接性。

我已经修复了访问列表时使用错误索引器导致的一些错误。因为所有列表都是相同的类型,即List<int>,所以编译器根本不提供任何类型安全性。例如:

// The below statement is wrong - it should be list1[list2[x]], 
// as x is an index into list2, not list1. 
// list2 returns indexes into list1.
// But the compiler is oblivious to this.
//
var f = list1[x]; 

因此,我开始思考,通过在每个只包装一个整数的列表中使用强类型索引,可以增加一定程度的类型安全性:

/// An index into the first list
struct Index1
{
    public int Value { get; set; }
}
/// An index into the second list
struct Index2
{
    public int Value { get; set; }
}

然后,声明正确索引类型的变量将在编译时捕获一类错误。(这不是万无一失的,这不是我想要的——更好就足够了。)

不幸的是,通用List<T>不能提供使用自定义索引类型的方法——索引器总是int。

还有其他方法可以完成我想要做的事情吗?我想到了一个定制的系列——我不介意付出努力,这很可能会有回报。但我想不出一个可以像图中那样使用的。(当然,我可以为每个索引器创建一个单独的集合类型——但如果可能的话,我想使用一个新的集合类型,因为否则代码重复就会成为一个问题。)

性能::算法被重写为使用列表来提高性能。我们甚至考虑使用数组,因为少了一个边界检查。因此,任何提出的解决方案都应该具有出色的运行时性能——至少可以与List<T>相媲美。

因此,结构(或任何其他技术)最好在编译时类型安全检查之后进行优化。

用例:用例有:

  1. 通过类型安全索引器访问列表中的随机元素
  2. 在列表上的for循环,也使用类型安全索引器
  3. 在列表上使用foreach。List优化其GetEnumerator()以返回结构,从而避免分配。我想保留它

将编译时安全性添加到C#列表索引器中是可能的

您应该能够使用SortedDictionary<TKey, TValue>来执行您想要的操作。请确保在自定义密钥类型上实现EqualsGetHashCode

SortedDictionary允许在保持秩序的同时对元素进行快速且类型安全的随机访问。

若您需要列表语义来避免重写太多代码,那个么创建一个以SortedDictionary作为后备存储的类似列表的类型就不难了。

我认为你遇到的根本问题是,对于你的结构,一个集合不可能采用泛型参数,然后在两个结构上都不添加一些接口(导致虚拟调用),在集合中有一个Func<TIndex, int>(就像Jacob的答案一样),或者使用一个更复杂的集合来抽象"索引"的概念,就无法获得Value

为每个Index*实现完整集合的直接方法并不一定会导致代码重复。几种方法是代码生成或IL生成。下面是一个设计时T4模板,它为每个索引结构生成代码,并带有一个带索引器的类:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ output extension=".cs" #>
using System.Collections.Generic;
<#
string[] indexTypeNames = { "Index1", "Index2" };
foreach (var name in indexTypeNames)
{
    string collectionName = name + "List";
#>
public struct <#= name #>
{
    public int Value { get; set; }
}
public class <#= collectionName #><T>
{
    private List<T> _inner = new List<T>();
    public T this[<#= name #> index]
    {
        get { return _inner[index.Value]; }
        set { _inner[index.Value] = value; }
    }
    // Other desired IList<T> methods.
}
<#
}
#>

构建T4模板时,它会生成以下代码文件:

using System.Collections.Generic;
public struct Index1
{
    public int Value { get; set; }
}
public class Index1List<T>
{
    private List<T> _inner = new List<T>();
    public T this[Index1 index]
    {
        get { return _inner[index.Value]; }
        set { _inner[index.Value] = value; }
    }
    // Other desired IList<T> methods.
}
public struct Index2
{
    public int Value { get; set; }
}
public class Index2List<T>
{
    private List<T> _inner = new List<T>();
    public T this[Index2 index]
    {
        get { return _inner[index.Value]; }
        set { _inner[index.Value] = value; }
    }
    // Other desired IList<T> methods.
}

显然,您希望在集合上实现接口,以便它们具有自然的API,但很清楚如何添加这些接口。

由于代码是在设计时生成的,因此重复不是维护问题。然而,我会将代码gen和IL gen归类为现有类型系统无法表达的解决方案,所以我也希望看到只使用泛型的更好答案。

也许您可以用提供通用索引类型的东西包装一个类似列表的集合类:

public class IndexedCollection<TIndex, TValue> : IList<TValue>
{
    private List<TValue> _innerList;
    private Func<TIndex, int> _getIndex;
    public IndexedCollection(Func<TIndex, int> getIndex)
    {
        this._getIndex = getIndex;
        this._innerList = new List<TValue>();
    }
    new public TValue this[TIndex indexObj]
    {
        get 
        {
            var realIndex = this._getIndex(indexObj);
            return _innerList[realIndex];
        }
        set 
        {
            _innerList[this._getIndex(indexObj)] = value;
        }
    }
    // All the other members
}

然后像这样实例化:

var list1 = new IndexedCollection<Index1, Foo>(i => i.Value);
var list2 = new IndexedCollection<Index2, Foo>(i => i.Value);