将编译时安全性添加到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>
相媲美。
因此,结构(或任何其他技术)最好在编译时类型安全检查之后进行优化。
用例:用例有:
- 通过类型安全索引器访问列表中的随机元素
- 在列表上的for循环,也使用类型安全索引器
- 在列表上使用foreach。List优化其GetEnumerator()以返回结构,从而避免分配。我想保留它
您应该能够使用SortedDictionary<TKey, TValue>
来执行您想要的操作。请确保在自定义密钥类型上实现Equals
和GetHashCode
。
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);