Đây có phải là hành vi mảng động Delphi dự kiến ​​không


8

Câu hỏi là - làm thế nào các mảng động được Delphi quản lý nội bộ khi chúng được đặt làm thành viên của lớp? Chúng được sao chép hoặc thông qua tham chiếu? Delphi 10.3.3 được sử dụng.

Các UpdateArrayphương pháp xóa phần tử đầu tiên từ mảng. Nhưng độ dài mảng vẫn ở mức 2. UpdateArrayWithParamPhương thức cũng xóa phần tử đầu tiên khỏi mảng. Nhưng độ dài mảng được giảm chính xác xuống 1.

Đây là một mẫu mã:

interface

type
  TSomeRec = record
      Name: string;
  end;
  TSomeRecArray = array of TSomeRec;

  TSomeRecUpdate = class
    Arr: TSomeRecArray;
    procedure UpdateArray;
    procedure UpdateArrayWithParam(var ParamArray: TSomeRecArray);
  end;

implementation

procedure TSomeRecUpdate.UpdateArray;
begin
    Delete(Arr, 0, 1);
end;

procedure TSomeRecUpdate.UpdateArrayWithParam(var ParamArray: TSomeRecArray);
begin
    Delete(ParamArray, 0, 1);
end;

procedure Test;
var r: TSomeRec;
    lArr: TSomeRecArray;
    recUpdate: TSomeRecUpdate;
begin
    lArr := [];

    r.Name := 'abc';
    lArr := lArr + [r];
    r.Name := 'def';
    lArr := lArr + [r];

    recUpdate := TSomeRecUpdate.Create;
    recUpdate.Arr := lArr;
    recUpdate.UpdateArray;
    //(('def'), ('def')) <=== this is the result of copy watch value, WHY two values?

    lArr := [];

    r.Name := 'abc';
    lArr := lArr + [r];
    r.Name := 'def';
    lArr := lArr + [r];

    recUpdate.UpdateArrayWithParam(lArr);

    //(('def')) <=== this is the result of copy watch value - WORKS

    recUpdate.Free;
end;

1
Mảng động được thông qua tham chiếu. Chúng được tính tham chiếu và được quản lý bởi trình biên dịch. Các tài liệu là khá tốt. (Và các chi tiết .)
Andreas Rejbrand

Vậy thì tại sao độ dài mảng không được cập nhật sau khi UpdateArray được gọi?
dwrbudr

Không, không. Sau khi recUpdate.UpdateArray được gọi, Độ dài (lArr) là 2
dwrbudr

Đó là vì Deletethủ tục. Nó phải phân bổ lại mảng động, và vì vậy tất cả các con trỏ tới nó "phải" di chuyển. Nhưng nó chỉ biết một trong những gợi ý này, cụ thể là, cái bạn đưa cho nó.
Andreas Rejbrand

Tôi đã phân tích vấn đề và phải thừa nhận tôi không thể giải thích nó. Nó cảm thấy như một lỗi. Nhưng có lẽ David hoặc Remy hoặc người khác biết nhiều về điều này hơn tôi.
Andreas Rejbrand

Câu trả lời:


8

Đây là một câu hỏi thú vị!

Deletethay đổi độ dài của mảng động - cũng như SetLengthvậy - nó phải phân bổ lại mảng động. Và nó cũng thay đổi con trỏ được đặt cho vị trí mới này trong bộ nhớ. Nhưng rõ ràng nó không thể thay đổi bất kỳ con trỏ nào khác sang mảng động cũ.

Vì vậy, cần giảm số tham chiếu của mảng động cũ và tạo một mảng động mới với số tham chiếu là 1. Con trỏ được cung cấp Deletesẽ được đặt thành mảng động mới này.

Do đó, mảng động cũ nên được xử lý (tất nhiên ngoại trừ số tham chiếu giảm của nó). Điều này về cơ bản là tài liệu cho SetLengthchức năng tương tự :

Theo một cuộc gọi đến SetLength, Sđược đảm bảo tham chiếu một chuỗi hoặc mảng duy nhất - nghĩa là một chuỗi hoặc mảng có số tham chiếu là một chuỗi.

Nhưng đáng ngạc nhiên, điều này không hoàn toàn xảy ra trong trường hợp này.

Xem xét ví dụ tối thiểu này:

procedure TForm1.FormCreate(Sender: TObject);
var
  a, b: array of Integer;
begin

  a := [$AAAAAAAA, $BBBBBBBB]; {1}
  b := a;                      {2}

  Delete(a, 0, 1);             {3}

end;

Tôi đã chọn các giá trị để chúng dễ dàng phát hiện trong bộ nhớ (Alt + Ctrl + E).

Sau (1), atrỏ đến $02A2C198trong lần chạy thử của tôi:

02A2C190  02 00 00 00 02 00 00 00
02A2C198  AA AA AA AA BB BB BB BB

Ở đây số tham chiếu là 2 và chiều dài mảng là 2, như mong đợi. (Xem tài liệu về định dạng dữ liệu nội bộ cho mảng động.)

Sau (2) a = b, nghĩa là , Pointer(a) = Pointer(b). Cả hai đều trỏ đến cùng một mảng động, hiện tại trông như thế này:

02A2C190  03 00 00 00 02 00 00 00
02A2C198  AA AA AA AA BB BB BB BB

Theo dự kiến, số tham chiếu bây giờ là 3.

Bây giờ, hãy xem điều gì xảy ra sau (3). abây giờ trỏ đến một mảng động mới 2A30F88trong lần chạy thử của tôi:

02A30F80  01 00 00 00 01 00 00 00
02A30F88  BB BB BB BB 01 00 00 00

Như mong đợi, mảng động mới này có số tham chiếu là 1 và chỉ có "phần tử B".

Tôi hy vọng mảng động cũ, bvẫn còn trỏ đến, trông giống như trước đây nhưng với số tham chiếu giảm là 2. Nhưng bây giờ nó trông như thế này:

02A2C190  02 00 00 00 02 00 00 00
02A2C198  BB BB BB BB BB BB BB BB

Mặc dù số tham chiếu thực sự giảm xuống còn 2, yếu tố đầu tiên đã được thay đổi.

Kết luận của tôi là

(1) Đây là một phần của hợp đồng của Deletethủ tục mà nó làm mất hiệu lực tất cả các tham chiếu khác đến mảng động ban đầu.

hoặc là

(2) Nó sẽ hoạt động như tôi đã nêu ở trên, trong trường hợp này là một lỗi.

Thật không may, tài liệu cho Deletethủ tục không đề cập gì cả.

Nó cảm thấy như một lỗi.

Cập nhật: Mã RTL

Tôi đã xem mã nguồn của Deletethủ tục và điều này khá thú vị.

Có thể hữu ích khi so sánh hành vi với hành vi của SetLength(vì hành vi đó hoạt động chính xác):

  1. Nếu số tham chiếu của mảng động là 1, SetLengthchỉ cần thử thay đổi kích thước đối tượng heap (và cập nhật trường độ dài của mảng động).

  2. Mặt khác, SetLengththực hiện phân bổ heap mới cho mảng động mới với số tham chiếu là 1. Số tham chiếu của mảng cũ bị giảm đi 1.

Bằng cách này, đảm bảo rằng số tham chiếu cuối cùng luôn luôn 1- có thể là từ đầu hoặc một mảng mới đã được tạo. (Một điều tốt là bạn không luôn luôn thực hiện phân bổ heap mới. Ví dụ: nếu bạn có một mảng lớn với số tham chiếu là 1, chỉ cần cắt bớt nó sẽ rẻ hơn so với sao chép nó sang một vị trí mới.)

Bây giờ, vì Deleteluôn làm cho mảng nhỏ hơn, nên cố gắng đơn giản là giảm kích thước của đối tượng heap nơi nó đang ở. Và đây thực sự là những gì mã RTL cố gắng System._DynArrayDelete. Do đó, trong trường hợp của bạn, phần BBBBBBBBđược chuyển đến phần đầu của mảng. Tất cả đều tốt.

Nhưng sau đó nó gọi System.DynArraySetLength, cũng được sử dụng bởi SetLength. Và thủ tục này có chứa các bình luận sau đây,

// If the heap object isn't shared (ref count = 1), just resize it. Otherwise, we make a copy

trước khi nó phát hiện ra rằng đối tượng thực sự được chia sẻ (trong trường hợp của chúng tôi, số lượng ref = 3), thực hiện phân bổ heap mới cho một mảng động mới và sao chép một cái cũ (đã giảm) vào vị trí mới này. Nó làm giảm số lượng ref của mảng cũ và cập nhật số ref, chiều dài và con trỏ đối số của cái mới.

Vì vậy, chúng tôi đã kết thúc với một mảng động mới. Nhưng các lập trình viên RTL quên rằng họ đã làm hỏng mảng ban đầu, hiện bao gồm mảng mới được đặt trên đầu của mảng cũ : BBBBBBBB BBBBBBBB.


Cảm ơn Andreas! Để đảm bảo an toàn, tôi sẽ chỉ sử dụng các con trỏ tới các mảng động. Tôi đã kiểm tra các trường hợp tương tự được xử lý bằng các chuỗi và nó hoạt động như (2), ví dụ: chuỗi mới được phân bổ (sao chép trên ghi) và trường hợp ban đầu (biến cục bộ) không được xử lý.
dwrbudr

1
@dwrbudr: Cá nhân, tôi sẽ tránh sử dụng Deletetrên một mảng động. Đối với một điều, nó không rẻ trên các mảng lớn (vì nó nhất thiết phải sao chép nhiều dữ liệu). Và vấn đề hiện tại này làm cho tôi thậm chí quan tâm nhiều hơn, rõ ràng. Nhưng chúng ta cũng hãy chờ xem liệu các thành viên Delphi khác của cộng đồng SO có đồng ý với phân tích của tôi không.
Andreas Rejbrand

1
@dwrbudr: Vâng. Tất nhiên, mảng động không sử dụng ngữ nghĩa copy-on-write, nhưng thủ tục thích SetLength, InsertDeleterõ ràng là cần phải phân bổ lại. Chỉ cần thay đổi một phần tử (như b[2] := 4) sẽ ảnh hưởng đến bất kỳ biến mảng động nào khác trỏ đến cùng một mảng động; sẽ không có bản sao
Andreas Rejbrand

1
Đừng sử dụng một con trỏ đến một triều đại
David Heffernan

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.