如何重构“箭头型”代码
本文主要起因是,一次在微博上和朋友关于嵌套好几层的if-else语句的代码重构的讨论(微博原文),在微博上大家有各式各样的问题和想法。按道理来说这些都是编程的基本功,似乎不太值得写一篇文章,不过我觉得很多东西可以从一个简单的东西出发,到达本质,所以,我觉得有必要在这里写一篇的文章。不一定全对,只希望得到更多的讨论,因为有了更深入的讨论才能进步。
文章有点长,我在文章最后会给出相关的思考和总结陈词,你可以跳到结尾。
所谓箭头型代码,基本上来说就是下面这个图片所示的情况。
那么,这样“箭头型”的代码有什么问题呢?看上去也挺好看的,有对称美。但是……
关于箭头型代码的问题有如下几个:
1)我的显示器不够宽,箭头型代码缩进太狠了,需要我来回拉水平滚动条,这让我在读代码的时候,相当的不舒服。
2)除了宽度外还有长度,有的代码的if-else
里的if-else
里的if-else
的代码太多,读到中间你都不知道中间的代码是经过了什么样的层层检查才来到这里的。
总而言之,“箭头型代码”如果嵌套太多,代码太长的话,会相当容易让维护代码的人(包括自己)迷失在代码中,因为看到最内层的代码时,你已经不知道前面的那一层一层的条件判断是什么样的,代码是怎么运行到这里的,所以,箭头型代码是非常难以维护和Debug的。
微博上的案例 与 Guard Clauses
OK,我们先来看一下微博上的那个示例,代码量如果再大一点,嵌套再多一点,你很容易会在条件中迷失掉(下面这个示例只是那个“大箭头”下的一个小箭头)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | FOREACH(Ptr<WfExpression>, argument, node->arguments) { int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj()); if (index != -1) { auto type = manager->expressionResolvings.Values()[index].type; if (! types.Contains(type.Obj())) { types.Add(type.Obj()); if ( auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L "CastResult" , true )) { int count = group->GetMethodCount(); for ( int i = 0; i < count; i++) { auto method = group->GetMethod(i); if (method->IsStatic()) { if (method->GetParameterCount() == 1 && method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() && method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor< void >() ) { symbol->typeInfo = CopyTypeInfo(method->GetReturn()); break ; } } } } } } } |
上面这段代码,可以把条件反过来写,然后就可以把箭头型的代码解掉了,重构的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | FOREACH(Ptr<WfExpression>, argument, node->arguments) { int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj()); if (index == -1) continue ; auto type = manager->expressionResolvings.Values()[index].type; if ( types.Contains(type.Obj())) continue ; types.Add(type.Obj()); auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L "CastResult" , true ); if ( ! group ) continue ; int count = group->GetMethodCount(); for ( int i = 0; i < count; i++) { auto method = group->GetMethod(i); if (! method->IsStatic()) continue ; if ( method->GetParameterCount() == 1 && method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() && method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor< void >() ) { symbol->typeInfo = CopyTypeInfo(method->GetReturn()); break ; } } } |
这种代码的重构方式叫 Guard Clauses
- Martin Fowler 的 Refactoring 的网站上有相应的说明《Replace Nested Conditional with Guard Clauses》。
- Coding Horror 上也有一篇文章讲了这种重构的方式 —— 《Flattening Arrow Code》
- StackOverflow 上也有相关的问题说了这种方式 —— 《Refactor nested IF statement for clarity》
这里的思路其实就是,让出错的代码先返回,前面把所有的错误判断全判断掉,然后就剩下的就是正常的代码了。
插播福利
1.近期整理了20G资源,包含产品/运营/测试/程序员/市场等,互联网从业者【工作必备干货技巧、行业专业书籍、面试真题宝典等】,获取方式:
微信扫码关注公众号“非典型互联网”,转发文章到朋友圈,截图发至公众号后台,即可获取干货资源链接;
2.互联网人交流群:
关注公众号“非典型互联网”,在公众号后台回复“入群”,人脉共享,一起交流;
抽取成函数
微博上有些人说,continue 语句破坏了阅读代码的通畅,我觉得他们一定没有好好读这里面的代码,其实,我们可以看到,所有的 if 语句都是在判断是否出错的情况,所以,在维护代码的时候,你可以完全不理会这些 if 语句,因为都是出错处理的,而剩下的代码都是正常的功能代码,反而更容易阅读了。当然,一定有不是上面代码里的这种情况,那么,不用continue ,我们还能不能重构呢?
当然可以,抽成函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | bool CopyMethodTypeInfo( auto &method, auto &group, auto &symbol) { if (! method->IsStatic()) { return true ; } if ( method->GetParameterCount() == 1 && method->GetParameter(0)->GetType()->GetTypeDescriptor() == description::GetTypeDescriptor<DescriptableObject>() && method->GetReturn()->GetTypeDescriptor() != description::GetTypeDescriptor< void >() ) { symbol->typeInfo = CopyTypeInfo(method->GetReturn()); return false ; } return true ; } void ExpressionResolvings( auto &manager, auto &argument, auto &symbol) { int index = manager->expressionResolvings.Keys().IndexOf(argument.Obj()); if (index == -1) return ; auto type = manager->expressionResolvings.Values()[index].type; if ( types.Contains(type.Obj())) return ; types.Add(type.Obj()); auto group = type->GetTypeDescriptor()->GetMethodGroupByName(L "CastResult" , true ); if ( ! group ) return ; int count = group->GetMethodCount(); for ( int i = 0; i < count; i++) { auto method = group->GetMethod(i); if ( ! CopyMethodTypeInfo(method, group, symbol) ) break ; } } ... ... FOREACH(Ptr<WfExpression>, argument, node->arguments) { ExpressionResolvings(manager, arguments, symbol) } ... ... |
你发出现,抽成函数后,代码比之前变得更容易读和更容易维护了。不是吗?
有人说:“如果代码不共享,就不要抽取成函数!”,持有这个观点的人太死读书了。函数是代码的封装或是抽象,并不一定用来作代码共享使用,函数用于屏蔽细节,让其它代码耦合于接口而不是细节实现,这会让我们的代码更为简单,简单的东西都能让人易读也易维护。这才是函数的作用。
嵌套的 if 外的代码
微博上还有人问,原来的代码如果在各个 if 语句后还有要执行的代码,那么应该如何重构。比如下面这样的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | for (....) { do_before_cond1() if (cond1) { do_before_cond2(); if (cond2) { do_before_cond3(); if (cond3) { do_something(); } do_after_cond3(); } do_after_cond2(); } do_after_cond1(); } |
上面这段代码中的那些 do_after_condX()
是无论条件成功与否都要执行的。所以,我们拉平后的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | for (....) { do_before_cond1(); if ( !cond1 ) { do_after_cond1(); continue } do_after_cond1(); do_before_cond2(); if ( !cond2 ) { do_after_cond2(); continue ; } do_after_cond2(); do_before_cond3(); if ( !cond3 ) { do_after_cond3(); continue ; } do_after_cond3(); do_something(); } |
你会发现,上面的 do_after_condX
出现了两份。如果 if 语句块中的代码改变了某些do_after_condX
依赖的状态,那么这是最终版本。
但是,如果它们之前没有依赖关系的话,根据 DRY 原则,我们就可以只保留一份,那么直接掉到 if 条件前就好了,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | for (....) { do_before_cond1(); do_after_cond1(); if ( !cond1 ) continue ; do_before_cond2(); do_after_cond2(); if ( !cond2 ) continue ; do_before_cond3(); do_after_cond3(); if ( !cond3 ) continue ; do_something(); } |
此时,你会说,我靠,居然,改变了执行的顺序,把条件放到 do_after_condX()
后面去了。这会不会有问题啊?
其实,你再分析一下之前的代码,你会发现,本来,cond1 是判断 do_before_cond1() 是否出错的,如果有成功了,才会往下执行。而 do_after_cond1() 是无论如何都要执行的。从逻辑上来说,do_after_cond1()其实和do_before_cond1()的执行结果无关,而 cond1 却和是否去执行 do_before_cond2() 相关了。如果我把断行变成下面这样,反而代码逻辑更清楚了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | for (....) { do_before_cond1(); do_after_cond1(); if ( !cond1 ) continue ; // <-- cond1 成了是否做第二个语句块的条件 do_before_cond2(); do_after_cond2(); if ( !cond2 ) continue ; // <-- cond2 成了是否做第三个语句块的条件 do_before_cond3(); do_after_cond3(); if ( !cond3 ) continue ; //<-- cond3 成了是否做第四个语句块的条件 do_something(); } |
于是乎,在未来维护代码的时候,维护人一眼看上去就明白,代码在什么时候会执行到哪里。 这个时候,你会发现,把这些语句块抽成函数,代码会干净的更多,再重构一版:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | bool do_func3() { do_before_cond2(); do_after_cond2(); return cond3; } bool do_func2() { do_before_cond2(); do_after_cond2(); return cond2; } bool do_func1() { do_before_cond1(); do_after_cond1(); return cond1; } // for-loop 你可以重构成这样 for (...) { bool cond = do_func1(); if (cond) cond = do_func2(); if (cond) cond = do_func3(); if (cond) do_something(); } // for-loop 也可以重构成这样 for (...) { if ( ! do_func1() ) continue ; if ( ! do_func2() ) continue ; if ( ! do_func3() ) continue ; do_something(); } |
上面,我给出了两个版本的for-loop,你喜欢哪个?我喜欢第二个。这个时候,因为for-loop里的代码非常简单,就算你不喜欢 continue ,这样的代码阅读成本已经很低了。
状态检查嵌套
接下来,我们再来看另一个示例。下面的代码的伪造了一个场景——把两个人拉到一个一对一的聊天室中,因为要检查双方的状态,所以,代码可能会写成了“箭头型”。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager) { if ( pA->isConnected() ) { manager->Prepare(pA); if ( pB->isConnected() ) { manager->Prepare(pB); if ( manager->ConnectTogther(pA, pB) ) { pA->Write( "connected" ); pB->Write( "connected" ); return S_OK; } else { return S_ERROR; } } else { pA->Write( "Peer is not Ready, waiting..." ); return S_RETRY; } } else { if ( pB->isConnected() ) { manager->Prepare(); pB->Write( "Peer is not Ready, waiting..." ); return S_RETRY; } else { pA->Close(); pB->Close(); return S_ERROR; } } //Shouldn't be here! return S_ERROR; } |
重构上面的代码,我们可以先分析一下上面的代码,说明了,上面的代码就是对 PeerA 和 PeerB 的两个状态 “连上”, “未连上” 做组合 “状态” (注:实际中的状态应该比这个还要复杂,可能还会有“断开”、“错误”……等等状态), 于是,我们可以把代码写成下面这样,合并上面的嵌套条件,对于每一种组合都做出判断。这样一来,逻辑就会非常的干净和清楚。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | int ConnectPeer2Peer(Conn *pA, Conn* pB, Manager *manager) { if ( pA->isConnected() ) { manager->Prepare(pA); } if ( pB->isConnected() ) { manager->Prepare(pB); } // pA = YES && pB = NO if (pA->isConnected() && ! pB->isConnected() ) { pA->Write( "Peer is not Ready, waiting" ); return S_RETRY; // pA = NO && pB = YES } else if ( !pA->isConnected() && pB->isConnected() ) { pB->Write( "Peer is not Ready, waiting" ); return S_RETRY; // pA = YES && pB = YES } else if (pA->isConnected() && pB->isConnected() ) { if ( ! manager->ConnectTogther(pA, pB) ) { return S_ERROR; } pA->Write( "connected" ); pB->Write( "connected" ); return S_OK; } // pA = NO, pB = NO pA->Close(); pB->Close(); return S_ERROR; } |
延伸思考
对于 if-else
语句来说,一般来说,就是检查两件事:错误 和 状态。
检查错误
对于检查错误来说,使用 Guard Clauses 会是一种标准解,但我们还需要注意下面几件事:
1)当然,出现错误的时候,还会出现需要释放资源的情况。你可以使用 goto fail;
这样的方式,但是最优雅的方式应该是C++面向对象式的 RAII 方式。
2)以错误码返回是一种比较简单的方式,这种方式有很一些问题,比如,如果错误码太多,判断出错的代码会非常复杂,另外,正常的代码和错误的代码会混在一起,影响可读性。所以,在更为高组的语言中,使用 try-catch
异常捕捉的方式,会让代码更为易读一些。
检查状态
对于检查状态来说,实际中一定有更为复杂的情况,比如下面几种情况:
1)像TCP协议中的两端的状态变化。
2)像shell各个命令的命令选项的各种组合。
3)像游戏中的状态变化(一棵非常复杂的状态树)。
4)像语法分析那样的状态变化。
对于这些复杂的状态变化,其本上来说,你需要先定义一个状态机,或是一个子状态的组合状态的查询表,或是一个状态查询分析树。
写代码时,代码的运行中的控制状态或业务状态是会让你的代码流程变得混乱的一个重要原因,重构“箭头型”代码的一个很重要的工作就是重新梳理和描述这些状态的变迁关系。
总结
好了,下面总结一下,把“箭头型”代码重构掉的几个手段如下:
1)使用 Guard Clauses 。 尽可能的让出错的先返回, 这样后面就会得到干净的代码。
2)把条件中的语句块抽取成函数。 有人说:“如果代码不共享,就不要抽取成函数!”,持有这个观点的人太死读书了。函数是代码的封装或是抽象,并不一定用来作代码共享使用,函数用于屏蔽细节,让其它代码耦合于接口而不是细节实现,这会让我们的代码更为简单,简单的东西都能让人易读也易维护,写出让人易读易维护的代码才是重构代码的初衷!
3)对于出错处理,使用try-catch异常处理和RAII机制。返回码的出错处理有很多问题,比如:A) 返回码可以被忽略,B) 出错处理的代码和正常处理的代码混在一起,C) 造成函数接口污染,比如像atoi()这种错误码和返回值共用的糟糕的函数。
4)对于多个状态的判断和组合,如果复杂了,可以使用“组合状态表”,或是状态机加Observer的状态订阅的设计模式。这样的代码即解了耦,也干净简单,同样有很强的扩展性。
5) 重构“箭头型”代码其实是在帮你重新梳理所有的代码和逻辑,这个过程非常值得为之付出。重新整思路去想尽一切办法简化代码的过程本身就可以让人成长。
相关文章:
让数百万台手机训练同一个模型?Google把这套框架开源了
作者 | 琥珀出品 | AI科技大本营(公众号id:rgznai100)【导语】据了解,全球有 30 亿台智能手机和 70 亿台边缘设备。每天,这些电话与设备之间的交互不断产生新的数据。传统的数据分析和机器学习模式,都需要在…
【OpenCV】cv::VideoCapture 多线程测试
cv::VideoCapture多线程测试结果: 在多线程中使用抓取摄像头视频帧时线程安全的,但是,多个线程会共用摄像头的总帧率。 比如,我用两个线程测试30帧的摄像头,每个线程差多都是15帧。
都有Python了,还要什么编译器!
编译的目的是将源码转化为机器可识别的可执行程序,在早期,每次编译都需要重新构建所有东西,后来人们意识到可以让编译器自动完成一些工作,从而提升编译效率。但“编译器不过是用于代码生成的软机器,你可以使用你想要的…
【Qt】Qt发布程序时,报错: could not find or load the Qt platform plugin xcb
问题描述 Qt程序在发布时,报错: This application failed to start because it could not find or load the Qt platform plugin “xcb” in “”. Reinstalling the application may fix this problem Aborted (core dumped) 原因 没有将libqxcb…
jsky使用小记
jsky是一款深度WEB应用安全评估工具,能轻松应对各种复杂的WEB应用,全面深入发现里面存在的安全弱点。 jsky可以检测出包括SQL注入、跨站脚本、目录泄露、网页木马等在内的所有的WEB应用层漏洞,渗透测试功能让您熟知漏洞危害。 打开——新建扫…
BSP场景管理方法简介
BSP(Binary Space Partition,二叉空间分割)方法,在大型3d游戏场景管理方面,可以认为是已经证明了的,最成熟的,最经得起考验的场景管理方法。诸如虚幻系列引擎(Unreal 1,2,3)…
【Qt】Qt样式表总结(一):选择器
官方资料 https://blog.csdn.net/u010168781/article/details/81868523 注释 qss文件中使用:/**/ 来注释 样式规则 样式表由样式规则序列组成。样式规则由选择器和声明组成。选择器指定受规则影响的部件;声明指定应在小部件上设置哪些属性。 如: QLabel { color: white;…
JVM-01:类的加载机制
本文从 纯洁的微笑的博客 转载 原地址:http://www.ityouknow.com/jvm.html 类的加载机制 1、什么是类的加载 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.…
CVPR 2019 | 惊艳的SiamMask:开源快速同时进行目标跟踪与分割算法
作者 | 周强(CV君)来源 | 我爱计算机视觉(公众号id:aicvml)责编 | Jane上面这张Gif图演示了 SiamMask 的效果,只需要鼠标滑动选择目标的包围框,即可同时实现目标跟踪与分割。这种视频里目标的像…
看看Entity Framework 4生成的复杂的分页SQL语句
之前发现Entity Framework 4生成的COUNT查询语句问题,今天又发现它生成的分页SQL语句问题,而LINQ to SQL却不存在这个问题。 >>> 来看一看,瞧一瞧! 上代码: 看生成的SQL语句: 1. Entity Framework…
这份“插件英雄榜Top20”才是Chrome的正确打开方式!
作者 | zhaoolee整理 | Jane出品 | AI科技大本营(公众号id:rgznai100)前言”一入开源深似海”!给大家推荐优秀的开源项目、实用工具已经成为 AI科技大本营的固定节目。“我待开源如初恋”,逛淘宝,点收藏&am…
【Qt】Qt样式表总结(二):冲突和命名空间
Qt样式表总结(一):选择器 解决冲突 针对同一个控件的相同属性,使用多种选择器时,会出现冲突。如: QPushButton#okButton { color: gray } QPushButton { color: red } 解决冲突的规则是:更…
编程自动化,未来机器人将超越人类?
近年,创业者陈曦正专注于一个项目——编程自动化。即机器人可以自己编程,这到底意味着什么呢? 在美国科幻大片《终结者2》中,20世纪末的1997年7月3日,人类研制的全球高级计算机控制系统“天网”全面失控,机…
Repeater 嵌套 Repeater
作为一个刚入行的IT小鸟,每天学习,是必须的! 光自学肯定是不够的!由于本人IQ比较低,经常一个小问题都会想不明白。 还好有媳妇儿的帮助,才把这个功能给实现了。 现在就在这里总结下,以示敬意。o…
【Qt】Qt样式表总结(三):QObject 属性
【Qt】Qt样式表总结(一):选择器 【Qt】Qt样式表总结(二):冲突和命名空间 QObject 属性 可以使用 qproperty < 属性名称 > 语法,设置任何可以Q_PROPERTY的控件 MyLabel { qproperty-pixmap: url(pixmap.png); } MyGroupBox { qproperty-titleColor: rgb(100, 2…
CVPR2019 | 斯坦福学者提出GIoU,目标检测任务的新Loss
作者 | Slumbers,毕业于中山大学,深度学习工程师,主要方向是目标检测,语义分割,GAN责编 | Jane本文是对 CVPR2019 论文《Generalized Intersection over Union: A Metric and A Loss for Bounding Box Regression》的解…
ASP.NET页面之间传递值的几种方式
页面传值是学习asp.net初期都会面临的一个问题,总的来说有页面传值、存储对象传值、ajax、类、model、表单等。但是一般来说,常用的较简单有QueryString,Session,Cookies,Application,Server.Transfer。 一…
下一代安全威胁的内幕故事
当伊朗总统马哈茂德艾哈迈迪-内贾德在去年11月份宣布该国的核计划遭到软件***后,他证实了许多安全研究人员的猜测:原因是Stuxnet大爆发,篡改了控制处理铀所用的离心机电机的关键系统。 内贾德对这起***造成的影响轻描淡写…
国内少儿眼中的编程:“Coding即是代码”?
作者 | Greg Satell译者 | 刘旭坤责编 | Jane出品 | AI科技大本营(公众号id:rgznai100)【编者按】上一个时代流行从小学奥数,现在“编程要从宝宝抓起”已经开始疯狂流行。随着 2017 年国务院印发《新一代人工智能发展规划》&#…
西门子发布最新版NX软件 助力零件制造的数字化
SiemensPLMSoftware近日发布最新版NXTM软件,集成了用于增材制造、计算机数控(CNC)加工、机器人和质量检测等新一代工具,以实现在统一的、集成的、端到端的系统中实现零件制造的数字化。 其中,用于计算机辅助制造(CAM)的先进自动化功能&#x…
【Qt】Qt国际化
参考博客:https://blog.csdn.net/hebbely/article/details/69388763 Qt官网:http://doc.qt.io/qt-5/linguist-manager.html 使用的工具 lupdate --> linguist --> lrelease 使用步骤 tr 在程序中将需要翻译的文本使用tr()函数来处理 修改pro…
回到未来 – 大胆畅想如何追赶并超越腾讯模式
其实,明天是什么样子,它就会是什么样子。 我总是喜欢幻想,无论是对过去还是对未来,对生活或是对爱情。 不过憧憬多过幻想。 一直比较关注互联网的动态,想象如果某某公司的某件产品如果是自己的&…
Python如何爬取实时变化的WebSocket数据
作者 | 韦世东来源 | 进击的Coder(公众号id:FightingCode)一、前言作为一名爬虫工程师,在工作中常常会遇到爬取实时数据的需求,比如体育赛事实时数据、股市实时数据或币圈实时变化的数据。如下图:Web 领域中…
【Qt】Qt样式表总结(四):CSS盒子模型
官网:http://doc.qt.io/qt-5/stylesheet-customizing.html#box-model 【Qt】Qt样式表总结(一):选择器 【Qt】Qt样式表总结(二):冲突和命名空间 【Qt】Qt样式表总结(三):QObject 属性 盒子模型 先来张图片,引自Qt官网 使用样式表时, 每个小部件都被视为具有四个同…
1.试述大数据对思维方式的重要影响。 2.详细阐述大数据、云计算、物联网之间的区别与联系。 3.简述你对大数据应用与发展的看法,以及你在这次大数据浪潮中想扮演什么角色。...
1.大数称巨量资料,指的是需要新处理模式才能具有更强的决策力、洞察力和流程优化能力的海量、高增长率和多样化的信息资产。所以利用大数据的人们思维更加的敏锐,也会对人们的思维方式产生扩大化,通过大量的数据进行分析,从而形成…
有关GetPrivateProfileString的使用方法
函数返回值为string的长度(long型),而从ini文件获得的字符串则保留在目的缓冲器中 DWORD GetPrivateProfileString( LPCTSTR lpAppName, //配置文件的section名 LPCTSTR lpKeyName, //配置文件的key名 LPCTSTR lpDefault, LPTSTR lpReturnedString, DWORD nSize, LPC…
【Qt】QDebug和log4cplus的联合使用
问题描述 项目开始时,只使用QDebug将调试信息打印到终端上。后期添加了日志管理系统,比如log4cplus。如何在不修改打印语句,比如还使用qDebug,就能将日志打印到文件中。 解决方法 使用qInstallMessageHandler将调试消息重定向功…
75道常见AI面试题,看看你的知识盲点在哪?(附解析)
整理 | AI科技大本营出品 | AI科技大本营(公众号id:rgznai100)【导语】正值求职、跳槽季,无论你是换工作还是找实习,没有真本事都是万万不行的,可是如何高效率复习呢?之前我们给大家推荐了一份 …
Flex画流程图
<?xml version"1.0" encoding"utf-8"?><mx:Application xmlns:mx"http://www.adobe.com/2006/mxml" layout"absolute" creationComplete"initApp()"> <mx:Canvas id"paper" x"30" y&q…
【Qt】Qt信号与槽使用不当,使程序崩溃
问题描述 跨线程使用Qt信号和槽,信号发送时间间隔小于槽函数处理时间时,造成程序崩溃。 原因分析 跨线程使用Qt信号和槽时,connect默认是QueuedConnection,队列连接方式。 信号传递给槽函数的参数,分配内存后放入队…