你如何在D中使用范围?

时间:2012-06-25 13:21:51

标签: d range phobos

每当我尝试在D中使用范围时,我都会悲惨地失败。

在D中使用范围的正确方法是什么? (请参阅内联评论以了解我的困惑。)

void print(R)(/* ref? auto ref? neither? */ R r)
{
    foreach (x; r)
    {
        writeln(x);
    }

    // Million $$$ question:
    //
    // Will I get back the same things as last time?
    // Do I have to check for this every time?

    foreach (x; r)
    {
        writeln(x);
    }
}

void test2(alias F, R)(/* ref/auto ref? */ R items)
{
    // Will it consume items?
    // _Should_ it consume items?
    // Will the caller be affected? How do I know?
    // Am I supposed to?
    F(items);
}

4 个答案:

答案 0 :(得分:8)

如果你没有,你应该阅读this tutorial on ranges

何时消耗和不消耗范围取决于其类型。如果它是一个输入范围而不是一个前向范围(例如,如果它是某种类型的输入流 - std.stdio.byLine就是这样的一个例子),那么以任何方式迭代它或者表格会消耗它。

//Will consume
auto result = find(inRange, needle);

//Will consume
foreach(e; inRange) {}

如果它是一个前向范围并且它是一个引用类型,那么每当你迭代它时它就会消耗掉,但你可以调用save来获取它的副本,并且使用副本将不会消耗它原件(也不会消耗原件消耗副本)。

//Will consume
auto result = find(refRange, needle);

//Will consume
foreach(e; refRange) {}

//Won't consume
auto result = find(refRange.save, needle);

//Won't consume
foreach(e; refRange.save) {}

事情变得更有趣的是前向范围,即值类型(或数组)。它们与save的任何前向范围的行为相同,但它们的不同之处在于,只需将它们传递给函数或在foreach隐式save中使用它们。

//Won't consume
auto result = find(valRange, needle);

//Won't consume
foreach(e; valRange) {}

//Won't consume
auto result = find(valRange.save, needle);

//Won't consume
foreach(e; valRange.save) {}

因此,如果您正在处理的输入范围不是前向范围,则无论如何都会消耗它。如果你正在处理一个前进范围,你需要调用save如果你想要保证它不被消耗 - 否则它是否消耗取决于它的类型。

关于ref,如果你声明一个基于范围的函数来接受ref的参数,那么它就不会被复制,所以传入的范围是否无关紧要是否是引用类型,但它确实意味着你不能传递一个非常烦人的rvalue,所以你可能不应该在范围参数上使用ref,除非你真的需要它总是变异原文(例如std.range.popFrontN采用ref,因为它显式改变原文而不是可能在副本上操作。)

对于使用正向范围调用基于范围的函数,值类型范围最有可能正常工作,因为代码经常使用值类型范围编写和测试,并且不总是使用引用类型进行正确测试。不幸的是,这包括Phobos的功能(虽然它将被修复;它在所有情况下都没有经过适当的测试 - 如果遇到任何情况下Phobos功能在参考类型前向范围内无法正常工作, please report it)。因此,引用类型转发范围并不总是按预期工作。

答案 1 :(得分:4)

抱歉,我无法将其纳入评论:D。考虑Range是否以这种方式定义:

interface Range {
    void doForeach(void delegate() myDel);
}

你的功能看起来像这样:

void myFunc(Range r) {
    doForeach(() {
        //blah
    });
}

当你重新分配r时,你不会发生任何奇怪的事情,你也不期望 能够修改来电者的范围。我认为问题在于您希望模板功能能够考虑范围类型的所有变化,同时仍然利用专业化。这不起作用。您可以将合同应用于模板以利用专业化,或仅使用常规功能。 这有帮助吗?

编辑(我们在评论中一直在讨论的内容):

void funcThatDoesntRuinYourRanges(R)(R r)
if (isForwardRange(r)) {
    //do some stuff
}

修改2 std.range看起来isForwardRange只是检查是否定义了save,而save只是一个基元,它创建了一种未链接的副本范围。文档指定save未定义为例如文件和插座。

答案 2 :(得分:1)

缺点;范围被消耗。这是你应该期待和计划的。

foreach上的ref不起作用,它只与范围返回的值有关。

漫长;消耗范围,但可能会被复制。您需要查看文档以确定将会发生什么。值类型被复制,因此传递给函数时可能不会修改范围,但是如果范围作为结构出现,则我不能依赖于数据流作为参考,例如,文件。当然,ref函数参数会增加混乱。

答案 3 :(得分:1)

假设您的print函数如下所示:

void print(R)(R r) {
  foreach (x; r) {
    writeln(x);
  }
}

这里,使用通用类型r使用引用语义将R传递给函数:所以这里不需要refauto会给出r编译错误)。否则,这将逐项打印auto myRange = [1, 2, 3]; print(myRange); print(myRange); 的内容。 (我似乎记得有一种方法可以将泛型类型约束到一个范围,因为范围具有某些属性,但我忘记了细节!)

反正:

1
2
3
1
2
3

...将输出:

x++

如果您将功能更改为(假设void print(R)(R r) { foreach (x; r) { x++; writeln(x); } } 对您的范围有意义):

myRange

...然后在打印之前会增加每个元素,但这是使用复制语义。也就是说,2 3 4 2 3 4 中的原始值不会更改,因此输出将为:

void print(R)(R r) {
  foreach (ref x; r) {
    x++;
    writeln(x);
  }
}

但是,如果您将功能更改为:

x

...然后myRange被还原为引用语义,引用语义引用2 3 4 3 4 5 的原始元素。因此输出现在将是:

{{1}}