(07)递归程序设计
最后更新于:2022-04-01 16:15:29
这篇谈谈递归程序设计的问题。从取名上来说是想刻意区别内容的侧重点不同。上一篇是构造,其重点是从递归程序的自身结构出发,试图用一种比较直观的方法来完成递归程序的构造。这篇的重点是设计,其中的区别在于,这次是从问题本身的结构出发来完成递归程序的开发任务。上一篇中介绍的方法,比较简单直观,八股文的意味非常浓郁,并且还有一个比较大的缺点,那就是在实际使用时往往会受制与方法本身而不能解决有一定难度的问题。实际上递归是一种客观存在的现象,递归的描述问题是对客观世界的一种认识。本文从对问题的认识,描述和分析这些步骤来介绍一下如何完成递归程序的设计。
**一.问题的描述方法—巴克斯范式**
在我上大学的时候,巴克斯范式出现在编译原理的课程中,是用来定义文法的。在数据结构课程中并没有介绍巴克斯范式。但是在实践中发现,这个范式对完成递归程序非常有帮助。因为根据巴克斯范式,我们可以自动生成词法分析程序,而这些程序就包含了各种递归程序及其调用。这里不打算从编译的角度来介绍巴克斯范式,而是借用巴克思范式的思想来帮助完成递归程序的开发。所以规范和严谨程度是远不如巴克斯范式的。
先从一个具体的例子开始引入巴克斯范式。现将前一篇“递归程序构造”中关于二叉树的定义再次描述如下:
n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。这是一个用严谨的自然语言描述的定义,下面用另一种形式等价的来描述这个定义:
~~~
<二叉树> = null | 节点<左子树><右子树>
<左子树> = <二叉树>
<右子树> = <二叉树>
~~~
上面的定义由三行文本组成,每一行文本是一个等式,称之为规则,所以一共是三条规则。等号的左边称为非终结符,等号的右边表示这个非终结符的组成内容。一般非终结符用“<”和“>”两个符号包围。这些是巴克斯范式中的内容。
以第一条规则为例,等号的右边首先是null,这表示空,这等效于二叉树定义中的“它或者是空集(n=0)”这段文字。最右边的“节点<左子树><右子树>”表示二叉树有一个节点及其所属的左子树和右子树组成,这个描述二叉树概念中的“由一个根结点及两棵互不相交的、分别称作这个根的左子树和右子树”这些文字对应。第二条和第三条规则表示左子树和右子树都是一棵二叉树,这个和定义中的最后几个字“二叉树组成”相对应。最后看一下第一条规则中的字符“|”。这个字符在巴克斯范式中表示或,其含义是该字符的左边或者右边只能取一个。这个符号和定义中“或者”这个词相对应。至此可以确认上述三条规则对二叉树的描述和定义对二叉树的描述是等价的。
有了这个等价的巴克斯范式版本的二叉树定义,我们就可以使用处理巴克斯范式的方式,或者说可以使用编译原理中词法分析的思路来完成递归程序的开发了。
**二.从规则集转换得到递归程序**
前一篇递归程序构造中使用了遍历二叉树的例子,这里还是使用相同的例子,看看从规则集是如何完成遍历二叉树的递归程序的开发的。事实上从规则集合转换得到递归程序的步骤是很简单的,也是可以自动化的。我们完全可以开发一个程序,通过扫描规则集自动生成递归程序。下面介绍手工完成的具体步骤。
首先为每一个非终结符定义方法,每一个方法只用来处理对应的非终结符。上述三条规则中包含了三个非终结符,所以我们需要三个方法,列出如下:
现在我们得到了三个方法,然后给这些方法定义参数。由于三个方法都是需要遍历,所以二叉树的根节点必须是方法的参数,否则遍历无法完成。增加参数后方法如下所示:
~~~
// 对应非终结符<二叉树>,表示遍历二叉树
VisitBinaryTree()
// 对应非终结符<左子树>,表示遍历左子树
VisitLeftBinaryTree()
// 对应非终结符<右子树>,表示遍历右子树
VisitRightBinaryTree()
~~~
第二步是在各个方法中对指定的非终结符的右边内容进行处理。首先看第一条规则。由于规则中有一个“|”符号,表示右边两部分内容不能同时处理,所以显然需要一个if语句做判断,然后分情况分别处理两部分的内容。先看“|”左边的内容null,这个含义是二叉树为空,如果是这样,那么就无需遍历,所以对应的代码应该如下:
~~~
if (node == null)
return;
~~~
如果二叉树不为空,那么需要处理“|”右边的内容,这些内容分别是根节点,左子树和右子树。对于根节点的处理可以抽象的使用一个方法ProcessNode来表示,而后面的左子树和右子树是非终结符,可以直接调用处理改非终结符的方法就可以了。修改完后代码如下所示:
~~~
if (node == null)
return;
else
{
ProcessNode(node);
VisitLeftBinaryTree(node.LeftTree);
VisitRightBinaryTree(node.RightTree);
}
~~~
对于第二和第三条规则,由于右边只有一个非终结符,所以其内部的代码就是直接调用对应的处理该非终结符的方法就可以了,完整的代码如下所示:
~~~
public void VisitBinaryTree(Nodenode)
{
if (node == null)
return;
else
{
ProcessNode(node);
VisitLeftBinaryTree(node.LeftTree);
VisitRightBinaryTree(node.RightTree);
}
}
public voidVisitLeftBinaryTree(Node node)
{
VisitBinaryTree(node);
}
public voidVisitRightBinaryTree(Node node)
{
VisitBinaryTree(node);
}
~~~
到这里代码就完成了,而且还是一个间接递归的版本。下面对这些规则和代码再做一个讨论,让问题更明晰透彻一些。
**三.若干细节讨论**
第一个需要讨论的就是间接递归的问题。我们熟知的遍历二叉树的递归程序都是直接递归,这里得到却是一个间接递归。其原因不是介绍的方法有问题,而是上述规则的设计问题。可以看到第二条和第三条规则表达含义就是<左子树>和<右子树>也是一棵二叉树。补充这个规则的用意是为了体现二叉树定义中出现的文字“分别称作这个根的左子树和右子树的二叉树组成”,这句话表明左子树和右子树也是二叉树,所以加入了上述规则。
既然非终结符<左子树>,<右子树>和非终结符<二叉树>是等价的,那么我们可以将规则一右边出现的<左子树>,<右子树>直接用<二叉树>代替。这样规则一就如下所示:
<二叉树> = null | 根节点<二叉树><二叉树>。还是使用相同的推导方法,这次我们可以得到直接递归版本的二叉树遍历程序,如下所示:
~~~
public void VisitBinaryTree(Nodenode)
{
if (node == null)
return;
else
{
ProcessNode(node);
VisitBinaryTree(node.LeftTree);
VisitBinaryTree(node.RightTree);
}
}
~~~
第二点是需要强调一下推导的步骤。我相信有些读者已经发现了间接递归的问题,并且也能够直接修改代码,将其改为直接递归。比如直接通过读代码就可以发现方法VisitLeftBinaryTree和VisitRightBinaryTree什么都没干,只是调用了方法VisitBinaryTree,所以就可以直接调用VisitBinaryTree从而替换掉对方法VisitLeftBinaryTree和VisitRightBinaryTree的调用。这样做是可以的,尤其在这个具体的简单问题上。但是当规则足够多,并且足够复杂时问题就不太可能如此直白,如此易于观察并得到结论。所以强烈推荐的做法是先修改规则,然后再根据规则推导出程序,这是工程化的做法。
第三点,不是需要给所有的非终结符都定义方法,然后再重构,如果能看清问题那么可以直接写出最终的代码。这也是不太规范的一个地方。
第四点是强调一下这里用到的规则和巴克斯范式的差异。前文已经提到巴克斯范式是一个规范而严谨的定义,而这里使用的规则只是借用了巴克斯范式的思路来描述问题,不是很规范和严谨。比如在巴克斯范式中规则一的右边不仅表示<二叉树>可以由根节点,<左子树>和<右子树>组成,同时也表示这三者先后出现顺序。但是这里使用的规则,仅仅表示组成内容。或者说仅仅想表示二叉树的结构,从而和二叉树定义的描述等价。注意二叉树定义中的描述没有规定左子树和右子树出现的先后顺序。所以在VisitBinaryTree方法中对处理内容的先后没有限制。由此可以推导出遍历二叉树的不同版本,只需要改变调用处理非终结符方法的先后顺序即可。
当然根据具体的问题,可以给规则加入其它的变化和含义,以便于等价的描述问题。这其中的取舍和尺度的把握是体现问题分析和程序设计能力的地方。下面再举一个例子来说明这个问题。
**四.规则的设计**
从前文的介绍可以看出,只要得到了规则,那么推导出递归程序是非常容易的。
这样开发递归程序的问题就转化为如何得到规则了,也就是规则的设计问题。我的建议是多练习,多实践。因为没有一个固定的做法可以让我们比较容易的得到规则集,所以通过练习和实践来提升问题的分析能力和程序的设计能力就是关键和捷径了。但是在有些时候思考问题的技巧对我们也是有辅助帮助作用的。这里举一个例子来说明一下,想以此扩展一下读者的思路。这个例子是:逆转字符串。
如何逆转一个字符串是非常容易的,但是如何写出递归版本的代码呢?请注意写出递归的关键是发现问题的递归结构,这个递归结构是事物本身的特性,而不是只指我们需要对该事物执行什么样的操作。这就是说逆转操作不是关键,关键是如何找到字符串的递归结构或者说如何找到字符串的递归定义。当然这个能力需要在实践中逐步培养。下面直接给出规则版本的定义:
~~~
<字符串> = null | <字符> | <字符><字符串><字符>
<字符> = …
~~~
先看第一条规则的右边,null表示空串,<字符>表示只有一个字符的字符串,最后部分表示有多个字符的字符串。第二条规则定义了<字符>可以是哪些字符,比如’a’,’b’,’c’或者’1’,’2’,’3’,之类的,由于比较多就不全写了。然后使用上文介绍的方法来推导,首先给<字符串>定义方法,然后分别处理右边的内容,代码如下所示:
~~~
public string ReverseString(stringstr, int start, int end)
{
if (start >= end)
return str;
else if (str == null || str.Length < 1)
return str;
else if (str.Length == 1)
return str;
else
{
char temp = str[start];
str[start] = str[end];
str[end] = temp;
return ReverseString(str, start + 1, end - 1);
}
}
~~~
方法的调用如下:
~~~
ReverseString(str, 0,str.Length - 1);
~~~
ReverseString中的第一个if是加入的递归出口判断,这不能从规则推导出来,需要自己加。关于递归的出口可以阅读前一篇:递归程序构造。另外还可以修改规则如下:
~~~
<字符串> = null | <字符> | <字符><字符串>
<字符> = …
~~~
依据这个规则也是可以推出递归程序的。
关于递归程序还有一些话题可以讲,比如数学归纳法,递推,递归程序的测试等等。这些扩展的话题留在以后再介绍了,这次就写到这里了。最后推广一下我的群244054966,欢迎正在创业的程序员加入。入群时请写明“csdn博文”,否则不加。
(06)递归程序构造
最后更新于:2022-04-01 16:15:26
这次谈谈递归程序的问题,之所以选递归这个话题主要是以下三个原因。第一个是自己的体会。在我的记忆中掌握递归程序是有一定难度的。最初在写递归程序时是全靠脑子想,一层一层的想着程序如何递归下去,然后又是如何返回的,最后整个递归程序又是如何结束的。对于一些简单的递归问题,特别是一些简单的习题,这个作法虽然笨拙,但是却有着相当的实用价值。只要脑子好使,一层一层的想下去,是可以解决一部分问题的。但是对于一些逻辑有点复杂,或者递归层数比较多的情况,这个方法就不好用了。尤其是在一些递归深度不确定的情况下,单凭脑子想就很难解决问题了。
第二个是应该有相当一部分的开发者认为递归程序不好写。这个结论来源于我的一个员工。这个员工大概有几年的开发经验,并且谈吐处事很得体稳重,给我的印象是不错的。在一次闲谈中他将会写递归程序作为一个亮点提出来的,言下之意自己的技术是很不错的。另一次经验来自一个面试的人。他把项目组的头会写递归程序作为一个敬佩的理由。由此我判断应该有相当一部分的开发者觉得递归程序不好写。
第三个理由就比较简单了,那就是递归程序确实很有用,很值得去掌握。
**一.递归的定义**
递归的概念的严格定义应该是来自数学,这个google一下就可以知道的。当然数学上的定义肯定是不太好理解的,有兴趣的可以自己看一下。这里给一个比较容易理解的版本,也是一个比较实用的说法。如果定义一个概念的时候使用到了这个概念本身那么这就是递归了。比如下面的二叉树的定义:
二叉树(BinaryTree)是:n(n≥0)个结点的有限集,它或者是空集(n=0),或者由一个根结点及两棵互不相交的、分别称作这个根的左子树和右子树的二叉树组成。
在上面的文字中,冒号后面的内容就是二叉树的定义。在这个定义中又出现了二叉树这个概念,所以这是二叉树的递归定义。当然需要区分一下这和循环论证是不一样的。
**二.递归程序的结构**
既然凭脑子想不能很好的解决问题,那么我们就需要使用一个更好的方法。我们可以从递归程序的结构出发来构造完成递归程序。所以这里介绍一下递归程序的结构。从结构上讲递归程序分为三个部分:递归出口,逻辑处理,递归调用。
1. 递归的出口
所谓递归的出口,就是指满足什么条件时程序不再需要递归调用了。这个时候往往是递归程序递归调用到最深层的时候,需要开始回归了。还有一种情况是做出判断决定是否执行当前的递归程序,比如对递归方法的参数的容错处理。
2. 逻辑处理
在考虑写递归程序的时候,至少需要知道在递归出口时需要执行的逻辑处理是什么。其次就是某一次递归调用前后需要执行的逻辑处理是什么。需要注意的是,这个时候的处理只是针对部分的数据,因为都是在某一次递归的执行中处理数据。不是对所有数据的完整的处理。完整的处理是整个递归程序执行完毕后才能完成的。
3. 递归调用
这个很好理解,就是在递归程序内部调用自己。
**三.递归程序构造举例1**
为了有一个感性的认识,这里举一个例子说明一下如何从递归程序的结构出发来完成递归程序的构造。这里就用教科书上遍历二叉树的例子来分析一下如何处理递归程序。首先我们考虑一下如果我们需要遍历一颗二叉树,那么什么情况下我们可以不用再递归遍历或者没必要继续遍历了?这个答案就是遇到一颗空树的时候就没有必要再遍历了。参考下面的方法定义:
~~~
public void VisitBinaryTree(NODE node)
{
…
}
~~~
其中参数node就表示了一颗二叉树的根节点,如果这个node的值是空的话,那么我们就没有必要再递归了,可以按照需求直接处理或者什么都不做直接返回了。所以上述方法的内部需要包含如下代码来结束递归,也就是所谓的递归的出口:
~~~
public void VisitBinaryTree(NODE node)
{
if (node == null)
return;
…
}
~~~
至此已经完成递归程序的第一部分了,当然也是最简单的一部分。需要强调的是这部分虽然是最简单的,但还是建议大家在构思递归程序时最好首先明确这部分的内容。否则一个递归程序没有出口的话,那么运行起来会把栈击穿的,从而导致崩溃。
下面第二步就要考虑核心的问题了,那就是如果node不为空时我们如何处理?首先需要明确我们要完成的逻辑是什么。一般教科书上的遍历例子,不会讲所谓逻辑处理的,只是描述遍历,这里我们可以假设一个虚拟的逻辑处理。我们假设这个逻辑处理由如下的方法完成:
~~~
public void DoSomething(NODEnode)
{
…
}
~~~
于是加上逻辑执行部分,我们的递归程序看上去就如同下面的样子了:
~~~
public void VisitBinaryTree(NODE node)
{
if (node == null)
return;
DoSomething(node);
}
~~~
很明显Dosomething按照既定的要求完成了对节点node的处理,但是我们需要处理二叉树中的每一个节点,只执行DoSomething(node)这一行代码是不够的。所以这时我们需要递归程序的第三部分,即递归调用。就这个例子而言,node表示一棵二叉树的根节点,并且在VisitBinaryTree方法内部我们调用了DoSomething方法完成了对node节点的处理。那么剩下的工作就是要处理node的左子树和右子树了,只有这样才算是完成了对node为根的整棵二叉树的处理。
这个时候我们可以再继续写代码来处理node的左子树和右子树,但是等等,由于我们现在构造的方法VisitBinaryTree就是用来处理二叉树的,而左子树或者右子树本身也是一棵二叉树,所以我们就没有必要再写额外的代码来处理而是直接递归调用该方法就可以了。所以加入递归调用的代码后,VisitBinaryTree方法差不多就是下面的样子了:
~~~
public void VisitBinaryTree(NODE node)
{
if (node == null)
return;
DoSomething(node);
VisitBinaryTree(node.LeftSon);
VisitBinaryTree(node.RightSon);
}
~~~
至此遍历二叉树的方法就完成了。下面我们来讨论一个细节问题,那就是根节点的判空问题。在这个例子中node的判空处理既是递归的出口,也是一种容错处理。因为如果不进行容错处理的话,那么DoSomething方法内容如果访问了node对象的属性或者方法,就会出现null对象方法的异常。但是有些人更习惯于在递归执行前对是否为空值作出判断,从而决定是否递归调用,这可以保证每次递归调用时,传入的值都不为空。因此代码差不多是下面这个样子:
~~~
public void VisitBinaryTree(NODE node)
{
DoSomething(node);
if (node.LeftSon != null)
VisitBinaryTree(node.LeftSon);
if (node. RightSon != null)
VisitBinaryTree(node.RightSon);
}
~~~
这样的代码确实保证了传入递归方法的根节点参数不为空。但是却忽略了一个问题,那就是第一次调用VisitBinaryTree方法时node为空的情况没有考虑。假设如下的情况,我们在一个方法OneMethod中如下调用的例子:
~~~
public void OneMethod(…)
{
…
VisitBinaryTree(null);
…
}
~~~
所以为了应对这个情况,最初判断node是否为空的代码还是需要的,这样代码就变成如下的样子了:
~~~
public void VisitBinaryTree(NODE node)
{
if (node == null)
return;
DoSomething(node);
if (node.LeftSon != null)
VisitBinaryTree(node.LeftSon);
if (node. RightSon != null)
VisitBinaryTree(node.RightSon);
}
~~~
好,现在重点来了。上面的代码中最后两个判空的if语句还需要么?答案是不需要了。假设去掉最后两个判空的判断,那么传入的参数确实有可能为空,但是当这样的参数传入VisitBinaryTree方法时,该方法的最开始就对这个参数执行了判空的处理,如果为空就直接返回了。所以达到了同样的目的。请体会一下,递归程序就是这个样子的。
**四.递归程序构造举例2**
在数据结构的教材上,遍历二叉树的方法有六种不同的版本,最常用的只有三种,分别是:先序优先遍历,左序优先遍历,右序优先遍历。上面的例子是用的先序优先遍历。下面看看用同样的方法来构造左序优先遍历的递归方法。所谓左序优先,是要求先遍历处理完二叉树的左子树,然后处理根节点,然后再遍历处理完二叉树的右子树。
好,我们还是首先考虑递归的出口,对于遍历二叉树而言,其出口仍旧不变,还是判空,如果为空就直接返回不处理了。所以第一步的代码是一样的,就不再列出来了。下面是如果节点不为空,我们需要先遍历处理左子树,再处理根节点,然后再遍历处理右子树。根据这个要求我们明确了可以执行的逻辑是处理根节点。至此,第二部分完成一半了,列出代码如下:
~~~
public void VisitBinaryTree(NODE node)
{
if (node == null)
return;
DoSomething(node);
}
~~~
下面就是关键了,那就是如何递归调用了。为了易于理解,我们可以先假设,需要处理的二叉树是没有任何左子树的,也就是说要么没有任何子树,要么就只有右子树。这样我们只需要考虑右子树就可以了,把左子树忘了吧。根据遍历处理的要求是先处理根节点然后再处理左子树,所以我们的代码如下:
~~~
public void VisitBinaryTree(NODE node)
{
if (node == null)
return;
DoSomething(node);
VisitBinaryTree(node.RightSon);
}
~~~
当VisitBinaryTree被递归调用时,传入的是node的右子树的根节点。这个右子树根节点,传入后首先是被判空,然后是调用DoSomething执行逻辑处理,然后再次递归调用来处理右子树的右子树。显然这样的递归调用逻辑是对的。
现在再假设需要处理的二叉树是没有任何右子树的,也就是说要么没有任何子树,要么就只有左子树。这样我们只需要考虑左子树就可以了,这次可以把右子树忘了吧。根据遍历处理的要求是先处理左子树然后再根节点,所以我们的代码如下:
~~~
public void VisitBinaryTree(NODE node)
{
if (node == null)
return;
VisitBinaryTree(node.LeftSon);
DoSomething(node);
}
~~~
当VisitBinaryTree被递归调用时,传入的是node的左子树的根节点。这个左子树根节点,传入后首先是被判空,然后是再次递归调用VisitBinaryTree遍历处理左子树的左子树。然后再处理根节点,显然这样的逻辑也是对的。好了,至此我们可以考虑既有左子树又有右子树的一般情况了,把两部分的代码合起来就可以了,如下所示:
~~~
public void VisitBinaryTree(NODE node)
{
if (node == null)
return;
VisitBinaryTree(node.LeftSon);
DoSomething(node);
VisitBinaryTree(node.RightSon);
}
~~~
**五.递归程序构造的比较**
举例1的构造过程是从递归程序结构本身直接推导出来的,是一个很自然的过程。在构造时并没有考虑是否为后续遍历,只是构造完成后正好和后续遍历一致。在举例2中的构造过程中使用了一点技巧,那就是为了简化问题,看清递归调用的位置,先后假设不存在左子树和右子树的情况,然后再将两部分合并,从而完成递归程序的构造。这就是说举例2中在使用这个方法时多了一个简化问题的步骤,这是使用已知的知识解决问题的一个例子。关于解决问题的更多讨论可以参考本系列中问题解决篇的讨论。再将这两个例子和教科书上的例子做一个比较。这里的讨论给出了递归程序构造的详细步骤,相比教科书上直接给出结果来说,我觉得这里讨论更容易理解。另一个区别是,由于本文的例子是从递归的结构出发完成构造递归程序的,所以没有涉及讨论所谓递归程序执行时会用到的工作栈的问题。有兴趣的可以再看一下其它相关的资料,对工作栈的了解应该多少对递归程序的认识是有帮助的。
这次就写到这里,感谢阅读。下一篇还是谈谈递归程序,介绍一个更强更"广谱适用"的方法来完成递归程序的设计。
(05)能力养成篇
最后更新于:2022-04-01 16:15:24
关于本系列文字的来源,初衷和内容定位可以参考第一篇的开头部分,链接地址如下:
http://blog.csdn.net/binarytreeex/article/details/8174445
http://www.cnblogs.com/WideUnion/archive/2012/11/12/2766397.html
本文前一篇地址如下,感兴趣的可以访问下面的连接:
http://blog.csdn.net/binarytreeex/article/details/8888607
http://www.cnblogs.com/WideUnion/archive/2013/05/06/3061994.html
这次讨论一个和解决问题有密切关系,并且也是大家非常熟悉的话题:能力。实际上能够解决问题就是一种能力的表现,但是两者并不完全等价。这篇博文打算从我个人成长经历的角度回顾分享一些关于如何提高开发者自身能力的一些体会和心得。当然这些体会不是什么特效药,可以保证开发者能够一天就能武功精进,相反还是需要开发者自己长期不断的积累,才能完成能力的体升。但是我的这些体会和心得可以帮助开发者尽量少走一些弯路,并且在提升之路上能获得一些帮助,带给开发者正能量。可以解决问题是能力表现,但是并不等同于能力。因为具体的人在解决问题时所处的环境,可以使用的资源,能够获得的帮助和信息都会直接影响到是否可以解决问题。所以评估能力不能简单从能否解决问题这一个思路上来考察,更要看解决问题的过程和思路。前三篇所讲的内容是从方法本身方面来说的,从某种角度上来说见效会快一些。本文则是谈如何直接提升或者培养自身的能力来提高解决问题的质量,更注重在一个日积月累的过程。
如果本文叙述的内容,和读者已有的体会存在相异之处的话,我非常欢迎能够和同行们交流。应该说文本的内容出自自己的实际经历,所以我是刻意去避免说一些大家都知道,或者经典书籍,培训课程上的东西(如果这些东西存在的话)。但是在实用性和实际效果方面,我相信是非常好的。同样再次强点,本文介绍的内容来自个人的实践,对于能力培养这样一个宏大的话题和全体开发者这样一个宽泛的群体来说,局限性和片面性是在所难免的。所以请同行们自行取舍,同时也要根据自己的经验,实际应用场合做出适当的变化,这样才能更好的应用本文介绍的内容。如果分享的内容可以为同行们带来实际的帮助的话,那么我的目的就达到了。
**一. 能力是什么?**
在开始讨论能力养成这个话题时候,首先要明确一下,能力是指什么,或者说什么是能力。在实际工作中,能力这个词是大家非常熟悉的,也被频繁使用,甚至可以说被用烂了。我这里不考虑各个公司对能力的定义和内容,本文所说的能力只被限制在一个相对狭隘的范畴。在这样一个范畴中,能力可以通过具体的实际的表现来衡量。这个标准就是一个开发者能够解决问题的数量的多少和质量的高低。
简单来说数量和质量可以用一个等价数字来表达,数字高的可以认为是能力强,而低的则相对较弱。这可以理解为是绝对标准。于此相对,还有一个所谓的相对标准。这个意思是不仅仅从数量和质量来考察,还需要考察问题的上下文环境,解决问题的开发者的自身情况等因素,综合考虑之后再确定开发者的能力处于一个什么水平。这就是所谓相对标准。
**二.能力的内容**
本文只讨论和专业相关的能力,就是所谓的智力因素相关的能力。对于更为普遍的所谓非智力因素的能力,本文就不讨论了,比如:如何培养能力从一个程序员成为一个项目经理,等等。
**三.提升能力的途径和方法**
总体来讲,提升能力的方法和途径大致有以下几点:学习,积累,思考,实践。这些方法是可以贯穿在整个职业生涯的。当然在不同的阶段,具体的操作和使用会有所区别。
下面我从一个大学生开始介绍一下提升能力的体会。作为起点从校园开始讲,那是因为我觉得大学是一个提升能力的很重要的阶段。
**1. 学生提升能力的方法**
普遍的观点认为,大学毕业生没有经验,所以在实际解决问题时表现出来的能力是比较差的。这点我不否认,但是这种观点过于流行的程度,使我认为这种看法是不全面的,学生是可以有能力的。我建议学生们做好以下几点:
a.读好你的书
在校园中的时间,学生主要的任务当然是学习。这种学习的效果在开始工作时可能不会有所体现,但是从长远来看,这是能力提升的一个瓶颈。所以在校园中尽量把课程学好,尤其是几门重要的基础课和专业课的课程学好是非常有必要的。具体课程的内容,可以参考本系列学习篇中的相关内容。当然我也相信在学校中会有很多相关的内容,帮助学生学好自己的课程,所以可以参考一下,这里不太过多讨论了。
b.做好你的作业
这个作业具体可以指这样几门课程:数学相关的课程,尤其是排列组合相关的内容;编译;数据结构;操作系统等等。完成作业是最直接使用知识的一个途径。我建议学生可以从使用知识,解决问题的角度来看待自己的作业。其实这样做应该不是太难,尤其像数据结构这样的课程,做到这点是比较容易的。对完成作业的解题方法的质量,我建议学生可以对自己提出一点要求。这个意思就是说,能不能找出多个不同的方法完成题目,或者努力一下找出一个更好的完成题目的方法。这样的要求可以帮助学生更多更好的思考,从一个相对要求更高的层面上来使用自己学过的知识。当然也能帮助学生对自己学过的知识更加熟悉起来,为以后在实际工作中使用这些知识做好准备。
c.多多练习
计算机专业是一个实践性很强的专业,所以作为计算机专业的学生,只是在作业本上完成习题是远远不够的。对于数据结构或者某一门具体的编程语言的课程,我建议学生保证一定量的编码练习。我觉得在学这类课程时,你每天晚上都应该是比较忙的,当然是忙着写代码和调试程序。
d.毕业设计
从你大学生涯的第一天学习计算机专业的课程开始,你就要对自己的毕业设计做什么有一个大致的看法。如果觉得有困难,那么我建议学生提前2年开始考虑这个问题。因为当年在我自己做毕业设计的时候,我的老师说过,毕业设计要综合运用到自己在大学里学过的知识。所以你的专业一定是在你的毕业设计之前就要学好的,否则你的毕业设计就会有问题。提前思考你的毕业设计,对你学好专业课会有一个更为明确的目标和动力。我看到的大部分学生完成的毕业设计的质量是很低的。之所以说低,是因为完成后的毕业设计没有使用到自己学过的知识,尤其是专业课程的知识。所以学生失去了一次非常好的实践机会。一个好的毕业设计你可以认为是一个mini项目。
e.尝试做一些项目
项目可以自己找,也可以去实习,这个自己看着办。我想强调的是要去做。比如利用寒暑假做一些小程序。我可以说一下自己当年做过的练手的程序:
挖地雷:
这个可以帮助你设计最基本的界面,可以熟悉GDI+,可以考虑地雷位置的生成算法,等等。是一个非常好的可难可易的练手的题目。
俄罗斯方块:
这个程序可以练习键盘控制,界面响应,图形操作,以及一些基本的面向对象的设计构思。我建议学生可以考虑多次反复的完成这种练习,每次会有新的体会,能力就逐步提高了。这个游戏我在不同的阶段用不同的语言做过好几遍。
那么做到以上几点对于一个学生提升能力有什么帮助呢?我觉得当你离开校园踏上工作岗位的时候,你具体以下的优势:
第一点, 扎实的基础功
这可以保证你在很长的职业生涯中维持着一个不错的状态也是你能够长期胜任开发岗位的一个重要因素之一。
第二点, 具备基本的或者较强的调试程序的能力
调试程序的能力不一定需要有实际工作经验以后才能具备。学生完成足够的编码,并且编码的内容具有足够的难度和质量,那么一个毕业生同样可以具备非常出色的调试程序的能力。这是任何一个愿意去努力的学生完全可以做到的。
第三点, 基本的解决问题能力。
学生解决自己作业中的问题,解决自己上机练习中的问题,这些经历实际上就是一个能力积累的过程。这为工作后解决实际问题或者更有难度的问题打下了一个非常好的基础。
**2. 工作中的能力提升**
在工作中要注意积累。对于大部分人来说,开发的岁月是在完成一个一个项目的过程中度过的。那么当一个项目完成后可以问一下自己我得到了什么?尤其是刚开始工作的那几年,正是提升幅度最大的阶段。思考一下自己收获,掌握的新的技能,如果可能还可以写一点笔记。我在最开始的两年能够做到每一周写一篇笔记。所以至今回忆起来,在我创业之前那个阶段里,收获最大的是我没入行自学的那个阶段。而到了北京入行以后,基本上是使用之前积累的经验。虽然不能说在北京工作的那段时间一点长进都没有,但是从编码开发的角度来说,长进确实不能和自学阶段相比。所以建议大家做好积累的工作。
勤于思考。这个思考主要是考虑项目中可以改进或者有缺陷的地方。当然具体的环境是否允许你提出来,这个自己看着办。我的意思是你需要主动的去这样思考。你的能力可以在这样的思考中慢慢提高。另外思考之后,可能会发现更好地作法,如果这样的话,我建议你再多做一点工作。那就是尝试去寻找证据来证明的你想法是对的,这点非常重要。在刚开始的时候,可能会觉得有难度,但是次数多了就会慢慢顺手了,你的能力也就得到提升了。
尽量高质量的完成自己的工作。做到这点可能会受制于具体的环境,比如时间非常紧的项目可能不允许花费过多的时间来做这个事情。但是如果有机会的,希望能尽量去尝试。其中一个非常有趣味的问题在于,把工作或者说把编码做到什么程度算是更好的质量呢?这个问题我相信不同的人会有不同的理解,大家可以自己去定位一个,然后努力去做。我可以给出一个参考标准:用尽量少的代码行数完成相同的工作。当然,前提是可读性和可维护性的损失在可接受的范围内,零损失当然是理想的。这种判定标准的确定和具体判定时的取舍,实际上也是一种能力的体现,多多练习也是很有裨益的。
**3. 生活中的能力提升**
我一直觉得从事开发工作是要有一点基础条件的。其中一个就是,是否能从程序员的角度来考察和思考现实生活中的事情。这属于勤于思考的范畴,但是内容却更为丰富,也更有趣味。下面我可以举两个例子。
一次和同事一起坐电梯,同事一边看自己的手机一边进入电梯,电梯门关上后他发现手机信号还是满的。这个问题如何解释?思考这个问题的答案和程序员有什么关系?首先从我们知道的事实来讲,手机可以显示所处环境的信号的强弱,所以可以肯定手机和基站有通信。如果在电梯门一关上手机就能马上察觉没有信号,那么手机必须保持足够高频率的检测。比如电脑扫描键盘是频率是每秒18.2次。以人类生理极限来说,这个频率可以足够保证你的每次按键都可以被捕捉到。那么手机有必要么?基于当时我做过的通信项目来说,是完全没有必要,保持一定间隔的查询就可以了。到这里和程序员的开发就有关系了。对于这个例子一个可能解释是,手机显示的信号是两次检测间隔中的信号,而上一次是在电梯外,下一次检测时间还没到,所以造成电梯门关上后,还显示满信号的情况。当然如果从来没有做过通信程序可能想不到这个答案,但是你可以在生活中尽量使用你知道的知识和经验来解释你看到现象。
第二个例子还是和手机相关的。我记得葛优在一部贺岁片中有这么一个桥段,不关机直接去下手机电池。这个时候别人打入电话时听到的提示是无法接通,而不是已关机。那么这是为什么呢?这个问题同样可以从通信角度给出一个解释,当然我不肯定这个解释是对的。手机和基站的存在通信这个是肯定的。正常关机的话,手机的操作系统一定是按照固定步骤完成关机的。显然假设其中存在一个步骤通知基站手机关机,这是非常合理的。从而更新数据库中的相关数据确认手机已关闭。所以正常关机后再打入电话就提示已关机。如果直接取下电池,那么所有程序立刻停止,基站或者服务器端没有得到任何信息,当然任然认为手机是开着的。这时再打入,那么服务器还是认为手机开的,然后发出呼叫信号,等待应答,当然等到花儿都谢了也不会有应答了。这样服务器端只能提示无法接通。
就这个问题而言其实还可以做更多实验,比如卸下电池后过一天再打,提示如果是已关机,那么说明服务器和手机之间通信有连接检测机制。或者,去下电池再装上,别开机,然后打入,再马上开机,看看是否能接到。如果可以,说明服务器端有等待并多次尝试连接的通信机制。
以上例子说明在生活中要注意观察勤于思考,用我们知道的知识来解释我们看到的现象。另外一个训练的法方法是,向自己提问:一件事情如何尽可能多的通过写程序让计算机来完成。从这个角度上来说,当一个程序员遇到事情时不能总是想着自己来做,而是要想着如何让电脑来做。坚持这么思考,你会慢慢发现计算机能做的事情会越来越多的,项目中要求实现的有难度的功能越来越少了。当然这种思考不会每次都有结果,但是保持这种习惯对提升能力很有帮助。
**4. 智力的培养**
在我的看法中智商是最重要的因素。当然智商是先天决定的,但是智商也是可以后天培养的。对于这个事实,我以前的看法是感觉有帮助,但是确认可以后天提升,那是不久前看到的一个报道。这个事实由英国科学家通过研究得到确认了。所以在这里介绍给大家。当然这种提升和先天的智商还有很大区别的,主要有以下几点:
1. 提升的幅度有限
也就是说只能在一定程度上改善智商,但是这点改善就我的体会来说已经很受用了。
2. 应用领域受限
先天的智商可以在不同的领域发挥作用,这取决于个人的选择。而后天培养的智商只能在培养的方向上起作用。当然这一点肯定不是一个问题,能提升能力就行,别的行业我们不关心。
3. 需要持续的培养
这就是说,如果一旦你停止,那么后天的智商就会衰退。所以啊,要勤奋的不间断的培养。
就智商提升的具体手段来说我的体会如下:
1. 看大公司的笔试题面试题
大公司的很多题目都是和专业知识无关的动脑筋的题目。不用去做这些题,你可以简单思考后直接看答案,然后体会一下,并记住思路。这样题目大家可以自己在平时的生活中点滴收集。我可以给一个例子,我曾被问道这样一个问题:如何判定一个单向链表中存在环。这个题目的解非常优秀,很值得欣赏。
2. 向智慧的案例学习
不要理解成益智的案例,或者益智的游戏,类似阿凡提之类的故事不要去看。我举两个例子:
某一届港姐选举时,一个选手被问到这样的问题:希特勒和莫扎特你会选择和谁结婚?如果答案是莫扎特,那么当然不错,但是没有亮点,显然很普通。该选手的答案是选希特勒,理由是这样世界将不会有第二次世界大战,这个答案大亮。
另一个例子是来自周恩来,一次外国记者问中国银行有多少钱,其用意是想通过回答的数字来讽刺中国政府很穷。当然总理看出其意图,总理回答是18元8毛8分。这个故事广为流传,这里就不多说了,不了解的自己google吧。
生活中注意这种训练是会有实际益处的。比如我曾在一次应聘开发职位的面试中被问到这样一个问题:如果给你足够的权力和资源,你会如何把长城从北京搬到温哥华?如果长期坚持对自己智商的训练,那么应对这类问题的难度就会降低,其中的一部分就有可能给出正确的回答。当然更为实际体现还是在我们的开发工作中。我相信应该还会有其他的方法来训练,我也愿意听到大家的经验,如果愿意告诉我,我表示非常感谢。
好了,这次就写到这里,如何解决问题的话题到这里就算是告一个段落了,通过这次回顾我自己也有相当的体会,确实也收获了一些东西。同行们有兴趣进一步交流的可以加我的群:244054966,这个群定位是创业,新手就不要去了。另一个是:231233168,这个群没什么限制。入群时请加上消息:CSDN博客。由于最近工作原因,这个系列是否可以定期发表我不敢保证了,但是我会尽量保持定期。下一篇的话题是什么现在还没有想好,可能是职场相关的,或者设计相关的。
(04)问题解决篇(下)
最后更新于:2022-04-01 16:15:22
关于本系列文字的来源,初衷和内容定位可以参考第一篇的开头部分,链接地址如下:
http://blog.csdn.net/binarytreeex/article/details/8174445
http://www.cnblogs.com/WideUnion/archive/2012/11/12/2766397.html
本文前一篇地址如下,感兴趣的可以访问下面的连接:
http://blog.csdn.net/binarytreeex/article/details/8625156
http://www.cnblogs.com/WideUnion/archive/2013/03/01/2938315.html
在实际工作中软件工程师大部分情况下都是在攻城拔寨,解决一个个自己遇到的问题。这些问题往往是在自己的知识和经验可以覆盖的范围内,所以基本上可以说是顺利的。但是也会有另一些情况的存在,那就是遇到一个自己不会的问题。这些问题具有这样一个外在的现象:问题涉及的内容是在职责或者当前开发任务范围内的,但是对如何实现或者解决这个问题没有思路和办法,简单的说就是第一反应之下是不知道该怎么做来解决这个问题。为了行文方便在下文中,我把所谓不会的问题命名为“难题”,所以在本文中难题这个说法特指程序员不会解决的问题,而不是其它的含义。
一般来说,我们总是力求用简单的方法来解决问题,所以一旦我们遇到一个自己不会解决的问题时,往往表示我们的处境已经不是很妙了,或者说我们的处境有点困难了。本文就是主要分享我个人在应对这些局面时的一些体会。希望帮助软件工程师们能更好的来处理这样的难局和困境。当然这里以难题必须被解决为前提来讨论问题,对于如何规避难题和是否需要解决难题就不讨论了。在这里还要对所谓的不会的问题做一个假设的前提。本文假设如果这个问题能被解决,那么我们缺少的不是知识,也不是经验。否则的话我们的话题会聚焦在学习和经验的积累方面,而不是直接讨论如何去解决一个自己不会的问题。当然我还是同样的再次强调,本文介绍的内容来自个人的实践,对于解决问题这样一个宏大的话题和全体开发者这样一个宽泛的群体来说,局限性和片面性是在所难免的。所以请同行们自行取舍,同时也要根据自己的经验,实际应用场合做出适当的变化,这样才能更好的应用本文介绍的内容。如果分享的内容可以为同行们解决实际工作中的问题起到积极作用的话,那么我的目的就达到了。当然如果能够达到庖丁解牛那样游刃有余的境界那是最好的。
**一.难题是什么样的问题**
这主要是和一般问题相比而言的,先从来源上来说。一般来讲我们不应该遇到难题,无论从风险还是进度来说,与项目有关的每一个人都应该在力求避免这种情况的出现。但是在某些情况下,难题还是会来的。其中原因是难题的客观存在性。工程师尝试解决实际的问题,而这些问题的难度并不以工程师的主观意志决定的,而是问题本身决定的。另一个原因可能项目本身的要求,比如为了有竞争优势等等,会在实现上提出一些有难度的要求。
从难度上讲,这类问题对于工程师而言在第一反应之下是不会的,工程师所具有的知识经验并不能直接告诉工程师能解决问题。这就是所谓的难。所以在尝试解决问题时问题解决篇(中)里面提到的行不行,在这里就格外重要了。如果一个难题是不能解决的,那就没有必要再去花时间了。
从方法上讲,难题的解决会多出一个步骤,那就是分析。这个分析是对问题本身的认识和理解,这是能够解决问题的先决条件。当然这个理解不仅仅只是审题层面的理解,而是问题本身结构上的认识。为了达到这样的认识往往要求工程师在动手解决问题前需要有一定量的实践活动,用于认识问题。
从策略上讲,解决难题时需要明确自己的战略,因为难题往往是一个产品或者项目的成败关键。所以往往也是资源投入比较多的地方,如果没有好的战略来应对问题,同时从全局把握的话,那么难题引入的风险有可能会失控,这有可能会带来很严重的后果。所以需要有一个恰当的战略。
**二.处理难题的战略**
在准备或者开始着手解决难题时,要求工程师对于解决问题的战略有一个清楚的认识,并且在解决问题的过程中严格恪守这个战略。这样做的目的是为了减少失败的损失。难题的解决往往需要时间,更有可能会增加对其它资源的消耗,那么万一解决难题失败,我们改如何应对?不成功则成仁的作法是不可取的,也不是一个聪明的选择。所以在遇到难题时,工程师就要确保自己处在进退都有余地的状态,这就是处理难题的战略。
处理难题的战略的第一条是尽量避免自己面对难题。这个意思当然不是说遇到难题时推卸掉或者踢皮球,而是说在构架,分析设计,算法,思路等等每个步骤和细节上都要注意采取简单化的策略,避免出现难题的情况。这个策略是有点矛盾的,但是和特警解决危机事件的策略一样。一方面要求特警的枪法尽可能的好,另一方面在临场处理事件时则采取尽量不开枪的策略。当然如果一定要开枪的话,那肯定要求一枪解决问题。不要为了显示个人的才能或者为了出风头而有事没事的就去挑战难题,这是非常不明智的作法,早晚会挂的很难看的。
第二个策略是时间管理。事实上解决任何问题都是需要花费时间的,只是简单的问题所花费的时间完全在可接受的范围之内,所以时间这个最重要最基本的限制条件往往不被我们意识到。但是解决一个难题所需要花费的时间是多少,往往是不确定的,当时间超出甚至远远超出我们预计时,就会产生很大的麻烦。所以团队中的leader最好能够预先确定可能出现的难题,并预留足够的时间来应对这个情况。这当然是消极的一面,但是也有积极的一面。因为我们也可以假设预留时间以后,难题是可以解决的,很多时候这个假设也是合理的。那么这样再安排开发计划时就可以方便一点了。
第三个策略是风险管理。这个意思就非常明确了,只需要考虑一点,那就是一旦解决难题失败,那么我们是否可以承受一切的后果。这是风险管理的底线,如果这点都做不到,那么在决定解决难题时就要非常非常慎重了,我的看法是最好就放弃吧。另一个一般尺度的作法是,如果难题解决失败,那么应该有一个候选方案可以选择。这样我们就能进能退了,会比较主动一点,心里也会踏实一点,这是一般分寸的作法。当然实际情况会更为复杂一些,往往是问题难度估计不对,或者有些难题没有预估到,等等。所以最终被遇到难题的局面是有可能出现的,尤其是处在一个新产品的研发或者做原型阶段时。这时候就要看内力和运气了。强调一下,至少不要在战术层面上肯定自己能够解决难题,永远不要这样。
**三.处理难题的方法以及解决难题的基础条件**
当面对一个会解决的问题时,我们一般就直接动手去解决了,即便是需要思考也是在“自己会的”这个前提下作思考。当然这是一种思考,但是面对一个难题时我们需要另一种思考,这种思考就是对问题本身的认识。我们需要通过一定量的反复的实践(或者说尝试)和思考,才能逐步的认识清楚我们面对的是一个什么样的问题,问题究竟是什么样子的。从而让我们对问题本质有一个清楚的认识,进而可以使用我们的知识来描述问题,或者看清楚问题的结构,知道一个难题是由什么和多少简单问题组成的。我把这个过程称之为分析。然后直接使用我们的知识或者依次解决每一个简单问题来解决这个难题。与问题解决篇(中)里面讨论的内容相比,这是一个重要的新的步骤。一旦分析的步骤完成,那么后面的工作就可以按照处理普通问题那样来做了。
上一篇中提到了两个解决问题的基础条件,这里需要再说一个,那就是智商或者说你有多聪明。我觉得智商是这个三个基础条件中是最重要的。当然这听上去不是一个什么好消息,因为智商貌似是老天给的。但是事实上不是这样的,后天的努力可以在很大程度上改善先天的不足。
上一篇中提到的知识在解决难题时当然还是毫无疑问的有用。于上一篇中使用的场景相比,在解决一个难题时如果只是简单重复使用自己的知识恐怕是不充分的。工程师们需要用更具有创造性的思维方式来使用自己的知识。在实践活动中还发现一些简单的知识,甚至是很基础的知识在解决问题时往往能够发挥更好的作用,扮演着主力的角色。
**四.思考方法**
介绍几个思考问题的方法,这些内容在我看来是非常重要的,也是非常有实用价值的。
1.数学家的思考方法
先说一个数学家思考问题的方法。这个例子是很多年以前我在电视中看到的,当时没觉得怎么样,但是随着年龄的增加和处理的问题难度的加大,越来越开始体会到它的作用了。电视中在介绍数学家如何思考问题时举了下面这个烧水的例子。
假设我们能做一件事情,这件事情是将一个装满水的水壶放到炉子上,然后把这壶水烧开。所以当有人给你一个装满水的水壶,并要求你烧开水时,你是直接能够完成。好,现在假设有人把一个空的水壶拿给你,并且还是要求你烧开一壶水,那么你怎么解决这个问题?生活中处理这个问题是不用想的,直接把水灌到水壶中,然后放到炉子上烧就是了。但是我们用数学家的思维方式来解决这个问题时却需要费一点周折。数学家是按照下面步骤来思考的:
第一步,首先是意识到问题的差异,我们会处理的是装满水的壶,现在是空的水壶。放到实际工作中,这一步就是要求我们有足够的观察力,辨识能力和一种敏感的职业嗅觉来发现问题的关键点和解决问题的正确方向。这个和之前博文中提到的对比法是雷同的,只是使用的场合不同,当然难度也不同。实际上这也是一个分析问题的步骤,但是这个步骤只是一个简单的比较。通过这个比较让我们发现了差异,这个差异告诉我们难题和我们会解决的问题之间的联系。
第二步,然后考虑有没有办法把空的水壶处理成装满水的水壶。如果可以那么这个问题就解决了。数学家的思路的亮点是在这一步,这是一个开启解决问题之门的思考问题的技巧,让我们看到了从不会到会的希望。很多情况下这个时候,我们的想法是换方法,或者是简单就做出判断我不会,而不是想办法去创造或者说尝试创造我们解决问题所缺少的条件。这点上数学家的思维方式提供了一个重要的启示。
第三步,发现将空水壶处理成装满水的水壶方法之一(你也可以花钱让人替你做)是将水灌到空水壶中。注意这是一个发现,也是向解决问题迈出了一步,就这个例子而言还是关键的一步。所以这个发现非常重要。在实际工作的表现就是看你的知识,经验和获取的信息是否足够让我们来找到这个做法,同时对你如何使用你的知识也提出了要求。上面两个步骤是遇到问题和分析问题,这一步则是在分析的基础上发现解决问题的方法。
第四步,现在需要考察将水灌到空水壶中这件事情我们会不会做。在实际工作中的表现是在对一个问题思考分析后,对其中子问题(或者子步骤)的又一次思考。显然实际的情况是我们会做的,至少绝大部分能用手敲代码的程序员是会做的。好了,思考到这一步我们就可以知道这个问题是可以解决的。这里第二步是关键,第三,四步是主体。
这个例子或者说思考问题的方法,非常精彩的演示了如何使用已有的知识来解决一个自己已有知识没有覆盖的问题。当然这样的例子有很多,限于笔墨,就不在说了。
2.牛顿求解曲边梯形面积的方法
牛顿在他的传世名著数学原理中使用我们中学里的知识(现在的中学已经讲极限和导数了,这个不算,我是说我那个时候)定义并证明极限和导数,进而给出了曲边梯形面积的计算方法。这是我目前为止看到的用自己会的简单知识,解决难题的最终极的例子了。各位工科科班出来的都是学过高数的,建议看一下数学原理中那部分的推演和证明,就知道牛顿为什么是牛顿了。当然我想强调的是,这个例子告诉我们还不止是这些。
将数学原理中的证明过程,当然还包括遣词造句的论述部分,和我们学过的高等数学教科书中的证明做一个比较,我们会发现数学原理中牛顿的证明很简单,如果可以的话我想说是简陋。如果我用这样的描述来证明一个题目的话,那么我的高数老师是不会允许我过关的。教科书中极限的概念是用ε-δ语言来描述的,而牛顿基本上使用的是自然语言,甚至连符号都没有(这里申明一下翻译的水平问题不考虑在内),相比较而言牛顿的证明不算什么。这个事实给我们的启示在于,当我们在解决难题时,不一定就可以很快发现一个满意的解,而往往只是一个看上去有可能是解的解。这个时候我们还是要坚持下去的,因为有一个解相对于没有解你已经是一个很大的进步了,毕竟离目标近了一步。如果当年牛顿一定要等到用ε-δ语言描述极限概念时才来完成微积分的建立,那么工业革命有可能就会推迟一百年了。
3.大爆炸理论的线索
天文学家观察到一个现象,那就是所有的星体相互之间的距离越来越远。基于这个事实我们可以得出什么结论?科学们就此提出了宇宙大爆炸理论,就是说宇宙从一个非常小的所谓奇点开始爆炸,从而产生了现在的宇宙。那么科学家是如何从这个简单的事实推出大爆炸理论的呢?其推演过程简单的令人吃惊。如果现在此时此刻所有的星体正在彼此远离,那么过去的某一个时刻,所有的星体的位置就比现在的位置要近,这个结论很显然是对的。那么过去的过去的某一个时刻,所有星体的位置就比过去的某一个时刻的距离会更近一些,这个显然也正确。好了,照这个思路推理下去,星体的距离就会越来越近,最后只能在一起。于是大爆炸理论(当然刚开始应该只是一个假说)就这么诞生了。从这个例子可以看出优秀或者巧妙的思维方式对解决问题会产生不可估量的作用。在大爆炸这个例子中根本没有用到天文相关的任何专业知识,只是通过正确的推理,就得出了这个假说。希望开发者能够体会一下并从中吸取营养。
上面的三个例子是给我留下印象并对我产生指导作用的例子,我相信类似的例子会有很多,大家可以找出适合自己的。数学家例子是最基础也是最重要的,我觉的所有的方法都可以从这个例子中推演出来。牛顿的例子说明的如何创造性的使用知识,最后的例子是考验我们的智商了,或者说观察力了。
**五.破解难题的方法**
破解难题之难有两个方法可供参考,第一个就是所谓的分析。这个分析通俗来讲就是认识问题,当然认识的内容除了审题和正确理解外,我们更关心的是能不能用一个严格描述来表达难题的结构。如果一旦可以做到这一点,那么解决问题的可能性就极大提高了。从分析的手段来说,大概只有一个那就是实践,通过和事务的接触和互动来考察问题的结构和性质,进而达到认识问题的目的。我有一次去面试,考官出了一个题,说是有三种图形:矩形,圆和椭圆,要求如何设计类来表示这三种图形。我当时没有仔细考虑,第一反应就是按照几何上的定义来设计类。对于矩形就是矩形类,属性是一个左上角的坐标,另外两个是宽和高;圆类的属性是两个:圆心坐标点和半径;椭圆的定义当时我忘了,所以向考官说明想法就不写了。如果这时让我用UML或者OOD来解释一下的,有可能我会说上一段。但是,实际使用的数据结构只有一种:矩形。这一点在以后接触GDI+编程时得到了证明。这个例子告诉我们看出三种本质在几何上完全不同的图形的外在差异,而问题的本质只是一个矩形,这才是表述问题的正确结构。
在分析问题或者说在采取某些活动尝试了解问题时,观察力是一个重要的辅助能力。因为很多活动是通过人机交互完成的,那么观察计算机的输出设备(一般是显示器)显示的信息是认识问题的极为重要的手段。任何细微的变化或者结果都有可能是破解的难题的重要线索。我曾经有过这样一个例子,要让控件的背景显示指定的颜色,当时我还在用VC,于是我就用红色做了一下尝试。结果是控件的背景色没有变化,当时就很被动了,纠结在绘制了红色为什么红色不显示,于是不知道该如何解决。结果在一次偶然的操作中,好像是窗口的最小化以后再最大化,结果发现控件的背景有一次闪动,重复操作一次再观察发现是红色闪了一下。这个现象说明,红色的背景被画上去了,但是又被控件当前的背景色覆盖了。于是破解问题的线索是如何不让控件的背景色覆盖我绘制的颜色。从这个线索出发最终还是解决了问题。
另外还需要强调一下,在采取的认识问题的活动,我认为应该是多样的并富有变化的,可以尽可能多的让自己了解问题。有些时候我们采取了一些行动,但是往往收获不大,这个时候就需要思考了。不能简单的重复活动,而是要采取不同的活动来认识问题,关键点是变化。举一个简单例子,如果我们实现一个文件复制的功能,结果复制失败。这里先忽略复制函数的返回值或者抛出异常的信息这些内容,直接问自己如何采取活动来了解不成功的原因进而帮助解决问题?如果是我的话,我采取的活动可能会是这些:
1.不写程序,新建一个文本文件,然后直接复制该文件
2.不写程序,直接复制程序中需要复制的文件
3.在程序中以直接执行命令的方式复制文件
4.不读取文件内容,而是将一个固定的字符串写入到目标文件
5.在目标位置新建文件是否可以
6.直接在目标位置新建一个同名文件,并且不读取文件直接向新文件写入需要复制的内容
7.读取文件内容,写入到一个已经复制成功过的文件中
8.读取文件内容,但是只写入成功复制文件的那部分内容
我估计这些活动基本上可以找出问题的原因了。发现不同的尝试途径和内容,有时候不难,有时候却相当有难,我建议大家要学会思考。当遇到这种场景可以考虑将双手离开键盘,人站起来离开电脑,到外面散散步,在散步中仔细考虑应该采取的尝试方法。
分析问题的另一个有力工具是排列组合,简单来说就是中学里学的加法定理和乘法定理,更完整的说法就是组合数学中的那些计数方法,定理及其性质。排列组合实际上只有两个核心内容:计数和枚举。如果能够对问题做到计数,那么我们就完成了对问题在结构上的认识;如果对计数结果能够完成枚举,那么枚举的方法和过程也就是解决问题的方法和过程。所以再次强调一下,书本上的知识是有用的,并且是最有用的。
难题破解的第二个方法是简化。假设有一个问题A我们解决不了那么就可以考虑先忽略一些问题中的要求,使得问题的难度降低,然后再尝试去解决这个简化后的问题。如果还是不能解决,那么可以考虑再次简化,或者可以考虑不断的简化。这个做法和迭代开发很类似。这里举一个WideUnion团队实际遇到的问题。Entity Model Studio支持图形化的UML建模,那么当初设计了一个功能,在生成代码前对用户设计的UML模型做语法检查,其中的一项就是不允许出现关系的环。比如继承关系的环:A类继承自B类,B类继承自C类,而C类又继承自A类。显然这个语言检查需要查出模型中所有的这样的关系环。更为规范的描述是:找出一个有向图中的所有的环。
在我学过的知识中,教课书只告诉我如何判断一个图中是否存在环而不是找出所有的环,所以这是超出我的知识范围的,第一反应当然是不会,真心不会。采取的第一个步骤是构造若干个构成环的实例,看着图让自己有一个感性的认识。然后简化问题,这里有两个策略可以走,第一个是假设是有一个环;第二个是有环并且是直接构成环,就是两个节点直接存在指向对方的边。我采取的是第一个策略。然后完善算法,使得算法可以完成这个功能:只要图中有一个环,那么一定能找出来。这样就进入到解决问题的第二个步骤,查找多个环,这一步实际上是分为两个小步骤来完成的。第一个小步骤是,限制构成不同的环只能出现不同的边,不能出现不同的节点。换句话说,新的环是有相同的节点之间的不同有向边构成的。然后再次完善算法,保证图中只要有这样的多个环一定都能找出来。完成这几步后,就可以迈向最后一步了。任何一个环都是由节点和边构成的,上述的步骤实现了相同节点中不同边构成的所有的环,这个算法简称为A的话,那么下面需要做的就是改变节点再次执行算法A就可以了。于是一个原来不会解的问题通过逐步简化就找到一个解了。最近在看书时偶然发现寻找哈密尔顿环的算法应该也是可以参考的。
有时候解决难题往往表现为缺少条件或者有些条件不明确。这个时候,有两个简单而实用的办法可以使用。第一个是假设,这方法在做逻辑判断题时经常使用,我们可以假设条件A成立,或者假设A的内容是XX,从而构造出一个明确的已知条件来帮助解决问题。另一个方法是非常熟悉的反证法。反证法的本质也是假设,只不过假设的是结论成立。一旦假设结论成立,那么结论成立所以依赖的条件也必须成立,那么成功构造出这些条件的方向往往就是解决问题的入口和起点。
**六.实例分析**
下面通过两个有代表性或者能说明问题的实例来讨论一下,如何使用上面提到的内容来解决难题。
我想先介绍一个偶然看到的电视节目。大概是3月中旬,央视的世界地理频道播出了一期节目,其中一个桥段是说一个大夫如何给一个小男孩致伤的事情。事件的来龙去脉简单的描述如下:
症状:小男孩的背部和大腿上大概有好几百根类似仙人球一样的刺。
问题:如何把这些刺拔出来?
方法:1.直接用器械一根一根的拔。大夫的实践证明效率极低,无法接受。
2.用胶带纸粘掉刺。结果也不行,胶带纸的粘性不够,刺还是拔不出来。
3.使用女性用的脱毛用品和胶带纸。成功,效果非常好。
这个故事和我们软件工程师如何解决问题的相关性是什么呢?简单来说有以下几点:
1.医生遇到的问题严格上来说不是治病,而是一种对人体的修理。同样的软件工程师遇到的问题也不一定是严格意义上的技术问题,但是只要你遇上了还是要你来解决的。不管会不会至少你要尽力去解决。
2.胶带纸的使用。我相信那个大夫在学校里读书的时候,他的教材上绝不会把胶带纸作为一个工具告诉学生去给病人治病的,但是在实际工作中他却这么做了。对软件工程师来说,问题的攻克往往也不是用正统的书本知识来解决的。所谓的“旁门左道”也是需要的。
3.脱毛用品的使用。这显然是一个亮点。如果说使用胶带纸是脱离了书本知识,那么脱毛用品的加入则是一个创造性思维的质变。这点对于程序员的借鉴意义在于,要求程序员们要有灵活和富有创造性的思维方式和能力。对于自己掌握的知识是否100%的用尽了,是不是创造性的使用了自己掌握的知识,而不是仅仅简单的重复。
还可以再提出几个问题,考察其中的若干细节,我们能发现这个过程中还有更多的内容可以给与我们启示与借鉴。
1.第一个方法是最正统的方法,但是效率低,那么什么理由决定放弃?
从节目看效率低是直接原因。但是一般来说同一个问题应该有不同的方法可以解决。我在看到这里时,第一反映是是否可以改进拔刺的动作,或者找另外的医生一起来做。那么与那个医生的选择就会不同,于是就有了不同的方法。工程师在解决实际问题时,就要根据自己所处的环境来决定方法的使用。
另外医生尝试后觉得效率慢,那么是不是就一定很慢呢?我相信不同的医生来做效率上是会有差异的。这个说明,解决问题是要发挥自己的技术特长,用长处来解决问题。
2.胶带纸的使用
刺插入身体,刺与身体会形成一个夹角,这个夹角导致刺和身体表面不是平行的。那么在用胶带纸时就会有一定的麻烦。另外在撕下胶带纸时,粘力和刺拔出的方向也不是平行的,显然这个动作对男孩来说是会带来痛苦的。这些细节告诉我们在方法的具体实施时,工程师自身的操作过程也很重要。操作技巧往往可以弥补方法的不足,也有可能会影响方法的效果。当我们决定是否放弃或者使用一个方法时,需要根据实际情况综合考虑的。所以可以看出胶带纸的使用表明那个医生确实遇到了麻烦。
3.脱毛用品的使用
大夫是在和护士不经意的聊天中获得了灵感,尝试使用脱毛用品的。这告诉我们解决问题的思路往往不是来自主战场,有可能来自生活中的点点滴滴的细节中。另外一个需要注意的是,一个创造性的方法的使用,需要考虑其负面的影响,这点非常重要,务必注意。比如,脱毛用品的使用不但没有解决问题,反而带来更大的麻烦,那么这个医生的处境会有多么的被动和狼狈?!
第二个例子来自Entity Model Studio产品,这个问题是我们在开发时序图时遇到的,先简单交代一下问题的背景。在绘制时序图时,用户可以通过鼠标拖动消息,从而改变消息和消息生命线在时序图中的位置,这是所有时序图都应该支持的功能。以Visual Studio中的时序图为例,当用户拖动消息后,直接相关的以及依次相邻的图形都会根据拖动的消息的新位置做出调整,从而保证拖动后各个图形还是在一个合理的位置,这个称之为自动调整。这就是我们想实现的功能。下面依次讨论几个要点。
1.问题的提出
这个功能如果能得到实现的话,用户操作会非常方便,否则用户就必须自己手工调整每一个相关的图形,很麻烦的,体验非常差。所以出于这个目的就提出了实现该功能的要求
2.实现的风险和难度
在我们考察的几款产品中,没有实现该功能的产品也是有的。所以从战略层面上来说,这个自动调整功能不是必须的。这样我们的风险就小很多了,因为就算实现失败,产品也是可行的,当然能实现是最好的。实现该功能的难度在于,这是我们第一次尝试解决图形位置自动调整功能,对相关的算法和知识我们一无所知,也没有相关的任何经验,所以在最初我们不知道该如何去解决这个问题。但是最终决定去的原因,除了风险不大之外,我确实想或者说非常非常想实现一个微软做到的功能,因为在历史上我曾两次挑战失败,所以这次想雪耻。
3.问题的分析
对这个问题的分析,是从使用Visual Studio的时序图开始的,反复的构造不同的图,然后拖动消息,考察其行为,从而找出规律,对这个问题有一个最初的感性认识。通过这个过程,认识到数据结构的设计是需要改动的。通过观察这些行为表现,我们还意识到有可能需要使用到某些我们不了解的定理和算法,这才是最可怕的。为了确认这个疑问,于是又做了进一步的分析。实践表明可以有变通方法解决,这个情况和牛顿用中学知识定义极限是有点类似的。都是在用已知的简单知识去描述一个未知的新的内容。这个过程中,观察力和思考力起着非常重要的作用。
4.问题的简化
问题简化分两步走,第一步尝试最简单的情况,然后考察实现的过程和结果,从反馈中获得信息,判断我们的认识是否正确,难度是否可以接受。第二步是分析可能的存在的各种不同的结构,对每一种不同的结构依次处理,从而把问题肢解掉。这里需要用到一些基础的知识,比如计数方法之类的。
通过上面几步,基本上就可以确定该问题从一个不会做的问题,变成了一个可以解决的问题。当然方法不是唯一的,我相信同行们会有更好的作法。
**七.最后想说的话**
总体来说解决难题是一项很具有难度和挑战的事情。从使用的方法和思路上来说,我发现扮演主力角色的非常意外的是一些简单的方法和知识,其难点是在于如何应用。医生拔刺,数学家,牛顿和天文学家的思考方法,给我们的启示是:解决问题的方法和思维方式是互通的,没有行业和领域的限制,正所谓它山之石可以攻玉。我们要学会去点滴积累这样的经验,吸收营养,丰富知识,扩展思路从而提升我们分析问题并解决问题的能力。这种现象可以用这样一句话来概括:功夫在题外。
解决难题的过程是一个痛并快乐着的过程,工程师们要有勇气和胆识去面对这样的挑战。你会经受折磨但是也会有快乐的体验。如果你爱一个就让他去解决难题吧,因为那是天堂;如果你恨一个人那也让他去解决难题吧,因为那是地狱。鉴于一些非常令人不愉快的行为,我这里申明一下如果达内需要转载或者修改本人写的任何博文,那么请先获得本人的许可。
好了,这次就写到这里,如何解决问题的话题到这里就算是告一个段落了,通过这次回顾我自己也有相当的体会,确实也收获了一些东西。同行们有兴趣进一步交流的可以加我的群:244054966,这个群定位是创业,新手就不要去了。另一个是:231233168,这个群没什么限制。入群时请加上消息:CSDN博客。下一篇谈谈能力相关的话题,名字暂定为:能力养成篇。
(03)问题解决篇(中)
最后更新于:2022-04-01 16:15:19
关于本系列文字的来源,初衷和内容定位可以参考第一篇的开头部分,链接地址如下:
http://blog.csdn.net/binarytreeex/article/details/8174445
http://www.cnblogs.com/WideUnion/archive/2012/11/12/2766397.html
本文前一篇地址如下,感兴趣的可以访问下面的连接:
http://blog.csdn.net/binarytreeex/article/details/8456766
http://www.cnblogs.com/WideUnion/archive/2013/01/01/2841335.html
本文是问题解决篇的第二部分,继续讨论与解决实际工作中的问题相关的内容。本文假设读者是具备一定开发经验的。这个所谓的经验是指需要满足这样一个底线:代码中的语法错误和逻辑错误可以自己独立解决,或者说基本上可以独立解决。如果是开发新手,尤其是没有经验的毕业生,我不推荐来看本文,对这部分程序员来说本文的难度有点大了。
对于有一定开发经验的程序员来说,工作中遇到的问题基本上是可以自己独立解决的。所以本文讨论的内容旨在帮助这部分程序员更好的解决问题,或者说在进一步提高解决问题的能力上,提供一些参考和有用的方法与思路。和上一篇一样你不能期望本文可以解决你所遇到的所有问题,或者看了本文之后就可以立马武功大增,这是不现实的。但是如果记住我个人的这些经验,然后在工作中去实践,在日积月累中可以帮助你(至少有一部分程序员是这样)更快,更好的提高解决问题的能力,这个是可以做到的。因为我就是这么走过来的,所以如果你也想这么走过来,那么我分享的经验对你一定是有帮助的。
再次强调本文介绍的内容来自个人的实践,对于解决问题这样一个宏大的话题和全体开发者这样一个宽泛的群体来说,局限性和片面性是在所难免的。所以请同行们自行取舍,同时也要根据自己的经验,实际应用场合做出适当的变化,这样才能更好的应用本文介绍的内容。如果分享的内容可以为同行们解决实际工作中的问题起到积极作用的话,那么我的目的就达到了。当然如果能够达到庖丁解牛那样游刃有余的境界那是最好的。
**一.解决问题的一般步骤**
当程序员在实际工作中遇到问题时该如何去解决呢?以我个人的经验来说大致是以下几个步骤。首先需要去认识问题,确认这是一个什么样的问题或者问题是什么。比如对于调试程序中出现的bug,往往是以能够重现bug为前提的,否则无从下手。这种可重现性对我们认清并判断问题是一个前提。在实现某一个功能时,程序员也需要对实现的功能有一个清楚的认识,然后才能选择合适的方法来解决。所以解决问题的第一步是认清问题。第二步是评估这个问题是否有解决的办法,或者说需要做一定尝试工作以确定这个问题是可以解决的。这一步也很重要,它的作用在于保证程序员在解决问题的一开始就朝一个正确地方向前进。这一步的另外一个作用在于评估解决一个问题是否值得去做。有时候解决一个问题,不总是以解决作为结果的,有时候放弃也是一个合理的选择。第三步是选择合适的方法解决问题。不同的人对于相同的问题可能会给出不同解,这个由解决问题的程序员自己决定。能力,环境等等因素都会导致不同的解决问题的方法。
**二.程序员通常会遇到什么样的问题**
在大部分的情况下程序员遇到的问题属于工程问题,而不是科研问题。与我们在学校中做习题相比实际工作中的问题具有一些不同的特征。
1.是否存在解
在学校中我们做的习题一定是有解的,但是实际工作中的问题则未必,或者说自己面对的问题不一定是可以最终被解决的,而是以失败告终的。
2.是否有最优解
习题往往存在一个所谓的标准答案,但是实际工作中的问题往往会存在多个或者若干个可行办法来解决问题,工程师需要从中选择一个。所谓的最优解有可能是不存在的。
3.选择解的标准不同
用于判断最优(或者说最合适)的解的标准不同。做习题往往是单纯的比较方法本身的差异。而在实际工作中需要考虑其它相关的或者周边的因素,比如:难易程度,成本等等。对我而言我把最终选择的方法的可理解性,可维护性和功能变化后可重构性放在非常重要的位置,作为选择解的标准。
4.可选解的选择范围不同
做习题只能是用知识正面解决问题,当然抄袭是不算的。但是实际工作中的问题可以使用非技术手段来解决。比如对于性能问题的改善,可以通过重新设计算法来解决,也可以通过改善硬件配置来解决;对于用户提出的bug,可以直接修改解决,也可以通过富有技巧性的沟通来解决,等等。
实际工作中的问题还有一个比较乐观的特点,那就是问题往往总是可以被解决的。其原因是有由于大环境和小环境决定的。大环境是公司或者团队在选择开发任务或者立项时会有一个评估,这和上面提到的解决问题步骤中的第一第二步类似。小环境是,在具体安排开发任务时,往往会根据各人不同的技术特长而安排有针对性的开发任务,或者有选择性的安排不同难度的任务。就我而言,当初是以VC入行的,结果公司开始就安排VC的开发任务,今后就一直以此为依据来安排开发任务的。从而导致我很少有机会接触其他的开发任务,比如Java之类的。
在实际工作中,程序员遇到的大部分问题都是如此的。所以从战略层面讲,没有必要畏惧问题。当程序员遇到问题时,请保持信心,要相信方法总是会比问题多的。这样的心理状态对解决问题也是很有帮助的。
**三.解决问题的基础条件**
就我个人的体会而言,解决问题的基础条件应该有三个,但是本文需要关注的只有两个,那就是知识和经验。实际上对我而言,在解决问题时我最担心的是需要用到我不知道的知识,这才是最可怕的。这次在给Entity Model Studio开发时序图时就出现过这样的担心,后文会讨论这个例子的。现在举另外一个例子来简单说明一下知识的作用。
有一个非常著名游戏叫魔法门,我在2000年之前玩过其中的几个版本。其中一个版本有这么一个任务,要求主角到一个修道院去拿一件物品。该物品在一条走廊的尽头,如果直接走过去拿是不行的,因为那里有一个牧师在做祷告,挡住了去路。按照攻略上的说法,需要去大堂的阁楼上按照一定的次序拉动五根绳索,然后回来,这时牧师就离开了,从而可以成功拿到物品。这五根绳子的拉动方式是这样,用鼠标选择然后点击,如果点击的顺序匹配上了,游戏就给出一个响声作为提示。否则你就可以一直没完没了的点下去。好,现在简单说明一下这个问题的核心焦点是什么。
首先说明这不是一个难问题,但是有一个不算问题的小障碍,那就是在处理输入数据时和我们通常的方式是不同的。我们回想一下自己实现过的功能,一般来说用户输入的数据(这个游戏场景里就相当于用鼠标点绳索)会有一个明显的结束操作。比如:用户注册,填完注册信息之后按提交按钮;查询时输入表示条件的数据,然后按查询按钮,等等。但是这个游戏中的场景却不是这样的,这个第一人称的角色扮演游戏允许用户可以不停的点击鼠标,而不是要求玩家每点五次鼠标然后再按提交按钮,程序做判断,匹配就通过,否则重新再点五次。与我们通常处理的情况相比这是最大的不同,这个问题的核心焦点就在这里。有兴趣的同行可以考虑一下自己会如何做。这里限于篇幅,我就不说明解决的过程了,而是直接给出我的作法。当然这个不是什么标准答案,但是从用到的知识和方法来说,我相信魔法门的真正开发者也应该是这么做的。
这里假设正确的点击次序是54321,也就是第一次点第五根绳子,第二次点击第四根绳子,以下类推,这个数据可以保存在一个数组里,比如整型数组。当玩家第一次点击时,获得点击的绳子的序号,然后和数组的第一个元素比较。如果相同,那么玩家第二次点击时就和数组中的第二个元素比较,如果一直正确那么每次点击后都依次比较后一个元素,一直比较到最后一个元素正确时,就可以让那个牧师走开了。如果比较的结果不对,那么当玩家再次点击时就要和数组中的第一个元素比较,重新开始了。比如,玩家连续三次点对,但是第四次点击错误的话,那么第五次点击时比较的数据是数组中的第一个元素。好了,解决问题的思路有了,下面的问题是代码怎么写?一般来说直接用嵌套的if语句或者嵌套的循环就算做出来也是不可取的。打分的话是零分,肯定是零分。我推荐一个做法是用状态转换图,到这里,书上学到的知识开始用上了。
下图是我画的状态转换图,当然只是一个示意,将就着看吧,不规范或者不正确的地方可以指出来。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-02_5750091522100.png)
由于问题比较简单,表示状态的变量和表示数组下标的变量可以用同一个来表示,并且初始化为零。玩家每点击一次就和下标指示的当前位置的元素比较,如果一致,下标就加一。然后判断下标是否超出数组长度,如果是那就表示通过了,否则将下标设置为零。所以,可能的代码大概是这样:
~~~
if (data[state] == inputData)
state = state + 1;
else
state = 0;
if (state >= 5)
{
触发通过剧情
}
~~~
或者简洁一点可以写成这样:
~~~
state = (data[state] == inputData)? state + 1 : 0;
if (state >= 5)
{
触发通过剧情
}
~~~
其中data保存的是正确次序的数组,state就是那个状态也就是下标,inputData表示玩家点击的绳子的编号。
这里需要强调一下,如果按照书本上的知识,严格规范的按照状态转换图的作法来解决这个问题的话,代码会比较臃肿。比如,我们在这个例子中还是遵守一个状态用一个独立方法来处理的话,那就很没有必要了。所以提醒工程师们在应用知识时做适当的有针对性的变化。另一个需要强调的是,知识在这个例子中对解决问题是非常有效的。想想开发魔法门的程序员和我们相比当然强的很多很多,但是只要有足够的知识,我们就可以和他们做的一样好,至少这个局部的小功能是这样的。请体会一下知识的重要性。
解决问题的另外一个基础条件是经验。这是在长期职业工作中逐步积累起来的一种敏锐的判断力和识别能力。这种职业的嗅觉或者直觉往往可以在第一时间告诉工程师自己,什么是正确的方法。但是遗憾的是,经验这个东西除了自己逐步积累,或者用功的去积累,好像没有什么别的办法可以替代。
基于上面的讨论:解决问题的步骤,问题的类型,解决问题的条件,在实际工作中解决问题时,我归纳出需要注意的事项有三点,它们是:行不行,好不好和对不对。很遗憾的是,在实际工作中这三点往往被忽略,或者没有主动的使用,即便是用上了,也是下意识的。下面依次讨论这三点。
1.行不行
所谓行不行就是,需要考虑面临的问题是可以解决的么?这个解决有两个层面的意思,第一个层面是能不能解决这个问题,第二个层面是我能不能解决这个问题。基于上面的介绍,一般来说或者大部分情况下,程序员是不会明显的体会到这个步骤的存在的。因为接受的开发任务都是被过滤过的,大部分情况下程序员自己都是知道应该怎么去完成工作的。所以这个步骤往往被忽略了。还有一种认识是,行不行这个问题往往是存在于一些比较难的或者大的问题中。比如考察项目的可行性,等等。这当然是事实,但是在实际工作中,在处理一些小问题时(或者相对较小的问题),也会需要程序员带着这种意识,去解决问题,从而避免自己在第一时间犯错误。这时行不行这个步骤的作用可以理解为,这个问题是否值得或者是否应该去解决。下面举几个例子说明一下。
第一次例子是在一个对日外包工作做得一个通信程序的项目。预计的工作量是三个程序员两个月完成,应该说是一个不起眼的小项目。但是日方在工作环境上提出了苛刻的要求,这些要求导致通信程序的调试将无法从服务器得到任何可用信息。另一个问题是中方项目经理对通信程序的开发经验不足,没有要求日方提供与服务器端通信的协议文档。在这样的前提下,两次会议先后经过三个项目经理,一个部门经理,一个技术总监的参与下,还是认为项目是可以完成的。最后实际耗费的开发时间是七个月,日方限制的条件全部作废,并且提供了通信协议文档,这才算勉强完成。这应该是一个教训,我觉得其最大原因可能是对日外包公司有一种奴性的文化,对员工提出的正确意见完全不予采纳,盲目强调下级对上级的服从,同时还唯日方要求为正确。
第二个例子也是一个外包的活。客户提出的要求是改进一下现有的程序,通过进一步了解需求后发现两个情况:
1. 修改的原因是性能问题
2. 客户很快就会实施新的系统
由于改善性能的前提是需要明确性能的瓶颈在哪里,根据对实际的工作环境的了解,发现这个工作做起来会比较困难。其次性能是否有足够的提高没有把握。所以这项目最后评估下来是不做了。
第三个例子是当初确定Entity Model Studio是否支持图形化的UML建模功能。这个判断相对上面两个例子就复杂一些了。正方的理由是:
1. Entity Model Studio从最初开始的定位就需要支持
2. 通过市场调研为了确立优势,也需要支持
3. 我们有自己的一些理解和特色的功能需要图形化的设计界面作为支持
而反方的理由基本上只有一个,那就是WideUnion的开发团队是否可以胜任这个开发任务。相对上面两个例子,是否支持这个决定事实上不是通过一次,两次沟通就完成的。而是在此后的一个相当长的时间段中才逐步完成的。最终还是认为需要支持,当然事实上我们也做到了。最终确定支持的理由来自同类产品的比较,Entity Model Studio产品的远景和定位,以及开发技能的积累和研发难点的突破。
上面的三个例子所涉及的问题都不是很大的问题,但是复杂程度有差异,做出判断的理由不同,最终的结果也各有不同。所以提醒开发者在类似场合下还是需要认真对待这个步骤的。
2.好不好
所谓好不好是指最终选择的用来解决问题的方法是否为最优的或者是否为足够好。作为一个独立的用于帮助程序员判断自己选择的方法是否足够好的提问,这是一个很有挑战的步骤,对工程师的能力是一个综合的考验,当然也是一个富有乐趣的工作。在实际工作中,工程师在第一判断下所选择的方法往往就是应该满足工作要求的,从这个角度来说就是足够好的。之所以出现这个情况是由于上面介绍的大小环境决定的。那么在这样的前提下,还有必要再独立的用好不好这样的提问步骤来帮助我们确认所选择的方法是否足够好呢?
回答当然是肯定的,有必要。我们从上文提到的问题解决步骤开始解答这个提问。作为解决问题的第一步,需要考察面临的问题是否可以解决。作为最直接也是最简单的回答就是给出一个可以解决该问题的方法。这样我们在解决这个问题的过程中就得到了第一个解。但是这个解往往是不能用于实际工作中的,因为这个解只是为了确认问题可解决,而在解的质量上可能会存在缺陷或者在可行性上存在难度,从而使得这个解在用于实际工作中时,往往不能满足工程上的要求。于是我们在解决问题的下一个步骤中需要在第一个解的基础上做优化或者再找出一个更好的解。这样会得到第二个解,基于第二个解,我们才真正开始着手解决问题。但是在这个解决过程中,我们会发现当前对问题的认识有可能是不全面或者错误的,也有可能若干细节我们还没有考虑到。这个时候需要对第二个解做修改,改进或者重新寻找第三个解。所以从这个步骤来说,我们在解决同一个问题的过程中很有可能会得到两个,三个解或者若干个解。因此面临好不好这个问题是经常性的。
这里再用上面提到的魔法门点绳索的例子来说明一下如何查找不同的解。在寻找新的解时,首先需要明确一点,我为什么需要去寻找新的解,或者说寻找新的解的理由是什么。这个步骤对开发者能力是有要求的,需要对具体的问题,环境和要求综合考虑。就本例来说寻找不同解的原因主要是两个。第一个是原来的解用到了状态图,这部分内容来自编译原理的自动机。显然没有学到这部分知识的程序员要用这个解是有困难的,也就是说这个解对使用的人是有要求的。第二个原因是来自另一个游戏中的场景。在暗黑2中为了解救凯恩首先需要在石头矿野中按照指定的次序点击五根石柱,从而激活一个传送门来解救凯恩。这里点击五根石柱和点击五根绳索两个场景几乎一模一样,唯一的区别是魔法门中点击的次序玩家是不知道的,而暗黑中点击的次序是知道的。这个差异是让我尝试找出不同解的第二个原因。
下面就直接给出新的解法。将绳索看作是按钮,开始时只允许第一个按钮可以点击,其他按钮为disable,当然这个disable不是不让点,而是说点了没有效果。玩家点击第一个绳索时,就enable下一个绳索,这个enable是指点击了有效果。然后玩家点击下一个绳索时,再enable下下一个绳索。直至所有绳索被点击后触发剧情。这个解对书本知识的要求可能几乎没有。
还可以给出第三个解法。这个解法需要我们再定义一个新的数组(或者链表),这个数组用于保存玩家过去五次点击鼠标的数据。但是这个数组需要用一个与众不同的数来初始化,这里可以选用-1,而绳子的编号是0,1,2,3,4,注意这点。当玩家每次点击鼠标时,将玩家点击的绳子的序号插入到最后,然后数组中的原来数据依次往前移一个位置,这样原来第一个元素就从数组中移出去了,不在数组中了。实际上这就是一个队列的基本操作,注意这里使用到了数据结构的知识。然后再将这个数组中的数据和data数组中的数据比较,如果一致就通过,否则继续等待玩家的输入。关于寻找不同解的其他例子可以参考我的其它博文,比如:
http://blog.csdn.net/binarytreeex/article/details/1595936
http://blog.csdn.net/binarytreeex/article/details/1585550
再简单说明一下,上面的例子在推广到一般情况下的应用。寻找不同的解首先需要一个理由,开发者需要根据自己的实际情况去发现这样的理由。一般来说这个理由往往是某一个存在的缺陷或者瑕疵。在开始寻找不同的解时,需要明确主要矛盾是什么,或者说新的解的目的是什么。如果把,团队成员的技能水平,成员人数,时间,问题难度,开发工具,可利用的资料,完成的质量数量等等统称为外部条件,那么当外部条件发生变化时,寻找不同的解可以帮助我们任然可以完成原来的目标和任务。需要强调一下,在我的观念中,根据不同条件,要求和场合找出不同的解来更好的达到目的,是一个工程师应该具备的能力,而且是一个很重要的能力。
下面说一下如何来判断一个解是否更好。如果面临的是一个算法问题,我的建议是时间复杂度放在优先考虑的地位,可以为了性能牺牲空间,只要不是巨大的牺牲空间都是可以接受的。如果是非算法问题,那么考虑的因素就会多一些,情况会有点复杂。我的建议是首先考虑选的方法是否可以满足功能的要求,其次是考虑稳定性和可靠性。选择的方法可以在各种条件或者情况下相对更可靠稳定的工作。然后是选择的方法是否够简单,够易于理解。应该说前面两个是硬指标,是必须满足的,而排在第三个标准就是简单和易于理解,这说明我对这点非常重视。其原因是简单和易于理解的方法有以下几个优点:
1. 可以得到的更好的质量和可靠性。
2. 便于实现和修改
3. 更少的调试和测试的工作量
4. 对编码人员的要求可以相对低一些
当然还有其他的一些可供参考的标准,同行们可以根据自己面对的实际问题和场景自己来确定。
3.对不对
对不对的意思是指,我们做的对么?这个步骤可能最有可能或者最高频被忽略掉的。对于一个有经验的程序员来说,一般不会去选择一个对错不确定的方法来解决问题。或者更恰当的表述是,有经验的程序员都是在认为自己正确的前提下才会去做出选择。而问题就恰好出在这里,自己认为正确并不表示事实上就是对的。这里需要明确一下,如果一个程序员需要一个独立的步骤来确认自己的选择是否正确,那么这个程序员面临的问题是比较难的或者比较复杂的,否则是没有必要这么做的。
最理想的确认自己的选择是否正确,是象数学证明那样得到证明,当然这并不容易做的。因为我们面临的问题往往不是一个可以用数学来描述的命题。其次是寻找足够的充分的理由来说明正确性,这时经验的作用至关重要。我曾在实际工作中发现这么一个情况。一个开发者按照我的要求去实现一个功能。等我去查看进度的时候发现解决的过程基本顺利,编码也完成,但是问题出在解的正确性的保证上。这个开发者对正确性的保证是用调试和测试来完成的。由于使用到了一个自己设计的算法,所以只靠调试和测试来确保正确是不对的,象这类问题的正确性的保证首先是算法正确性需要得到证明。调试和测试只是例证,不是推演证明,只能作为辅助手段。这个步骤有可能被很多开发者忽略,或者没有意识到。就这个例子来说,经过沟通后发现所采用的算法的正确性的证明难度太大,所以重新设计了一个易于理解的算法来完成功能。如果不这么做的话,完成的软件就有可能会表现为不稳定,时不时出错,导致用户觉得质量太差。
作为一个解决问题的思维和沟通上的方法,对自己提问对不对也是很有用处的。在一次和其他部门的同事配合解决问题时就是如此。那次的背景情况如下:我的同事是做PHP的,有经验但是不太强;我是做C的,做过通信,但是我刚去什么情况都不了解。C写的通信程序拼接字符串,然后通过socket直接向PHP服务器的端口发送字符串,以模拟http通信。现在问题来,测试程序发现数据没有保存到数据库中。PHP一侧有日志,从日志看数据没有发送过去,而C这边也有日志,日志表明数据发送了。于是双方各持一词,都怀疑对方的程序问题。那么谁出问题了?这次遇到的问题的关键是我刚去那个公司上班,接手时也没有交接,直接通过读程序了解相关的事情。所以无论从哪一个角度讲,我都不是很清楚,再说我对PHP一窍不通,从而给沟通和解决问题带了难度。这个时候就需要冷静的问一下自己,我怀疑对方的程序有问题,那么我有证据么?或者说我的判断对么?于是和相关的PHP程序员沟通了一下,要求在服务器端的PHP代码中独立加入写文件的代码,并且这部分代码是不受如何业务逻辑干扰的。测试后发现日志文件可以生成,从而证明数据发送到了服务器端。然后,还是使用这个方法在PHP代码的不同位置,加入写日志文件的代码,最终找出了问题代码的位置。
这次就写到这里,下一篇还是继续讨论如何解决问题这个话题。下一次讨论的是在遇到一个自己不知道如何解决的问题时应该采取什么样的方法和思路最终解决该问题。下一篇的题目是问题解决篇(下)。如何解决问题这个话题原本只打算写一篇的,现在最终会写成三篇,应该说这在实际工作中是一个很有份量也很有实际价值的话题。我相信靠博文分享是不够的,如果有兴趣的可以加我的QQ群,群号是:244054966,这个群创业的人多一点;231233168,这个群新手多一点。希望能在群中和同行们互相分享共同提高。加入时请说明:CSDN博文
(02)问题解决篇(上)
最后更新于:2022-04-01 16:15:17
关于本系列文字的来源,初衷和内容定位可以参考第一篇的开头部分,链接地址如下:
http://blog.csdn.net/binarytreeex/article/details/8174445
http://www.cnblogs.com/WideUnion/archive/2012/11/12/2766397.html
这里就不再重复了。本文的前一篇是讲程序员学习方面的话题,感兴趣的可以访问下面的连接:
http://blog.csdn.net/binarytreeex/article/details/8174445
http://www.cnblogs.com/WideUnion/archive/2012/11/12/2766397.html
问题解决篇主要讨论的是,在实际工作中解决问题的方法和心得。在我读大学的时候,我的老师曾说过工程师的责任就是解决问题。无论一个开发者的能力有多强,工作态度有多好,如果不能解决问题的话那么就什么都不是。所以能够解决实际问题是一个软件工程师的核心价值所在。本文分享内容的定位和第一篇中的一样依然不谈经典或者范例的东西,我相信这和大部分或者相当一部分的教科书,培训课程中的内容会有所区别。我想和同行们分享的仍然是我在实践中的体会与心得。这些内容主要侧重在解决问题的方法和思维技巧方面,所以本文不能解决某一个很具体的问题。比如,你不能在本文中找到如何实现分页查询的答案;如何给线程传参数的实现方法,等等。另外常规的一些所谓的解决问题的方法在本文中也是不讨论的,比如:论坛发帖,google或者百度,qq群提问,向老手提问,等等。当然这些方法也是有用甚至是最常用的,不讨论是定位的原因,不是我想否定这些方法的作用和价值。由于本文介绍的内容来自个人的实践,对于解决问题这样一个宏大的话题和全体开发者这样一个宽泛的群体来说,局限性和片面性是在所难免的。所以请同行们自行取舍,同时也要根据自己的经验,实际应用场合做出适当的变化,这样才能更好的应用本文介绍的内容。如果分享的内容可以为同行们解决实际工作中的问题起到积极作用的话,那么我的目的就达到了。当然如果能够达到庖丁解牛那样游刃有余的境界那是最好的。
作为问题解决篇的上半部分主要是针对新手来说的。我想讨论一下几个影响新手解决实际问题的因素,这些因素是我在实际工作中观察发现的。总体来说新手遇到的问题是比较简单的,因为一般情况下是不会把一个有难度的问题交给新手来解决的。因此新手的问题通常不应该被称作问题。我当然没有任何轻视新手的意思,只是针对问题本身而言的。基于这个前提新手注意改善一下自己的工作方法,从最终结果看是可以提高甚至显著提高解决实际问题的能力的。这些因素主要是以下三个:
1.不清楚或者不知道自己在做什么
2.实现功能不到位
3.程序调试不通
下面开始依次讨论这三个问题
**一.不清楚或者不知道自己在做什么**
能力差的一个表现是花了时间解决问题,但是最终没有解决。造成这一结果的原因当然是多种多样的,其中有一部分是新手选择了完全错误的方法或方向来解决问题。对于选择的方法或方向不能解决问题这一点来说,新手往往完全没有任何感觉,给人的第一印象是对于自己当下在做的事情意味着什么,往往不清楚甚至完全不明白。对需要解决的问题本身的理解应该也是一个原因。如果是这样的话那么这个解决方法还是相对很简单的,那就是每次接到任务或者准备解决问题时,需要确认自己是否理解清楚了。但是更多的情况却不是由于对问题本身的理解引起的,那么这个时候怎么办呢?
首先需要确立一个信念:上级安排给新手的问题一般来说都是简单的,容易解决的。所以不要慌张,要有必胜的信心。其次,在动手前先思考一下我将打算如何解决这个问题。当然由于经验的匮乏和各人能力的差异,这种思考在相当的情况下很有可能是效果不太理想的,但是我还是建议坚持这么做,次数多了能力就慢慢来了。第三点需要确认自己解决问题所需要的信息都了解清楚了。新手工作时间不长接触的东西都是以前没有了解的。特别是涉及到具体于项目相关的内容更是如此。而解决一个实际问题不可能就是一个孤立的问题,需要解决的问题往往会和其它已存在的内容是有联系的。而这些有联系的内容往往是新手不了解的。所以新手需要确认这些内容自己已经了解了,否则就需要向上手询问了,这个是合情合理的提问。
我举一个简单例子来说明一下。我有一次面试被要求直接修改一个界面上的功能,是在真实的项目中修改。当打开solution后发现里面有很多工程和N多的窗体,于是第一个面对的问题就是需要修改的位置在哪里?我见到过的新手很可能会有以下一些做法:
1.在整个solution中慢慢找,而且找了N个小时后也不怀疑自己是否可以找到。
2.执行程序,找到主窗体(这多少是一个进步),然后在主窗体中改。至于是不是应该在主窗体中改就不考虑了。如果没有在主窗体中找到需要改的内容,那么自己就加进去然后实现这个功能。
3.没有做什么实际事情,可能是在发呆,可能是想了一会又开一会小差,如此不断交替。
4.从自己看到的第一行代码开始读代码,然后试图了解其中的逻辑,其思路是不读懂代码如何修改呢?
5.…
第一点就是方法选择错误。其错误不是在于找那个窗体,而是找了N个小时后还在找。假设给你一封信,上面没有地址,只是告诉你内容然后要求你去给某一个和信的内容匹配的人。那么你会从一个城市的第一条街道的第一个门牌开始依次把整个城市遍历一遍么?另一个问题是没有从自己尝试的实践活动中得到的反馈来修改自己的行为或者决策。给人的印象就是不知道自己做的事情意味着什么,对能不能完成任务一点感觉也没有。第二点是属于不动脑经的作法,要求是改程序而不是新实现一个功能。并且加入代码的位置是否正确也不考虑,这是对自己所做的事情完全没有感觉。第三点是比较糟糕的,应该尽快改变。第四点方法和思路都有问题。新手不妨评估一下自己的读程能力,如果认为自己足够强那还可以不妨一试,否则就不应该这么做。可以说这种新人对自己能力的认识都有问题。实际上,有足够读程能力的人一般是不会那么盲目的就开始读程的。敢这么做的基本上都是读程能力比较差的。
我说一下自己的作法,当然我不是说这个就是标准答案,只是介绍一个可供选择的方法以供参考。修改功能的第一步是需要知道在哪一个窗体中修改,而面对那么多的窗体最佳的选择就是提问,请熟悉项目的人告诉我那个窗体在哪里。原因如下:
1.我不了解项目,我是在一无所知的情况下开始的,这样的提问是合情合理的。当然我是大致看了那些窗体的,但是发现自己找出来大有难度。
2.我当时状态是在上机笔试,我不能把有限的时间花费在找窗体上。
第二个理由就我的例子来说更强一些。当然实际工作的情况会有所不同,所以大家需要根据自己的具体情况选择不同的作法。但是一条原则是把主要的精力和时间用在解决主要矛盾上,不要被次要或者支流问题分散了注意力。我通过询问就将解决问题需要的相关信息获取了,这是能够解决问题的基础。第二步是打开那个正确的窗体根据要求修改代码,那么这时又有一个问题,需要修改的代码在哪里,或者说应该在哪里改代码?那么这时是不是仍然可以使用上面的两个理由再次提问呢?就我的情况来说就不能再提问了,理由如下:
窗体中的代码不太复杂,应该可以独立解决。这个事实是在读了代码以后才知道的。所以新手可以尝试去实践一下,关键是要根据实践得到的反馈结果及时修改自己的方法,采取最佳途径解决问题。如果当时我基于上述的两个理由不去读代码而直接再次提问,那么面试你的人是知道那些代码的复杂程度的,所以他就很有可能会认为我的能力有问题了。因此提问要注意分寸和度。新人可以以这个例子作为参考,当然自己具体遇到场合会有所不同,所以要结合自己的具体情况作判断。这里顺便提醒新手,不要在自己不理解的情况下在真实项目中擅自修改别人写的代码,这是很不明智的作法。如果实在要改,那么在改的地方调用一个方法,自己想写的代码写在那个方法中,然后给那个方法和调用该方法的地方写上注释和日期。对于我遇到的这个问题来说,逻辑不是很复杂,看一下代码也就能够确认位置了。那时我是一个有工作经验的程序员,但是作为上机测试题目还是给出了一个不太难的问题,这说明一般来说不会给新手太难的问题,所以应该要有信心。然后我就在相关的位置修改代码,实现要求的功能。
接着又来一个新的问题,面对那么多的工程,如何执行程序?执行后如何操作才能执行到我需要调试的代码呢?这也是解决问题需要的相关信息。没有这些信息,程序无法调试,那么问题也就无法解决。改完代码只是改完,调试通过才能算是解决问题。这个问题的处理原则上面提到了,这里可以再给一个量化的尺度,以便实践中执行。假设修改代码花费了30分钟,这是解决主要矛盾花费的时间。那么解决次要问题的时间原则上应该小于这个时间,或者远小于这个时间。具体我的例子来说,如果尝试执行程序花费了30分钟还没解决,那就是方法选择有问题了。
所以不要花费过多的时间在次要矛盾上,不要无谓的浪费时间。自己找可能需要一小时,了解的人告诉你只需要几秒钟。另外需要注意,在类似的这些场合新人要习惯提问,敢于提问,但是问了之后需要记住,同样的或者同类型的问题最好不要重复提问。对于这个上机笔试问题我的解决过程是这样的:我看了一下那个窗体所在的工程,发现不是默认启动执行的工程。所以就将这个项目设置为默认启动。新手注意,这时修改了solution的设置,最好记住原来的启动工程是哪一个,以便改回来。然后就是执行那个工程。结果是程序抛异常无法执行,说是初始化失败。
这是遇到的第四个问题,现在是应该尝试解决这个问题还是直接提问?就我的例子来说是直接提问,理由如下:
1.解决初始化失败这个问题超出了笔试题考查的范围
2.我对那个工程不了解,初始化中的逻辑有多复杂不确定,所以是否可以在笔试规定的时间内(实际上时间没有明确规定,但是一般主观上会有一个可接受的范围)完成没有把握
3.退一步讲,我能找到初始化失败的原因;再退一步讲,我还有能力和时间解决初始化抛异常的问题,那么我真就去fix那个bug么?我的回答是:不。因为我不是该公司的员工,我不应随便修改那些没有要求我改的代码。
第三条理由在这个场合是最强的。新人可以结合自己的在公司中遇到具体情况灵活的确定自己的决策。于是我就将默认启动工程改回来,然后提问要求告知如何操作才能执行到自己修改的代码。在获得这些与解决问题相关的信息后,这个调试工作很快就完了,上机笔试题也就顺利做完了。由于是在真实项目中修改的,所以一个细节是临走时告知相关人员我修改的代码的位置和新增的方法。
新手可以体会一下,什么时候提问什么样的问题,以及什么事情自己可以去尝试解决,尝试的度的把握;什么事情自己不要求尝试解决。我相信这一点上部分有工作年份的开发者或多或少也会有的。如果把这个问题扩展一下,其应用的场合可以扩展到对问题的主要矛盾的辨识上,进而可以应用在解决更复杂的问题上。
**2.功能实现不到位**
经验的不足,业务知识的匮乏是我观察到的新手实现功能不到位的主要原因。当然出现这种问题也是正常的,而且也不是什么严重的问题或者过错,简单提一下在工作中注意避免就可以了。下面列举一下几种不到位的情况:
a.如果要求实现用户注册功能,并且没有给出具体需求,那么一个细节是:要求用户重复输入一遍密码是应该实现的。以目前的使用习惯没有明确告知做或者不做,而程序员事实上没有做出来就可以理解为功能实现不到位。
b.窗体界面上控件的大小,颜色,字体是否和已存在的控件一致。比如窗体上已有按钮控件的大小都是一样的,那你新加的按钮大了点或者小了一点,那就要注意了。
c.操作习惯是否一致。比如,控件内容填充是否联动;鼠标移动到控件上面是否有选中效果;错误信息显示位置等等。
d.编码风格,注释书写格式是否一致等等。
再举一个我在实际工作上遇到的一个例子。一个新手实现完成了一个用户登录界面及其中的逻辑。但是如果由于忘记密码或者其它原因导致无法登录成功时,登录界面无法关闭,用户无法完成其它操作,比如退出软件。这是一个真实案例,而且在我提出不完善的地方后,那个新人还拒绝修改。我最后是用一起玩过的暗黑登录战网的例子才让他明白功能上的缺陷。主体上登录功能是实现的,也测试通过了,这个没有问题。但是遗留一个无法退出登录操作问题,这就是功能实现不到位,或者说不完整。在真实开发中,这和没有完成功能基本上是等价的。
另外一个造成实现功能不到位的原因是缺乏质量意识和产品意识。这个错误我是犯过的,在一个外包公司工作时就有这个问题。那是第一次开发有用户可以直接操作的界面的软件产品。之前的想法总是关注在功能是否实现上,而忽略了上面提到的那些注意点,所以我开发的模块没有通过测试。注意,当时我是有六年开发经验,还有高程证书的,绝对不是一个新手。这里强调一下,希望新手注意。
**三.程序调试不通**
从整体上来说,调通程序对新手是一个坎。能够顺利的调通程序是开发者可以独立工作的一个重要考察标志。回想自己学习C语言的时候,还是吃了点苦头才走过来的。先谈一下编译错误,主要是两个:语法错误和链接错误。
对于编译中的语法错误,可以选择出现在前面的错误先解决。这是因为编译器在检查代码的语法错误时,有可能前面的语法错误导致后面的错误。这时先尝试解决后面的语法错误,就不太明智,难度相对就会大点。所以建议解决语法错误时从第一个或者靠前面的开始。另一个策略是先解决容易的语法错误。有时候出现语法错误很多,看着是比较烦的,这时可以先把简单的明显的错误解决掉。比如,看到书上的一段代码,想试一下,于是将代码敲进去然后编译。这时出现语法错误就比较多了,那么可以将类似标识符未定义(产生这个错误的原因可能是敲代码时的手误)之类的错误先解决掉。这样就可以逐步减少错误的数量,从而可以让我们的注意力能更好的集中在难度大的错误上。从总体上来说,语法错误的排查解决应该算是一个比较容易处理的问题。以当前流行的开发工具来说,对语法错误的排查提供了越来越好的支持,所以即便是新手我还是建议能够靠自己的能力完全排查掉语法错误。
相对于语法错的简单而言,排查编译时的链接错误的难度就会大一点了。在C或者C++这类编译器中,没有将必要的库文件引用到项目中是产生链接错误的一个频率很高的原因。当出现链接错误时,可以先肯定一点,源代码本身已经没有问题了。因为代码只有在通过词法和语法检查才会编译生成代码,所以产生链接错误时,代码本身的出问题的可能性就不大了。这个线索可以告诉我们解决问题方向就不要在源代码本身去找了。需要注意一点,对于编译时的错误,一定要看清错误的信息,明确错误信息说的是什么然后再去排查错误,切记。因为很多的情况下错误信息就直接给出了答案的。举一个我在开发EntityModelStudio时自己遇到的问题。我在一个工程里引用了另一个工程的dll,但是由于操作问题,实际引用的dll是另一个位置的不同版本的同名dll文件。产生的后果是dll工程调试什么问题都没有,但是引用后调试时怎么做都是错的。新手可以体会一下实际问题是不太按规矩出牌的。
除了编译错误,另外一类就是所谓的逻辑错误了。这才是开发者调试程序的重点。实际上在很多场合两,三个小时排除几百,上千的语法错误都不算什么,所以排除实现不了指定功能的逻辑错误才是核心问题。逻辑错误的具体表现是程序执行的结果达不到预期的结果,或者只是部分达到要求而不是完全符合要求。造成这个结果的原因当然是各种各样的,可能是选择算法有问题,调用的方法不正确,等等。对于这个问题我给出的建议是请掌握调试程序的基本技巧,就是断点设置和变量值的查看。
断点的设置可以从发现出错位置的方法的开始处设置,或者某一个肯定在出错位置前面的地方,当程序停在断点后按F10(Step Over的意思)逐步跟踪代码。当发现执行某一行代码执行完时结果不对,那么就要查看这行代码了。如果是单一的代码那么根据错误的结果修改代码,如果是方法调用,那么就要进入到这个方法的内部(按F11,Step Into意思),使用相同的办法继续跟踪。用这个办法可以逐步逼近错误的位置。有时为了能提高跟踪代码的效率,在自己对错误和代码比较熟悉的情况下,可以在某些关键点设置断点,查看程序的执行情况。这个事情听上去比较简单,但是遇到一些新手却会犯这么一个错误。F10和F11的功能是清楚的,但是在看到方法调用后结果不对时,却不知道要进入该方法的内部继续跟踪代码。这里提醒新手,F10和F11就是这么交替使用的,直至定位到最终出错的位置。如果仅仅使用这样的方法就可以定位并排除错误,那么这样的错误还是比较容易的,有可能是最容易的情况。下面介绍稍微有一点难度的情况。
有时候我们可能需要写一点代码(这些代码还不能算是测试代码),为使用断点设置这个调试手段起到辅助的作用。比如,错误的位置是在一个循环体内。如果我们直接简单的在循环体内设置断点,那么循环执行的第一次就停在断点处,然后开始单步跟踪。这时就会有一个问题,这个循环要跟踪多少次,才能等到出现错误的那次循环呢?如果是第一次,或者前几次那还好,如果是100次,200次或者更多次那就麻烦了。对于这样的场合就需要写点代码来辅助调试了。假设循环是for循环,那么在循环体内的恰当位置写下类似下面的代码:
~~~
for (int i = 0; ...; i++)
{
// 这些是需要调试的代码,已经存在的,假设有若干行
...
// 这个if语句是需要加入的调试代码
if (i == 100)
{
int a = 10; // 断点设置在这行代码上
}
// 这些是需要调试的代码,已经存在的,假设有若干行
...
}
~~~
调试代码起到的效果是,在循环到第101次时程序就会停在断点处了。这可以显著提高调试的效率。还有一些场合我们调试的程序和Windows的消息有关,这个时候断点的设置位置和时机就会比较麻烦。比如,当需要把断点设置在鼠标事件中时或者OnPaint事件时,就会这样。因为直接设置的话,那么每次鼠标事件或者OnPaint事件触发时都会导致程序停下来。而这时还远远没到错误出现的时候。这时可以考虑先不设置断点,等到最后一步操作前再设置断点。比如,需要将用户输入的数据在OnPaint事件中显示在客户区。那么可以考虑在恰当的位置执行一行刷新客户区的代码(对于C#的窗体是Invalidate,对于MFC还可以考虑发消息),断点先设置在那行刷新的代码上。等程序执行停在那行代码时,再在OnPaint方法中设置断点,然后按F5,这样就可以让OnPaint中断点直接停在我们需要的时刻上。再给出一个可供选择的方案是,如果可能,将OnPaint中的代码拿出来,放到按钮的点击事件中,这样调试就可以避开原来的麻烦了。更一般的思路是,在程序中加入调试代码,一般是一个if语句。该语句的条件表示了你希望程序停下来单步跟踪的时刻,然后将断点设置在这个if语句内部的代码上就可以了。
有些场合可以考虑使用控制台输出信息的办法。当然也可以选择写文件,作用是一样的,但是对于Visual Studio开发环境来说控制台输出对调试程序更为方便些。在程序的特定位置写入一些输出信息到控制台的代码,C#中我用Console类的WriteLine方法,或者Debug类的同名方法。这样程序的执行不会被打断,同时又能看到必要的信息。这是一个很不错的优点。在调试的时候,如果觉得有困难,错误位置无法确定,那么我建议可以采取逐步解决的办法。先实现最简单的情况,然后调试通过,接着再实现下一个情况,然后再调试通过。如果可能这里我想强调一下,这里分步依次实现的情况最好能够做到独立。比如,代码可以用明显的if else语句或者switch语句的分支隔开,或者代码放在不同的方法中。这样在调试程序时可以让我们每次只关注在一个情况上,并且处理不同情况的代码至少在视觉上没有相互干扰,这对我们解决问题是有帮助的。这个方法在很多场合是很有效的,用好的话还是简化问题的方法。在开发EntityModelStudio的时序图时我采用的就是这个方法,所不同的是时序图的各种操作行为的分类计数对新手来说是一个有点难度的问题。
**四.两个有用的技巧**
再介绍两个个人认为很有用的技巧,那就是对比法和关键点查找,这是我在工作第一年维修家电时积累的经验,实践发现在软件开发中也是有用的。所谓对比法就是手上有一份代码(或者例子)可以实现要求功能,而我们现在需要实现相同的功能,那么我们就可以对照着例子改,直至实现需要的功能。听上去很简单,事实上这个方法的表述也确实很简单,也许会有人觉得这和google或者百度后的copy/paste有区别么?应该说都会用到copy/paste,这点相同,但是思路上不同,这是区别。我用前几天在CSDN上看到的一个帖子作为例子来说明。
帖子中的有如下的代码(不是原文,但是意思相同):
~~~
public class MyClass
{
int Age {get; set;}
}
~~~
帖子的问题是:给MyClass对象的Age属性赋值,提示出错。这当然是一个很新手的初级问题,直接加上public修饰符就可以解决问题。下面试一下用对比法来解决。
问题的现象是不能通过对象访问属性,而事实是应该可以访问,那么就要试图去找一个例子,而那个例子是可以通过对象访问属性的。这样的例子上网很容易找,然后对比差异,应该很快发现差一个public,加上就可以了。这个和copy/paste还是接近的。但是有的时候找不到这个例子怎么办?那么我们可以通过对比别的内容来尝试解决问题。比如,现在问题是通过对象访问不了属性,那么可以通过对象访问方法么?如果找到通过对象可以访问方法的例子,那么就可以考虑通过调用方法的例子来修改属性。这个就是思维技巧的差异了,要灵活应用方法。在加大一点难度,那么上不了网怎么办?这时可以考虑通过现有工程中已有的代码作为例子。这个问题的难度应该说不大,但是思考的步骤对于新手来说应该是有点挑战的。从解决问题的思考技巧来说,如果一个新手自己就可以有这样的思路,那我就认为这个新手是有才的。希望新手可以体会一下。不同的技巧使用对比法可以在更为复杂的情况下解决难得多的问题,这部分在下篇讨论了。
从我个人的经验来说,新手在学习开发或者一个开发者开始一个新的开发方向的时候,典型的就是使用一门新的语言,会遇到一些无法用常理可以解释的问题。比如我自己的第一个Windows程序。我用了很短时间将30行左右的代码敲入计算机,但是却用了几乎整整八天的时间才调试通过。这个Hello World级别的程序,最终查出的原因是工程名不对。我用的是abc,改成aaa就可以了。这类问题在此后的开发经历中也遇到过,但是总体是越来越少,解决问题所耗费的时间越来越短。但是其共同的特征是无法用常理解释或者莫名其妙的自己消失了。遇到这类问题时,首先确保当前的工程足够简单。如果没有足够简单的工程,可以考虑新建一个。然后可以尝试逐步注释代码找出问题原因。或者先构造一个足够简单并可以通过调试的程序,然后一步一步的修改朝目标逼近。在这一过程中哪一步修改出了问题,那么问题就在那一步上。当然最好每逼近一步就做一次备份。总体上来说这个方法也是属于对比法的范畴。
所谓关键点查找是指程序执行在时间上是顺序的,由此总体上实际的代码也是顺序执行的。那么一旦程序出现问题,我们就可以把程序在出问题的点上分成两部分,出问题之前的和出问题之后的。如果我们看到的结果是正确的,那么问题点应该在当前时间点后执行的代码中,注意我说的是时间点的先后,不是源代码物理位置上的先后。所以我们应该在那些代码中去查找,并在恰当的位置设置断点。前面提到的F10和F11也是这个意思的具体表现。这里再次强调这个思路的意思是,希望新手在遇到问题而困惑时这个思考的技巧可以帮助自己理顺思路,而不是仅仅把F10和F11的作用看成是两个按键对应的功能。
**四.给新手的建议**
好了,这篇博文大致就写到这里,感觉谈的内容对新手来说可能有点多了。这些方法和技巧在使用中我相信会对新手是有帮助的。但是任何方法的应用都需要一个逐步熟练的过程,就像人的成长一样。所以新手也不能抱着一口吃成一个胖子的心理。务实的做好自己的工作,在实践中逐步提高和进步才是正确的作法。另外也要对自己有信心。其实新手还是有不少优势的,比如学习速度快,适应能力强,什么事情都有热情去做或者愿意做,面对困难有冲击力,这些都是长处。所以面对经验缺乏,能力相对较差的(较差这个词可能用得不好)情况用不着着急或者影响心情。总之尽力而为了就可以了,今天比昨天好,这次比上次好就行了。
**五.提问解答**
最后我解释一下学习篇中被提问到的一个问题。学习篇中我提到读一本书最好是用20到40分钟能过一遍。有人提问做起来有困难,那么在此解释一下。首先我知道的快速阅读技巧有两个。第一个是正统的快速阅读法,据说斯大林用这个方法可以在四个小时内看完一本五百页的书。通常的阅读方法是用视力最清楚的那个点一次看一个字,快速阅读方式是要求读者用眼睛一次看一段字,比如:三个,七个之类的。这样就会比通常的阅读方法快三倍或者七倍。这个方法是需要练习才能掌握的。第二个方法实际上是一个技巧。那就是只阅读每一段的第一句话和最后一句话,这个方法据说可以用10%的时间获得50%的信息量。这个技巧我没有试过,效果如何就不清楚了。其次,在读计算机书时,可以用视线扫描文字,这个不是快速阅读,这比快速阅读还要快很多。然后用视线滤出敏感的词汇,发现是重点的,感兴趣的或者不懂的就停下来看一下。另外已有的开发经验也至关重要。比如,阅读ADO开发数据库的书,我就直接用视线扫面书上的例子代码,ADO的初始化也看了一下,然后关注使用的步骤和其中的方法名。其原因是在这之前我有用C++使用ODBC的开发经验。另外需要注意这个方法对新手可能不太适用,因为新手经验少,调试程序的能力有限。新手看懂代码不等于能调通程序,所以新手一定要上机敲代码试一下才算会。但是老手就不用了,足够的开发经验可以保证这类入门级的例子,看懂就能调通。所以开发经验在读书时也是有用的。另外一个需要澄清的问题是本系列的文字与任何培训机构没有任何关系。如果还有别的问题或者愿意交流的可以加入我的群:231233168。
下一篇还是讨论解决问题的方面的内容,但是难度会增加很多,题目应该是问题解决篇(下)。这是针对有一定开发经验的开发者的。
(01)学习篇
最后更新于:2022-04-01 16:15:15
很偶然的写了一篇博文“十八年开发经历小结”,本来打算只是简单回顾一下自己经历。结果没想到被CSDN放到了首页,反映也还可以,感兴趣的可以访问我在CSDN上的博客,点击[这里](http://blog.csdn.net/binarytreeex/article/details/7999853#comments),或者访问我在博客园上的博客,点击[这里](http://www.cnblogs.com/WideUnion/archive/2012/09/19/2692465.html)。既然我写的东西还有人愿意看,于是我就萌发了再写一个系列的文字的想法。从第一次在CSDN上获得帮助,第一次在CSDN上帮助别人,一直到现在,期间帮助和被帮助已经很多很多次了。我认为专利和知识产权是独占并被保护的,但是经验和知识是应该分享的。为了这十八年来的帮助与被帮助,为了我写的文字还有人愿意看,我想写出这个系列来,与各位同行分享自己的经验,共同进步。这个系列的内容主要来自个人的实际经历,我不想谈一些什么经典或者范例的东西。我觉得自己体会到的东西和同行分享才更有意义,也更具有实用价值。同时我相信写出这个系列的文字,也是我对自己经验的一个认真的回顾和归纳,这一定是很有裨益的。
作为本系列的第一篇,想谈谈程序员的学习问题。之所以第一个要谈的问题就是学习,是因为我觉得这个问题最重要,也是最让相当一部分程序员比较犯愁发憷的问题。本文如果能给这部分程序员带来一点帮助或者启发,那么目的就达到了。学习问题在那篇小结中写了一小段,这次可以相对较为详细的谈谈这个问题了。
既然要谈学习,那么首先需要明确一个问题,我们打算学什么?这里先对这个纷繁世界中的知识做一个分类:
**A.教材上的知识**
这部分内容来自计算机专业的课程教材。也有可能会涉及一部分来自其他相关专业或者相关课程的内容。
**B.编程语言**
每一个程序员只有在会使用一门语言的情况下才有可能从事开发工作,所以学习并掌握一门语言是最低要求了。
**C.SDK**
光有一门语言是不够的,从事任何实际的软件开发都需要一个类库或者开发包才可以完成。比如C语言中的库函数,C#中的.NetFramework类库,Windows的API等等就属于这个范畴。
**D.开发工具**
以如今的情况来说,没有开发工具理论上也是可以开发软件的,但效率就是一个问题,所以掌握并使用一个开发工具完成开发任务应该也是一个最低要求。
**E.领域知识**
软件总有用户,于是开发这些用户使用的软件,那么程序员就需要了解用户所在行业的知识,至少需要知道一些基本的必须的知识。还有一部分的内容也划分为领域知识,比如从事Photoshop这类软件的开发那么图形相关的知识就必须了解一些,从事工控软件的开发,那么对控制方面的知识也要有所了解。
以上的分类是在本文中我对知识的理解,一个程序员知道这些知识后从事一个软件的开发应该是没有问题了。下面分别来讨论一下这些知识的学习问题。
**一.教材知识的学习**
做为一个已经从业的程序员来说,我不认为计算机专业的所有专业课程(包括专业基础课,我在读大学的时候还有这个说法)都是有用的。实际上对于大部分程序员来说,只需要很少的一部分知识就足够了。这些知识主要由三门课程组成:数据结构,编译原理,操作系统。对于大部分的程序员来说,其他课程的内容不是没用,而是在实际工作中用不上。
数据结构这门课程的重要性,可以理解为是程序员的圣经,怎么如何形容其重要性都是不过过分的。这门课程中需要掌握的内容,我个人观点如下:
1. 掌握所有线性数据结构的知识,比如表,栈,队列等(广义表可以不作要求)
2. 二叉树的基本操作和基本使用
3. 图中需要知道遍历和了解最短路径算法,以及相关的一些概念
当然对于某些程序员来说,这是不够的,因为从事的具体的软件开发工作会有不同的要求。但是对于大部分从事MIS软件开发的程序员来说,这些知识够了。掌握这些知识可以有两个层面的要求。第一个是完成足够的习题,从而可以熟练的答题,第二个是能够在实际工作中使用数据结构描述实际的事物。做到这两点要求应该说不算太高,注意多加练习就可以了。目前来说这门课程的经典教材也不少,相信只要按部就班的学习完就是合格的了。
编译原理这门课程主要是学习方法和思想而不是课程中的知识本身。因为毕业出来能从事编译器开发的人实在是太少太少了。这门课程需要掌握了解的东西不多,我个人的观点主要是以下几个:
1. 确定有限自动机和非确定有限自动机的使用
2. 词法分析程序的实现
3. 语法分析的方法
自动机在实际应用中的体现就相当于是状态转换图,这个工具非常的重要,希望能够务必掌握。我们在开发EntityModelStudio时,设计界面交互部分的内容就是先设计出状态转换图然后再写代码的,否则直接开发的话就会面临开发失去控制的风险,同时重构和维护也会相当麻烦。所以这个工具极其强大,非常实用。另外提一下,非确定有限自动机,这个工具的能力和确定的有限自动机是等价的。但是由于它的不确定性,更符合人的自然思维习惯,从而在某些设计场合相对会方便很多。这一点是很实用的,也是很吸引人的。
掌握词法分析程序的实现,可以大幅度拓展开发能力和思考能力。这部分东西理论上描述可能比较麻烦,但是实际使用时还是很容易上手的,所以非常值得学习一下。语法分析程序不需要掌握了,毕竟开发编译器的机会是微乎其微的。但是相关的方法和思想希望能够了解,这可以帮助程序员用电脑的思维来思考问题。
操作系统需要掌握的东西只有两个:
1. 五大管理的基本方法,尤其是涉及内存管理的策略
2. 线程或者进程的同步技术
操作系统是复杂的,但是教材中介绍的这些管理方法相对来说是简单易懂很多了。这一难一简之间体现了基本知识的重要性,基本知识在实际开发中的应用的广泛性。好好的体会,就可以明白用简单方法解决复杂问题的技巧。线程进程的同步,这个就不用多说了,大家都知道它的作用,如果实在不想掌握的话那我也非常愿意相信你的理由一定是充分的,否则你绝对不会那么做。
最后我想强调的是,无论你如何看待这些知识:可能觉的没用,可能觉的太难,可能是不感兴趣,但是如果你想做程序员的话,那么请你务必最大可能牢固,最大可能熟练的掌握它。
**二.编程语言**
对于一个程序员来说,一般需要掌握2,3门语言是基本的,并且学习一门新的编程语言也是基本功级别的能力,所以这部分主要谈谈快速学习一门新的编程语言的方法。我学过的语言有这些(这里编译器和语言的概念等同了并且不按先后次序):Foxbase,C,C++,汇编,Visual C++,Delphi,FoxPro,VB,C#。就我个人的体会来说,这些语言可以分为三种类别:非面向对象的,面向对象以及支持可视化设计的。
这三种类别的语言有一些共同的内容,而这些内容也是我们在学习一门新的编程语言时首先需要知道的,可以说是关键的知识点。这些内容大致如下:
**1.常量,变量,数组,不同的数据类型**
这部分需要掌握常量,变量,数组的定义,初始化,不同数据类型的使用。数组中元素的读写,作为参数如何定义,作为返回值如何定义。有些语言还支持数组大小的重新定义。
**2.函数(或者叫子程序)**
函数如何定义(比如参数和返回值),如何调用(这里存在异步调用和同步调用的问题),全局的还是非全局的。
**3.流程控制**
分支结构:if语句,if else语句,switch语句;循环结构:for语句,while语句,do…while语句,有些语言可能是Loop。
**4.最基本的输入输出和文件操作**
最基本的输入输出语句可以帮助你在学习语言的过程中完成简单程序的练习任务,比如:输出到控制台,dos操作系统中输出到屏幕等等。文件操作也要知道,至少以后写个程序生成日志文件就会了。
以上内容在学习一门新的编程语言时,希望能首先掌握,这能让你很快的入门,并尽快使用新语言写出代码。另外还可以关注一下其他方面的内容,比如:
**1.了解语言的新特性**
这个阶段只需要了解,不需要掌握,记住有这些新特性,在需要用的时候想起它们就可以了。
**2.了解一下帮助文档中,该语言的所有关键字**
这部分内容有可能让你发现一些很有用的东西。****
好了,知道这些内容差不多一门新的语言就算入门了。当然还有其他很多东西,但是这些内容可以在具体开发中遇到时再去找例子就可以了。下面谈谈这些语言的差异。对于面向对象的语言来说,需要知道面向对象三大特征:封装,继承,多态在具体的一门编程语言中是如何表达的或者等价表达的。对于支持可视化设计的语言来说,还需要知道如何设计窗体,以及常用控件的使用。按照这个方法,从一门已经会的编程语言到学习另一门新的编程语言应该是比较快的。对于还在大学中学习的人来说,我的建议是C++或者Pascal中的一个,VB或者C#中的一个或者其它可视化开发语言中的一个学习一下。如果可能学习一下汇编是最好的。
**三.SDK**
掌握一个SDK才能使程序员在掌握一门语言的基础上进行实际的开发,如果仅仅是一门语言那是不够的。所谓SDK举例子来说就是Foxbase的命令和函数,C的库函数,C++的类库(比如微软的MFC),Windows的API,.NetFramework,这些都是我所说的SDK。程序员可以根据自己的实际开发需要,有选择的学习相关的内容。我的建议是,可以先google,然后查文档,一般的问题都可以很快解决的,慢慢的也就逐步掌握了。比如说我不知道C#如何使用线程,那么我就用google查找,关键词是“C# 线程”,然后从结果中找到需要的内容。很多时候结果中的代码是可以直接使用的。然后再去看一下MSDN的帮助文档,了解一下相关的类和方法的说明,这样这部分的知识就可以认为是掌握了。下次使用时就知道怎么用了。我的C#就是这么入门的,大概google问了二三十个问题左右。
另外一个建议是买一本书学习也是可以考虑的,这也是一个不错的方法,只是买到好的书需要缘分。就我个人来说,绝大部分的情况下是看电子书,直接从网上下载的。
**四.开发工具**
除非你只用独立的文本编辑器写代码,并且用命令行编译,否则你一定需要一个开发工具,尤其是一个带IDE的开发工具。对于你使用的开发工具而言,需要了解的基本内容如下:
1. 项目或者工程的创建,属性修改,打开关闭等基本操作
2. 具体开发时的环境设置
3. 项目中的文件组织及管理
4. 常用功能的使用,比如:编译,执行,断点设置,代码跟踪,调试信息输出,实用的快捷键,调试时变量查看,查找/替换等等
5. 从帮助文档中了解IDE的新功能。因为这些功能有可能对你是非常有帮助的。
6. 帮助文档的获取
如果有自己的使用习惯的话,还可以了解一下如何定制IDE环境以满足自己的开发习惯。首先了解这些内容可以帮助你相对快一点适应一个新的IDE。
**五.领域知识**
一个从事技术工作的程序员需要了解与技术不相干的领域知识,确实有点无奈。但是在具体的开发中,不了解这些知识就无法更好的理解用户的需求,也无法更好的完成开发任务以及与同事领导的沟通。所以这个步骤是重要的必要的,有时候有可能还会带来更严重的后果。在有些项目中如果不能很好的了解这些领域知识,项目中的成员有可能会被替换掉,我个人就有过这样的经历。所以这里特别列出来强调一下。
差不多这些知识应该够用了,下面再提几个额外的内容,这几点虽然和开发不是太直接相关,但是确实也很重要。它们是英语,数学,读源代码和读书,有余力的程序员可以尽量提高这几方面的水平,这是很有用的学习途径和方法。对于英语而言主要是读和写,这样就可以阅读英文资料并用邮件,论坛或者聊天工具和老外沟通。由此获得的帮助是非常显著而高效的。这里要说明一下,微软论坛上的回复的质量非常之高。
对于数学我的理解主要是三个部分,都是很具体的:
**1.中学里学过的知识**
这部分知识很重要,这是我们用简单方法解决复杂问题的基础,同时使用的几率也非常高。如果全部忘记的话,建议多少复习一下,或者用到的时候回顾一下。****
**2.离散数学**
我需要承认在开发中直接使用离散数学知识的场合我一次都没有遇到,但是如果没有离散数学的知识,那么我就无法思考,很多问题就无法解决。
**3.组合数学**
这门课程属于研究生级别了,相对难度会大一些。我的观点是你不需要全部掌握,知道一部分就可以了,比如:鸽巢原理,母函数,以及常用的计数方法和技巧。尤其是技术方法这部分在问题的分析简化,工作量的评估,算法设计以及软件测试方面都有非常实用和具体的应用价值,是很值得掌握的。是否可以使用这部分知识,在实际工作中表现出来的效果至少相差一个等级。
一个好的源代码具有不可估量的价值,潜心学习一下可以让你从一个门外汉变成一个开发老手,所以注重培养从读源代码学习编程知识的能力。我的体会是,阅读源代是一个非常有效(有用并且高效率)的方法来提高自己的开发水平或者解决实际问题的能力。我第一次认真学习的源代码来自当初的程序员大本营。一个例子是实现Visual Studio 6.0中Workspace的界面,另一个是如何实现给主菜单加入图标。两个例子大概花了我一个半月的时间并且写了几篇心得,记录下学习的内容。应该说收获很大。再比如,下一个版本的EntityModelStudio中会加入代码编辑器,这个支持语法高亮和行号的编辑器就是在读懂开源代码后我们自己独立重新开发的。在阅读源代码的时候希望能注意两点:
1. 最好能配置好环境可以单步跟踪代码,这样理解代码的速度和效果会好很多。
2. 快速的定位那些自己想看的代码。这里建议可以使用IDE提供的查找功能,看文件名,类名等方式来定位。如果实在不行,考虑注释代码,来快速定位。
第四个内容是读书,阅读是学习的一个最基本和最重要的途径。在这里我不想列出任何需要阅读的书目,这是因为当下流行的所谓经典或者著名的开发书籍我读得很少,所以也说不出体会。我看过的书都比较老了,比如:
1. BorlandC++4.5使用及开发指南
这是我的C++的教材,C++部分先后看了不下6次
2. 一本1970年发行的软件工程的书,这是我第一次接触软件工程
3. 代码大全第一版,我觉得第二版没有第一版好
4. 用于面向对象的设计和分析方法,这是美国哥伦比亚大学的一个教授写的。是清华大学原版教材中的一本,非常好,是OOD的绝好教材。
目前有印象的就这些,以后想到了再补充吧。其他读过的书还有很多,都是具体的编码的书就不再一一列举了。有些书需要仔细阅读的,比如讲设计,讲方法的书,有些书需要很快的浏览完,比如讲具体编程的书。我的体会是,一本几百页的书,你应该花1,2小时就能过一遍,最好是20分钟到40分钟就能过完。在实际开发中,用到的时候再看书,查找需要的内容。如果你需要花很长一段时间全部学完一本书的话,那么你看的第一本书可以这样,否则我觉得你的学习方法就有问题了。至少一本书中不可能所有的东西都是你马上要用到的,你没有必要立刻学习,所以应该学会快速阅读的技巧。当然这是个人观点,取舍对错自行判断吧。
你不能寄希望于一次就能买到一本理想的书,也不能希望在一本书中学到自己需要的所有内容。遇到一本好书是需要点运气和缘分的。我的总体感觉是,外国知名出版社的图书的质量明显好一些,还有台湾一些出版社的图书也还不错。建议大家可以买一些绝对知名和权威的书籍,这样相对风险会小一些。对于那些书名为XXX大全,XXX宝典,精通XXX,XXX权威这样的书,我是很不看好的,当然这是自己的看法,仅供参考。
最后说一下不要学习的东西,这是在本文发布前刚发现的问题。几天前在群里聊天,一个人说想解析暗黑的通信协议,然后做外挂。我对这方面很不在行,但是这明显是一个非常耗费时间,难度也非常大的事情。我在这里给出的建议是,一个职业的程序员需要知道自己的价值,自己的知识和精力应该花在能够创造实际价值的地方。不要仅仅出于爱好或者热情去做一些成本很大的事情,与其炫耀自己的能力,不如踏实的做好本职工作。如果实在想做可以作为业余爱好,适可而止。
好了这次就写到这里了,感觉还是有点仓促。再次声明以上内容都是一些个人的看法,限于本人的经历和知识面,不妥或疏漏之处在所难免,希望同行们能指出来,让我也提高一下。下一篇的题目暂定为“问题解决篇”,主要介绍如何在工作中使用这些知识解决实际问题的方法和心得。
前言
最后更新于:2022-04-01 16:15:12
> 原文出处:[十八年开发经验分享](http://blog.csdn.net/column/details/binarytreeex.html)
作者:[binarytreeex](http://blog.csdn.net/binarytreeex)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# 十八年开发经验分享
> 回顾自己十八年的开发经历,分享十八年开发经验,与同行共同进步