Lambda表达式里面修改外部变量问题

 更新时间:2020年03月24日 08:51:38   作者:WolfcsTech  
这篇文章主要介绍了Lambda表达式里面修改外部变量的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

先看下Lambda表达式里面修改外部变量问题

因为平台用的是JDK8,而且发现自己对那些新特性好像一点都不了解,就例如Lambda表达式,所以就开始对自己的代码进行改进了。。。

        例如遍历Map,像我们正常遍历肯定是下面这样子的。

String result = "select * from where id = '#userId#' and name = '#userName#'";
Map<String,String> sysParams = new HashMap<String,String>();
sysParams.put("#userId#", "userId");
sysParams.put("#userName#", "userName");
sysParams.put("#realName#", "realName");
sysParams.put("#orgIds#", "orgIds");
sysParams.put("#departname#", "departname");
sysParams.put("#roleId#", "roleId");
 
for(Map.Entry<String, String> entry : sysParams.entrySet()){
 if(result .contains(entry.getKey())){
 result = result .replaceAll(entry.getKey(), AppDataUtils.replaceSysData(entry.getValue()));
 }
}

但是如果用Lambda表达式呢,将会非常的简单,

但是我们会发现,result那里是会报错的:Local variable result defined in an enclosing scope must be final or effectively final

String result = "select * from where id = '#userId#' and name = '#userName#'";
Map<String,String> sysParams = new HashMap<String,String>();
sysParams.put("#userId#", "userId");
sysParams.put("#userName#", "userName");
sysParams.put("#realName#", "realName");
sysParams.put("#orgIds#", "orgIds");
sysParams.put("#departname#", "departname");
sysParams.put("#roleId#", "roleId");
 
sysParams.forEach((key,value)->{
 if(result.contains(key)){
 result = result.replaceAll(key, value);
  
 }
});

这是因为:Java会将result的值作为参数传递给Lambda表达式,为Lambda表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明result变量。可能很多同学会问为什么非要建立副本呢,直接访问外部的result变量得多方便呢。答案是:这是不可能滴,因为result定义在栈中,当Lambda表达式被执行的时候,result可能已经被释放掉了。

当然啦,你要是一定要在Lambda表达式里面修改外部变量的值也是可以的,可以将变量定义为实例变量或者将变量定义为数组。

下面我就改为数组咯,实例变量不方便。

String result = "select * from where id = '#userId#' and name = '#userName#'";
 //将变量定义为数组就好了
  String[] arr = new String[]{result};
 Map<String,String> sysParams = new HashMap<String,String>();
 sysParams.put("#userId#", "userId");
 sysParams.put("#userName#", "userName");
 sysParams.put("#realName#", "realName");
 sysParams.put("#orgIds#", "orgIds");
 sysParams.put("#departname#", "departname");
 sysParams.put("#roleId#", "roleId");
 
 sysParams.forEach((key,value)->{
 if(arr[0].contains(key)){
      //都是对数组进行操作了
  arr[0] = arr[0].replaceAll(key, value);
  
 }
 });

下面看下C++ lambda 捕获模式与右值引用详解

lambda 表达式和右值引用是 C++11 的两个非常有用的特性。

lambda 表达式实际上会由编译器创建一个 std::function 对象,以值的方式捕获的变量则会由编译器复制一份,在 std::function 对象中创建一个对应的类型相同的 const 成员变量,如下面的这段代码:

int main(){
 std::string str = "test";
 printf("String address %p in main, str %s\n", &str, str.c_str());
 auto funca = [str]() {
 printf("String address %p (main lambda), str %s\n", &str, str.c_str());
 };
 std::function<void()> funcb = funca;
 std::function<void()> funcc;
 funcc = funca;
 printf("funca\n");
 funca();
 std::function<void()> funcd = std::move(funca);
 printf("funca\n");
 funca();
 printf("funcb\n");
 funcb();
 std::function<void()> funce;
 funce = std::move(funcb);
 printf("funcb\n");
// funcb();
 printf("funcc\n");
 funcc();
 printf("funcd\n");
 funcd();
 printf("funce\n");
 funce();
// std::function<void(int)> funcf = funce;
 return 0;
}

这段代码的输出如下:

Stringaddress0x7ffd9aaab720 in main, strtest
funca
Stringaddress0x7ffd9aaab740 (main lambda), strtest
funca
Stringaddress0x7ffd9aaab740 (main lambda), str
funcb
Stringaddress0x55bdd2160280 (main lambda), strtest
funcb
funcc
Stringaddress0x55bdd21602b0 (main lambda), strtest
funcd
Stringaddress0x55bdd21602e0 (main lambda), strtest
funce
Stringaddress0x55bdd2160280 (main lambda), strtest

由上面调用 funca 时的输出,可以看到 lambda 表达式以值的方式捕获的对象 str,其地址在 lambda 表达式内部和外部是不同的。

std::function 类对象和普通的魔板类对象一样,可以拷贝构造,如:

std::function<void()> funcb = funca;

由调用 funcb 时的输出,可以看到拷贝构造时是做了逐成员的拷贝构造。

std::function 类对象可以赋值,如:

std::function<void()> funcc;
funcc = funca;

由调用 funcc 时的输出,可以看到赋值时是做了逐成员的赋值。

std::function 类对象可以移动构造,如:

std::function<void()> funcd = std::move(funca);

由移动构造之后,调用 funca 和 funcd 时的输出,可以看到移动构造时是做了逐成员的移动构造。

std::function 类对象可以移动赋值,如:

 std::function<void()> funce;
 funce = std::move(funcb);

 printf("funcb\n");
// funcb();

这里把移动赋值之后对 funcb 的调用注释掉了,这是因为,作为源的 funcb 在移动赋值之后被调用是,会抛出异常,如:

String address 0x562334c34280 (main lambda), str test
funcb
terminate called after throwing aninstanceof 'std::bad_function_call'
 what(): bad_function_call

同时,由调用 funce 时的输出可以看到,该输出与 funcb 在移动赋值之前被调用时的输出完全相同。 即移动赋值是将对象整体 move 走了,这与移动构造时的行为不太一样。

std::function 类对象的拷贝构造或者赋值,也需要满足类型匹配原则,如:

std::function<void(int)> funcf = funce;

这行代码会造成编译失败,编译错误信息如下:

../src/DemoTest.cpp: In function ‘intmain()':
../src/DemoTest.cpp:64:36: error: conversion from ‘std::function<void()>' to non-scalar type ‘std::function<void(int)>' requested
   std::function<void(int)> funcf = funce;
                                    ^~~~~
make: *** [src/DemoTest.o] Error 1
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed

在 lambda 中以值的方式捕获的右值对象,只是在 lambda 的 std::function 对象中做了一份被捕获的右值对象的拷贝,而原来的右值则没有任何改变。

接下来再来看一段示例代码:

#include<iostream>
#include<functional>
#include<string>

using namespace std;

void funcd(std::string&&str){
 printf("String address %p in funcd A, str %s\n", &str, str.c_str());
 string strs = std::move(str);
 printf("String address %p in funcd B, str %s, strs %s\n", &str, str.c_str(), strs.c_str());
}

void funcc(std::stringstr){
 printf("String address %p in funcc, str %s\n", &str, str.c_str());
}

void funcb(std::string&str){
 printf("String address %p in funcb, str %s\n", &str, str.c_str());
}

void funca(std::string&&str){
 printf("String address %p in funca A, str %s\n", &str, str.c_str());
 std::string stra = str;
 printf("String address %p in funca B, str %s, stra %s\n", &str, str.c_str(), stra.c_str());
}

int main(){
 std::string str = "test";
 printf("String address %p in main A, str %s\n", &str, str.c_str());

 funca(std::move(str));
 printf("String address %p in main B, str %s\n", &str, str.c_str());

// funcb(std::move(str));
 printf("String address %p in main C, str %s\n", &str, str.c_str());

 funcc(std::move(str));
 printf("String address %p in main D, str %s\n", &str, str.c_str());

 std::string stra = "testa";
 printf("String address %p in main E, stra %s\n", &stra, stra.c_str());

 funcd(std::move(stra));
 printf("String address %p in main F, stra %s\n", &stra, stra.c_str());

 return 0;
}

上面这段代码在执行时,输出如下:

String address 0x7ffc833f4660 in main A, str test
String address 0x7ffc833f4660 in funca A, str test
String address 0x7ffc833f4660 in funca B, str test, stra test
String address 0x7ffc833f4660 in main B, str test
String address 0x7ffc833f4660 in main C, str test
String address 0x7ffc833f4680 in funcc, str test
String address 0x7ffc833f4660 in main D, str
String address 0x7ffc833f4680 in main E, stra testa
String address 0x7ffc833f4680 in funcd A, str testa
String address 0x7ffc833f4680 in funcd B, str , strs testa
String address 0x7ffc833f4680 in main F, stra

funca 函数接收右值引用作为参数,由 funca 函数内部及函数调用前后的输出可以看到, std::move() 本身什么都没做,单单调用 std::move() 并不会将原来的对象的内容移动到任何地方。 std::move() 只是一个简单的强制类型转换,将左值转为右值引用。同时可以看到,用右值引用作为参数构造对象,也并没有对右值引用所引用的对象产生任何影响。

funcb 函数接收左值引用作为参数,上面的代码中,如下这一行注释掉了:

//  funcb(std::move(str));

这是因为, funcb 不能用一个右值引用作为参数来调用。用右值引用作为参数,调用接收左值引用作为参数的函数 funcb 时,会编译失败:

g++ -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/DemoTest.d" -MT"src/DemoTest.o" -o "src/DemoTest.o" "../src/DemoTest.cpp"
../src/DemoTest.cpp: In function ‘int main()':
../src/DemoTest.cpp:34:18: error: cannot bind non-const lvalue reference of type ‘std::__cxx11::string& {aka std::__cxx11::basic_string<char>&}' to an rvalue of type ‘std::remove_reference<std::__cxx11::basic_string<char>&>::type {aka std::__cxx11::basic_string<char>}'
   funcb(std::move(str));
         ~~~~~~~~~^~~~~
../src/DemoTest.cpp:17:6: note:   initializing argument 1 of ‘void funcb(std::__cxx11::string&)'
 void funcb(std::string &str) {
      ^~~~~
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed
make: *** [src/DemoTest.o] Error 1

不过,如果 funcb 接收 const 左值引用作为参数,如 void funcb(const std::string &str) ,则在调用该函数时,可以用右值引用作为参数,此时 funcb 的行为与 funca 基本相同。

funcc 函数接收左值作为参数,由 funcc 函数内部及函数调用前后的输出可以看到,由于有了左值作为接收者,传入的右值引用所引用的对象的值被 move 走,进入函数的参数栈对象中了。

funcd 函数与 funca 函数一样,接收右值引用作为参数,但 funcd 的特别之处在于,在函数内部,右值构造了一个新的对象,因而右值引用原来引用的对象的值被 move 走,进入了新构造的对象中。

再来看一段示例代码:

#include<iostream>
#include<functional>
#include<string>

using namespace std;

void bar(std::string&&str){
 printf("String address %p in bar A, str %s\n", &str, str.c_str());
 string strs = std::move(str);
 printf("String address %p in bar B, str %s, strs %s\n", &str, str.c_str(), strs.c_str());
}

std::function<void()> bar_bar(std::string &&str) {
 auto funf = [&str]() {
 printf("String address %p (foo lambda) F, stra %s\n", &str, str.c_str());
 };
 return funf;
}

std::function<void()> foo(std::string &&str) {
 printf("String address %p in foo A, str %s\n", &str, str.c_str());

// auto funa = [str]() {
// printf("String address %p (foo lambda) A, str %s\n", &str, str.c_str());
// bar(str);
// };
// funa();
//
// auto funb = [str]() {
// printf("String address %p (foo lambda) B, str %s\n", &str, str.c_str());
// bar(std::move(str));
// };
// funb();

// auto func = [str]() mutable {
// printf("String address %p (foo lambda) C, str %s\n", &str, str.c_str());
// bar(str);
// };
// func();

 auto fund = [str]() mutable {
 printf("String address %p (foo lambda) D, str %s\n", &str, str.c_str());
 bar(std::move(str));
 };
 fund();

 auto fune = [&str]() {
 printf("String address %p (foo lambda) E, str %s\n", &str, str.c_str());
 bar(std::move(str));
 };
 fune();

 std::string stra = "testa";
 return bar_bar(std::move(stra));
}

int main(){
 std::string str = "test";
 printf("String address %p in main A, str %s\n", &str, str.c_str());

 auto funcg = foo(std::move(str));
 printf("String address %p in main B, str %s\n", &str, str.c_str());

 funcg();

 return 0;
}

上面这段代码的输出如下:

Stringaddress0x7ffc9fe7c5c0 in main A, strtest
Stringaddress0x7ffc9fe7c5c0 in foo A, strtest
Stringaddress0x7ffc9fe7c540 (foo lambda) D, strtest
Stringaddress0x7ffc9fe7c540 in barA, strtest
Stringaddress0x7ffc9fe7c540 in barB,str, strstest
Stringaddress0x7ffc9fe7c5c0 (foo lambda) E, strtest
Stringaddress0x7ffc9fe7c5c0 in barA, strtest
Stringaddress0x7ffc9fe7c5c0 in barB,str, strstest
Stringaddress0x7ffc9fe7c5c0 in main B,str
Stringaddress0x7ffc9fe7c560 (foo lambda) F, stra����

在函数 foo() 中定义的 funa 及对 funa 的调用被注释掉了,这是因为这段代码会导致编译失败,具体的错误信息如下:

Invoking: GCC C++ Compiler
g++ -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/DemoTest.d" -MT"src/DemoTest.o" -o "src/DemoTest.o" "../src/DemoTest.cpp"
../src/DemoTest.cpp: In lambda function:
../src/DemoTest.cpp:25:12: error: cannot bind rvalue reference of type ‘std::__cxx11::string&& {aka std::__cxx11::basic_string<char>&&}' to lvalue of type ‘const string {aka const std::__cxx11::basic_string<char>}'
     bar(str);
            ^
../src/DemoTest.cpp:7:6: note:   initializing argument 1 of ‘void bar(std::__cxx11::string&&)'
 void bar(std::string &&str) {
      ^~~
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed
make: *** [src/DemoTest.o] Error 1

如我们前面提到的,在 lambda 表达式中,以值的方式捕获右值引用时,会在编译器为该 lambda 表达式生成的 std::function 类中生成一个 const 对象,const 对象是不能作为右值引用来调用接收右值引用为参数的函数的。

在函数 foo() 中定义的 funb ,相对于 funa ,在调用 bar() 时,为 str 裹上了 std::move() 。不过此时还是会编译失败。错误信息如下:

Invoking: GCC C++ Compiler
g++ -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/DemoTest.d" -MT"src/DemoTest.o" -o "src/DemoTest.o" "../src/DemoTest.cpp"
../src/DemoTest.cpp: In lambda function:
../src/DemoTest.cpp:31:18: error: binding reference of type ‘std::__cxx11::string&& {aka std::__cxx11::basic_string<char>&&}' to ‘std::remove_reference<const std::__cxx11::basic_string<char>&>::type {aka const std::__cxx11::basic_string<char>}' discards qualifiers
     bar(std::move(str));
         ~~~~~~~~~^~~~~
../src/DemoTest.cpp:7:6: note:   initializing argument 1 of ‘void bar(std::__cxx11::string&&)'
 void bar(std::string &&str) {
      ^~~
make: *** [src/DemoTest.o] Error 1
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed

在 funb 中, str 是个 const 对象,因而还是不行。

在函数 foo() 中定义的 func ,相对于 funa ,加了 mutable 修饰。此时还是会编译失败。错误信息如下:

Invoking: GCC C++ Compiler
g++ -O0 -g3 -Wall -c -fmessage-length=0 -MMD -MP -MF"src/DemoTest.d" -MT"src/DemoTest.o" -o "src/DemoTest.o" "../src/DemoTest.cpp"
../src/DemoTest.cpp: In lambda function:
../src/DemoTest.cpp:37:12: error: cannot bind rvalue reference of type ‘std::__cxx11::string&& {aka std::__cxx11::basic_string<char>&&}' to lvalue of type ‘std::__cxx11::string {aka std::__cxx11::basic_string<char>}'
     bar(str);
            ^
../src/DemoTest.cpp:7:6: note:   initializing argument 1 of ‘void bar(std::__cxx11::string&&)'
 void bar(std::string &&str) {
      ^~~
make: *** [src/DemoTest.o] Error 1
src/subdir.mk:18: recipe for target 'src/DemoTest.o' failed

无法将左值绑定到一个右值引用上。

在函数 foo() 中定义的 fund ,相对于 func ,在调用 bar() 时,为 str 裹上了 std::move() 。此时终于可以编译成功,可以 move const 的 str 。

在函数 foo() 中定义的 fune ,相对于 funb ,以引用的方式捕获了右值引用。在 fune 中调用 bar() ,就如同 foo() 直接调用 bar() 一样。

在函数 foo() 中调用接收一个右值引用作为参数的函数 bar_bar() 生成一个函数。在函数 bar_bar() 中用 lambda 定义的函数对象 funf ,以引用的方式捕获一个右值,并在 lambda 中访问改对象。该 lambda 作为 bar_bar() 函数生成的函数对象。 foo() 中调用 bar_bar() 时传入函数栈上定义的临时对象 stra ,并将 bar_bar() 返回的函数对象作为返回值返回。在 main() 函数中用 funcg 接收 foo() 函数返回的函数对象,并调用 funcg ,此时会发生 crash 或能看到乱码。crash 或乱码是因为,在 funf 中,访问的 str 对象实际上是 foo() 函数中定义的栈上临时对象 stra , foo() 函数调用结束之后,栈上的临时对象被释放, main() 函数中调用 funcg 实际在访问一个无效的对象,因而出现问题。

总结

到此这篇关于Lambda表达式里面修改外部变量问题的文章就介绍到这了,更多相关C++ lambda 表达式内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • C++实现字符串元音字母反转的两种方法

    C++实现字符串元音字母反转的两种方法

    在处理字符串问题时,我们经常需要对其中的字符进行操作,例如反转、替换等,本文将详细讨论如何在C++中实现仅反转字符串中的所有元音字母,并返回结果字符串,需要的朋友可以参考下
    2024-07-07
  • C语言的三种条件判断语句你都了解吗

    C语言的三种条件判断语句你都了解吗

    这篇文章主要为大家详细介绍了C语言的三种条件判断语句,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • Qt实现简单动态时钟

    Qt实现简单动态时钟

    这篇文章主要为大家详细介绍了Qt实现简单动态时钟,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-07-07
  • 嵌入式C语言二级指针在链表中的应用

    嵌入式C语言二级指针在链表中的应用

    这篇文章主要为大家介绍了嵌入式C语言二级指针在链表中的应用,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04
  • 深入VC回调函数的使用详解

    深入VC回调函数的使用详解

    本篇文章是对VC回调函数的使用进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • C++ OpenCV实现白平衡之灰度世界算法

    C++ OpenCV实现白平衡之灰度世界算法

    灰度世界算法是白平衡各种算法中最基本的一种。本文将利用C++和OpenCV实现白平衡中的灰度世界算法,文中示例代码讲解详细,感兴趣的可以了解一下
    2022-05-05
  • C++多线程编程详解

    C++多线程编程详解

    这篇文章主要介绍了c语言多线程编程使用示例,小编觉得这篇文章写的还不错,需要的朋友可以参考下,希望能够给你带来帮助
    2021-09-09
  • C++静态库与动态库文件的生成和使用教程

    C++静态库与动态库文件的生成和使用教程

    库文件是计算机上的一类文件,可以简单的把库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量、函数和类,下面这篇文章主要给大家介绍了关于C++静态库与动态库文件的生成和使用的相关资料,需要的朋友可以参考下
    2023-03-03
  • C++实现LeetCode(104.二叉树的最大深度)

    C++实现LeetCode(104.二叉树的最大深度)

    这篇文章主要介绍了C++实现LeetCode(104.二叉树的最大深度),本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-07-07
  • 详解VS2019+OpenCV-4-1-0+OpenCV-contrib-4-1-0

    详解VS2019+OpenCV-4-1-0+OpenCV-contrib-4-1-0

    这篇文章主要介绍了详解VS2019+OpenCV-4-1-0+OpenCV-contrib-4-1-0,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-04-04

最新评论