【问题标题】:Is it bad practice to alter dynamic arrays that have references to them?更改引用它们的动态数组是不好的做法吗?
【发布时间】:2011-03-25 21:42:39
【问题描述】:

我看了一点 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);

来自数组规范

为了最大限度地提高效率,运行时总是尝试调整 数组到位以避免额外的复制。它总是会做一个副本 如果新的大小更大并且数组不是通过 新操作员或以前的操作员 调整大小操作。

因此更改长度不一定会破坏参考:

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 中动态数组的全部意义?

【问题讨论】:

    标签: arrays d slice


    【解决方案1】:

    总的来说,您似乎对事情理解得很好,但您似乎误解了ptr 属性的用途。它确实 not 指示两个数组是否引用同一个实例。它的作用是让您指向下面的有效 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 对两个数组相等表示它们的第一个元素在内存中的相同位置。特别是,他们的lengths 可能会有所不同。但是,这确实意味着如果您在其中一个数组中更改它们,那么两个数组中的任何重叠元素都会被更改。

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

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

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

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

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

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

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

    【讨论】:

    • 感谢您提供详细而详细的回答。我还听说过 .capacity 属性,它表示数组在需要重新分配之前可以具有的最大长度。在执行“auto b=a”时,他们会得到不同的参考; &a != &b,但似乎“is”在后台使用 .ptr 来检查引用相等性。
    【解决方案2】:

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

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

    你可以试试这个:

    a = a ~ 2;
    

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

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

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

    //最大

    【讨论】:

    • 是的,其他人刚刚为我指出了这一点,但我在规范中找不到这个......它只是说“a op = b”与“a = a op b”相同”。在规范中找不到数组区别..
    • 我想我记得它来自 Andrei 的 TDPL(书)。至少我确定我已经在某处明确阅读过这篇文章。
    • 实际上,op 和 op= 是分别重载的,所以如果文档另有说明,那就错了。你用opBinary 和op= 用opAssign 超载op。所以,理论上他们可以做完全独立的事情(尽管他们不应该这样做)。我相信这样做是为了进行优化,因为op= 可以就地做事,而不必用结果创造新的价值。因此,如果数组对~ 的作用与对~= 的作用不完全相同,那么它并不是特别特别。
    • 好吧,很多东西的规格/文档已经过时了,但是新闻组中正在讨论更彻底/定期更新它。希望这将很快得到解决,并且从现在开始对语言/标准库的贡献将包括对文档的更改。
    猜你喜欢
    • 1970-01-01
    • 2014-02-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-05
    相关资源
    最近更新 更多