【问题标题】:Container Semantics and Functional Style in DD 中的容器语义和功能风格
【发布时间】:2012-10-31 10:12:14
【问题描述】:

默认情况下,D 中的容器是否具有值或引用语义?如果它们具有引用语义并不会从根本上阻碍函数式编程风格在 D 中的使用(与 C++11 的移动语义相比),例如以下(学术)示例:

auto Y = reverse(sort(X));

X 是一个容器。

【问题讨论】:

  • 我不想告诉你这个,但它介于两者之间......有时它更像是引用语义,有时它更像是值语义。 :\
  • 我不理解对 C++11 移动语义的引用 - 移动语义与函数式编程有何关系? (顺便说一句,D 也有移动语义)

标签: functional-programming pass-by-reference d pass-by-value move-semantics


【解决方案1】:

容器是否具有值语义或引用语义完全取决于容器。唯一的内置容器是动态数组、静态数组和关联数组。静态数组具有严格的值语义,因为它们位于堆栈上。关联数组具有严格的引用语义。动态数组大多具有引用语义。它们是不会被复制的元素,但它们会被复制,因此它们最终会产生一些特殊的语义。我建议阅读 D 数组上的 this article 以了解更多详细信息。

对于官方但非内置的容器,std.container 中的容器都有引用语义,一般来说容器应该是这样,否则效率非常低。但是由于任何人都可以实现自己的容器,因此任何人都可以根据需要创建值类型的容器。

但是,与 C++ 一样,D 并没有采取让算法在容器上运行的路线,因此就算法而言,容器是否具有引用或值语义几乎无关紧要。在 C++ 中,算法在迭代器上运行,因此如果您想对容器进行排序,您可以执行 sort(container.begin(), container.end()) 之类的操作。在 D 中,它们对范围进行操作,因此您可以使用 sort(container[])。在这两种语言中,您实际上都不会直接对容器进行排序。因此,容器本身是否具有值或引用语义与您的典型算法无关。

但是,D 在使用算法进行函数式编程方面比 C++ 做得更好,因为范围更适合它。迭代器必须成对传递,这对于链接函数来说效果不佳。另一方面,范围链很好,Phobos 就利用了这一点。它的主要设计原则之一是,它的大多数函数都在范围内运行,以允许您在代码中执行通常最终在带有管道的 unix 命令行上执行的操作,其中有许多通用工具/函数可以生成输出您可以通过管道/传递给其他工具/功能进行操作,允许您链接独立的操作来执行特定于您的需求的操作,而不是依赖某人编写的程序/功能直接执行您想要的操作。 Walter Bright 最近在 this article 中讨论过它。

所以,在 D 中,很容易做这样的事情:

auto stuff = sort(array(take(map!"a % 1000"(rndGen()), 100)));

或者如果您更喜欢 UFCS(通用函数调用语法):

auto stuff = rndGen().map!"a % 1000"().take(100).array().sort();

在任何一种情况下,它都会生成一个包含 0 到 1000 之间的 100 个随机数的排序列表,并且代码采用函数式风格,而 C++ 将很​​难做到这一点,并且库在容器而不是迭代器或范围会更难做。

【讨论】:

  • 静态数组不只是放在堆栈上,任何变量的存储都取决于很多因素,静态数组不限于局部函数变量。
  • @jA_cOp 它们就像整数或浮点数之类的,因为它们在堆栈上,除非您专门将它们放在堆上或将它们放在堆上的对象中,即与动态或关联数组完全不同。它们的内存被准确地分配在它们被放置的位置(通常是在堆栈上),而不是它们指向其他地方的内存,这就是我这样说的原因。
  • 我不知道take!这个例子是不是有点像 Haskell 的惰性求值集概念?
  • @Nordlow 许多基于范围的函数返回惰性范围,类似于在 Haskell 中得到的。 maptake 都是这样的例子。它节省了不必要的计算,并允许使用无限范围。虽然实际上我只是意识到我在这种特殊情况下犯了一个错误。 sort 需要在随机访问范围内操作,在这种情况下,take 的结果将不起作用。我必须对其进行调整,使其在调用sort 之前调用array(以便将take 的结果放入array)。
【解决方案2】:

内置容器

D 中唯一的内置容器是切片(也称为数组/动态数组)和静态数组。后者具有值语义(与 C 和 C++ 不同)- 整个数组在传递时被(浅)复制。

对于切片,它们是具有间接性的值类型,因此您可以说它们同时具有值​​和引用语义。

T[] 想象成这样的struct

struct Slice(T)
{
    size_t length;
    T* ptr;
}

其中ptr 是指向切片第一个元素的指针,length 是切片边界内的元素数。您可以访问切片的.ptr.length 字段,但是虽然数据结构与上述相同,但它实际上是一个内置的编译器,因此没有在任何地方定义(名称Slice 仅用于演示目的)。

知道了这一点,您可以看到复制切片(分配给另一个变量,传递给函数等)只是复制一个长度(无间接 - 值语义)和一个指针(具有间接 - 引用语义)。

换句话说,切片是一个视图到一个数组(位于内存中的任何位置),并且可以有多个视图到同一个数组中。

算法

sort 和来自std.algorithmreverse 就地工作以迎合尽可能多的用户。如果用户想要将结果放入切片的 GC 分配副本中并保持原始不变,则可以轻松完成 (X.dup)。如果用户想将结果放在自定义分配的缓冲区中,也可以这样做。最后,如果用户想要就地排序,这是一个选项。无论如何,任何额外的开销都是明确的。

但是,需要注意的是,标准库中的大多数算法都不需要变异,而是返回延迟求值的范围结果,这是函数式编程的特征。

用户定义的容器

当涉及到用户定义的容器时,它们可以有任何他们想要的语义 - 在 D 中任何配置都是可能的。

std.container 中的容器是引用类型,带有用于制作副本的.dup 方法,因此稍微模拟了切片。

【讨论】:

    猜你喜欢
    • 2011-04-30
    • 1970-01-01
    • 1970-01-01
    • 2014-07-09
    • 2011-06-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多