8

我看了一下 D2 中的动态数组,发现它们很难理解。我似乎也错误地解释了规范。在更改数组时,处理动态数组的引用或切片似乎很容易出错......或者我只是不了解基本原理?

引用同一个数组只共享实际项目:

auto a = [1];
auto b = a;
assert(&a != &b); // different instance; Doesn't share length
assert(a.ptr == b.ptr); // same items
assert(a == [1]);
assert(a == b);

当它们引用相同的数组时,更改一个会更改另一个:

auto a = [1,2];
auto b = a;
a[1] = 20;
assert(a == [1,20]);
assert(a == b);

从数组规范

为了最大限度地提高效率,运行时总是尝试调整数组的大小以避免额外的复制。如果新大小更大并且数组不是通过 new 运算符或先前的调整大小操作分配的,它将始终进行复制。

所以改变长度并不一定会破坏参考:

auto a = [1];
auto b = a;
b.length = 2;
assert(b == [1,0]);
assert(a == [1]); // a unchanged even if it refers to the same instance
assert(a.ptr == b.ptr);  // but still the same instance

// So updates to one works on the other
a[0]  = 10;
assert(a == [10]);
assert(b == [10,0]);

从数组规范

连接总是会创建其操作数的副本,即使其中一个操作数是长度为 0 的数组

auto a = [1];
auto b = a;
b ~= 2; // Should make a copy, right..?
assert(a == [1]);
assert(b == [1,2]);
assert(a != b);
assert(a4.ptr == b.ptr); // But it's still the same instance
a[0] = 10;
assert(b == [10,2]); // So changes to a changes b

但是当数组相互踩踏时,这些值将被复制到一个新位置并且引用被破坏:

auto a = [1];
auto b = a;
b ~= 2;
assert(a == [1]);
assert(b == [1,2]);

a.length = 2; // Copies values to new memory location to not overwrite b's changes
assert(a.ptr != b.ptr);

在进行更改之前更改两个数组的长度会产生与上述相同的结果(鉴于上述情况,我希望如此):

auto a = [1];
auto b = a;
a.length = 2;
b.length = 2;
a[1] = 2;
assert(a == [1,2]);
assert(b == [1,0]);
assert(a.ptr != b.ptr);

更改长度或连接时也是如此(鉴于上述情况,我希望如此):

auto a = [1];
auto b = a;
b.length = 2;
a ~= 2;
assert(a == [1,2]);
assert(b == [1,0]);
assert(a.ptr != b.ptr);

但随后切片也出现在画面中,突然变得更加复杂!切片可能是孤立的......

auto a = [1,2,3];
auto b = a;
auto slice = a[1..$]; // [2,3];
slice[0] = 20;
assert(a == [1,20,3]);
assert(a == b);

a.length = 4;
assert(a == [1,20,3,0]);
slice[0] = 200;
assert(b == [1,200,3]); // the reference to b is still valid.
assert(a == [1, 20, 3, 0]); // but the reference to a is now invalid..

b ~= 4;
// Now both references is invalid and the slice is orphan...
// What does the slice modify?
assert(a.ptr != b.ptr);
slice[0] = 2000;
assert(slice == [2000,3]);
assert(a == [1,20,3,0]); 
assert(b == [1,200,3,4]);

所以......对同一个动态数组有多个引用是不好的做法吗?并传递切片等?或者我只是在这里,错过了 D 中动态数组的全部要点?

4

2 回答 2

10

总体而言,您似乎对事物了解得很好,但您似乎误解了ptr财产的目的。它不指示两个数组是否引用同一个实例。它的作用是让您指向下面的有效 C 数组的指针。D 中的数组是它length的一部分,因此它更像是一个具有长度和指向 C 数组的指针的结构,而不是 C 数组。ptr允许您获取 C 数组并将其传递给 C 或 C++ 代码。您可能不应该将它用于纯 D 代码中的任何内容。如果要测试两个数组变量是否引用同一个实例,则使用is运算符(或!is检查它们是否是不同的实例):

assert(a is b);   //checks that they're the same instance
assert(a !is b);  //checks that they're *not* the same instance

两个数组的所有ptr相等都表明它们的第一个元素在内存中的相同位置。特别是,它们length的 s 可能不同。但是,这确实意味着如果您在其中一个数组中更改它们,则两个数组中的任何重叠元素都会被更改。

在更改length数组的 时,D 会尝试避免重新分配,但它可能会决定重新分配,因此您不一定要依赖它是否会重新分配。例如,如果不这样做,它将重新分配另一个数组的内存(包括那些具有相同值的数组ptr)。如果没有足够的内存来调整自己的大小,它也可以重新分配。基本上,它会重新分配,如果不这样做会踩到另一个数组的内存,否则它可能会或可能不会重新分配。因此,当您设置数组时,依赖数组是否会重新分配通常不是一个好主意length

我本来希望附加到始终按照文档进行复制,但是根据您的测试,它的行为似乎就像length确实一样(我不知道这是否意味着需要更新文档或者它是否是一个错误 - 我的猜测会是文档需要更新)。在这两种情况下,您当然不能依赖对该数组的其他引用在追加后仍然引用同一个数组。

至于切片,它们按预期工作,并且在 D 中被高度使用——尤其是在标准库 Phobos 中。切片是数组的范围,范围是 Phobos 的核心概念。但是,就像许多其他范围一样,更改范围/切片所针对的容器可能会使该范围/切片无效。这就是为什么当您在 Phobos 中使用可以调整容器大小的函数时,如果您不想冒险使该容器的范围无效,则需要使用以 stable(例如stableRemove()或)开头的函数。stableInsert()

此外,切片是一个数组,就像它指向的数组一样。因此,很自然地,更改其length或附加到它将遵循与更改或附加到任何其他数组的规则相同的所有规则,length因此它可以被重新分配并且不再是另一个数组的切片。

几乎,您只需要注意length以任何方式更改数组的 都可能导致重新分配,因此如果您希望引用继续引用同一个数组实例,则需要避免这样做。如果你绝对需要确保它们指向同一个引用,那么你需要使用dup来获取数组的新副本。如果您根本不弄乱length数组的 ,那么数组引用(无论是切片还是对整个数组的引用)将继续愉快地引用同一个数组。

编辑:事实证明,文档需要更新。如果可以,任何可以调整数组大小的东西都会尝试在适当的位置进行(因此它可能不会重新分配),但如果必须这样做,则会重新分配以避免踩到另一个数组的内存或者如果它没有足够的空间重新分配到位。length因此,通过设置属性来调整数组大小和通过附加来调整数组大小之间应该没有任何区别。

附录:任何使用 D 的人都应该阅读这篇关于数组和切片的文章。它很好地解释了它们,并且应该让您更好地了解数组在 D 中是如何工作的。

于 2010-08-05T18:12:35.523 回答
2

我真的不想把它变成一个完整的答案,但我还不能评论以前的答案。

我认为连接和附加是两个略有不同的操作。如果将 ~ 与数组和元素一起使用,则它是附加的;有两个数组,它是连接。

你可以试试这个:

a = a ~ 2;

看看你是否得到相同的结果。

此外,如果您想定义行为,只需使用 .dup(或 .idup 用于不可变)属性。如果您有一组引用,这也非常有用;您可以修改主数组和 .dup 切片以进行处理,而不必担心竞争条件。

编辑:好的,我弄错了,但无论如何。串联!=附加。

//最大限度

于 2010-08-05T19:01:53.780 回答