那些年,我用工业控制开发语言
前言
4年本科,2年研究生,3年工作生活, 那些年,我试过一些开发环境,形形色色…
不同的平台,不同的操作,不同的语言,那些年,我开发过各种程序,千奇百怪…
用着用着,渐渐明白,君子生非异也,善假于物也,只有在合适的时机,使用正确的工具,才能顺利的解决工作中的问题。
语言的魅力
小时候,喜欢看小说,喜欢打游戏,一直觉得写小说和写程序的人是非常厉害的,因为在他们的手中,可以创造一整个世界。
后来上学就有意识的往程序的方向发展,接触接触过不少语言:
按上学时的掌握度来说
- 硬件编程: C > Verilog
- 软件编程用: LabVIEW>C++>Python,C#
- 算法方面: MATLAB
按照工作后的标准来看,不常用的基本退化到了初级阶段,常用只有LabVIEW、C++、Python
语言的选择
接触这些语言后发现,如果每一个的掌握程度都只停留在初学者阶段,很多问题都无法彻底解决,只能疲惫的应对各种实现不了的难题。因此,精通或者说是深入一门语言,成为了编程者所必须面临的问题。我的选择随时间推移在不断变化,不同阶段思考的内容也不一样。
开始选择
在大三的时候,曾经问黄晶学长(全国虚拟仪器国赛特等奖大神): "我会好多语言,究竟应该以哪一门语言为主呢?"
学长反问我:“你确定会好多种?只拿LabVIEW来说,我也不能说会。”
当时心想,学长LabVIEW已经超级厉害还说自己不会,那我这个小菜鸟岂不是什么都不懂,羞愧之下决定好好精通一门。
当时LabVIEW最熟悉,而且能够快速做漂亮的界面,有学习的偶像,所以选择了它。
第二次的选择
熟练LabVIEW是非常有帮助的,它解决过各种工程问题,包括我的毕设、上学期间做的许多上位机、工作后的各种自动化控制程序。整体而言,LabVIEW的深入让我可以应对大部分的工程问题,无惧很多很棘手的问题。
然而,随着工作时间的增长,越加发现LabVIEW不适合在私企做更大的程序。
LabVIEW优势是快速原型验证,在时间紧迫,任务重的时候,可以降低学习成本,加速开发。然而,它面向的通常是硬件工程师或者不太懂软件编程的研究者。为了达到容易上手的目的,很多复杂的功能都通过封装简化,很多软件理论也都进一步弱化,从而达到的短时间内出成果的目的。,这也导致LabVIEW易学难精。
但是,在程序开发过程中,不可避免的遇到需求不断增加,功能不断累积。当程序规模越来越大时,打包,编译,框架,人员等初期验证并不需要太多关注的问题反而成为制约开发的重要因素。
公司一款商业测控程序,打包需要1天多的时间,而且经常失败。其实,产品本身没有问题,但定位比较宏大,选择LabVIEW进行开发就不再明智。
某创业公司想要做原型产品,选择NI方案人员不好招募,自己培养又没有太多的时间。
某创业公司在做消费类电子的自动化产品,由于公司规模不可能用盗版,NI的软件较高,远不如自己开发特定需求的功能"便宜".
我为了回避LabVIEW带来的一些瓶颈,学习更好的架构体系,写"牛的程序,开始尝试资料丰富的C++作为第二语言,用于系统的学习软件理论,学习面向对象,学习设计思想。
第三次的探索
半年多过去了,当C++可以解决工作中大部分问题时,又遇到另一个问题。
工作中还是要写Client程序,C++由于UI操作极其不便,设计UI界面仍需大量编码,单人开发效率并不高。
也可能是习惯了LabVIEW的节奏,用C++开发UI速度降低太多。
于是,业余时间探索了一下Python和C#,发现:
Python作为一个胶水语言,能找到各种各样的库文件,写小的测试脚本非常方便。在不复杂的程序,如测试序列,非常适合使用Python。
C#为C++的升级版本,在Windows上开发客户端程序比C++效率高太多了,除了LabVIEW在工业显示的控件比较有优势外,大多炫酷的界面在C#中都有很好的库支持。
复盘总结
回顾我的学习曲线,过去的时间和精力已经全部投入且无法挽回。如果可以重来,我希望在最开始有一些建议,这里可以给学习的是自动化,测控相关专业同学一些小的参考。
如果你将来想从事工业控制相关的工作
- 毕业留高校研究所:优先熟悉LabVIEW,掌握基础C语言
理由:高校研究对成本不是特别敏感,可以使用较贵的NI仪器,配合LabVIEW可以加快研究的步伐,减少软件学习成本。
使用C/C++语言可同时帮助开发硬件程序,毕竟研究所通常要求一个人完成一整个软硬件体系的搭建。
- 毕业去测控、自动化类私企:优先熟悉C#,掌握基础LaBVIEW
从公司角度讲:测控类的私企对成本很敏感,虽然在初创的时候使用LabVIEW可以迅速打开市场,快速完成给定目标,但长期发展受到硬件成本限制。自研软硬件是公司发展到一定程度的必然选择。
从个人角度讲:私企人员流动比较大,LabVIEW就业范围较窄,同一行业的优秀公司只有一两家,未来如果想要去其他方向或者行业,文本编程占据主导地位的概率大,所以尽可能的掌握一门文本编程语言。
- 毕业去测试类国企:优先熟悉C#,其次熟悉python
理由:较大的企业通常会有话语权,不用选择语言。供应商使用LabVIEW做硬件驱动开发,你只需要掌握容易的编程语言来调用即可(甚至不再编程)。
- 如果将来想从事互联网相关行业,建议精通java,C++,python中的一门,并且熟练使用Linux系统。
注意:这里的建议仅供参考,每个人的选择还会受到各种因素的制约,比如老师的课题要求,公司的团队语言,甲方的项目需求等等。
未来发展
从事工控行业,使用C#+python/MATLAB做开发一直是我认为比较看好的方向之一。较低的学习成本,简单明确的框架体系,较多的成熟UI库,后续可以深入的软件理论,这些都是工程师软件开发的必备之物。并且,中国工程方面的强大软件还很缺乏,能够有自主创作的软件出来,对于工程控制方向发展还是大有裨益的。
然而,当下来看,C#对于工业控制的支持力度还是不够,远不像LabVIEW一样成熟,需要更多工程师的去完善和发展。
中国本土的企业也逐渐出现自己的东西,简仪科技在不停学习NI公司,推广基于C#的一些开发库。虽然功能和解决方案都还处于比较初级的阶段,但麻雀虽小五脏俱全,作为国内的测控发展,还是很期待他们的发展。
最近刚好有两个思考分享给大家:
2018年3月23日,中美贸易战,美国针对高科技软硬件进行限制。虽然这次的限制概率不大,但无疑给我们工程技术人员一个警钟,将来限制进口的风险不可避免,只有提前储备自研的软硬件,才可避免未来依旧受制于人。
一直可以看到国内IT软件发展迅速,却不见任何工业级别软件有所建树。国内工程开发所用的软件基本上都来自国外,工程软件在项目中基本都是附赠产品,接项目总是靠关系和忽悠,自动化设备永远是你抄我,我抄你,处于原始竞争状态。
所以,工程行业需要搅局者,需要变革者,需要引领者。对国内出现自主创新的企业,我们当以包容的态度去看着他们成长,静静的等待他们壮大,等待有一天撑起中国的工程发展。
推荐阅读
-
那些年,我用工业控制开发语言
-
F#探险之旅(二):函数式编程(上)-函数式编程范式简介 F#主要支持三种编程范式:函数式编程(Functional Programming,FP)、命令式编程(Imperative Programming)和面向对象(Object-Oriented,OO)的编程。回顾它们的历史,FP是最早的一种范式,第一种FP语言是IPL,产生于1955年,大约在Fortran一年之前。第二种FP语言是Lisp,产生于1958,早于Cobol一年。Fortan和Cobol都是命令式编程语言,它们在科学和商业领域的迅速成功使得命令式编程在30多年的时间里独领风骚。而产生于1970年代的面向对象编程则不断成熟,至今已是最流行的编程范式。有道是“*代有语言出,各领风骚数十年”。 尽管强大的FP语言(SML,Ocaml,Haskell及Clean等)和类FP语言(APL和Lisp是现实世界中最成功的两个)在1950年代就不断发展,FP仍停留在学院派的“象牙塔”里;而命令式编程和面向对象编程则分别凭着在商业领域和企业级应用的需要占据领先。今天,FP的潜力终被认识——它是用来解决更复杂的问题的(当然更简单的问题也不在话下)。 纯粹的FP将程序看作是接受参数并返回值的函数的集合,它不允许有副作用(side effect,即改变了状态),使用递归而不是循环进行迭代。FP中的函数很像数学中的函数,它们都不改变程序的状态。举个简单的例子,一旦将一个值赋给一个标识符,它就不会改变了,函数不改变参数的值,返回值是全新的值。 FP的数学基础使得它很是优雅,FP的程序看起来往往简洁、漂亮。但它无状态和递归的天性使得它在处理很多通用的编程任务时没有其它的编程范式来得方便。但对F#来说这不是问题,它的优势之一就是融合了多种编程范式,允许开发人员按照需要采用最好的范式。 关于FP的更多内容建议阅读一下这篇文章:Why Functional Programming Matters(中文版)。F#中的函数式编程 从现在开始,我将对F#中FP相关的主要语言结构逐一进行介绍。标识符(Identifier) 在F#中,我们通过标识符给值(value)取名字,这样就可以在后面的程序中引用它。通过关键字let定义标识符,如: let x = 42 这看起来像命令式编程语言中的赋值语句,两者有着关键的不同。在纯粹的FP中,一旦值赋给了标识符就不能改变了,这也是把它称为标识符而非变量(variable)的原因。另外,在某些条件下,我们可以重定义标识符;在F#的命令式编程范式下,在某些条件下标识符的值是可以修改的。 标识符也可用于引用函数,在F#中函数本质上也是值。也就是说,F#中没有真正的函数名和参数名的概念,它们都是标识符。定义函数的方式与定义值是类似的,只是会有额外的标识符表示参数: let add x y = x + y 这里共有三个标识符,add表示函数名,x和y表示它的参数。关键字和保留字关键字是指语言中一些标记,它们被编译器保留作特殊之用。在F#中,不能用作标识符或类型的名称(后面会讨论“定义类型”)。它们是: abstract and as asr assert begin class default delegate do donedowncast downto elif else end exception extern false finally forfun function if in inherit inline interface internal land lazy letlor lsr lxor match member mod module mutable namespace new nullof open or override private public rec return sig static structthen to true try type upcast use val void when while with yield 保留字是指当前还不是关键字,但被F#保留做将来之用。可以用它们来定义标识符或类型名称,但编译器会报告一个警告。如果你在意程序与未来版本编译器的兼容性,最好不要使用。它们是: atomic break checked component const constraint constructor continue eager event external fixed functor global include method mixinobject parallel process protected pure sealed trait virtual volatile 文字值(Literals) 文字值表示常数值,在构建计算代码块时很有用,F#提供了丰富的文字值集。与C#类似,这些文字值包括了常见的字符串、字符、布尔值、整型数、浮点数等,在此不再赘述,详细信息请查看F#手册。 与C#一样,F#中的字符串常量表示也有两种方式。一是常规字符串(regular string),其中可包含转义字符;二是逐字字符串(verbatim string),其中的(")被看作是常规的字符,而两个双引号作为双引号的转义表示。下面这个简单的例子演示了常见的文字常量表示: let message = "Hello World"r"n!" // 常规字符串let dir = @"C:"FS"FP" // 逐字字符串let bytes = "bytes"B // byte 数组let xA = 0xFFy // sbyte, 16进制表示let xB = 0o777un // unsigned native-sized integer,8进制表示let print x = printfn "%A" xlet main = print message; print dir; print bytes; print xA; print xB; main Printf函数通过F#的反射机制和.NET的ToString方法来解析“%A”模式,适用于任何类型的值,也可以通过F#中的print_any和print_to_string函数来完成类似的功能。值和函数(Values and Functions) 在F#中函数也是值,F#处理它们的语法也是类似的。 let n = 10let add a b = a + blet addFour = add 4let result = addFour n printfn "result = %i" result 可以看到定义值n和函数add的语法很类似,只不过add还有两个参数。对于add来说a + b的值自动作为其返回值,也就是说在F#中我们不需要显式地为函数定义返回值。对于函数addFour来说,它定义在add的基础上,它只向add传递了一个参数,这样对于不同的参数addFour将返回不同的值。考虑数学中的函数概念,F(x, y) = x + y,G(y) = F(4, y),实际上G(y) = 4 + y,G也是一个函数,它接收一个参数,这个地方是不是很类似?这种只向函数传递部分参数的特性称为函数的柯里化(curried function)。 当然对某些函数来说,传递部分参数是无意义的,此时需要强制提供所有参数,可是将参数括起来,将它们转换为元组(tuple)。下面的例子将不能编译通过: let sub(a, b) = a - blet subFour = sub 4 必须为sub提供两个参数,如sub(4, 5),这样就很像C#中的方法调用了。 对于这两种方式来说,前者具有更高的灵活性,一般可优先考虑。 如果函数的计算过程中需要定义一些中间值,我们应当将这些行进行缩进: let halfWay a b = let dif = b - a let mid = dif / 2 mid + a 需要注意的是,缩进时要用空格而不是Tab,如果你不想每次都按几次空格键,可以在VS中设置,将Tab字符自动转换为空格;虽然缩进的字符数没有限制,但一般建议用4个空格。而且此时一定要用在文件开头添加#light指令。作用域(Scope)作用域是编程语言中的一个重要的概念,它表示在何处可以访问(使用)一个标识符或类型。所有标识符,不管是函数还是值,其作用域都从其声明处开始,结束自其所处的代码块。对于一个处于最顶层的标识符而言,一旦为其赋值,它的值就不能修改或重定义了。标识符在定义之后才能使用,这意味着在定义过程中不能使用自身的值。 let defineMessage = let message = "Help me" print_endline message // error 对于在函数内部定义的标识符,一般而言,它们的作用域会到函数的结束处。 但可使用let关键字重定义它们,有时这会很有用,对于某些函数来说,计算过程涉及多个中间值,因为值是不可修改的,所以我们就需要定义多个标识符,这就要求我们去维护这些标识符的名称,其实是没必要的,这时可以使用重定义标识符。但这并不同于可以修改标识符的值。你甚至可以修改标识符的类型,但F#仍能确保类型安全。所谓类型安全,其基本意义是F#会避免对值的错误操作,比如我们不能像对待字符串那样对待整数。这个跟C#也是类似的。 let changeType = let x = 1 let x = "change me" let x = x + 1 print_string x 在本例的函数中,第一行和第二行都没问题,第三行就有问题了,在重定义x的时候,赋给它的值是x + 1,而x是字符串,与1相加在F#中是非法的。 另外,如果在嵌套函数中重定义标识符就更有趣了。 let printMessages = let message = "fun value" printfn "%s" message; let innerFun = let message = "inner fun value" printfn "%s" message innerFun printfn "%s" message printMessages 打印结果: fun value inner fun valuefun value 最后一次不是inner fun value,因为在innerFun仅仅将值重新绑定而不是赋值,其有效范围仅仅在innerFun内部。递归(Recursion)递归是编程中的一个极为重要的概念,它表示函数通过自身进行定义,亦即在定义处调用自身。在FP中常用于表达命令式编程的循环。很多人认为使用递归表示的算法要比循环更易理解。 使用rec关键字进行递归函数的定义。看下面的计算阶乘的函数: let rec factorial x = match x with | x when x < 0 -> failwith "value must be greater than or equal to 0" | 0 -> 1 | x -> x * factorial(x - 1) 这里使用了模式匹配(F#的一个很棒的特性),其C#版本为: public static long Factorial(int n) { if (n < 0) { throw new ArgumentOutOfRangeException("value must be greater than or equal to 0"); } if (n == 0) { return 1; } return n * Factorial (n - 1); } 递归在解决阶乘、Fibonacci数列这样的问题时尤为适合。但使用的时候要当心,可能会写出不能终止的递归。匿名函数(Anonymous Function) 定义函数的时候F#提供了第二种方式:使用关键字fun。有时我们没必要给函数起名,这种函数就是所谓的匿名函数,有时称为lambda函数,这也是C#3.0的一个新特性。比如有的函数仅仅作为一个参数传给另一个函数,通常就不需要起名。在后面的“列表”一节中你会看到这样的例子。除了fun,我们还可以使用function关键字定义匿名函数,它们的区别在于后者可以使用模式匹配(本文后面将做介绍)特性。看下面的例子: let x = (fun x y -> x + y) 1 2let x1 = (function x -> function y -> x + y) 1 2let x2 = (function (x, y) -> x + y) (1, 2) 我们可优先考虑fun,因为它更为紧凑,在F#类库中你能看到很多这样的例子。 注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。 F#系列随笔索引页面