7

每当我尝试在 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

4 回答 4

8

如果您还没有阅读范围,您可能应该阅读本教程。

范围何时会被消耗和不会被消耗取决于它的类型。如果它是输入范围而不是前向范围(例如,如果它是某种类型的输入流 -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隐式saves 中使用它们。

//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,那么它不会被复制,所以传入的范围是否是引用类型并不重要,但这确实意味着你不能传递一个右值,这真的很烦人,所以你可能不应该ref在范围参数上使用,除非你真的需要它来总是改变原始值(例如std.range.popFrontN,需要 aref因为它显式地改变原始值而不是潜在地操作 a复制)。

至于使用前向范围调用基于范围的函数,值类型范围最有可能正常工作,因为经常使用值类型范围编写和测试代码,而不总是使用引用类型正确测试。不幸的是,这包括了 Phobos 的功能(尽管这将得到修复;它只是尚未在所有情况下都经过适当的测试 - 如果您遇到任何情况下 Phobos 功能在引用类型前向范围内无法正常工作,请报告)。因此,引用类型前向范围并不总是按应有的方式工作。

于 2012-06-25T18:08:57.717 回答
4

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

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

你的函数看起来像这样:

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

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

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

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

编辑 2 std.range看起来isForwardRange只是检查是否save已定义,并且save只是一个原语,它制作了一种未链接的范围副本。文档指定save没有为例如文件和套接字定义。

于 2012-06-25T15:31:42.793 回答
1

不足之处;范围被消耗。这是您应该期待和计划的。

foreach 上的 ref 对此没有任何作用,它只与范围返回的值有关。

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

于 2012-06-25T15:00:57.620 回答
1

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

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

在这里,r使用引用语义传递给函数,使用泛型类型R:所以你不需要ref在这里(并且auto会给出编译错误)。否则,这将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
于 2012-06-25T15:02:09.280 回答