跳轉到內容

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 切片" 是一個極好的資源。
華夏公益教科書