はじめに
以前公開したc#でヒープにも乗る固定長リストなんですが、インデクサーが無いので使い勝手が終わっていました。
例えば何らかの処理の結果のint集合から、奇数を除外したい場合を考えます。
var list = Buffer16<int>.List();// リスト作成
for(int i=0;i<16;i++)list.Add(i);// リストに何らかの値が追加
var span = list.AsSpan(); // 一回スパンを取得
for(int i = span.Length-1; 0 <= i; i--)
{
if(span[i]%2==1)list.RemoveRange(i,1);//リストから奇数を削除
}
上記のコードの問題点なのですが
・コード内にlistやらspanやら集合を表す名前が頻発して読みにくい
・要素数が変更する操作を行った場合、以前に取得したものが不正スパンになる。(アクセスしても問題ないが、実態を表してないスパンになっている)
インデクサーを実装しなかった理由
インデクサを実装する場合、以下のようなコードになると思うんです。
public ref T this[int index] => ref AsSpan()[index];
で、自分はこの関数呼び出しのコストとかが気になってしまう人間なんですよ。パフォーマンスに困ってないのに。そのくせ、コンパイルした時の比較とかはめんどくさがってしないんですけどね。
コード
リスト(c#)ってなんだと言うと、「バッファと要素数」を関連して管理するものだと言えます。データとref構造体の操作部に分けることで、問題が解決できました。
using System;
public struct FixedList<T, BufferT> where BufferT : IStructBuffer<T>
{
private int count;
private BufferT buffer;
public ListOperator<T> GetOperator()
=> new ListOperator<T>(buffer.AsSpan(), MemoryMarshal.CreateSpan<int>(ref count, 1));
}
public ref struct ListOperator<T>
{
private Span<int> countPointer;
private Span<T> buffer;
public int Count
{
get => countPointer[0];
set => countPointer[0] = value;
}
public ref T this[int i]
{
get
{
IndexCheck(i);
return ref buffer[i];
}
}
public ListOperator(Span<T> bufferSpan, Span<int> countPointer)
{
buffer = bufferSpan;
this.countPointer = countPointer;
}
public Span<T> AsSpan() => buffer.Slice(0, Count);
public Span<T> BufferSpan() => buffer;
public bool ContainsIndex(int index) => (uint)index < (uint)Count;
void IndexCheck(int index, string message = "")
{
// 参考 https://qiita.com/Kujiro/items/9569e91b942bcf9d528b
if ((uint)buffer.Length <= (uint)index) throw new IndexOutOfRangeException(message + " バッファ外にアクセス");
}
public void Clear() => Count = 0;
public void Add(in T add)
{
IndexCheck(Count, "追加後の最後のインデックス");
buffer[Count++] = add;
}
public void RemoveRange(int start, int range)
{
IndexCheck(start, "削除開始インデックス");
IndexCheck(start + range - 1, "削除する最後のインデックス");
Count -= range;
int backCount = buffer.Length - start - range;
Span<T> copyFrom = buffer.Slice(start + range, backCount);// 削除範囲含まない後ろの全部
Span<T> copyTo = buffer.Slice(start, backCount);// 削除開始を含む後ろ
copyFrom.CopyTo(copyTo);
}
public void Insert(int index, in T insert)
{
int add = 1;
int newCount = Count + add;
IndexCheck(index, "追加地点のインデックス");
IndexCheck(newCount - 1, "追加後の最後のインデックス");
Count = newCount;
int backCount = buffer.Length - index - add;
Span<T> copyFrom = buffer.Slice(index, backCount);
Span<T> copyTo = buffer.Slice(index + add, backCount);
copyFrom.CopyTo(copyTo);
buffer[index] = insert;
}
public void Insert(int index, in T t0, in T t1, in T t2)
{
int add = 3;
int newCount = Count + add;
IndexCheck(index, "追加地点のインデックス");
IndexCheck(newCount - 1, "追加後の最後のインデックス");
Count = newCount;
int backCount = buffer.Length - index - add;
Span<T> copyFrom = buffer.Slice(index, backCount);
Span<T> copyTo = buffer.Slice(index + add, backCount);
copyFrom.CopyTo(copyTo);
buffer[index++] = t0;
buffer[index++] = t1;
buffer[index] = t2;
}
}
使用例
public struct A
{
public int age;
public string name;
}
class Program
{
static void Main(string[] args)
{
FixedList<A, Buffer4<A>> list = new();
var ope = list.GetOperator();
for(int i = 0; i < 4; i++)
{
ope.Add(new A());
ope[i].age = i;// こんな風に要素にアクセスしたり
ope[i].name = i.ToString();
}
ope[0] = new A() { age = -9999, name = "zero" }; // こんな風に要素を書き換えられる
Console.WriteLine("リストの要素のデータ");
foreach (var a in ope.AsSpan())
{
Console.WriteLine($"name = {a.name}, age = {a.age}");
}
/*
リストの要素のデータ
name = zero, age = -9999
name = 1, age = 1
name = 2, age = 2
name = 3, age = 3
*/
ope.RemoveRange(1, 2);// 1と2を削除
Console.WriteLine("リストの要素のデータ");
foreach (var a in ope.AsSpan())
{
Console.WriteLine($"name = {a.name}, age = {a.age}");
}
/*
リストの要素のデータ
name = zero, age = -9999
name = 3, age = 3
*/
Console.WriteLine("リストが確保してる範囲のデータ");
foreach (var a in ope.BufferSpan())
{
Console.WriteLine($"name = {a.name}, age = {a.age}");
}
Console.WriteLine("リストの要素数"+list.count);
/*
リストが確保してる範囲のデータ
name = zero, age = -9999
name = 3, age = 3
name = 2, age = 2
name = 3, age = 3
リストの要素数2
*/
}
}