分类“Web开发杂谈”的存档

共 3 页: [1] 2 3 » | 下一页»

Web开发杂谈(12) —— 编程语言的新动向

2011年04月11日 星期一

两年前,我写过两篇文章谈到“编程语言的流行度”和“解读 TIOBE 指数”,谈到关于编程语言的一些问题。转眼两年就过去了,真要感慨时间飞逝。在这两年里,当然又出现了很多新动向。

我们也可以简单说说这些新动向,以及对我们有什么影响。数据主要还是来自TIOBE的。

主流语言依然稳定

主流语言的范围变化很小,跟两年前相比,按照排名前10的语言来看,除了Ruby语言跌出前10名,Objective-C入围之外,其他语言没有变化。这一点很好理解,一个开发者,使用哪种编程语言,通常是相对稳定的,不会非常频繁和轻易地改变。熟练掌握一门语言,也需要相当长的时间,这都决定了各种语言占有率的变化是相当缓慢的,如果用10年、20年的时间间隔,来考察编程语言,才有可能有较明显的差异,一两年的差异不会很大。

tiobe.jpg

Objective-C 和 C# 耀眼

虽然,各种主流语言的变化并不很大,但是也有着一些差异。表现最好的是 Objective-C 和 C# 两种语言。

objective-c1.jpg c-sharp1.jpg

其中 Objective-C 属于异军突起型,爆发力极好,09年中开始突然爆发,这显然与苹果公司产品的优势分不开的,Objective-C是苹果产品,如iPhone、iPad的首选语言。

而 C# 则属于稳步前进型,耐力性选手,从发布第一个版本开始到现在的10年时间里,一直稳步提高,不急不躁。

静态语言保持优势

在开发社区,关于动态语言的讨论十分很热烈,但是在实际应用中,从2007年开始的几年来,动态语言使用的比例在缓慢下降。实际上动态语言还有很多实际问题需要解决。这些问题不解决,是很难成为真正的主力语言的。下图中,蓝线是动态语言的使用率,红线是静态语言的使用率。

dynamic.jpg

当然,静态与动态也不是绝对的。C# 语言在2010年推出了第4个版本,即 C#4,在这个版本中,最重要的改进,就是支持动态类型。

从哲学角度看,动态语言具有更好的弹性,可以让系统具有更好的可扩展性和灵活性。但是实际上在绝大多数场合,使用动态语言进行开发,对程序员的要求高很多,静态语言中编译器可以做的很多事情,都需要由程序员来做,这些具体问题不解决,动态语言的优势很难得到体现。就好像目前而言,要建一座大楼,还是要靠混凝土这样的刚性材料。

移动开发日益重要

Objective-C 异军突起,显然是由于苹果 iPhone 、iPad以及AppStore的成功引发的。大家也很容易发现,现在移动开发的火爆,经常可以听到一些朋友说到,移动开发人员紧缺,当然也就是高薪的同义词了。

所以,如果有兴趣的读者,不妨也关注一下相关的发展情况。总体来说,我认为在移动终端上的开发开发客户端软件,就想当于在10到20年前,PC上的主流开发。当时大部分开发都是在PC开发本地程序。现在,移动终端来了,那么自然也需要很多开发人员来进行相应的程序的开发。

但是我们看一看,PC是的历史,从单机程序逐渐变为开发“C/S模式,也就是客户端/服务器模式的程序,把很多核心业务放到了服务器上,客户端越来越瘦。后来,互联网来了,向大比例的C/S开发变成了“B/S”模式,也就是客户段痩到了不再开发单独的客户端程序,而是都用浏览器来作为操作界面了。

那么移动终端未来的趋势将会如何?我估计,一部份必须的客户端软件,都会在移动终端上出现,这是一种“补课”。此外还有大量业务,是基于网络的,在移动终端上,就像PC上一样,要么是做一个独立的客户端程序,要么用浏览器来作为界面。

目前来看,与PC上的一个区别是,PC的浏览器屏幕大,操作容易,因此客户端形式的程序并不多,除非是一些和系统结合紧密地,比如Windows自带的一些程序,Office这类软件(身子也出现了给予浏览器的在线版本),还有就是杀毒软件等等。

而移动终端的最大特点就是屏幕小,这就限制了流览器在移动终端上的应用,因此客户端软件(即所谓的“应用”)又显示出了优势 —— 可以让操作界面适应小屏幕的需要。

了解了上面的一些基本背景后,我们就可以想象,所谓“移动开发”,真正和PC上差异比较大的就是开发移动终端上的客户端软件,或者叫做“应用”这个部分上。其他的(包括给予浏览器的前端开发,以及服务器端程序的开发)则和PC任何区别。而一个比较复杂的系统,真正工作量绝大部分还是集中在服务期端的。

所以关于移动开发,大家也不用觉得是个很新鲜东西,其实我感觉本质上并没有太多的新东西。

语言 < 平台

在任何开发社区,关于语言的讨论都特别多,而且特别容易引起争论。按照我目前个人的理解,经过了近50年的发展,设计开发出一个新的高级语言,已经不像几十年前那么困难了。一些计算机领域中的明星人物,一个人就可以设计出一个非常牛B的语言,这些语言在一些社区中发展,很有特色。现在数得出名字的语言有几百种,Tiobe跟踪的有几十种。但是真正成为主流的还是很少的,比如现在,使用率超过1%的高级语言,不过11种。最主流的4种语言,就拥有了超过50%的使用者。

现在关于语言的竞争,本质上已经不在于语言本身了,而在于语言所在的平台的竞争。你会发现,所有发展的好的语言,都又一个很好的平台作为支撑。选择语言的本质是选择平台。这一点对于开发人员的选择是比较重要的因素。

总结

还是那句话,编程语言既是一个新技术层出不穷,又是一个相当稳定的东西。

不少人提这样一个观点,我非常赞同:真正精通一两个语言,然后多了解一些其他的语言。精通一两种语言,是你工作的武器,绝对不能丢。然后当有新技术、新热点出现时,可以花一些时间做一些尝试和试验,可以跟上新潮流,而且可以不断开阔眼界,加深对技术的理解,是一件很好的事情。

Web开发杂谈(10) —— 原型设计与工具

2009年12月09日 星期三

任何一个网站都是经过一定的过程,才能逐渐由头脑中的概念成为一个真正的网站的。一个网站的设计过程往往是需要经过大量的修改、甚至若干次的推翻重来,才可以达到最终的结果。今天来简单谈谈这个过程中使用的方法和工具。

问题的背景

在最初的设计阶段,最重要的一点是与客户进行良好而充分的沟通。尤其是现在的网站功能越来越复杂,而客户往往只能描述一些非常抽像的概念、想法和特征

因此需要我们做设计和开发的人充分理解客户的想法,但是一个重要的问题是,我们如何验证我们是否真正理解了客户的想法呢?当然我们通常会写一些文档,作为与客户签订合同的附件。但是即使用再多的文字描述,毕竟客户看不到最终的效果,而最终开发出来的网站或系统如果不符合客户的心意,就是一件非常麻烦的事情了。要么为了符合客户的要求,而进行大量的修改工作,其中的成本是非常巨大的,而你向客户追加费用是很困难的事情了,即使客户同意,也不是件令人非常开心的事。要么就只能凑了,也许这并不是设计和开发公司的责任,但是毕竟问题发生了。

那么从比较理想的方式应该是如何的呢?显然,如果我们能够在还没有真正开始投入大量人力进行设计和开发之前,就可以让用户看到最终的效果和功能,那就是最好的了!这就是所谓的“原型设计”的作用。

我们用尽可能小的代价,把我们理解的用户需求表现出来,或者叫“模拟”出来,甚至用户可以实际操作,就好象这个系统已经做好了一样,这样将来做出来的系统和这个“原型”系统非常接近,那么在开发完成后在发现问题的可能性就小多了。

静态原型

因此,原型系统是非常重要而有效的一个手段,那么有什么好的方法来实现“原型”呢?

当然,最基本的方法就是手工绘制一些图纸了,此外,比如Fireworks软件中,提供了一些更为方便的进行原型设计的功能,但是这些方法和软件还是停留在“静态”展示的层面,本质上和手工绘制一些图给客户,进行纸上谈兵是一样的。

而现在网站的交互功能越来越强,一些基于Web的信息管理系统,操作起来就跟们更为复杂,比如各个页面之间的跳转,如果使用了Ajax等技术,页面还会局部刷新,如果都要用静态的图像来表达,实际上是非常困难的。

SkecthFlow 增加原型系统的动态性

因此,如果我们可以方便地制作出,可以模拟最终的动态效果的原型,那么就好多了!这是基于这个原因,近年来出现了一些具有模拟效果的原型设计软件,比如我们以前介绍过的Axure RP Pro,不我实际使用过一两个项目以后,感觉这个工具还是不够方便。而今天介绍一个我刚刚开始使用的软件 SketchFlow,感觉非常棒!实际上这不是一个独立的软件,而是附属于 Expression Blend 的一部分。而 Expression Blend则是微软的 Expression Studio 套件中的一个。如果再往下说,就要到大名鼎鼎的 Silverlight (微软指望靠它和Flash大干一场呢)上了—— Expression Blend 正是用来设计和开发 Silverlight应用的软件,因此对于微软来说,这可是一个重头戏。

而在 Expression Blend 3 中,包括了一个部分叫做“SkechFlow”,我们今天要讲的就是它了。 因此现在这个软件的全称叫做“Microsoft Expression 3 + SketchFlow”,够长的名字。

Sketch 就是草图的意思,Flow就是流程的意思,因此合在一起,就是“流程草图”的意思。那么它具有什么用呢?

SkecthFlow 的用途

首先,这个Blend这个软件本身就是可以绘制矢量图形和动画的,因此用它来绘制页面的草图是很方便的,比如假设我们要租一个网站,首先是一个登录页面,然后进入到一个内容页,我们就可以非常方便地绘制出如下的图形,注意这种风格是专门为此设计的草图风格,看起来就像是手绘的风格,主要的目的就是给用户展现的是页面的核心概念和功能,而不会是非常细节的样式。我个人非常喜欢这种风格,看起来分舒服。

Sketchflow

当然,如果只是把图像绘制的看起来手绘风格,那么也就没有太的改进了,更更为重要的是,使用SketchFlow制作的网站原型,可以精确地模拟实际操作过程,比如上面图中,登录框中的用户名输入框、密码输入框、滑动条、选择框、按钮都是可以实际操作的,而且通过简单控制,就可以非常方便地使他实际工作起来。在比如,上面的右图中,“作者介绍”和“读者评价”部分,各有一个滚动条,而这些滚动条都是有实际作用的,因此使用这种方式制作出来的原型,可以使客户非常直观而精确地了解最终这个网站完成以后,会是什么样子的,这样如果他又不满意的地方,在一开始就可以明确地指出来,这样对于我们开发成本的控制就大有好处了。

此外,软件中给出了方便的导航图,以显示各个页面之间的导航关系,也非常清晰实用,如下图所示。

Sketchflow 2

有兴趣的读者可以在浏览器中实际看一下演示的效果,如果没有安装Silverlight插件的话,需要安装一下。请点击这里查看实际效果,在登录页面随意输入,然后按Login按钮,导航至内容页面,可以在左侧调整大小。

此外,SketchFlow还可以自动生成Word格式文档,包括目录、文字、导航图、各个页面都准备好了,这样稍作修改后,提供给客户交流,就非常方便,也非常专业了。如下图所示。

Sketchflow 2 word

当然,这个软件的功能还远远不止于添加几个导航链接的功能,因为它本身用C#或者VB就可以进行深入而灵活的控制了,不过我自己也没有深入学习过Silverlight的开发,并不能说的非常清楚,此外这些操作并非几句话可以说清的了,有兴趣的读者,可以参看前不久的微软开发者大会上关于 SketchFlow 的讲演: http://microsoftpdc.com/Sessions/CL23 。讲演者说的英语不算快,还比较容易听懂,另外有很多屏幕操作,也很有帮助。

本文小结

今天重点介绍了两个问题,1)在实际开发之前,制作原型系统的重要性,以及能够给我们带来的好处。 2)使用 SketchFlow 可以比较方便地制作出高度模拟实际效果的原型系统,给我们带来很大的益处。

Web开发杂谈(9) ——网站空间故障的排查

2009年11月04日 星期三

我们从去年6月份开始为我们的一些读者提供实际练兵的的虚拟主机空间——“学以致用”计划,现在有了不少的会员。本文主要是针对我们的会员遇到问题时参考来解决问题用的,如果您拥有自己的网站,使用的是其他的空间商提供的空间,如果有类似问题的读者也可以参考,希望对您有所帮助。

先了解一些基本原理

当您制作完成了一个网站,租用一个“学以致用”空间(或者称为虚拟主机),把网站发布到了空间,这时全世界的人都可以访问你的网站了。有的时候,会遇到一些问题,比如网站连不上了,或者觉得网页装载的速度不够快等等,当然遇到这种情况,可以给我们发信,我们会帮你检查。

但是在这里介绍一下,你应该做哪些自己可以做的检查工作,这也大致可以判断出问题出在哪里。这里先介绍一些简单的原理性知识.

我们知道,访问一个网站,输入一个网址,最终看到了你要看的网页,数据的传递实际上经过了千山万水,很多个环节,大致包括以下几个部分:

1:你自己的电脑。

2:如果你是在一个局域网,比如单位或家庭的局域网,则要经过局域网的路由器链接到互联网上。

3:互联网会从你家附近的电信局一个节点一个节点地找到目标服务器。

4:如果你的服务器在国外,还会经过海底光缆,进入其他大陆的互联网(我们给会员提供的服务器是位于美国的)。

5:进入服务器所在的机房(数据中心)。

6:到达最终的服务器,服务器根据请求的页面,产生网页内容,再沿着上述路径返回传输到你的电脑上。

因此,如果在这一些列的传输过程中,任何一个节点出现故障,都会导致无法看到你要看的网页。如果你的网站不能够正常打开了,首先要判断一下是在上述6个环节中的哪一个环节出了问题。

自检步骤

请根据下列操作进行分析:

1:首先如果可以正常上新浪、Google、百度等网站(注意确认打开的不是缓存网页,最好使用Google随便搜索一个词,看看返回的结果是否正常),说明你的电脑和外界的互联网连接正常,反之说明你的电脑或者内部的局域网连接有问题了。

2:如果可以访问新浪等大网站,而无法访问你自己的网站,那么就需要判断是哪里断了?可以用下面两种方法

A: 比较快速的方法,使用 Ping 命令。选择Windows 的开始菜单,选择“所有程序 > 附件 > 命令提示符”,这时出现一个命令行窗口。用键盘输入”ping 你要诊断的网站的域名”,然后回车,这时,如果网络连接正常时,将会看到如下所示的结果。

ping结果

而如果网络连接的某个环节发生中断时,将会看到如下所示的结果,说明无法连接到你要看的网页所在的服务器。

ping结果

B:同过上面的 Ping 命令,可以检查从你的电脑到目标服务器之间的网络连接是否通畅。此外,还有一个比Ping命令更为详细的命令 —— Tracert 。

对于服务器在国外的情况,比如我们的虚拟主机,如果出现了中断,需要判断一下中断的路由器在国内还是国外。可以进入命令行窗口,然后使用“Tracert”命令。

对于链接通畅的网站,结果应该是如下所示。

tracert-1.gif

其结果显示的就是从你的电脑到目标服务器之间经过所有路由器的IP地址,注意每一行中间有一个几十或几百的数字,其确切含义这里不多解释了,只是你会注意到,从起点开始这个数值一般情况下会越来越大,特鄙视你会看到,其中有一跳之后数字突然从几十毫秒变成200多毫秒,这就是从国内的网络进入了国外的网络。

如果某一个途中的某个路由发生故障,那么在从该节点开始,原来现实毫秒数的数值就会显示为星号,如图所示,表示连接发生故障了。

tracert-2.gif

某一个节点发生了故障而导致中断,你就可以判断故障发生在国内还是国外。看一下上面正常的那个tracert结果图,数字很小的那些节点如果变成星号了,就说明是国内的网络出问题了,而如果时是后面数字比较大的节点变成星号了,就说明国外的某个网络节点出问题了。

从我这四五年的经验来看,这种骨干网发生故障的情况并不多见,尤其是国外一侧几乎一年于不到两三次,每次最多几分钟,国内相比之下就会多不少,特别是似乎几个月会调整路由,会在某一天或几天中,频频发生故障,而我们用户也没有什么办法,只能等待。

因此,当你发现国内或者国外的路由节点发生故障了,就耐心等待一会儿,一般就会好了,因为这些骨干线路发生故障,影响面很大,会很快修好的。

3:如果使用Tracert命令发现整个路由的最后一个节点无法,也就是目标服务器显示的星号,那就说明是这台服务器当机(死机 、停机,总之是故障了),可以给我们发邮件,我们给你查一下。给我们发邮件的时候,请附一个路由结果。

4:如果服务器可以Ping通,即最后一节点也可以正常显示出正常的毫秒数值,但是网站打不开,那很可能是服务器还没有完全死机,但是一些服务,比如HTTP服务已经无法正常运行了,可以给我们发邮件,我们给你查一下。 给我们发邮件的时候,请附一个路由结果。

上述第3、4两点如果发生故障,那就是服务器提供商的责任了,就我们的经验看来,我们选用的服务商的服务还是相当稳定的。

上面说的是如果你的网站完全无法连接的时候,如何判断故障原因。接下来,有的时候,你会觉得打开速度不够快。这个问题就很复杂了。特别是中国的网络情况是非常复杂的,全国各地访问同一台服务器,结果速度可能就相差很多。

因此,建议你这样做:

1:平常没事儿的时候,也可以用tracert命令查一下从你的电脑到目标服务器之间的路由结果,这样平常有个印象,当某一个时刻,你感觉网站的速度不如平常的时候,tracert一下,看一看结果是否和平常相同,如果结果正常,那么或者是你的网站的设置出问题了,或者可能你是你的心理作用了,既不是网络的链接问题。

2:如果发现路由中,某个节点的数值突然变大了很多,那就说明是这个节点的路由器有点问题了,这个也做不了大多,一般来说经过一段时间,就会恢复正常了。

3:如果你没有发现任何问题,但是觉得速度很慢,可以这样检测一下:

首先,安装一个Firefox浏览器,然后安装Firefox的一个插件,叫做Firebug,利用这个插件,可以精确地测量一个页面的详细的装载过程和时间,例如

firebug

如图所示,安装好Firebug以后,可以在Firefox浏览器的右下角看到一个小虫子的图标,单击该图标,在浏览器窗口的下半部,会出现一个新的窗口,如图中的红色方框所示,选择“网络”、“所有”,然后浏览一个网页,这是下侧的窗口里就会显示出该页面中包括的所有文件,比如html文件、CSS文件、图像文件等等,完整的如下图所示。

firebug监视网页加载的时间

可以看到,列出了每个文件的大小,以及装载的次序,每个文件的后面有一个彩色的横柱图示,不同颜色代表装载该文件的不同阶段,其实就是对应于本文开头说的那些步骤。

你用这个插件查看一下你的网站的装载情况,看看主要慢在哪里,比如是否页面的体积太大?图排太多?图片没有压缩?如果你还是无法判断,把这个图发新给我们,我们给你看一下,是否正常,以及可能出现的问题。注意Firebug窗口的最底下计算了整个页面的所有文件的总的大小,以及完整装入浏览器的总时间,比如图中这个页面,所有文件加在一起200多K字节,一共用了5秒多装载,基本上可以接受。当然这里面要说的话,还有很多很多讲究的,并非几句话可以说清的了。

顺表说一句题外话,Firebug这个插件功能非常强大,对于网页设计,特别是要跟CSS打交道的设计师,以及要做Javascript开发的人员,Firebug都是必不可少的工具。以后有时间的时候,我会写一些相关的文章,介绍一下。

本文总结

如果遇到访问或者网络连接问题,请仔细阅读上文,如果仔细看过之后仍然无法找到原因,请给我们发邮件,在邮件中,请附带执行tracert命令的图,如果您的网站可以打开,但是觉得速度慢,在邮件初中附上Tracert的图,以及firebug的装载时间图,以便我们帮你查找原因。

如果你发现你的“学以致用”主机的网站无法访问,或者感觉速度不好,经过上述检查手段,仍然无法发现原因,请给我们发邮件。在邮件中请包括必要的内容:

1:一定要包括一个 Tracert 你的网站 的结果截图。

2:如果网也打得开,但是你觉得网站打开的速度慢,请包括一个firebug的页面装载时间图,就像上面第二个firebug图那样的图。

如果您使用的是其他虚拟主机,遇到类似问题,也可以把这些结果发给客户服务的技术支持人员。就像到医院看病一下,关键是要找出问题的原因在哪里,然后再能够解决。用一些工具去进行判断,就像看病的时候要做化验、做CT扫描等检查一样,都是很重要的手段。

Web开发杂谈(8) —— 一个很基础的问题(表达式求值顺序)

2009年10月22日 星期四

前几天一位读者在这里留言问了一个程序设计中很基础,其实也很经典、而且历史悠久的问题,我简单回答了一下,后来发现还有不少读者对此有兴趣,因此就展开来说一说吧。

他的问题是这样的,对于下面这段的代码,执行完成后,变量m的值应该是是多少?

1
2
3
4
5
6
main()
{
    int i=2,m=2 ;
    m+=(i++) + (++i) + (i++) ;
   printf( "%d" , m);
}

这位读者在TC(我猜想用的是Turbo C 2.0 ?)中得到的结果是 11 。具体读者讨论可以看这里

当时我给出了一个很简略地回答:

这个问题其实挺复杂的,这个表达式在不同的语言中的结果是不一样的,比如在 C、C#、Perl、Java中的结果是不一样的,每种语言对这个求值得逻辑有各自不同的定义。

所以最好不要写这样的表达式。也没有必要,要计算什么就要最明确的方式写出来。

不过后来有几位读者仍然围绕着为什么等于11讨论了几个帖子,这几位读者似乎并没有注意俺当时的回复。在上面简单回答中,并没有非常深入,只提到了不同语言对此有不同,其实即使同一种语言,也有更为复杂的情况。因此这里就展开谈一谈。

问题化简

实际上,这种类似的题目在20年前的C语言教科书或者考试经常出现,但是现在如果再出这种题目,那么要么是对学生是很不负责任的老师,要么是非常负责任的老师。

我们先不管上面问题中的代码,而是先看一个更简单的表达式求值问题:

1
2
    int x=1;
    x = x++;

请读者考虑一下,对于C(C++)语言,上面代码执行完成后,x的值应该是多少?

思路一: 先计算表达式 x++ ,此时其值为1,然后将 x++的值将其赋给x,因此x的值等于1。

思路二:先计算表达式 x++ ,此时其值为1,然后将其赋给x,此时x的值等于1,然后执行x加1的运算,因此最终x的值等于2。

那么上面两种思路,哪个是正确的呢?

正确的答案是,都不对,而正确的回答是:“这个值是不确定的”。

“语言”与”实现”

首先要了解一个“语言”(比如C语言、C++语言),和一个语言的“实现”(比如Turbo C 2.0 和 Visual C++ 6.0)之间的区别。这个类似于“CSS规范”与“浏览器”之间的关系,尽管CSS的标准是统一的,但是各个浏览器厂商有各自对CSS的实现方式,并且存在着差异。

同样,一个语言仅仅是一个规范,它规定了一个语言的语义,而各个编译器厂商对一个语言有各自的实现。和浏览器实现CSS之间的差异不同,在这里,并不是由于各编译器厂商对语言本身的理解不同导致的差异,而是在规范中就给出了明确的自由空间给编译器厂商。例如在linux下用gcc编译出来的程序和用turbo C编译出来的程序, 得到的结果就会不同。

下面具体看一下规范是如何定义的。

A:表达式的值与副作用

表达式有两种功能。每个表达式都产生一个( value ),同时可能包含副作用( side effect )。副作用是指改变了某些变量的值。

比如:

1: 20 //这个表达式的值是20;它没有副作用,因为即它没有改变任何变量的值。

2: x=5 // 这个表达式的值是5;它有一个副作用,因为它改变了变量x的值。

3: x=y++ // 这个表示有两个副作用,因为改变了两个变量的值。

4: x=x++ // 这个表单时也有两个副作用,因为变量x的值发生了两次改变。

B:求值顺序点

表达式求值规则的核心在于 顺序点( sequence point ) [ C99 6.5 Expressions 条款2 ] [ C++03 5 Expressions 概述 条款4 ]。

顺序点的意思是在一系列步骤中的一个“结算”的点,语言要求这一时刻的求值和副作用全部完成,才能进入下面的部分。 C/C++中大部分表达式都没有顺序点,只有下面五种表达式有:

1: 函数。函数调用之前有一个求值顺序点。

2 :&& || 和 ?: 这三个包含逻辑的表达式。其左侧逻辑完成后有一个求值顺序点。

3 :逗号表达式。逗号左侧有一个求值顺序点。

注意,他们都只有一个求值顺序点,2和3的右侧运算结束后并没有求值顺序点。

C: 最重要的一点

C1:对于C和C++语言:

在两个顺序点之间,子表达式求值和副作用的顺序是不同步的。如果代码的结果与求值和副作用发生顺序相关,称这样的代码有不确定的行为(unspecified behavior)。 而且,假如期间对一个内建类型执行一次以上的写操作,则是未定义行为(undefined behavior)。

举例来说,对于表达式 x=x++ ,副作用发生的顺序,也就是赋值和自增1运算在何时执行,C/C++语言本身没有规定,而将其交给编译器厂商来自行决定。

C2:对于C#和Java语言,对于表达式的求值顺序和副作用发生顺序都有严格的定义,这样任何符合C#(或者Java)规范的编译器,对于相同的表达,求出来的值都是相同的。

D:为什么要这么做呢?

对于C/C++语言:

因为对于编译器提供商来说,未确定的顺序对优化有相当重要的作用。编译器可以重新组织表达式的求值,以便尽量不使用额外的寄存器以及临时变量。更加严格的说,即使是编译器提供商也无法完全彻底序列化指令(比如无法严格规定读和写的顺序),因为CPU本身有权利修改指令顺序,以便达到更高的速度。

对于C#和Java语言:

在规范中严格定义副作用的发生顺需,可以保证相同的代码产生相同的结果,这样对于程序的维护、升级都会方便得多。

E:重要结论

在写程序的时候,不要写依赖于“实现”的代码。因为代码在未来有可能要移植、要升级,也就是相同的源代码可能会到不同的“实现”中进行编译,从而带来潜在的问题,有可能这些问题在未来某个未知的时间才会发作,到那个时候,再想找到某个深藏着的问题,就会非常困难了。

再回到我们的例子中

对于这段简单的代码:

1
2
    int i=1;
    i = i++;

这里仅给出来Visual studi 2008中,的C++语言 和 C#语言中的不同表现,并进行一些说明。

如果使用 C++,运算完成后, i 的值等于2;而如果使用C#,运算完成后,i的值等于1。

这里可以分别看一下,在C++和C#中,上面这两行代码分别对应的汇编代码,就可以非常清楚了。在Visual Studio中可以很方便地查看语句对应的汇编代码。

在C++中得到的汇编代码是:

1
2
3
         // i =  i++ ;
         nop              
         inc  dword ptr [ebp-4]

ebp-4 就是变量i的地址, [ebp-4] 表示的就是变量i的值很简单,可以看到,这里的处理方式是直接把变量i的值加1。

而在C#中得到的汇编代码是:

1
2
3
4
5
6
          // i =  i++;
         mov         eax,dword ptr [ebp-3Ch] 
         mov         dword ptr [ebp-40h],eax 
         inc         dword ptr [ebp-3Ch] 
         mov         eax,dword ptr [ebp-40h] 
         mov         dword ptr [ebp-3Ch],eax

ebp-3Ch 就是变量i的地址, [ebp-3Ch] 表示的就是变量i的值,上述代码的执行过程是:

1:首先把变量i的值复制到寄存器EAX中,
2:把寄存器EAX中值复制到一个临时地址中,
3:把变量i的值加1,
4:把第2步中的临时变量的值复制回EAX中,
5:把EAX中的值复制回变量i中。

由此可见,自增运算的实际上并没有产生实际的作用。

现在再看一下最开始的问题,m+=(i++) + (++i) + (i++) 是如何等于11的呢?还是来看一下汇编代码,这是最准确的答案了。在Visual Studio中,这个语句对应的汇编代码是:

1
2
3
4
5
6
7
8
9
          //m+=(i++)+(++i)+(i++);
           inc         dword ptr [ebp-4] 
           mov         eax,dword ptr [ebp-8] 
           add         eax,dword ptr [ebp-4] 
           add         eax,dword ptr [ebp-4] 
           add         eax,dword ptr [ebp-4] 
           mov         dword ptr [ebp-8],eax 
           inc         dword ptr [ebp-4] 
           inc         dword ptr [ebp-4]

从上面的汇编代码,就很清楚11如何计算出来的了,首先把变量i的值加了1,这时i就等于3了,然后累加了3次,即 m = 2+3+3+3 ,从而 m = 11 。上面汇编代码中最后两行又把i两次加1,但是和m的值已经无关了。

因此,用清晰的写法,m+=(i++) + (++i) + (i++) 在C++中等价于:

1
2
3
4
5
6
7
        //int i=2;
        //int m=2;
 
        i=i+1;             // m=2, i=3
        m=m+i+i+i;   // m=11,i=3
        i=i+1;            // m=11,i=4
        i=i+1;            // m=11,i=5

那么再看看在C#语言中,同样的语句,产生的汇编代码又是如何的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
           // m += (i++) + (++i) + (i++);
           mov         eax,dword ptr [ebp-3Ch] 
           mov         dword ptr [ebp-44h],eax 
           inc         dword ptr [ebp-3Ch] 
           inc         dword ptr [ebp-3Ch] 
           mov         eax,dword ptr [ebp-44h] 
           add         eax,dword ptr [ebp-3Ch] 
           mov         dword ptr [ebp-48h],eax 
           mov         eax,dword ptr [ebp-3Ch] 
           mov         dword ptr [ebp-4Ch],eax 
           inc         dword ptr [ebp-3Ch] 
           mov         eax,dword ptr [ebp-40h] 
           add         eax,dword ptr [ebp-48h] 
           add         eax,dword ptr [ebp-4Ch] 
           mov         dword ptr [ebp-40h],eax

通过上面的汇编代码,可以看到在C#中,m+=(i++) + (++i) + (i++) 这个表达式等价于下面的程序段:

1
2
3
4
5
6
7
8
9
10
11
        //int i=2;
        //int m=2;
 
        int t1=i;           // m=2, i=2, t1=2
        i= i+1;             // m=2, i=3, t1=2
        i= i+1;             // m=2, i=4, t1=2
        int t2= t1 + i  // m=2, i=4, t1=2,t2=6
        int t3= i          //  m=2, i=4, t1=2,t2=6,t3=4
        i= i+1            //  m=2, i=5, t1=2,t2=6,t3=4
        m=m+t2       // m=8, i=5, t1=2,t2=6,t3=4
        m=m+t3       //m=12, i=5, t1=2,t2=6,t3=4

可以看到,里面使用了3个临时变量(即内存单元),而且步骤也比在C++中复杂。但是可以看到,在C#中,++运算符的副作用发生时间与求值过程是同步的,更接近与本来的语义。相比在C++中,二者就不是同步的了。

面试指南

前面已经强调,这样写代码是很不好的风格,建议不要写出这样的代码。但是如果你要是参加面试,或者其他考试碰到了这种题目应该怎么做呢?其实很简单。首先确定,如果用的是什么语言:

如果是C#或者Java,那么这就按照求值顺序,同步地对++ (- -)运算符计算相应的副作用发生顺序就可以了。前置或者后置的加减1,都是紧挨着求值进行的,参考上面从汇编代码翻译出来的C#代码。

如果是C/C++语言,就要问一下,使用什么编译器,使用Turbo C,还是VC,还是gcc,这样会显得你很专业,当然我并不知道所有编译器的表现,现在我知道是,Turbo C和VC9(visual Studio 2008,我猜测其他版本的VC应该大致相同,我没有验证过)中,这个求值顺序是这样的:

在开始计算之前,先把所有前置 ++ (- -)运算都计算完成(不考虑位置),比如上面的例子中,只有一个前置 ++,因此先把 i 加1,然后就开始计算表达式的值,而不再计算增减运算,直到下一个顺序点之前,把剩下的后置 ++ (- -)运算统一处理完,比如上面里中,有两个后置++,所以在最后赋值之前,把i变成5。

对其他编译器有兴趣的读者可以自己试试看,效果如何。

不仅如此

通过上面的讲解,似乎我们已经理解了表达式求值的过程,而实际上还有更为有趣的内容在等着你。

对于 m+=(i++) + (++i) + (i++) 这个表达式,在各种语言,比如C、C++、C#里面都是可以通过编译检查,并且能够计算出一个结果的,比如上面的例子,在C++和C#中分别等于11和12,但是实际上,这个写法是错误的——严格地说,是不符合标准的。也就是说,如果按照C/C++语言的标准,这个表达式的写法是错误的。

其原因是,在标准中要求,任何表达式在两个顺序点之间的求值过程中,任何一个变量的值只能修改一次。而m+=(i++) + (++i) + (i++) 这个表达式中,变量 i 被修改了3次,因此这个语句本身就是错误的。

因此,上面给出的简单的例子 x=x++ ,同样是一个错误的写法,因为x被修改了两次。

实际上,在任何一个语言的规范中,相关的内容还有非常非常多,当然对于一般的开发人员,不是打算自己写一个语言的编译器的话,不需要搞得太清楚,知道大致的原理就可以了。实际上,世界上真正写编译器的人并不多,比如在微软,参加编写 .net 框架的程序员有上千人(2002年的数字),而真正设计和编写C#编译器的人不超过5、6个人而已。

总结

1:可以看到,在表达式求值的过程中,具体副作用是什么时候发生的,这一点在不同语言,甚至同一种语言的不同的编译器上,结果都有可能不同。概括来说,就是求值的步骤,和修改变量的步骤并非同步的,由此会导致不同的结果。

因此,在写程序的时候,要尽量避免这种情况发生,写出语义清晰明确的代码,以避免由此带来的不确定性。

2:搞清楚一个问题真正的原因才能真正解决问题,关于表达式求值的问题,实际上是有着非常多问题可以深入的,例如上面提到了“不确定”这个结果,实际上严格来说,还有更为复杂和严谨的内容值得探讨,本文就不再细谈了。

建议有兴趣的读者参考下面两篇文章,上面文章中有部分文字来自于这篇文章:关于C/C++ 表达式求值顺序C++中的求值|副作用|序列点所导致的模糊语义

3:掌握一些底层的基础知识,比如了解一些简单的汇编语言知识,在需要的时候,就可以用来解决一些问题,因此作为开发人员还是要深入掌握基础才好。

4:如果对某个语言有兴趣的,希望真正掌握好它,可以仔细读一读这个语言的标准,或者说规范,这对于很多概念的理解是非常有帮助的。例如对于C#的规范,安装了Visual Studio之后,在安装的文件夹 Microsoft Visual Studio 9.0\VC#\Specifications\1033 中有一个500多页的Word文档, 标题叫做《C# Language Specification (Version 3.0)》,很多很多你不理解的问题都可以在里面找到答案。

Web开发杂谈(7) ——软件开发与数学基础

2009年08月11日 星期二

在上一篇文章《Web开发的入门建议》中,我稍微提了一下关于数学的问题,提到数学是很多计算机课程的基础,有几位网友也讨论了这个问题。我就由此想到了一些话题,今天随便聊聊。先来举一个例子吧。

一个C#中的例子——lambda表达式

我们多次提到,当前来说,Web开发领域,开发工具(或者叫平台)的“三巨头”是 .Net、JAVA和PHP。如果用.Net,那么一般用C#语言(或者VB),在C#语言3.0版中,引入了一个非常重要的新特性称为“lambda表达式”。首先来普及一下“lambda表达式”的概念和作用。lambda 是一个希腊字母“λ”的英文读音,我们中国人一般念作“拉姆达”。

首先,在C#中,可以用 lambda表达式来定义一个匿名的函数。例如

x => x%2==0

就定义了一个匿名的函数,这个函数的作用是判断一个参数是不是偶数。其中 “=>” 是C#中用用于定义lambda表达式的运算符,他前面的x是函数的输入参数,他后面是返回的结果,当x是偶数,即x除以2的余数等于0时返回true,否则返回false。那么定义了这么一个lambda表达式有什么用呢?还是看一个例子。

比如说,有一个一维数组,保存着若干整数,现在需要计算出其中大于0的个数。那么按照传统方法可以这么写:

int c=0;
foreach(int i in numbers)
{
if(i>0) c++;
}

而如果在C#3.0中,使用 lambda表达式,代码就可以缩减到这样一个语句:

int c = numbers.Count(x => x>0 );

其中,Count() 是数组类型的一个方法,它的参数是一个lambda表达式,通过这个lambda表达式可以过滤参加统计的元素,就是说,只有大于零的元素才进行统计,这样,一句话就可以非常清晰地完成了上面的任务。

而这仅仅还是一个非常简单的要求,如果是这个例子的要求更复杂一些:“将一个整数数组中的所有正数选出来,各自求出其平方,然后按大小排序,构造出一个新的数组”。那么这段程序可就需要做不少事情了,首先要对数组中的所有元素进行过滤,筛出大于0的数,然后每个数求出他的平方,然后对他们进行排序,并且要构造一个新数组并填充结果。而在C#3.0中,通用可以使用一个语句完成上面的任务:

int[] n = numbers.Where(x => x>0 ).Select(x=>x*x).Orderby(x=>x).ToArray();

可以看到,上面的语句中,出现了3次 lambda表达式。Where、Select、Orderby这个三个方法调用,分别实现了过滤、平方、排序,他们都是以 lambda表达式作为参数传递到方法中,可见它的灵活和方便。

lambda表达式的数学来历

我们看到了lambda表达式的作用,很容易我们会想到,为什么他们叫做“lambda”表达式这个“奇怪”的名字呢?实际上,这就倒回去80年了,在计算机还处于“纸上谈兵”的年代,20世纪30年代,美国数学家阿隆佐.丘奇(Alonzo Church, 1903 – 1995)给出了一套用于研究函数定义和应用的形式系统,称为”λ演算“(lambda calculus)。

如果没有学习过相关内容,要完整地理解 lambda 演算,并不是很容易的一件事。这里给出一个最简单的例子,在 lambda 演算中,每个表达式都代表一个只有单独参数的函数,这个函数的参数本身也是一个只有单一参数的函数,同时,函数的值是又一个只有单一参数的函数。函数是通过 lambda 表达式匿名地定义的,这个表达式说明了此函数将对其参数进行什么操作。

例如,一个函数 f(x) = x + 2 ,这是我们都很熟悉的书写方式,表示自变量(参数)为x,函数的值为“x+2”。如果用 lambda 演算表示,他的形式就变为了: λ x. x + 2。这里λ是一个固定的符号,后面的x表示参数,圆点后面表示函数的值是x+2,而 f(3) 的值写作 (λ x. x + 2) 3。更多的 lambda 表达式 相关的内容,这里不再介绍,可以参考wiki百科中相关的介绍。

那么如果仅仅是这么性形式上更换一个写法,当然也就没有什么实际意义了,而这么做的巨大意义在于,基于这样一套非常简明的规则,可以把“计算”这件事说清楚,即什么是可以计算的,以及如何使用通用的方式描述一个“计算”的过程?

就好象CSS为了描述页面布局,而设计出一套“盒子模型”,它是把页面布局中的最通用、最基本的一些特征提取出来,形成的若干规则。同样,”λ演算“是一套“计算的模型”,它是对计算的本质特征的描述。 lambda 演算与现代的计算机之间,还看不出直接的联系。而到了阿隆佐.丘奇的学生阿兰.图灵(Alan Turing)的时候,就把二者直接关联起来了。

阿兰.图灵 图灵机(英语:Turing Machine,又称确定型图灵机)是英国数学家阿兰·图灵于1936年提出的一种抽象计算模型,其更抽象的意义为一种数学逻辑机,可以看作等价于任何有限逻辑数学过程的终极强大逻辑机器。

也就是说,图灵“从理论上”设计出了现代的计算机,并给出了计算机的具体运行方式。而这个运行在“纸面上”的计算机,仅仅用几行文字就可以描述清楚。在图灵发表了他的关于图灵机的理论十年之后,真正的计算机被真正制造出来。直到今天,我们使用的计算机,本质上看,不管你用的是“奔腾”还是“酷睿”,仍然都是在在图灵机的模型上建立的,这就是数学的力量。人类的最重要的思维能力之一是“抽象”的能力,牛顿从苹果落地“抽象”出万有引力定律,图灵从千变万化的计算中“抽象”出图灵机,包括CSS的设计者从页面布局中“抽象”出合资模型,本质都一样的,就是从纷繁复杂的表象之中,看到了隐藏在现象背后的本质的规律。

由此,我们可以看到,数学的作用,正是从纷繁复杂的现象中寻找事物最本质的规律。

至于Lambda表达式,C#中引入并不算早的,在它之前,就已经有很多现代的编程语言中都实现了Lambda表达式。更为重要的是,我们当前进行开发,大多使用的C语言为代表这类过程性语言,实际上,还有一大类编程语言,与之区别甚大,称为“函数式语言”。

比如现在很热门的Erlang,以及很古老的Lisp,在国外大学教学中非常流行的SCHEME,甚至微软都已经推出了自己的函数语言F#。而全世界的第一个函数语言,就是这个并非为计算机设计的”λ演算“。实际上,在计算机高级语言出现之初,函数语言和过程性的语言是同时开始的,但是几十年来的普及程度,过程性语言远远超过了函数式语言,具体原因大概是函数语言并没有过程性语言方便。但是世道就是这样有趣,随着计算机硬件的发展,现在多核CPU的出现和普及,并行计算的要求越来越高,又使得函数式语言的优势大大增加,从而使函数式语言大为热门。所以,研究历史、对比当前,发现到问题的本质,实在是很有趣的一件事。

如果对编程语言感兴趣,可以参考《编程语言的入门科普》和《编程语言分类学》。

再看C#中的lambda表达式

上面我们了解了C#新引入的“lambda表达式”这个特性,以及他的来历和数学基础,也简单演示了一下在C#中使用“lambda表达式”的作用,但是如果仅仅是为了节省几个循环语句,是代码简洁一些,就太小看了“lambda表达式”的作用。实际上微软引入“lambda表达式”的意义远不止于此,还有更大作用,这里就不再深入研究了。

写过一些程序的,比如用C语言或者C++、C#语言的读者有没有考虑过一个问题,尽管我们在程序中计算一个表达式的值很容易,比如这样一句话:

int x=3*(2+3);

就给x这个变量赋值为15了。

但是如果要您写一个程序,实现用户输入一个数学表达式的字符串,然后计算出结果,可就不那么容易了。写一个能够计算加减乘除、带有括号的四则运算的程序,至少要在计算机系学到3年级的《编译原理》课程,才够用的。为什么呢?因为里面包含了很复杂的逻辑的,比如你需要能够把一个字符串,解析为运算的操作数和运算符号,同时还要考虑,先乘除后加减的规则,以及括号的处理。若你可以没有学习任何相关基础的情况下,自己能够成功地写出这样一个程序,说明你绝对是个非常非常聪明的人。

理论上说,计算机程序中包括两个部分:“数据”和“指令”,当我们把一个四则运算的表达式存储在字符串中的时候,它仅仅是一些数据,而不是指令,要真正计算出结果,需要把这个数据转换成指令,才能够进行计算。

因此,如果从更深层次看待这个问题的话,C#引入“lambda表达式”、以及“表达式树”这几个新特性以后,从基础层面,在“数据”和“指令”之间搭建了一座桥梁,可以非常方便地相互转化。当一个表达式的逻辑通过“表达式数”以数据的形式保存以后,就可以方便地对他进行各种操作。具体内容这里不再详细讨论。

Anders Hejlsberg当然这方面还有很多工作可做。实际上,由Anders Hejlsberg主持的C#语言从1.0到即将发布的4.0,一直保持着相当快速的的前进步伐,C# 1.0实现了代码的托管,C# 2.0实现了泛型,C# 3.0引入了LINQ,C# 4.0将引入动态特性。一步一个脚印,而且每一个新特性都为后面的发展打下了坚实基础。

总而言之

在上面,我们结合C#语言中的一个特性,说明了一下它背后的数学背景。可以看出以下的一些结论,希望对读者有所帮助:

计算机科学的本质是数学

计算机成为一种科学,其实历史很短,不过几十年的时间。尽管现在的计算机已经成了日常消费品,而当初计算机科学的先驱们可都是大数学家。当然现在计算机以及软件开发已经成为了一个巨大的产业,很多很多工作,都不需要数学家才能胜任了。否则,依靠着几个数学家,这么多事情可做不过来啊。

但是有一点我们要认清的是,越是重要的、底层的、基础性的工作,和数学的关系越密切。因此,我们作为普通人,可以看看自己的能力到哪层,就去寻找适合子的工作。

软件开发人员要学到什么程度才够用呢?

就像上面我提到的,现在整个产业已经变得巨大无比,因此层次也非常多,已经不存在一个明确的界限了,无论你有的基础如何,总是可以找到适当的位置的。当然,如果你希望做一个程序员,数学还是一个需要重要因素的。做程序的人数学不好,就好象唱歌不认识乐谱,或者绘画不懂素描,对你的影响是很大的,

至于应该如何学习数学?我的感觉是: 1)先侧重于“广度”,2)然后再有重点地考虑“深度”,3)最重要的是提高学习能力。

第一句话的意思是:先侧重于“广度”的意思是,你先不管能理解多少,至少把那写必备的基础,都略知一二,即使不是真懂,至少也要知道一些重要的概念、大原则、大的思考方法。

第二句话的意思是:然后再考虑“深度”的意思是,在大致了解的基础上,在找一些相对直接有用的,深入地学习。

第三句话的意思是:任何人都不可能什么都掌握,很多东西都是在用到的时候,才会去具体研究学习,因此学习能力才是最重要的。

共 3 页: [1] 2 3 » | 下一页»