在读这篇博文的时候,觉得一些观点很有道理,因此做笔记于此,以便以后参考。
英文版:http://stackoverflow.com/questions/63668/confessions-of-your-worst-wtf-moment
中文翻译:http://coolshell.cn/articles/3980.html
在StakeOverflow上有这样一个贴子叫“Confessions of your worst WTF moment”(WTF就是What the fuck的缩写),挺有意思的,我摘几个小故事过来,希望大家在笑过之后能从中学到什么——所有的经验都是从错误中来的(我在其中加了一些点评)
我们公司的软件是给警察局用的,那是一个对用来处理被逮捕的人的系统,此系统还需要收集脸部特征和指纹信息,并且,这个系 统和会向FBI的系统提交这些信息。当我们在测试这个系统的时候,我们一般都是用我们自己的指纹,当然,数据库联着的是我们的测试数据库。不过,有一次, 在我们测试完后,我们忘了把系统切换回生产库,于是我们的测试数据库就联上了生产环境,于是我们的指纹信息和照片就散布到了其它系统中……清除我们警察局 这边的还好办,但是,你需要波士顿警察局警司去法院签字才能从FBI的数据库中清除我们的信息。
点评:测试环境和生产环境的数据不要混在一起。
有一次,我需要向新系统中导入一堆数据,因为数据量太大,需要5个小时,只能在夜里来干,在系统需要正式使用前2个小时, 数据导完了,此时是凌晨4点。随后,我需要删除一些数据,于是我在SQL命令地上输入了“DELETE from important_table; where id=4”。是的,我没有看到哪里还有个分号,天啊。
点评:这就是加班工作的恶果。另,在delete之前最好先做一次select。
我把我的管理员口令提交到了一个开源软件的源码里。
点评:1)版本管理器里的东西是删不掉的。2)一些用户和口令要hard code在代码里,所以,不要混用代码使用的权限和管理员的权限,小心管理程序的运行权限,为其注册专门的用户。
我为一个很大的银行开发软件,在我的代码里,我为一段理论上根本不可能执行到的代码加了一个报错信息。有一天,不可思异的 事发生了,这条报错信息显示在了该银行的1800个分行的超过10000个终端上——“如果你看到这个信息,说明整个系统被Fuck了,回家吧,祝你过得 愉快!”
点评:“假设是恶魔”,Assume意为Ass – u – me,意为——搞砸你和我。对于一些关键东西,永远不要做假设。小心你言语中的——“可能、应该、觉得、不应该”等词语,程序可不认这些东西。
我远程登录到服务器上加几个防火墙规则。第一件我想干的事是在不允许任何人的任何连接,第二件是,为某个端口打开访问权限。不过,我在做完第一件事后就把配置保存了,结果其生效了……
点评:这样的事经常发生,做远程网络管理的人多少会有那么几次发生这样的错误。在你将你的网络配置生效前,你得想一想,断线了你是否还能登得上去。改配置不要太冲动,生效前检查几次。
我们的代码中有一个模块完美地工作了很多年了,只是代码太乱了。我说服了我的老板,我可以重写这个模块,于是我花了三个星 期来重写这个模块。今天 ,我还记得,我的老板站在我的后面看着我,而我在在流着斗大的法汗珠去fix被我重写的“超级漂亮”的那个模块中一个接一个的bug。从那以后,我再也不 重写代码了,除非有重大的利益。
点评:这就所谓的屠宰式编程。这个案例告诉我们两个道理,1)维护代码要用最最最保守的方法来进行。2)重构代码前要像一个商人一样学会计算利益。当然,ThoughtWorks的咨询师一定会告诉你TDD,结对,极限等等方法告诉你如果实践重构。但我想告诉你,一个程序在生产环境里运行好几个年能没有问题是一件很不容易的事,那怕其中的代码再烂,你再看不过去,你都要有一个清醒的头脑明白这几点,1)软件的运行质量是远远大于代码质量的,2)你的测试案例是远远小于生产环境的,3)软件的完美的质量,是靠长时间的运行、测试和错误堆出来的,而不是某种方法论。
————————————————
相信大家做程序员这一生中也有很多发生在自己身上的悲催的事儿,欢迎分享。我先分享几个我亲身经历过的事。
一个发生在我的领导身上。
我98年刚参加工作的时候,在某单位网络部门,一次,我们整个部门去给下属单位培训Cisco路由器,结果我们发现带去培 训地点的设备少带了集线器HUB,设备连不起来。于是领导很不高兴,质问我们为什么没有带集线器?那几个对领导平时就不满的老员工说办公室里没有集线器 了,都借给别的部门了。领导想了想,问我:“陈皓,我记得上次我给过你个集线器”,我说,“好像没有吧,我记不起来了,什么牌的?几口的?”,领导说: “什么牌子想不起来了,不过我记得那个集线器是一个口的”。“一个口的?!”,我心里嘀咕着,“真敢说啊”。但我不敢接话了。那几个老员工来劲了——“哪有一个口的HUB啊,一个口的怎么联两台电脑啊?”,领导说:“用两个一个口的不就行了”。领导这话一出,全场一片寂静,无言以对……
后来:我们所有的组员都离开了我们的这个领导,我们的这个领导今天还在那里工作。我想告诉大家,很多时候该走的是领导(包括外企,我上一东家正在裁人,不过我觉得该被裁掉的应该是那些经理)。我们的领导经常出这样或那样的笑话,这让我随时随地地警醒自己——“不要当一个被人笑话的经理”,于是,今天我还在努力地学习技术。
另一个发生在我身上
刚刚接触Linux的时候,还不是很懂,那时的PC还只有奔3,编译公司的程序好慢啊,有时候为了调查一个问题,需要不断 地打log,来来回回地编译,很不爽。直到有一天,硬盘不够了,df一下,发现/dev/shm还有空间。于是,把全部程序copy了过去,发现编译起程 序超快无比,爽得不行。于是就把工作环境放在/dev/shm下了,连开发都放在这里了。这一天,开发一个功能,改了十来个文件,加班很晚,觉得基本搞 定,大喜,回家睡觉。第二天一来,发现/dev/shm下空了,一个文件都没有了,问同事,同事不知,同事还安慰我说,上次他的文件也不知道被 谁删了,于是我大怒,告老板!老板也怒,发邮件到整个公司质问大家谁删了陈皓的程序,无人应答。IT部门答,“昨晚唯一的操作就是重启了linux服务 器,什么也没干,不过我们天天备份服务器,可以恢复”,IT部门问我丢的文件在哪个目录下?于是,我reply to all – “在/dev/shm下……”,哎,人丢大发了……
后来:我很感谢我以前犯的这个错,从那天以后,我开始立志学好Linux,这个错误让我努力,让我发奋。所以,我想告诉大家——尤其是刚出道的程序员,你们要多多犯错,要犯错那种丢死人的错,这样你才会知耻而勇。
再来一个发生在我同事身上的
01年,我们开发银行系统,在AIX上开发,RICS6000很贵,只能在客户那里开发,开发进度很紧张,慢慢地硬盘就不 够用了,系统中有大量的垃圾文件,于是需要清除一些文件,于是有一个同事写了一个脚本,可以自动清除的各种不重要的文件,里面有一条命令大致是这个样子“ rm -rf ${app_log_dir}/*”,意为清除程序运行的日志。为了使用这个脚本,需要在root用户下运行,一开始还不错。直到有一天,某人一运行,整 个根就没了。搞得整个团队只能用一周前的备份重写已写好的代码。后来,才发现原因是${app_log_dir}变量为空,于是成了“rm -rf /*”……
后来:这个事后,我的那个同事,把rm命令改了名,并自己写了一个rm命令,把删除的文件先放到一个临时目录下。而我也因为这个事情,到今天,每次当我在root目录下使用rm时,敲击回车的手都是抖的。(另,rm时永远使用绝对路径)这里,我想告诉大家——犯错不可怕,可怕的是不会从中总结教训,同一个错犯两次。
欢迎分享发生在你身上那些悲催的事。
看看这些在线的演示,真的很酷
http://hakim.se/experiments/html5/trail/03/
http://arapehlivanian.com/wp-content/uploads/2007/02/canvas.html
http://www.benjoffe.com/code/demos/canvascape/
http://hakim.se/experiments/html5/sinuous/01/
http://hakim.se/experiments/html5/core/01/
字符串在C语言中以\0结束,字符串相关的函数定义在<string.h>中,都是以str开头的函数,操作的字符串必须有结束符。
strlen字符串长度
strcpy复制一个字符串到另一个字符串
strcmp比较两个字符串
stricmp比较两个字符串,不区别大小写
strchr在字符串中查找一个字符,如果有在返回第一个出现为止的指针
strstr查找子字符串
strcat将一个字符串附加到另一个字符串后面。
不过有时候为了操作没有结束符的字符串,也是用操作内存的函数来处理字符串:
memset设置一段内存中所有字节为某个值,常用语初始化结构体。
memcpy复制一段内存到另一个短内存
memcmp比较两个端内存
解析下面HTTP请求的各个部分,将值填入结构体:
char request[] = "POST /Login.aspx HTTP/1.1\r\nHost: test.baidu.com\r\nContent-Length: 39\r\n\r\nusername=cs&password=123456&login=login";
#define MAX_PARAMS_COUNT 8
struct Params {
char Name[20]; // 参数名字
char Value[30]; // 参数值
};
struct Request {
char Method[10]; // 请求 方法,对上述字符串应为:POST
char Path[512]; // 请求路径,应为:/Login.aspx
char Host [64]; // 服务器,应为:test.baidu.com
int ContentLength; // 39
int ParamCount; // 3
struct Params Param[MAX_PARAMS_COUNT];
};
C语言输入输出系列函数讲解:printf函数用于将数据按一定格式打印到屏幕上,它的第一个参数是一个字符串,用于设置打印的格式,其余参数是要打印的内容。例如:char girl[] = "Jane";printf("Hi, %s!", girl); // 该行输出:Hi, Jane!int a = 1, b = 2;printf("%d + %d = %d", a, b, a+b); // 改行输出:1 + 2 = 3你已经看到格式化字符串中有一些以百分号%, 它用于指定要输出数据的格式,每一个%对应一个输出数据,因此需要在printf函数中带有相应的数据做参数。常用的输出格式有:%d 十进制整数%x 十六进制整数%f 浮点数%s 字符串另外可能还要输出一些特殊的字符,例如回车、换行、制表符,他们分别用\r\n\t表示。如果要输出百分号,则需要使用%%scanf函数用于从键盘获取输入,同printf一样,它的第一个参数表示输入的格式,后面的参数为要输入数据的存放位置(数据的指针)。int a, b, r;char c;char girl[16];scanf("请输入一个表达式:%d%c%d=", &a, &c, &b); // 这时候你用键盘输入:1+2=并回车,则a的值为1,b的值为2,c的值为'c'。可以看出scanf的参数和printf的不同之处在于它是数据的地址。scanf("请输入你的名字:%s", girl); // 你输入:Jane并回车,则girl中存储字符串Jane,包括字符串结束符\0。注意,字符串参数的格式是和printf相同的,都是指针。switch(c){case '+': r = a + b; break;case '-': r = a - b; break;case '*': r = a * b; break;case '/': r = a / b; break;default: printf("不识别的运算符\n"); break;}printf("%d %c %d = %d\n", a, c, b, r);sprinft函数和sscanf函数和上面两个函数类似,只是输入和输出不是屏幕和键盘,而是内存中的字符串。例如:char str[256];int a=1, b=2;sprintf(str, "%d-%d=%d", a, b, a+b); // 则str中存放的字符串为:1-2=3char *str2 = "0x521";sscanf(str2, "0x%x", &a); // 则a的值变成0x521总结
输入输出函数,是用printf打印格式化字符串到屏幕,是用scanf从键盘读取格式化字符串并转换成特定格式(例如整数)。两个函数的第一个参数都是格式字符串,后面可以跟多个参数,printf的参数需要是数据的值,而scanf的参数则是数据的地址(指针)。不过对于字符串,两个函数都是传递指针。
sprintf将结果输出到一个字符数组中,而sscanf则从字符串中获取输入。
作业
课后作业,根据上两节讲述的内容,写一个计算器程序,用户输入表达式,计算表达式的结果。
输入:12+23=
输出:35
输入:12 + 23
输出:35
输入:a+b=
输出:错误的表达式
输入:1+2+3=
输出:6
输入:1+2*3=
输出:9
由上面的输入输出可以看出,程序中需要识别数字(都是正整数)和运算符,等号是可选的,空格要过滤掉,不对的表达式要报错,由前到后进行计算,不要求判断运算的优先级。
提示:定义两个缓冲区,一个用于接收用户的输入(char expr[]),以用于保存工作中的临时状态(char temp[])。接收用户的一行表达式输入,从前到后扫描输入中的每一个字符,将数字存储temp中,直到遇到一个运算符或者表达式结束,通过sscanf将temp中的字符串转变成数字,然后继续扫描下一个数字,根据不同的运算符进行不同的计算,将中间结果保存。如果遇到等号或者表达式结束,则计算最后一个运算的值,打印输出。例如针对输入1 + 20*3=,扫描过程为:
char *p = expr; 当前字符为数字,存入temp中,继续下一个字符
当前为空格,跳过
当前为加号,转变第一个数字,得到值1,前面没有运算符,当前运算为+,继续
空格跳过
数字存入temp
数字存入temp
*运算,转变temp为整数得到20,前一个运算符为+,进行运算得到中间结果21,当前运算为*,继续
数字存入temp
=号,从temp获得整数3,前一个运算为*,计算中间结果为63,当前运算为=,打印结果63,退出
C语言的文件操作需要包含头文件<stdio.h>,相关函数通常以f开头。打开的文件在内存中有一个结构体,叫FILE。fopen函数用来打开文件,他返回FILE类型的指针。
FILE * f = fopen("c:\\a.txt", "r");if (f == NULL) printf("文件打开失败");fopen的第一个参数为文件的路径,其中斜杠需要两个代表一个。第二个参数为打开文件的模式,常用的模式有:
r 只读
w 只写,如果文件已存在,内容会被删除
a 追加内容到文件末尾
r+ 读写
如果需要打开的文件是二进制文件,则需要在上面的模式后面添加一个b字符,例如以读写模式打开一个二进制文件:
if (!(f = fopen("c:/a.bin", "rb+")) {exit(0);}操作文本模式打开的文件采用函数fprintf和fscanf,除了多了文件指针作为第一个参数以外,其使用方式与printf和scanf相同。
fwrite用来写二进制文件,
size_t fwrite( const void *buffer, size_t size, size_t count, FILE *stream );第一个参数为要写入文件的内存地址,第二个为一个数据块的大小,第三个是数据块的个数,第四个参数为文件指针。通常将结构体写入二进制文件,例如:struct Record {char name[16];int age;};struct Record record[8];int i;memset(record, 0, sizeof (record)); // 注意sizeof关键字取得是字节大小for(i=0;i<sizeof(record)/sizeof(Record);i++){sprintf(record[i].name, "I'm record %d", i);record[i].age = i * 4; }if (!(f = fopen("c:/a.bin", "wb")) {exit(0);}fwrite(record, sizeof(Record), 8, f);fclose(f);最后不要忘了用fclose函数关闭打开的文件。通过fread函数将二进制内容读入内存,例如:struct Record {char name[16];int age;};struct Record record[8];if (!(f = fopen("c:/a.bin", "rb")) {exit(0);}fread(record, sizeof(Record), 8, f);fclose(f);和文件相关的函数还有:fseek用于设置当前读取位置,ftell用于获取当前读取位置。总结
文件分成文本文件(例如.txt,.c,.h等)和二进制文件(例如.rmvb,.exe,.zip等),文件打开方式有:只读(r或者rb)、只写(w或者wb)、追加(a或者ab)、读写(r+或者rb+),其中带b的表示是二进制文件。
fopen打开文件后返回文件指针FILE*,其后每个文件的操作都需要带着这个指针,注意有些函数将这个指针作为第一个参数,而另一些则将它作为最后一个参数。文件读写位置只有一个变量(相对于文件开头的偏移量),例如当你以rb+打开一个二进制文件,然后fread进内存32字节,则文件位置编程32,然后再写进去32字节,则写进去的内容被写到文件中偏移32字节处,文件读写位置编程64,用ftell获取当前读写位置,用fseek设置当前读写位置。
对于文本文件,可以用fprintf和fscanf进行格式化输入输出。fprintf需要手动添加换行符"\n"在文件中写入换行。scanf系列函数能够自动过滤空白字符(空格、\t\r\n等)(影响中是这样)。
最后,不要忘记用fclose关闭文件。
作业
维护一个用户名密码文件,该文件的格式为:每一行存储一个用户名密码对,用户名和密码之间用冒号分隔。用户通过命令行输入命令,要求支持的命令如下:
- load 文件路径
- add 用户名 密码
- get 用户名
- remove 用户名
- save 文件路径
第一个命令加载一个存在的密码文件,该文件是一个文本文件。第二个命令向该密码文件中添加一行记录。第三个命令获取一个用户的密码。第四个为删除一个用户的哪一行。第五个是保存并关闭密码文件。
要求:
- 用户名只能由大小写字母数字和下划线组成,第一个字符不能为数字。
- 用户名不能重复。
- 密码不能少于6个字节。
提示:
- load的时候使用r打开文件,这个文件必须存在,将所有记录读入内存并关闭文件。
- fscanf(fp, "%s:%s", username, password);来读取一行记录。
- 保存的时候使用w打开文件,这个文件可以不存在,如果存在会被自动清空。
- 使用fprintf(fp, "%s:%s", username, password);保存一行记录。
假设最大支持256个用户,每个用户的用户名密码都最大为15个字节。可以定义:
#define MAX_USER_NUMBER 256
#define MAX_STRING_LENGTH 16
struct User {
char Name[MAX_STRING_LENGTH ];
char Password[MAX_STRING_LENGTH ];
};
struct User users[MAX_USER_NUMBER ];
int userNumber=0;
然后将所有存在的用户读入数组users中。
本系列会包含一些C语言的基本知识,主要介绍各种标准库的使用。假设你有一定的C语言基础,因此很少讲解最基本的语法知识。
本系列计划讲解内容如下:
ObjectQuery支持HQL的各种Join操作,例如下列的HQL:
from Eg.Cat as cat
inner join cat.Mate as mate
left outer join cat.Kittens as kitten
from Eg.Cat as cat left join cat.Mate.Kittens as kittens
from Formula form full join form.Parameter param用ObjectQuery可以这样写:
| Query.From(DB.Cat |
| .InnerJoin(DB.Mate, DB.Cat.Mate) |
| .LeftOuterJoin(DB.Kitten, DB.Cat.Kittens)) |
| Query.From(DB.Cat.LeftJoin(DB.Kitten, DB.Cat.Mate.Kittens)) |
| Query.From(DB.Formula.FullJoin(DB.Parameter, DB.Formula.Parameter)) |
支持的Joins类型有:
inner join
left outer join
right outer join
full join (not usually useful)
From 语句是最简单的对象查询:
Query.From(DB.Cat);
这将生成HQL语句:
from Cat as _Cat_
其中_Cat_为默认的alias。Criteria类中的每一个Criteria内部类都有一个没有参数的构造函数和一个带有字符串参数的构造函数,当你构造它的一个实例的时候,如果给它一个字符串参数,它将以这个字符串为alias。否则将采用默认的alias,为对应实体类的类名两端各加一个下划线,例如CatCriteria的默认alias为_Cat_。
DB中定义Criteria内部类的采用没有参数的构造函数,它们使用默认的 alias,你可以自定Criteria的实例:
Criteria.CatCriteria cat = new Criteria.CatCriteria("cat");
Query.From(cat);
这将产生HQL语句:
from Cat as cat
From语句可以接受多个类的Criteria,这将产生一个笛卡尔交叉连接:
Query.From(DB.Formula, DB.Parameter);
Criteria.FormulaCriteria form = new Criteria.FormulaCriteria();
Criteria.ParameterCriteria param = new Criteria.ParameterCriteria();
Query.From(form, param);
采用DB中与定义的Criteria有时候比较方便,但是在写查询条件的时候可能显得比较繁琐。
下一部分 ObjectQuery for Castle.ActiveRecord 使用指南(1):Joins