一、地址
GitHub地址:
本人学号:201731062329
结对编程伙伴作业地址:
作业地址:
二、结对过程
我和我的搭档。选定了两方都有空的时间出来讨论,现制定了PSP表,然后根据各自水平,分配任务。各自的任务完成过后,先自审,再交由对方复审,然后汇总,封装成dll文件,进行单元测试和效能分析,并且改进代码,最后一起撰写博客。这是我们结对编程过程中的某一照片
三、PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
Planning | 计划 | 880 | 1395 |
· Estimate | · 估计这个任务需要多少时间 | 880 | 1395 |
Development | 开发 | 760 | 1270 |
· Analysis | · 需求分析 (包括学习新技术) | 40 | 50 |
· Design Spec | · 生成设计文档 | 30 | 35 |
· Design Review | · 设计复审 (和同事审核设计文档) | 50 | 60 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 20 | 15 |
· Design | · 具体设计 | 30 | 50 |
· Coding | · 具体编码 | 400 | 900 |
· Code Review | · 代码复审 | 60 | 40 |
· Test | · 测试(自我测试,修改代码,提交修改) | 100 | 120 |
Reporting | 报告 | 120 | 125 |
· Test Report | · 测试报告 | 60 | 60 |
· Size Measurement | · 计算工作量 | 30 | 40 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 30 | 25 |
| 合计 | 880 | 1395 |
四、解题思路
刚开始看到题目有点懵,感觉无从下手。于是就和自己结对编程的伙伴讨论了一下,确实是自己被题给“吓”到了,静下心来思考了一下,其实就是读写文件,将每一行读取的文件用字符串的Split()方法将其分割成字符串数组,对字符串数组中的每个元素进行讨论即可。例如统计字符总数,其实就是读取文件每一行数据,直接用str.Length,即可获取其一部分长度,无论是制表符还是空格,都能够准确得到其长度,唯一要注意的就是,回车符,这个也可以通过其文件的行数通过逻辑判断得出,最后汇总即可得到文件中字符的总数。详细的请看下面代码说明部分。在结对过程中找资料主要是上网查资料,以及通过自己以前的《面向对象程序设计》获取一些文件操作信息。
五、设计实现过程
此次作业设计,由于编程经验不足,没有将功能设计规划的很好。对于此次作业,只有一个FileOperate类,在该类中将作业要求的几个点都封装在一个方法中,即一个方法对应一个功能。各个方法之间的关系除了判断一个字符串是否为一个单词的IsWords()方法会被其它方法调用,方法之间都是相互独立的。获取文件中字符总数的charNumber()方法、统计文件中单词总数的wordNumber()方法、得到文件行数的lineNumber()方法思路并不是很复杂,在此展示判断一字符串是否是一个单词的isWords(string word)方法、获取文件中的频率最高的n个词的wordTimes(int n)方法、获取文件中指定词组的wordGroup(int len)方法的流程图:
由于双重循环在流程图中太复杂,能力有限将在后面代码说明处将对wordTimes(int n)方法、wordGroup(int len)方法进行详细的说明,实在抱歉。
针对于单元设计,由于设计的方法都有返回值,所以在生成的单元测试环境中,都是创建一个FileOperate的对象,然后通过调用相应的方法,然后使用断言判断进行单元测试。
六、代码规范等
结对讨论出的代码规范如下:
1.命名规则:使用驼峰命名法。给类或函数或字段命名,使用具有相应中文意思的英文单词;
2.分行:不把多条语句放在一行上,不把多个变量定义在一行上;
3.断行与空白的{}行:”{“、”}”单独在一行;
4.注释:在类或方法的上面使用文档注释,在方法中使用普通注释;
针对于FileOperate类中的charNumber()方法、lineNumber()方法、wordNumber()方法、isWords()方法、wordTimes()方法、wordGroup方法,搭档之间将本人负责后面四个部分,在我们完成相应的方法之后,自己先测试,通过以后,再相互审查,求同存异。在这其中发现了很多问题,令我印象深刻的一个就是,搭档提醒我,英语的语法特性,“,”、“.”、“!”前面如果有单词,这个字符串也应该算作一个单词,这个问题直到现在在脑海里还非常的清晰。
七、改进程序
刚开始的main函数比较臃肿,通过vs的性能分析工具发现,main函数消耗最大。通过观察发现,由于一些必要对命令行参数的操作,里面循环很多。在改进过程中时间大概花了10分钟,原来在刚开始的对"-o"、"-i"、"-help"命令的遍历时,如果找到了执行相应操作就可以终止了,但是实际上循环会有不必要的消耗,于是我就对循环添加了break关键字,以减少了不必要的消耗,使消耗略微的减少了。第二大消耗是我使用了linq语句用于字典排序,但这个,我并不是很熟悉其内部的消耗,所以就没有改进。
消耗最多的函数
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
1 static void Main(String[] args) 2 { 3 //StreamWriter writeFile = new StreamWriter("out.txt", false);//false表示将文件覆盖,而不是追加 4 //FileOperate fileOperate = new FileOperate("input.txt", writeFile); 5 //初始化 6 string[] arg = new string[] { "-i", "input.txt","-o","out.txt","-n","3","-m","2" };//测试代码 7 StreamWriter writeFile = null; 8 FileOperate fileOperate = null; 9 //创建读文件对象 10 for (int i = 0; i < arg.Length; i++) 11 { 12 if(arg[i] == "-help" || arg[i] == "help")//可以直接使用help命令 13 { 14 Console.WriteLine("https://www.cnblogs.com/haveadate/ [version 3.0]"); 15 Console.WriteLine("(c) 2019 haveadate 保留所有权利。\n"); 16 Console.WriteLine("-i 参数设定读入的文件路径 (必要) 格式:-i [file]"); 17 Console.WriteLine("-o 参数设定生成文件的存储路径 (必要) 格式:-o [file]"); 18 Console.WriteLine("-m 参数设定统计的词组长度 (非必要) 格式:-m [number]"); 19 Console.WriteLine("-n 参数设定输出的单词数量 (非必要) 格式:-n [number]"); 20 Console.WriteLine("注: 参数顺序不对结果产生影响"); 21 return; 22 } 23 if(arg[i] == "-i") 24 { 25 fileOperate = new FileOperate(arg[i + 1]); 26 break;//减少不必要的运行 27 } 28 } 29 //创建写文件对象 30 for (int i = 0; i < arg.Length; i++) 31 { 32 if (arg[i] == "-o") 33 { 34 writeFile = new StreamWriter(arg[i + 1], false); 35 //三个必须输出 36 writeFile.WriteLine("characters: " + fileOperate.charNumber()); 37 writeFile.WriteLine("words: " + fileOperate.wordNumber()); 38 writeFile.WriteLine("lines: " + fileOperate.lineNumber()); 39 break; 40 } 41 } 42 //附加命令处理 43 for (int i = 0; i < arg.Length; i++) 44 { 45 if(arg[i].StartsWith("-"))//表明该项是输入参数 46 { 47 switch(arg[i]) 48 { 49 case "-m": 50 int len = int.Parse(arg[i + 1]);//将字符串转换成数字 51 Dictionarytemp1 = fileOperate.wordGroup(len); 52 foreach (KeyValuePair keyValuePairs in temp1)//Dictionary的foreach遍历对象自动转换成KeyValuePairs 53 { 54 writeFile.WriteLine(keyValuePairs.Key + ": " + keyValuePairs.Value); 55 writeFile.Flush();//将缓冲区的文件写到基础流 56 } 57 break; 58 case "-n": 59 int n = int.Parse(arg[i + 1]);//将字符串转换成数字 60 //Console.WriteLine(n); 61 Dictionary temp2 = fileOperate.wordTimes(n); 62 foreach (KeyValuePair keyValuePairs in temp2)//原理转换不是很清除,命名是推荐的 63 { 64 writeFile.WriteLine(keyValuePairs.Key + ": " + keyValuePairs.Value); 65 writeFile.Flush();//将缓冲区的文件写到基础流 66 } 67 break; 68 case "-help": 69 Console.WriteLine("https://www.cnblogs.com/haveadate/ [version 3.0]"); 70 Console.WriteLine("(c) 2019 haveadate 保留所有权利。\n"); 71 Console.WriteLine("-i 参数设定读入的文件路径 (必要) 格式:-i [file]"); 72 Console.WriteLine("-o 参数设定生成文件的存储路径 (必要) 格式:-o [file]"); 73 Console.WriteLine("-m 参数设定统计的词组长度 (非必要) 格式:-m [number]"); 74 Console.WriteLine("-n 参数设定输出的单词数量 (非必要) 格式:-n [number]"); 75 Console.WriteLine("注: 参数顺序不对结果产生影响"); 76 break; 77 case "-i": 78 case "-o": 79 break; 80 default: 81 Console.WriteLine("无效命令行参数,请重新输入!"); 82 Console.WriteLine("命令输入格式为:exeName.exe -[命令代号] [命令对应内容],命令可以无序使用" + 83 ",但必须有-i -o命令。\n想要了解更多,请在输入exeName.exe后输入-help命令"); 84 break; 85 } 86 } 87 } 88 89 //fileOperate.wordTimes(); 90 /* 91 fileOperate.charNumber(); 92 fileOperate.wordNumber(); 93 fileOperate.lineNumber(); 94 fileOperate.wordTimes(); 95 */ 96 fileOperate.closeFiles();//关闭文件流 97 writeFile.Close(); 98 Console.WriteLine("The result has been saved to the file, please check"); 99 Console.ReadKey();100 }
八、代码说明
1 ///2 /// 判断一个字符串是否是单词 3 /// 4 private bool isWords(string word) 5 { 6 char[] ch = word.ToCharArray();//将单词转换成字符数组 7 if(ch.Length < 4) 8 { 9 return false;10 }11 else12 {13 for(int i = 0; i < word.Length; i++)14 {15 if(i < 4)16 {17 //如果前四个字符不为字母,就不是单词18 if(!((ch[i] >= 'a' && ch[i] <= 'z') || (ch[i] >= 'A' && ch[i] <= 'Z')))19 {20 return false;21 }22 }23 else24 { 25 //如果第五个字符开始出现非字母、数字,就不是单词26 if(!((ch[i] >= 'a' && ch[i] <= 'z') || (ch[i] >= 'A' && ch[i] <= 'Z') || (ch[i] >= '0' && ch[i] <= '9')))27 {28 if(i == word.Length - 1)29 {30 if(ch[i] == ',' || ch[i] == '.' || ch[i]=='!')//若最后一个字符为","、"."、"!"前面也应该是单词31 {32 return true;33 }34 }35 return false;36 }37 }38 }39 return true;40 }41 }
此方法请参照上面的流程图,不再赘述
1 ///2 /// 统计每个单词出现的次数 3 /// 4 public DictionarywordTimes(int number) 5 { 6 reader.BaseStream.Seek(0, SeekOrigin.Begin);//将文件重新读取一遍 7 string temp;//临时的字符串变量 8 string[] tempArray;//临时的字符串数组,用于存储每一行的字符串数组 9 List wordList = new List ();//存储单词的动态链表10 //将单词存储到List集合中11 while((temp=reader.ReadLine())!=null)12 {13 tempArray = temp.Split(' ');14 for(int i = 0; i < tempArray.Length; i++)15 {16 if(isWords(tempArray[i]))17 {18 if (tempArray[i].EndsWith(".") || tempArray[i].EndsWith(",") || tempArray[i].EndsWith("!"))//这里将英语标点符号的特性处理掉19 {20 char[] tempCharArray1 = tempArray[i].ToCharArray();21 tempArray[i] = new string(tempCharArray1, 0, tempCharArray1.Length - 1);//用字符数组的起始位置和长度创建字符串22 }23 wordList.Add(tempArray[i]);24 }25 }26 }27 int count;//临时变量count记录单词出现的次数28 Dictionary wordsAndTimes = new Dictionary ();//用于存储单词以及他们的次数29 for(int i = 0; i < wordList.Count; i++)30 {31 count = 1;//单词本身就已经出现了一次32 temp = wordList[i];33 for(int j = i+1; j < wordList.Count; j++)34 {35 if(temp.ToLower().Equals(wordList[j].ToLower()))//不区分大小写的比较36 {37 count++;38 }39 }40 try41 {42 wordsAndTimes.Add(temp, count);//添加元素43 }44 catch//主要处理重复的情况45 {46 continue;47 }48 } 49 //使用linq语句排序,以前了解过,很管用,参考https://www.cnblogs.com/wt-vip/p/5997094.html50 var desSort = from tempElement in wordsAndTimes orderby tempElement.Value descending,tempElement.Key ascending select tempElement;51 /*52 foreach(KeyValuePair keyValuePairs in desSort)//原理转换不是很清除,命名是推荐的53 {54 count++;55 writeFile.WriteLine(keyValuePairs.Key + ": " + keyValuePairs.Value);56 writeFile.Flush();//将缓冲区的文件写到基础流57 if (count == 10)58 {59 break;60 }61 }62 */63 Dictionary keyValues = new Dictionary ();64 count = 0;65 foreach (KeyValuePair keyValuePairs in desSort)//原理转换不是很清除,命名是推荐的66 {67 count++;68 keyValues.Add(keyValuePairs.Key, keyValuePairs.Value);69 if (count == number)70 {71 //Console.WriteLine("kkk");72 break; 73 }74 }75 return keyValues;76 }
此方法首先通过循环读取输入文件中单词存储到List集合中,具体过程是没循环一次就读取文件中的一行数据,通过Split()方法将其分割成字符串数组,然后对字符串数组中的每个元素进行判断,若是单词就将其存储到List集合中。然后通过双重循环对List集合中的单词进行搜索,这个过程有点像交换法排序的方式,依次比较,在这个过程中用字典对单词及其出现的次数进行存储,,内层循环一次后就会得出某个单词的频率,用try语句尝试对单词进行存储,但可能单词已经存储了,所以catch直接进行下一次循环(后面的单词出现的次数没前面想通过单词的次数多,不够准确)。最后得到了每个单词及其出现的次数的集合,由作业要求,次数优先输出,其次考虑单词的字典顺序,上网查了一下,linq语句非常适合字典排序。最后就是返回指定长度的字典对象。这里,发现了一个小知识点,就是字典不能够直接用foreach语句输出,而是用对应的KeyValuePairs作为基本的对象。
1 ///2 /// 词组统计:能统计文件夹中指定长度的词组的词频,约定,词组不能跨行 3 /// 4 /// 5 ///6 public Dictionary wordGroup(int len)//代码复用比较多 7 { 8 reader.BaseStream.Seek(0, SeekOrigin.Begin);//将文件重新读取一遍 9 string temp;//临时的字符串变量10 string[] tempArray;//临时的字符串数组,用于存储每一行的字符串数组11 List wordList = new List ();//存储单词的List集合12 while ((temp = reader.ReadLine())!=null)//读取文件13 {14 tempArray = temp.Split(' ');15 for(int i = 0; i < tempArray.Length + 1 - len; i++)//+1-len的原因是后面根本没有足够的词组成词组16 { 17 for (int j = 0; j < len; j++)//词组的判断18 {19 if((i+j) wordsAndTimes = new Dictionary ();//用于存储单词以及他们的次数46 for (int i = 0; i < wordList.Count; i++)47 {48 count = 1;//单词本身就已经出现了一次49 temp = wordList[i];50 for (int j = i + 1; j < wordList.Count; j++)51 {52 if (temp.Equals(wordList[j]))//约定词组区分大小写53 {54 count++;55 }56 }57 try58 {59 wordsAndTimes.Add(temp, count);//添加元素60 }61 catch//主要处理重复的情况62 {63 continue;64 }65 }66 //使用linq语句排序,以前了解过,很管用,参考https://www.cnblogs.com/wt-vip/p/5997094.html67 var desSort = from tempElement in wordsAndTimes orderby tempElement.Value descending, tempElement.Key ascending select tempElement;68 Dictionary keyValues = new Dictionary ();69 //count = 0;70 foreach (KeyValuePair keyValuePairs in desSort)//原理转换不是很清除,命名是推荐的71 {72 //count++;73 keyValues.Add(keyValuePairs.Key, keyValuePairs.Value);74 /*75 if (count == number)76 {77 //Console.WriteLine("kkk");78 break;79 }80 */81 }82 return keyValues;83 }
其实wordGroup()方法实际上只是在wordTimes()的基础上扩展了一下,很多地方都直接复用了wordTimes()的代码,在此之说明不同之处:应作业需求,词组的长度是可变的,即通过传入的参数确定词组的长度,同样的遍历每行元素,不过在遍历每个元素的同时,在下标不越界的条件下,通过循环将后面len - 1个元素也遍历一次,若发现后面元素中有非单词的字符串,则以这个元素开头得不到相应的词组,若都满足,那么就将其存入List的集合中,后面操作与wordTimes()方法大致一样。
九、单元测试
charNumber()方法的单元测试
设计思路:由于每个方法都有返回值,故创建一个FileOperate对象,然后调用该方法,进行测试即可,其它单元测试见下
十、说明
基本功能的实现:
健壮性:
基本实现了错误提示功能;
新添功能: 仿照像cmd、matlab等等中的help命令:
获得帮助路径不唯一:
十一、总结
通过构建之法学习到的相关内容以及结对项目的实践经历,深刻认识到团队合作中方方面面的重要性,比如代码规范、团队交流、代码合并、单元测试等等都对项目至关重要,通过本次与搭档一起共同解决项目的过程中,学到了相互沟通、任务分配、规范管理等技巧,从这次两人结对编程映射出了将来参加工作的团队合作,总的来说收获蛮大的。此次解决项目,我认为是1 + 1 < 2,因为两人刚开始合作,需要时间去磨合,我相信在后面,做的越多,1 + 1 >> 2 !