d闭包不尊重域

原文

int main() {
    void delegate()[] dgs;
    
    for (int i = 0; i < 10; i++) {
        dgs ~= () {
            import std.stdio;
            writeln(i);
        };
    }
    
    dgs ~= () {
        import std.stdio;
        writeln("带缓存变量");
    };
	
    for (int i = 0; i < 10; i++) {
        int index = i;
        dgs ~= () {
            import std.stdio;
            writeln(index);
        };
    }
    
    foreach (ref dg; dgs)  {
        dg();
    }
    
    return 0;
}

//输出:

10
10
10
10
10
10
10
10
10
10
//用缓存变量
9
9
9
9
9
9
9
9
9
9

期望10系列,因为每次捕捉相同i变量.而9系列,不是.
每次迭代都会声明新变量,因此应创建新闭包
不这样,导致奇怪,如不变变量中的改变(在@safe中应禁止或过时).期望为10系列0到9.
语义与C#类似,


js中:

var dgs = [];

for (var i = 0; i < 10; i++) {
	dgs.push(function() {
		console.log(i);
	});
}

dgs.push(function() {
	console.log("带缓存变量");
});

for (var i = 0; i < 10; i++) {
	var index = i;
	dgs.push(function() {
		console.log(index);
	});
}

for (var dg of dgs) {
	dg();
}

与最上面输出一样.D行为与其他语言差不多.把var改为let更接近.
D当前传递可变λ,这样,当前值为λ状态一部分.


代码是易错的,jsvar变量中声明,用创建正确域的let声明,再试试.


重写为不用闭包的:

int test() @safe {
    int j;
    int*[20] ps;

    for (int i = 0; i < 10; i++) {
        ps[j++] = &i;
    }

    for (int i = 0; i < 10; i++) {
        int index = i;
        ps[j++] = &index;
    }

    int x;
    foreach (p; ps)  {
        x += *p;
    }

    return x;
 }

代码与用引用和域等价,用-dip1000编译.产生:
错误:用更长生命期赋值i变量地址给ps.这是闭包``示例行为,因为闭包也存储了变量指针.
指针周围有抽象(如数组,闭包,引用,出,类引用等)时,用指针显式重写,来确定应怎样.然后就暴露了可能奇怪的行为.且行为应匹配.
诚然,编译器中有个有时会把闭包引用变量放进垃集分配的闭包中的特殊组件,但并不表明,在函数域结束前可离开域.未为他们造闭包,且此功能比加值更复杂,因此应是个错误.

D中,在堆上分配闭包.


同意是八哥,你用新状态创建闭包/λ.
循环中,为多个闭包创建仅一个共享状态的原因是啥?用例?
如果,D不破坏当前行为,可加与闭包完全匹配的新的λ关键字,它在创建时取全新状态,只需更改编译时生成代码.
C++按值或按引用更可行.

如果捕捉,C++标::函数也在堆中分配,等价C++代码也在循环中分配.

函数可在上分配,但要根据实现/λ状态大小.仅单个分配不会在上分配.
函数是否分配不相关.

#include <functional>
#include <vector>
#include <iostream>

int main(int argc, char** argv) {
    std::vector<std::function<void()>> dgs;

    for (int i = 0; i < 10; i++) {
        // 按引用捕捉,与`D`语义一样
        dgs.emplace_back([&i] () {
            std::cout << i << "\n";
        });
    }

    dgs.emplace_back([] () {
        std::cout << "带缓存变量" << "\n";
    });

    for (int i = 0; i < 10; i++) {
        int index = i;
        // 同上.
        dgs.emplace_back([&index] () {
            std::cout << index << "\n";
        });
    }

    for (auto& dg: dgs) {
        dg();
    }

    return 0;
}

clang调试构建打印10们,后面是9们.使用gcc调试构建会打印垃圾值.两个的优化构建打印垃圾值.沃尔特的重写清楚地说明了原因.,D的输出至少是可预测的非垃圾.
注意注释,这是问题所在.D捕捉始终是按引用,因此等效C++代码也应这样.如果按复制捕捉,可以肯定,在"用缓存变量"后看到[0,10),但是代码不是等效的.
方法是转换循环体匿名λ调用来强制新帧:

for (int i = 0; i < 10; i++) (int index) {
        dgs ~= () {
            import std.stdio;
            writeln(index);
        };
    } (i);

但就不能用break/continue了.
理想是,在D中复制或移动捕捉.
.C++做到了这一点:禁止隐式捕捉并让程序员准确指定如何捕捉.D应该这样.

建议,强制按引用捕捉循环变量闭包应是@system,@安全代码应报错误.同时,还应有进一步增强.应允许不要求的愚蠢的包装器.


Dλ不是按值,而是按引用捕捉.
也是循环变量的直接结果,"i""index"虽然在域尾消失了,但在相同位置重建.因此,所有λ引用指向每个引用指向的相同位置.
问题是λ生命期都超过了iindex变量生命期.
赋值引用iλ给比i生命期更长的dgs[]时,应发出错误.

@safe
    void test() {
        int delegate() dg;
        foreach (i; 0 .. 10) {
            dg = () { return i; };
        }
    }

上面简化版本,应编译失败.
即使在垃集堆上分配闭包也没用,因为每个相同行为λ只会共享唯一的分配.

posted @   zjh6  阅读(7)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示