每当我尝试在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);
}
答案 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
传递给函数:所以这里不需要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
的原始元素。因此输出现在将是:
{{1}}