跳转到内容

D(编程语言)/d2/切片

50% developed
来自维基教科书,开放的书籍,为开放的世界

第 9 课:切片和深入了解动态数组

[编辑 | 编辑源代码]

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

视觉表示

[编辑 | 编辑源代码]

capacity 属性

[编辑 | 编辑源代码]

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 切片" 是一个极好的资源。
华夏公益教科书