D(编程语言)/d2/切片
D中的切片是该语言最强大和最有用的方面之一。本课实际上是对上一课的继续 - 您还将更深入地了解D的数组是如何工作的。
import std.stdio;
void writeln_middle(string msg)
{
writeln(msg[1 .. $ - 1]);
}
void main()
{
int[] a = [1,3,5,6];
a[0..2] = [6,5];
writeln(a); // [6, 5, 5, 6]
a[0..$] = [0,0,0,0];
writeln(a); // [0, 0, 0, 0]
a = a[0 .. 3];
writeln(a); // [0, 0, 0]
a ~= [3,5];
writeln(a); // [0, 0, 0, 3, 5]
int[] b;
b.length = 2;
b = a[0 .. $];
writeln(b.length); // 5
b[0] = 10;
writeln(b); // [10, 0, 0, 3, 5]
writeln(a); // [10, 0, 0, 3, 5]
writeln_middle("Phobos"); // hobo
writeln_middle("Phobos rocks");
}
您可以在 D 中使用此语法对数组进行切片
arr[start_index .. end_index]
end_index
处的元素不包含在切片中。请记住,动态数组只是具有指向第一个元素的指针和长度值的结构。对动态数组进行切片只是创建一个指向同一数组元素的新指针结构。
string a = "the entire part of the array";
string b = a[11 .. $]; // b = "part of the array"
// b points to the last 17 elements of a
// If you modify individual elements of b, a will also
// change since they point to the same underlying array!
请注意,$
会自动替换为被切片的数组的长度。以下三行是等效的,并且都创建了数组arr
的整个切片。
char[] a = arr[0 .. $];
char[] a = arr[0 .. arr.length];
char[] a = arr[]; // shorthand for the above
D中的所有动态数组都有一个.capacity
属性。它是可以在不将数组移到其他位置(重新分配)的情况下追加到该数组的最大元素数量。
int[] a = [1,2,3,45];
writeln("Ptr: ", a.ptr);
writeln("Capacity: ", a.capacity);
a.length = a.capacity; // the array reaches maximum length
writeln("Ptr: ", a.ptr, "\nCapacity: ", a.capacity); // Still the same
a ~= 1; // array has exceeded its capacity
// it has either been moved to a spot in memory with more space
// or the memory space has been extended
// if the former is true, then a.ptr is changed.
writeln("Capacity: ", a.capacity); // Increased
为了提高效率,确保追加和连接不会导致过多重新分配是有好处的,因为重新分配动态数组是一个昂贵的过程。以下代码可能会最多重新分配 5 次
int[] a = [];
a ~= new int[10];
a ~= [1,2,3,4,5,6,7,8,9];
a ~= a;
a ~= new int[20];
a ~= new int[30];
确保数组capacity
在开始时足够大,以允许在稍后进行高效的非重新分配数组追加和连接,如果性能是一个问题的话。您无法修改.capacity
属性。您只允许修改长度,或者使用reserve
函数。
int[] a = [1,2,3,45];
a.reserve(10); // if a's capacity is more than 10, nothing is done
// else a is reallocated so that it has a capacity of at least 10
请记住,D的数组是按值传递给函数的。当静态数组被传递时,整个数组被复制。当动态数组被传递时,只有指向底层数组的指针和长度的结构被复制 - 底层数组没有被复制。
import std.stdio;
int[] a = [1,2,3];
void function1(int[] arr)
{
assert(arr.ptr == a.ptr); // They are the same
// But the arr is not the same as a
// If arr's .length is modified, a is unchanged.
// both arr and a's .ptr refer to the same underlying array
// so if you wrote: arr[0] = 0;
// both arr and a would show the change, because they are both
// references to the same array.
// what if you forced arr to reallocate?
arr.length = 200; // most likely will reallocate
// now arr and a refer to different arrays
// a refers to the original one, but
// arr refers to the array that's reallocated to a new spot
arr[0] = 0;
writeln(arr[0]); // 0
writeln(a[0]); // 1
}
void main()
{
function1(a);
}
正如您所看到的,如果您将动态数组传递给如下所示的函数,则有几种可能性
void f(int[] arr)
{
arr.length = arr.length + 10;
arr[0] += 10;
}
- 第一种可能性:数组的容量足够大,可以容纳调整大小,因此没有发生重新分配。原始底层数组的第一个元素被修改了。
- 第二种可能性:数组的容量不足以容纳调整大小,但 D 的内存管理能够在不复制整个数组的情况下扩展内存空间。原始底层数组的第一个元素被修改了。
- 第三种可能性:数组的容量不足以容纳调整大小。D的内存管理不得不将底层数组重新分配到内存中的一个全新的空间。原始底层数组的第一个元素没有被修改。
如果您想确保以下内容有效,该怎么办?
int[] a = [0,0,0];
f(a);
assert(a[0] == 10);
只需更改函数f
,使其按引用传递动态数组
void f(ref int[] arr)
{
arr.length = arr.length + 10;
arr[0] += 10;
}
当您对动态数组进行切片,然后追加到该切片时,该切片是否被重新分配取决于切片结束的位置。如果切片在原始数组数据的中间结束,那么追加到该切片会导致重新分配。
int[] a = [1,2,3,4];
auto b = a[1 .. 3];
writeln(b.capacity); // 0
// b cannot possibly be appended
// without overwriting elements of a
// therefore, its capacity is 0
// any append would cause reallocation
假设您对动态数组进行切片,并且该切片在动态数组结束的地方结束。如果将动态数组追加到该切片,以便该切片不再在动态数组数据结束的地方结束,会发生什么?
int[] a = [1,2,3,4];
writeln(a.capacity); // 7
auto b = a[1 .. 4];
writeln(b.capacity); // 6
a ~= 5; // whoops!
// now the slice b does *not* end at the end of a
writeln(a.capacity); // 7
writeln(b.capacity); // 0
切片的.capacity
属性确实取决于对同一数据的其他引用。
赋值到切片看起来像这样
a[0 .. 10] = b
您正在将 b
赋值到 a
的一个切片。您实际上在过去两节课中已经见过赋值到切片,甚至在您学习切片之前。还记得这个吗?
int[] a = [1,2,3];
a[] = 3;
请记住,a[]
是 a[0 .. $]
的简写。当您将 int[]
切片赋值给单个 int
值时,该 int
值将被赋值给该切片中的所有元素。赋值到切片总是会导致数据被复制。
int[4] a = [0,0,0,0];
int[] b = new int[4];
b[] = a; // Assigning an array to a slice
// this guarantees array-copying
a[0] = 10000;
writeln(b[0]); // still 0
注意!无论何时使用赋值到切片,左右两边的 .length
值必须匹配!如果不匹配,将出现运行时错误!
int[] a = new int[1];
a[] = [4,4,4,4]; // Runtime error!
您还必须确保左右切片不重叠。
int[] s = [1,2,3,4,5];
s[0 .. 3] = s[1 .. 4]; // Runtime error! Overlapping Array Copy
假设您想将数组中每个整数元素都翻倍。使用 D 的向量操作语法,您可以编写以下任何一种
int[] a = [1,2,3,4];
a[] = a[] * 2; // each element in the slice is multiplied by 2
a[0 .. $] = a[0 .. $] * 2; // more explicit
a[] *= 2 // same thing
同样,如果您想执行以下操作: [1, 2, 3, 4] (int[] a) + [3, 1, 3, 1] (int[] b) = [4, 3, 6, 5] 您将这样写
int[] a = [1, 2, 3, 4];
int[] b = [3, 1, 3, 1];
a[] += b[]; // same as a[] = a[] + b[];
就像赋值到切片一样,您必须确保向量操作的左右两边具有匹配的长度,并且切片不重叠。如果您不遵守此规则,结果将是未定义的(既不会出现运行时错误,也不会出现编译时错误)。
您可以通过编写第一个参数为数组的函数来定义自己的数组属性。
void foo(int[] a, int b)
{
// do stuff
}
void eggs(int[] a)
{
// do stuff
}
void main()
{
int[] a;
foo(a, 1);
a.foo(1); // means the same thing
eggs(a);
a.eggs; // you can omit the parentheses
// (only when there are no arguments)
}
- 如果您想了解更多信息,史蒂文·施韦格霍夫 (Steven Schveighoffer) 的文章 "D 切片" 是一个极好的资源。