当前位置: 首页 > 编程日记 > 正文

把三千行代码重构为15行

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

如果你认为这是一个标题党,那么我真诚的恳请你耐心的把文章的第一部分读完,然后再下结论。如果你认为能够戳中您的G点,那么请随手点个赞。 

把三千行代码重构为15行 

那年我刚毕业,进了现在这个公司。公司是搞数据中心环境监控的,里面充斥着嵌入式、精密空调、总线、RFID的概念,我一个都不懂。还好,公司之前用Delphi写的老客户端因为太慢,然后就搞了个Webform的替代,恰好我对Asp.Net还算了解,我对业务的不了解并不妨碍我称成为这个公司的一个程序员。小公司也有小公司的好,人少,进去很快负责代码开发。我当然也就搞这个数据中心智能管理系统啦。 

这个系统非常的庞大,尤其牛逼的是支持客户端组态,然后动态生成网页,数据还能通过Socket实时监控(那时我还真就不懂网络编程)。这个对于当时的我来说,真真是高、大、上呐!!当时跟着了解整个系统大半个月才算能够调试,写一些简单的页面。 

在维护系统的过程中,时不时要扩展一些功能,也就接触了下面这个类: 


02160119_UFuo.jpg 


看到没有,就是当年最最流行的三层架构的产物,对于刚出茅庐的毛头小子来说,这是多么专业的文件头注释,还有反射也就算了,这构造函数还能静态的,还能私有的?那时刚接触这么高大上的代码的我,瞬间给跪了! 

但是,类写多了,我就感觉越来越别扭,就是下面这段代码: 


02160119_ac9C.jpg 


每增加一个表,除了要改接口、要改DAL、要改BLL之外,还得在这个工厂类添加一个方法,真真是累到手抽筋,即使有当时公司了的G工给我推荐的神器——动软代码生成器,这粘贴复制的几遍,也是让我感觉到异常繁琐,有时候打键盘稍微累了点,还把复制出来代码改错了,你妹的,难道这就是程序员该干的事情,不,绝对不是!我想起了一句至理名言:当你觉得代码重复出现在程序中的时候,就应该重构了。是的,在这句话的指导下,我开始了折腾,决定挑战这个高大上的代码,事实证明,思想的力量是无穷的。 

那么,怎么修改呢,仔细观察之后,发现其中className的生成跟返回的类型非常类似,只是一个是类名,一个是字符串,这两者之间应该能够关联起来。于是google了一下(当时GFW还没猖獗起来哈),隐隐约约就找到了“反射”这两个字,深入了解之后,确定可以完成。 

接下来,就是返回的类型了,返回的类型并不固定,但是似乎很有规律……这个似乎好像在哪里见过,对了,模板,C++课程上有讲过的,于是再次google,了解到了C#中使用了泛型代替了C++中的模板。在学习完泛型和反射之后,并参考了网上的一些文章,我捣鼓出了下面的代码: 


02160119_Tck6.jpg 


没错,就是它了,三层架构年代最流行的工厂类…… 

看着原来滚十几屏幕的代码,变成了十多行的代码,真是爽到了骨子里去了,太干净了!唯一让我担忧的是,我进公司的时候,帮忙整理公司申请软件著作权都是需要代码量的,根据代码多少行来评估软件的大小,万一老板知道了我非但没有帮公司增加代码量,还减少了,会不会立即把我开掉?我没敢给我们老板展示我优秀的成果,所幸,这段代码非但没有出过任何问题,还避免了以前同事老是在新增一个类之后,把代码复制过来,但是没有正确修改的问题,大大提高了效率。虽然,我没敢大事宣布我的劳动成果,但是这次成功的修改,则彻底让我走上了代码重构的不归路。 

看到这里,大家应该知道这个案例是否真实的了吧。我相信,从08年开始的码农们,看到这种类似的代码绝对不比我少。那么,我想告诉你们的是什么呢? 

  • 要在编程过程中多思考

  • 编程的思想很重要,请多看点经典的书

  • 从小处着眼,慢慢重构,尤其在应对一个大型的系统

  • 当重复出现的时候,你应该考虑重构了

  • 粘贴复制的代码越少,你的系统越稳定


少用代码生成器 

我们来分析一下,为什么我之前的前辈会写出上面的代码。我归结起来有以下几点: 

  • 因为使用了动软代码生成器,生成代码方便,就没多想了。

  • 三层架构的概念倒是了解了,但是没有去深入思考就拿来应用

  • 遇到重复的代码,没有重构的概念,这是思想的问题——思想比你的能力重要


至今为止,还是很多人使用代码生成器,那么我们应该怎么对待这个问题呢。我认为,代码生成器确实可以减少你不少工作,但是少用,那些重复性的工作,除了部分确实是没有办法的,其他大部分都是可以通过框架解决的,举例来说,像三层架构,真正需要用到代码生成器的,也就是Model类而已,其他的完全可以在框架中完成。因此你要竭尽全力的思考怎么在框架中来减少你的重复性工作,而不是依赖于代码生成器。 

另外,如果你还是在用相关的代码生成工具,请重新定义“动软代码生成器”的代码模板,自己写一个模板;或者使用CodeSmith来完全制定自己的代码生成,因为动软给的代码模板真心乱,比如下面这段代码: 

Js代码

  1. for (int n = 0; n < rowsCount; n++)  

  2. {  

  3.     model = new DBAccess.Model.eventweek();  

  4.     if(dt.Rows[n]["GroupNo"].ToString()!="")  

  5.     {  

  6.         model.GroupNo=int.Parse(dt.Rows[n]["GroupNo"].ToString());  

  7.     }  

  8.     if(dt.Rows[n]["Week0"].ToString()!="")  

  9.     {  

  10.         model.Week0=int.Parse(dt.Rows[n]["Week0"].ToString());  

  11.     }  

  12.     if(dt.Rows[n]["Week1"].ToString()!="")  

  13.     {  

  14.         model.Week1=int.Parse(dt.Rows[n]["Week1"].ToString());  

  15.     }  

  16. }  


首先,你就不能用 var row=dt.Rows[n] 替代吗?其次,直接用int.Parse效率多低?再次,dt.Rows[n]["Week0"]为NULL怎么办? 

不要重复发明轮子 

我们再来看看其他的一些代码: 

Js代码

  1. public List<string> GetDevices(string dev){  

  2.     List<string> devs=new List<string>();  

  3.    

  4.     int start=0;  

  5.     for(int i=0;i<dev.Length;i++){  

  6.         if(dev[i]=='^'){  

  7.             devs.Add(dev.SubString(start,i));  

  8.             start=i+1;  

  9.         }  

  10.     }  

  11.    

  12.     return devs;  

  13. }  


有没有很眼熟,没错,这就是对String.Split()函数的简单实现。我的前辈应该是从c++程序员转过来的,习惯了各种功能自己实现一遍,但是他忽略了C#的很多东西。我们不去评判这段代码的优劣,而实际上他在很长一段时间都运行得很好。我们来看看使用这一段代码有什么不好的地方: 

  • 重复发明轮子。花费了额外的时间,函数的健壮性和很差

  • 可读性差。其实是一个很简单的功能,但是用上了这么一段函数,起初我还以为有什么特别的功能。


那么,我们应该怎样去避免重复发明轮子呢?我从个人的经历来提出以下几点,希望能够对各位有所帮助: 

  • 了解你所学的编程语言的特性。你可以看一本基础的入门书籍,把所有的特性浏览一遍,或者上MSDN,把相关的内容过一遍。

  • 在你决定动手发明一个轮子之前,先搜索一下现成的解决方案。你还可以到CodeProject、GitHub之类的网站搜索一下。在知乎上有很多大牛其实都在批评,为什么你提问之前,不能首先去搜一下是否有现成的答案,反而指责没有回答他的问题。

  • 你有一定的基础之后,还应该去读一下相关的经典书籍,深入了解其中的原理。比如,你觉得你有一定的基础了,我建议你去吧《CLR Via C#》多读几遍,你了解原理越多,你越是能够利用这编程语言的特性,从而来实现原本那些你认为要靠自己写代码的功能。


这里我再举一个我自己的例子。在我现有的程序中,我发现我需要越来越多的线程来执行一些简单的任务,比如在每天检测一下硬盘是否达到90%了,每天9点要控制一下空调的开启而在网上6点的时候把空调关掉。线程使用越来越多,我越是觉得浪费,因为这些现场仅仅只需完成一次或者有限的几次,大部分时间都是没有意义的,那么怎么办呢?我决定自己写一个任务类,来完成相关的事情。说干就干,我很快把这个类写出来了。 

Js代码

  1. public abstract class MissionBase : IMission  

  2. {  

  3.     private DateTime _nextExecuteTime;  

  4.     protected virtual DateTime[] ExecuteTimePoints { get; private set; }  

  5.     protected virtual int IntervalSeconds { get; private set; }  

  6.     protected IEngine Engine { get; private set; }  

  7.    

  8.     public bool IsCanceled{get{……}}  

  9.     public bool IsExecuting{get{……}}  

  10.     public bool IsTimeToExecute{get{……}}  

  11.    

  12.     public abstract bool Enable { get; }  

  13.     public abstract string Name { get; }  

  14.    

  15.     protected MissionBase(IEngine engine)  

  16.     {  

  17.         ExecuteTimePoints = null;//默认采用间隔的方式  

  18.         IntervalSeconds = 60 * 60;//默认的间隔为1个小时  

  19.    

  20.         Engine = engine;  

  21.     }  

  22.    

  23.     /// 任务的执行方法  

  24.     public void Done()  

  25.     {  

  26.         if (Interlocked.CompareExchange(ref _isExecuting, 1, 0) == 1) return;  

  27.    

  28.         try  

  29.         {  

  30.             ……  

  31.         }  

  32.         finally  

  33.         {  

  34.             Interlocked.CompareExchange(ref _isExecuting, 0, 1);  

  35.         }  

  36.     }  

  37.        

  38.     ///实际方法的执行  

  39.     protected abstract void DoneReal();  

  40. }  


但是,实际上这个任务方法,并不好用,要写的代码不少,而且可靠性还没有保障。当然,我可以继续完善这个类,但是我决定搜索一下是否还有其他的方法。直到有一天,我再次阅读《CLR Via C#》,看到线程这一章,讲到了System.Threading.Timer以及ThreadPool类时,我就知道了,使用Timer类完全可以解决我的这个用尽量少的线程完成定时任务的问题。 

因为从原理上来说,Timer类无论你声明了多少个,其实就只有一个线程在执行。当你到了执行时间时,这个管理线程会用ThreadPool来执行Timer中的函数,因为使用的ThreadPool,执行完成之后,线程就马上回收了,这个其实就完全实现了我所需要的功能。 

等你无法重构的时候再考虑重写 

我带过很多优秀的程序员,也与很多优秀的程序员共事过。有一大部分的程序员在看到一套系统不是那么满意,或者存在某些明显的问题,就总是忍不住要把整套系统按自己觉得可以优化的方向来重写,结果,重写结构往往并不令人满意。系统中确实存在很多不合理的地方,但是有不少的这种代码,恰恰是为了解决一些特定场景下的问题的。也就是说,所有的规范以及编程的原则,其实也是有条件限制的,他可能在大部分的时候是正确的,能够指导你完成你的任务,但是,并不是在所有地方都是适用的。比如数据库范式,但实际中我们的设计往往会考虑冗余,这是违背范式的,但是为什么还有那么多人趋之若鹜呢?因为我们可能需要用空间换时间。 

如果我们一开始就考虑重写,那么你可能会陷入以下的困境: 

  • 需要花更大的精力来完成一些看似简单的BUG

  • 你要知道,有一部分看似错误或者非常不优美的代码,其实恰恰是为了解决一些非常刁钻的问题的。

  • 再也无法兼容老的系统了

  • 你急于把原有系统重写,却往往忽略了对原有系统的兼容,那么你新的系统的推进则会十分缓慢。而老系统的维护,又会陷入及其尴尬的情况。

  • 过度设计,导致重写计划迟迟无法完成

  • 有重写冲动的程序员往往是在架构设计上有一些读到的见解,他们善于利用所学的各种设计模式和架构技巧来建立系统,但是越是想尽可能的利用设计模式,越是陷入过度设计的困局,导致重写的计划迟迟都无法完成。

  • 无法有效利用现有系统已经完成并测试的代码

  • 如果你确实有必要进行重写,我还是建议你把代码尽可能的重构。因为重构之后的系统,能够让你更轻易的重写,又最大限度了保留以前可用的业务代码。



我举个例子,说明如何通过重构更好的利用现有代码的。 

我有一个非常庞大的系统,其中有一块功能是用于数据采集、存储、告警管理以及电话、短信等告警通知。大致的结构如下: 

Js代码

  1. class MainEngine:IEngine{  

  2.     public MainEngine(ConfigSettings config){  

  3.            

  4.     }  

  5.    

  6.     public void Start();  

  7.     public void Stop();  

  8. }  


需要增加新的业务功能时,程序员写的代码往往是这样的:首先时修改配置类 

Js代码

  1. class ConfigSettings{  

  2.     public bool NewFuncEnable{get;private set;}  

  3.     public ConfigSettings(){  

  4.         NewFuncEnable=xx;//从配置文件读取  

  5.     }  

  6. }  


接着修改主程序: 

Js代码

  1. class MainEngine:IEngine{  

  2.     private NewFuncClass newCls=new NewFuncClass();  

  3.     public MainEngine(ConfigSettings config){  

  4.     }  

  5.    

  6.     public void Start(){  

  7.         if(config.NewFuncEnable)  

  8.             newCls.Start();  

  9.     }  

  10.     public void Stop(){  

  11.         if(config.NewFuncEnable)  

  12.             newCls.Stop();  

  13.     }  

  14. }  


在修改的过程中,往往是根据配置文件来判断新功能是否启用。上面代码会造成什么问题呢: 

  • 主程序代码和扩展功能耦合性太强,每增加一个功能都要修改主程序代码,这里非常非常容易出错。尤其是新的人进度开发组,很容易就忘主程序中增加了一些致命性的代码。比如上述的扩展功能,可能是在特定的项目中才会有这个扩展功能,但是,写代码的人忘记增加是否启用的配置选项了,导致所有的项目都应用了这个功能,而这个功能需要特定的表,这样就悲剧了。即使是你增加了配置,也是非常的不美观,因为在通用的版本中使用了这个配置,往往会让定制项目以外的人员感到困惑。

  • 增加扩展功能的人还需对整个MainEngine代码有一定的熟悉,否则,他根本就不知道在Start方法和Stop方法进行newClas的对应方法的调用

  • 如果你打算对这段代码进行重写,那么,你会感到非常的困难,因为你分不清楚newCls这个新实例的作用,要么你花大精力去把所有代码理清楚,要么直接就把这段新增的业务代码去掉了。


那么我们如何对这段代码进行重构呢。首先,我们把新功能注册的代码抽取出来,通过反射来实现新的功能的注册。 

Js代码

  1. private void RegisterTaskHandlerBundles()  

  2.    {  

  3.        var bundles = xxx.BLL.Caches.ServiceBundleCache.Instance.GetBundles("TaskHandlerBundle");  

  4.        if (bundles != null && bundles.Count > 0)  

  5.        {  

  6.            var asmCache = new Dictionary<string, Assembly>();  

  7.            foreach (var bundle in bundles)  

  8.            {  

  9.                try  

  10.                {  

  11.                    if (!asmCache.ContainsKey(bundle.Category)) asmCache.Add(bundle.Category, Assembly.Load(bundle.AssemblyName));  

  12.                    var handler = (ITaskHandler)asmCache[bundle.Category].CreateInstance(bundle.ClassName, false, BindingFlags.Default, null,  

  13.                        new object[] { this, bundle }, nullnull);  

  14.                    _taskHandlerBundles.Add(bundle, handler);  

  15.                }  

  16.                catch (Exception e)  

  17.                {  

  18.                    NLogHelper.Instance.Error("加载bundle[Name:{0},Assembly:{1}:Class:{2}]异常:{3}", bundle.Name, bundle.AssemblyName, bundle.ClassName, e.Message);  

  19.                }  

  20.            }  

  21.        }  

  22.    }  


修改MainEngine代码 

Js代码

  1. class MainEngine:IEngine{  

  2.     private NewFuncClass newCls=new NewFuncClass();  

  3.     public MainEngine(ConfigSettings config){  

  4.         RegisterTaskHandlerBundles();  

  5.     }  

  6.    

  7.     public void Start(){  

  8.         _taskHandlerBundles.Start();  

  9.     }  

  10.     public void Stop(){  

  11.         _taskHandlerBundles.Stop();  

  12.     }  

  13. }  



OK,现在我们再来看看怎么实现原来的新增功能:你只需按规范新建一个类,继承ITaskHandler接口,并实现接口的方法。最后在XTGL_ServiceBundle表中新增一条记录即可。我们再来看看这么做有什么好处: 

  • 新增的类只需按规范写即可,完全对MainEngine代码没有任何影响。你甚至可以把这个MainEngine代码写在一个新建的Dll中。

  • 新增功能的这个业务类跟原来的代码解耦,非常方便进行新功能的业务测试,而无需考虑原有框架的影响

  • 新增功能的业务类与架构完全分离,我们在重写代码中只要保证接口的稳定性,无论我们怎么把系统架构重写,我们可以马上就重用上原有的业务功能代码。


重构的目标之一,就是把框架和业务完全分离。 

有志于深入了解的同学,可以了解下反射、Ioc和插件话编程等。 
学会单元测试,培养你的重构意识 

可能上面说了这么多,还是有很多人并不理解重构。没关系,在这里我教你们一个快速入门的办法,就是单元测试。什么是单元测试,请自行google。单元测试有什么要求?就是要求你要把每个方法都弄成尽量可以测试的。尽量让你的方法变成是可测试的,就是培养你重构意识的利器。在你要求把方法变成可测试的过程,你就会发现你必须得不断的修改你的方法,让它的职责尽量单一,让它尽量的与上下文无关,让它尽可能通过方法参数的输入输出就能完成相关的功能,让依赖的类都尽量改为接口而不是实例。最终,你就会发觉,这就是重构!而且是在不知不觉中,你重构的功力就会大大提升,你编程的水平也会大大提升! 

看到这里,有经验的程序员就会问,你这是在鼓励我使用TDD吗?不,不是的。TDD(Test-Driven Development)鼓励的是测试驱动开发,未开发之前先编写单元测试用例代码,测试代码确定需要编写什么产品代码。这是一种比较先进的开发方法,但是在编程的实践过程中,我认为它过于繁琐,很多中小企业很难实施,更别提我们个人开发者。我这里提倡你用单元测试培养你的重构意识,可以说是一种后驱动,用于提高你的重构能力和重构愿望,你完全可以把我的这个方法称为“TDR(Test-Driven Refactoring)——测试驱动重构”。当然,在开发之前如果你有意识的让方法可测试,那么你写出来的函数将会是比较高质量的代码。当你的函数都是一个个可重用性高的函数之时,你将会发现,写代码其实就像堆积木一样,可以把一个大型的需求分解成无数细小的功能,很快的把需求实现。 

以下是一个超大方法中的一段代码,如果你懂得怎样让这段代码编程一个可测试的方法,那么,恭喜你,你入门了。 


02160119_LSKe.jpg 


所谓重构 

如果你有耐心看到这里,你应该知道,我并非一个标题党,而这篇文章也许称为“如何在编程中应用重构的思想”更为贴切,但是我不想用这么严肃的标题。 

很多编程初学者,或者有多年编程经验的人都觉得阅读别人的代码非常困难,重构更是无从谈起,他们要么对这些代码望洋兴叹,要么就是推翻从来。但是,如果我们有重构的意识,以及在编程的过程中熟悉一些代码调整和优化的小技巧,你自然而然就会培养出重构的能力。 

重构,其实很简单: 

  • 把基础打牢固

  • 多看点优秀的代码

  • 避免复制粘贴,如果看见重复代码时应该有意识要消灭它

  • 减少对代码生成器的依赖

  • 在处理现有代码时尽量用重构代替重写,在重写之前一定要先重构

  • 尽量让所有的方法都是可测试的


如果你坚持这么去做了,一段时间之后感觉自然就出来了。 

重构的目的,是让你的代码更为精简、稳定、能够重用,是最大程度的让功能和业务分离。在重构的过程中,你的阅读代码的能力、写出优秀代码的能力以及系统架构能力都会稳步提升。你成为一个优秀的程序员将指日可待。


转载于:https://my.oschina.net/u/1244156/blog/351474

相关文章:

一起学WPF系列(2):第一个WPF应用程序

概述 Windows Presentation Foundation (WPF) 是下一代显示系统&#xff0c;用于生成能带给用户震撼视觉体验的 Windows 客户端应用程序。使用 WPF&#xff0c;您可以创建广泛的独立应用程序以及浏览器承载的应用程序。一直以来&#xff0c;我对界面的东西是不怎么感兴趣的&am…

Java学习总结:29

线程间的经典操作案例——生产者与消费者案例 程序基本模型&#xff1a; package Project.Study.Multithreading;class Message{private String title; //保存信息的标题private String content; //保存信息的内容public void setTitle(String title) {this.title title;}…

Blender终极角色创造:从初学者到专业人士

Ultimate character creation in Blender: From beginner to pro 流派:电子学习| MP4 |视频:h264&#xff0c;1280720 |音频:AAC&#xff0c;48.0 KHz 语言&#xff1a;英语中英文字幕&#xff08;根据原英文字幕机译更准确&#xff09; |大小解压后:24.8 GB 含建模参考图 |时长…

2022-2028年中国离心机行业市场研究及前瞻分析报告

【报告类型】产业研究 【报告价格】4500起 【出版时间】即时更新&#xff08;交付时间约3个工作日&#xff09; 【发布机构】智研瞻产业研究院 【报告格式】PDF版 本报告介绍了中国离心机行业市场行业相关概述、中国离心机行业市场行业运行环境、分析了中国离心机行业市场…

Shell 十三问 的学习记录

在 BBS上看到了Shell十三问的帖子&#xff0c;由于比较就远了&#xff0c;怕以后再也找不到了&#xff0c;就把笔记贴过来了&#xff0c; 原帖地址&#xff1a; shell 十三问http://bbs.chinaunix.net/thread-2033675-1-1.html 贴出我做的笔记&#xff1a; <一>、为何叫做…

图解八大排序算法——我见过的最详细的讲解(转)

一、分类 1.内部排序和外部排序  内部排序&#xff1a;待排序记录存放在计算机随机存储器中&#xff08;说简单点&#xff0c;就是内存&#xff09;进行的排序过程。外部排序&#xff1a;待排序记录的数量很大&#xff0c;以致于内存不能一次容纳全部记录&#xff0c;所以在排…

UE4创建第一人称射击游戏学习教程 Unreal Engine 4: Create Your Own First-Person Shooter

UE4创建第一人称射击游戏学习教程本课程包含38节视频课&#xff0c;将逐步指导您完成以下主题: 云桥网络 平台获取课程&#xff01; 如何创建6种可定制的武器(包括手枪、突击步枪、猎枪、狙击枪、榴弹发射器和火箭发射器) 如何制作基于命中扫描和投射的武器 如何制作第一人…

PS多形式的部分之间复制“笨办法”

PS剪切页面&#xff0c;有时候你可能会遇到这样的情况&#xff1a;设计改进&#xff0c;但是&#xff0c;我们要具有相同的切片。 在此假设&#xff0c;可以直接用于切割片。我们可以节省大量的时间&#xff0c;又分为片。 但是&#xff0c;人们一般不会在你的上跨片设计PSD在变…

Java学习总结:30

线程的生命周期 suspend()方法&#xff1a;暂时挂起线程&#xff1b; resume()方法&#xff1a;恢复挂起的线程&#xff1b; stop()方法&#xff1a;停止线程。 对于以上三个方法不推荐使用&#xff0c;它们已经被慢慢废除掉了&#xff0c;主要原因是这三个方法在使用时容易产…

SVN优化(一) SVN忽略maven项目的target

SVN优化(一) SVN忽略maven项目的target 一 eclipse刚开始导入的项目: 二 解决办法 方式一&#xff1a; 在项目代码路径,如: F:\xyx\sl 鼠标右键,“TortoiseSVN”-- >“Settings” -->"Subversion"-->"Global ignore pattern" 添加:target *.…

“WPF老矣,尚能饭否”—且说说WPF今生未来(上):担心

2019独角兽企业重金招聘Python工程师标准>>> 近日微软公布了最新的WPF路线图&#xff0c;一片热议&#xff1b;对于老牌控件提供商葡萄城来说&#xff0c;这是WPF系列控件一个重要的机遇&#xff0c;因此&#xff0c;Spread Studio for WPF产品做了一次重要更新&…

C语言新手写扫雷攻略3

界面绘制好后&#xff0c;雷数也布置了&#xff0c;接下来就是游戏的运行过程了&#xff0c;今天先不说具体过程&#xff0c;再来看看需要用到的辅助函数 先是简单的画红旗&#xff0c;鼠标右键的功能是画红旗&#xff0c;至此我们都是在使用函数自己绘图&#xff0c;效率是低&…

制作欧比旺·克诺比逼真的CG角色学习教程

艺术站-制作欧比旺克诺比逼真的Cg角色 大小解压后&#xff1a;4.98G 含课程素材文件 1920X1080 .mp4 语言&#xff1a;英语中英文字幕&#xff08;根据原英文字幕机译更准确&#xff09; 课程获取&#xff1a;制作欧比旺克诺比逼真的CG角色学习教程 信息: 欧比旺是我一直以来…

Java学习总结:31(StringBuffer类)

StringBuffer类 在Java中String类不适合使用于频繁修改字符串的操作上(因为其字符串常量一旦声明则不可改变&#xff0c;只能改变字符串对象&#xff0c;改变的是其内存地址的指向)&#xff0c;所以我们可以使用StringBuffer类方便用户进行内容的修改。 例&#xff1a;观察Str…

c语言常用数据类型转换整理

你要发送原始数据流 还是 格式化输出&#xff1f; 如果是格式化 按原子说的 &#xff0c;用sprintf / printf; 如果发送原始内存数据流&#xff0c; 可按下面发送&#xff0c; 发送 #define BYTE0(pointer) (*((char*)(&pointer)0)); #define BYTE1(pointer) (*((char…

2022-2028年中国老年旅游市场深度调研及开发战略研究报告

【报告类型】产业研究 【报告价格】4500起 【出版时间】即时更新&#xff08;交付时间约3个工作日&#xff09; 【发布机构】智研瞻产业研究院 【报告格式】PDF版 本报告介绍了中国老年旅游行业市场行业相关概述、中国老年旅游行业市场行业运行环境、分析了中国老年旅游行…

Andriod使用webview控件往APP里内嵌网页

1.布局文件片段&#xff1a;res-layout <WebView android:id"id/Toweb" android:layout_width"fill_parent" android:layout_height"fill_parent" /> 2.Java片段&#xff1a;src Overrideprotected void onCreate(Bundle savedInstance…

垃圾回收器ZGC应用分析总结

ZGC 是一款低延迟、高吞吐的垃圾回收器,由 Oracle 公司开发。它适用于大型、多核、内存容量较大的应用程序。ZGC 的设计目标是在最大限度地减少停顿时间的同时,为大型内存提供可伸缩性,并为生产部署提供高吞吐量和稳定性。它的目标是以不到 10 毫秒的暂停时间来控制 100MB 到 4TB 的内存。此外,ZGC 还致力于避免全局 JVM 暂停,从而提高系统的可用性。简单来说,它的设计目标是在不超过 10 毫秒的暂停时间内,尽可能地回收大量的堆内存。低延迟:ZGC 的主要目标是最小化 GC 暂停时间。

14个Java并发容器,你用过几个?

不考虑多线程并发的情况下,容器类一般使用ArrayList、HashMap等线程不安全的类,效率更高。在并发场景下,常会用到ConcurrentHashMap、ArrayBlockingQueue等线程安全的容器类,虽然牺牲了一些效率,但却得到了安全。上面提到的线程安全容器都在java.util.concurrent包下,这个包下并发容器不少,今天全部翻出来鼓捣一下。仅做简单介绍,后续再分别深入探索。ConcurrentHashMap:并发版HashMap。

MySQL主从复制(基于binlog日志方式)

主从复制,是用来建立一个和主数据库完全一样的数据库环境,称为从数据库;主数据库一般是准实时的业务数据库。主从复制的作用1.做数据的热备,作为后备数据库,主数据库服务器故障后,可切换到从数据库继续工作,避免数据丢失。2.架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。3.读写分离,使数据库能支撑更大的并发。a.从服务器可以执行查询工作(就是我们常说的读功能),降低主服务器压力;(主库写,从库读,降压)

RabbitMQ安装

windows上安装过程&#xff1a;https://blog.csdn.net/linsongbin1/article/details/80170567运行中给新建用户赋权限&#xff0c;如果界面创完用户忘给权限了可以用这个方法&#xff1a;https://blog.csdn.net/hello_world_php/article/details/83859767 备注&#xff1a;我的…

Complete C# Unity Game Developer 2D

你会学到什么 从头开始学习C#&#xff0c;一门强大的现代语言。没有编程经验是必要的。 在使用Unity游戏引擎方面变得优秀。 为游戏设计和游戏开发打下坚实的基础&#xff0c;这将有助于您构建自己的游戏。 了解面向对象编程在实践中如何工作。 创建可玩的游戏项目——对你的投…

玩转android自定义控件二——自定义索引栏listview

带索引栏的listview&#xff0c;在android开发非常普遍&#xff0c;方便用户进行字母索引&#xff0c;就像微信通讯录这样&#xff1a; 今天&#xff0c;我们就从零到一实现这个具有索引栏的listview. 怎么实现这个控件了&#xff0c;我们应当梳理出一个思路。 ①首先应当将字母…

172. 阶乘后的零

给定一个整数 n&#xff0c;返回 n! 结果尾数中零的数量。 示例 1: 输入: 3 输出: 0 解释: 3! 6, 尾数中没有零。示例 2: 输入: 5 输出: 1 解释: 5! 120, 尾数中有 1 个零.说明: 你算法的时间复杂度应为 O(log n) 。 话不多说&#xff0c;先上代码&#xff1a; class So…

Mysql函数group_concat、find_in_set 多值分隔字符字段进行数据库字段值翻译

Mysql函数group_concat、find_in_set进行数据库字段值翻译 场景 配方表&#xff1a;记录包含的原料 sources表示原料&#xff0c;字段值之间用逗号分隔 原料表&#xff1a;对应原料id和原料名称 现需要查询出的原料结果显示为原料名称&#xff0c;并以逗号分隔。可通过函数g…

网站速度优化-前端篇

2019独角兽企业重金招聘Python工程师标准>>> 页面中最好不要有js片段&#xff0c;合并多个js为一个js&#xff0c;并将js压缩 &#xff08;百度搜索&#xff0c;有在线js压缩、解压缩工具&#xff09;&#xff0c;并放在文档底部页面中最好不要有css片段&#xff0c…

HashMap总结

为什么用HashMap HashMap是一个Hash桶(数组链表)&#xff0c;桶存储的内容是键值对(Key-value)映射HashMap采用了数组和链表的数据结构&#xff0c;能在查询和修改方便继承了数组的线性查找和链表的寻址修改HashMap是非synchronized&#xff0c;所以HashMap很快(哈哈哈)与HashT…

Blender与Substance painter制作三维手枪

你会学到: Blender和Substance painter的基础知识 建模 纹理制作 烘焙 Uv展开 Boolens和斜角修改器 如何制作游戏准备枪 课程获取&#xff1a;Blender与Substance painter制作三维手枪 – 云桥网络-CG技术学习平台 要求 Blender Substance painter 你好&#xff0c;我是3d艺术…

Java学习总结:32(Runtime类)

Runtime类 该类用于表示虚拟机(JVM)运行时的状态&#xff0c;每次启动JVM都对应一个Runtime实例&#xff0c;且只有一个实例&#xff0c;利用Runtime类可以启动新的进程或进行相关运行时环境的操作。此外&#xff0c;该类采用单例模式设计&#xff0c;对象不可以直接实例化。所…

转:查看系统是64位还是32位

1、getconf LONG_BIT or getconf WORD_BIT 例如&#xff1a; 2、file /bin/ls 例如&#xff1a; 查看linux的版本: 转载于:https://www.cnblogs.com/lei-lei/p/5029120.html