【问题标题】:How do you use ranges in D?你如何在 D 中使用范围?
【发布时间】:2012-06-25 13:21:50
【问题描述】:

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

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

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);
}

【问题讨论】:

  • 范围是集合的接口,就像其他语言中的迭代器一样。虽然你总是可以迭代一个迭代器(或一个范围),但你不能有效地对其他行为进行广泛的概括。我的理解是范围本身始终是一个引用类型,但底层集合可以是任何支持接口的东西,所以如果你要修改范围的元素,你应该使用 ref 并且你应该使用约束来确保基础集合是可写的。如果你不打算修改元素,那么你不需要参考。
  • 对不起,我要澄清一下:如果你要修改范围的元素,你应该在 foreach 循环中使用 ref (如果集合的元素是值类型),我不'不认为您需要该函数的 ref 限定符,因为范围应该始终是一个参考,但我可能会忽略一些东西。
  • @Tim:我认为你误解了这个问题(或者我不明白你的意思)。我说的是修改范围,而不是items。 (比如,想想:LockingTextReader(stdin),而不是数组。其中一个是单通道,另一个是多通道。)
  • 嗯,好的,所以我在让 DMD 强制执行 const 和 immutable 应该做出的保证时遇到了很多麻烦,但理论上......无论如何,std.range.isForwardRange 是内置的在“可重复”范围合同中。
  • 没问题,感谢您的讨论,它帮助我理清了自己的想法:D

标签: d range phobos


【解决方案1】:

如果你还没有读过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 中隐含地使用它们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,除非你真的需要它来始终改变原始值(例如@987654323 @ 采用 ref,因为它显式地改变了原始文件,而不是潜在地对副本进行操作)。

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

【讨论】:

    【解决方案2】:

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

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

    你的函数看起来像这样:

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

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

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

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

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

    【讨论】:

    • +1 是的,我想我毕竟需要一个前锋范围,我花了一段时间才意识到这一点。 :-) 猜猜我必须为控制台输入做一个包装器..
    【解决方案3】:

    它的短处;范围被消耗。这是您应该期待和计划的。

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

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

    【讨论】:

    • “这是你应该期待和计划的”->那是否意味着我不能使用foreach?还是之后我popFrontN?当它是刚刚传递给我的泛型类型时,我如何“查看文档”?
    • 另外,我对 foreach 上的 ref 很不满意......我知道它们与这个问题无关;我也有另一个问题,并且碰巧把它们放在那里。我已经删除了它们。
    • @Mehrdad:某些范围本质上是破坏性的,因为它们从不支持反向移动当前位置的东西中读取(例如,无缓冲的网络流)。对于那些,无论您如何阅读它们,您都会将所有别名修改为较低级别的东西。
    【解决方案4】:

    假设您的 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
    

    【讨论】:

    • 我想你误解了这个问题——我问的是范围本身的修改,而不是项目。有些范围实际上是相同的,因此修改一个范围会修改所有范围。其中一些(如切片)不是。问题是,我该如何处理?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-12-16
    • 1970-01-01
    • 2018-03-22
    • 1970-01-01
    • 2012-03-05
    • 1970-01-01
    • 2021-12-23
    相关资源
    最近更新 更多