(2017/11/20 「参照としてのスライス」節を追加、Span構造体について言及)
スライス(slice)とは、列(sequence)から部分列(subsequence)を取り出す操作のことを呼ぶもの、とここでは定義しておきます。
スライスは色んな言語に実装されている機能で、特に文字列(string)から部分文字列(substring)を得る機能を実装している言語は多いでしょう。文字列に限定しないときスライスという言葉が用いられている気がします。どこ由来なのか知らないため定義もわかりません。
C#にもそれっぽい機能はありますが、ちょっと使いにくいので、Pythonのスライスに近い機能を実装してみます。
C#のスライス
C#ではスライスという言語機能は存在しません。文字列などに個別に部分列を得るメソッドを実装するという形で提供されます。
部分文字列ならSubstring()メソッドで取得できます。引数としては、部分文字列の開始インデックス、部分文字列の長さを与えます。長さを省略した場合は、指定インデックスから末尾まで取得します。
var result = "0123456789".Substring(5, 3); // "567"
同様に、ListならGetRange()メソッドで取得できます。部分列の取得なのにGetRangeという独特な名前のせいで存在を見落としがち。string.Substring()と違って、第2引数の省略版がありません。
var list = new List<int>{ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var result = list.GetRange(5, 3); // 5, 6, 7
さて、配列なんですが、部分列そのものを直接返してくれるメソッドがありません。必要なサイズの配列をnewして、そこにCopyTo()する形になります。こういった配列の微妙な使いにくさは何なんですかね。
var array = new int[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var result = new int[3];
Array.CopyTo(array, 5, result, 0, result.Length); // 5, 6, 7
Pythonのスライス
今回C#に実装したいスライス機能を紹介します。
Pythonではスライスを言語機能としてサポートしていて、シーケンス型(文字列やリストなど)に対して、角括弧の中にコロン区切りでインデックスを指定すると部分列を取り出せます。パラメータは開始位置と終了位置です。終了位置は切り出す最後の文字の次を指します。
result = '0123456789'[3:6] # '345'
Pythonの面白いところは、スライスのパラメータに負数を指定できるところです。負数を指定すると、末尾から辿った位置を指定したことになります。例えば、長さ5の列に対して、インデックス-1を指定すると、インデックス4を指定したことと同義です。
result = '0123456789'[-3:9] # '678'
result = '0123456789'[3:-3] # '3456'
result = '0123456789'[-3:-2] # '78'
このマイナス指定が割と便利なので欲しいなと思ったわけです。
ちなみに、Pythonの場合、スライスの3番目のパラメータとしてスキップ(インデックスの増分)を与えられますが、今回そこまでやらないです。
実装
拡張メソッドとして実装します。ついでに負数によるインデックス指定に対応したget/setも用意します。
文字列
public static class StringExtension
{
public static char get(this string str, int index)
{
if(index < 0) {
index += str.Length;
}
return str[index];
}
public static string slice(this string str, int begin)
{
if(begin < 0) {
begin += str.Length;
}
return str.Substring(begin);
}
public static string slice(this string str, int begin, int end)
{
if(begin < 0) {
begin += str.Length;
}
if(end < 0) {
end += str.Length;
}
var length = Math.Max(0, end - begin);
return str.Substring(begin, length);
}
}
配列
public static class ArrayExtension
{
public static T get<T>(this T[] array, int index)
{
if(index < 0) {
index += array.Length;
}
return array[index];
}
public static void set<T>(this T[] array, int index, T value)
{
if(index < 0) {
index += array.Length;
}
array[index] = value;
}
public static T[] slice<T>(this T[] array, int begin)
{
if(begin < 0) {
begin += array.Length;
}
return slice(array, begin, array.Length);
}
public static T[] slice<T>(this T[] array, int begin, int end)
{
if(begin < 0) {
begin += array.Length;
}
if(end < 0) {
end += array.Length;
}
var length = Math.Max(0, end - begin);
var newArray = new T[length];
Array.Copy(array, begin, newArray, 0, length);
return newArray;
}
}
List
public static class ListExtension
{
public static T get<T>(this IList<T> list, int index)
{
if(index < 0) {
index += list.Count;
}
return list[index];
}
public static void set<T>(this IList<T> list, int index, T value)
{
if(index < 0) {
index += list.Count;
}
list[index] = value;
}
public static List<T> slice<T>(this List<T> list, int begin)
{
if(begin < 0) {
begin += list.Count;
}
return slice(list, begin, list.Count);
}
public static List<T> slice<T>(this List<T> list, int begin, int end)
{
if(begin < 0) {
begin += list.Count;
}
if(end < 0) {
end += list.Count;
}
var length = Math.Max(0, end - begin);
return list.GetRange(begin, length);
}
}
参照としてのスライス(2017/11/20追加)
今回、スライスとは部分列を取り出すもの、と定義して、部分列をコピーとして取り出す機能をPython的なインターフェースで実装しました。
ただ、スライスを言語機能として持つ場合、たいてい元の列への参照であって、コピーではないと思います。実はPythonもそうです。スライスが参照として実装されていると、コピーが発生しない分、パフォーマンス的に優れています。
ちなみに、Pythonでは、下記のように部分列を他の列で置き換えすることもできます。
l = [x for x in range(10)]
print(l)
# => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
l[3:8] = [None]
print(l)
# => [0, 1, 2, None, 9]
要素数が異なっていても置き換えてしまうパワフルさ(リストは内部表現が配列なのでswapのコストは発生するでしょう)。但し、シーケンス型なら何でもいいわけではなくて、文字列はイミュータブルなので、スライスで変更を加えようとすると実行時エラーとなります。
さて、Pythonすごいねという話でもあるんですが、スライスに関してはC#でも取り組みがあることを最近知りました。
Span<T>
構造体が、配列や文字列に対するスライスに相当します。まだ.NET Frameworkには標準入りしていなくて、System.Memoryライブラリを導入すれば使えるそうです。
内部的には部分列の先頭要素への参照と、要素数を持ちます。内部配列を確保し直すことのあるListでは当然使えません。構造体ということは値型なので、スライス利用に伴うアロケーション(ヒープ確保)は発生しません。
なかなかスマートですが、静的型付けであるC#では使い所が難しいと思っています。配列や文字列を引数として持つメソッドに渡すなら、結局コピーを作るしかないためです。となると、結局、今回実装した部分列コピー機能が欲しくなってしまうのです。