Github项目地址(伙伴的) | 地址 |
---|---|
结对编程伙伴博客地址 | 地址 |
作业要求地址 | 地址 |
1.1结对过程
1.2 PSP表格
PSP2.1 | Personal Software Process Stages | 预估耗时(分钟) | 实际耗时(分钟) |
---|---|---|---|
· Planning | · 计划 | 20 | 20 |
· Estimate | · 估计这个任务需要多少时间 | 25 | 25 |
· Development | · 开发 | 890 | 1290 |
· Analysis | · 需求分析 (包括学习新技术) | 60 | 90 |
· Design Spec | · 生成设计文档 | 30 | 30 |
· Design Review | · 设计复审 (和同事审核设计文档) | 20 | 20 |
· Coding Standard | · 代码规范 (为目前的开发制定合适的规范) | 10 | 10 |
· Design | · 具体设计 | 30 | 60 |
· Coding | · 具体编码 | 600 | 900 |
· Code Review | · 代码复审 | 60 | 60 |
· Test | · 测试(自我测试,修改代码,提交修改) | 30 | 60 |
· Reporting | · 报告 | 30 | 30 |
· Test Report | · 测试报告 | 30 | 30 |
· Size Measurement | · 计算工作量 | 20 | 20 |
· Postmortem & Process Improvement Plan | · 事后总结, 并提出过程改进计划 | 60 | 60 |
合计 | 1025 | 1415 |
2.1 思路
题目划分了三个阶段,我们就分三次入手分析了各个要求的做法
- 基础功能
基础功能是统计字符数,有效行数,单词总数,有效单词的种类数,频数,按指定顺序输出频数前十的单词。难点和重点是如何统计有效的单词,从文本中剥离出一个个单词。为此我们的方法是使用正则表达式,在正则表达式中限定了条件,是可以筛选出以英文字母开头,长度大于等于4,但不可以是数字开头的字符串
- 扩展功能,命令行解析
扩展功能中要求实现一个命令行程序,像Linux
的Shell
命令一样有着一些参数选项。这一功能的难点在于命令行参数解析。为此,我们原打算通过判断Main
··的入口args
参数顺序以此比较来判断是否要进行某些功能。但是在实现过程中,发现题目要求命令行的参数有必填参数还有选填参数,参数的顺序还可以不固定。对此我们的方法就不再适用。通过请教同学,查阅资料,我们使用了NuGet
包CommandLineParse
工具来帮助我们实现命令行参数的解析工作。 - 扩展功能,窗体程序
在实现窗体程序前,我们把第二版的扩展功能的计算核心封装成DLL类库,在窗体程序中引用DLL服务,方便了程序的编写。
2.1 设计实现过程
我们设计了两个类,CalcCore类负责统计功能,包含5个功能函数,Options类负责解析命令行参数,函数与函数、类与类间没有关联关系
2.2程序结构图和流程图
程序结构图
命令行程序流程图
2.3 单元测试
单元测试中我们针对每个函数设计了两个测试样例
测试代码如图
测试txt文件如图
3.制定规范
Pascal——所有单词的第一个字母都大写;
一个通用的做法是:所有的类型/类/函数名都用Pascal形式,所有的变量都用。
类/类型/变量:名词或组合名词,如Member、ProductInfo等。例如单词数量取名CountOfWord
函数则用动词或动宾组合词来表示例如计算行数方法取名CalcLine
缩进设置Tab为4空格
在复杂条件表达式中使用括号表达优先级
花括号采用{}
各占一行的风格
在初始化变量时一定赋初值为默认
下划线在窗体程序中命名中采用
注释,对于计算核心的每个方法都注明方法的目的,参数,为什么这样做
错误处理,对于没有包含的操作,都要有配套的异常处理
4.代码互审
- 虽然制定了规范,但我仍有些习惯问题,比如FileStream,StreamReader对象我喜欢命名为fs,sr,这是不符合规范的,但是通常一个函数里只有一个FileStream,StreamReader对象,所以同伴没有强制改正
- 我和同伴都只习惯在给函数注释表示函数的作用,没有具体功能的注释,导致合并代码时总得询问对方的思路
- 同伴的功能函数包含了写入文件的功能,我认为函数功能应该单一,所以在整合代码时将写入文件的功能放在了主函数里
- 同伴的统计词频的排序功能写的太冗杂,在与同学交流后发现使用Linq的排序能大大减少代码量和降低开发难度
5.性能分析
我们发现程序中消耗最大的函数是统计词频函数
其中获得MatchCollection元素数量函数占比最大
于是我们修改了代码,减少了调用该函数的次数老实说我没想通为什么调用两次与调用近三百万次的百分比居然相差不多
6.代码说明
- CalcChar 传入文件路径,读取所有字符,剔除中文字符,返回字符串长度
/// <summary> /// 统计字符数 /// </summary> /// <param name="path"></param> /// <returns></returns> public int CalcChar(string path) { int charNum; string rest, str; FileStream fs = new FileStream(path, FileMode.Open); StreamReader sr = new StreamReader(fs); str = sr.ReadToEnd(); string pattern = @"[\u4e00-\u9fa5]"; rest = Regex.Replace(str, pattern, ""); charNum = rest.Length; sr.Close(); fs.Close(); Console.WriteLine("字符总数:" + charNum); return charNum; }
- CalcWords 传入文件路径,用正则表达式得到所有符合条件的单词,返回单词的个数
/// <summary> /// 统计单词总数 /// </summary> /// <param name="path"></param> /// <returns></returns> public int CalcWords(string path) { FileStream fileStream = new FileStream(path, FileMode.Open); StreamReader streamReader = new StreamReader(fileStream); string tool = @"\b[a-zA-z]{4,}\w{0,}"; string rest = streamReader.ReadToEnd(); MatchCollection mc = Regex.Matches(rest, tool); int res = mc.Count; Console.WriteLine("单词总数:" + res); streamReader.Close(); fileStream.Close(); return res; }
- CalcLine 传入文件路径,当读取的行为空时,不计数,返回有效行数
/// <summary> /// 计算文件中的行数 /// path为文件路径 /// </summary> /// <param name="path"></param> /// <returns></returns> public int CalcLine(string path) { int res = 0; FileStream fileStream = new FileStream(path, FileMode.Open); StreamReader streamReader = new StreamReader(fileStream); string Line = ""; while ((Line = streamReader.ReadLine()) != null) { if (Line.Length > 0) res += 1; } streamReader.Close(); fileStream.Close(); Console.WriteLine("有效行数:" + res); return res; }
CalcWordFrequence 传入文件路径和参数n,用正则表达式得到所有符合条件的单词,存入字典中,用Linq排序,新建字典,将n个键值对存入新字典(如n>键值对个数,则将所有键值对存入新字典),返回新字典
```///
/// 统计单词词频
///
///
///
public Dictionary<string, int> CalcWordFrequence(string path,int n)
{
string tool = @"\b[a-zA-z]{4,}\w{0,}";
Dictionary<string, int> keyValuePairWord = new Dictionary<string, int>();
FileStream fs = new FileStream(path, FileMode.Open);
StreamReader sr = new StreamReader(fs);
string rest = sr.ReadToEnd();
MatchCollection mc = Regex.Matches(rest, tool);
int number = mc.Count;for(int i = 0; i < number; i++) { string tmp = ""; tmp = mc[i].ToString(); if (!keyValuePairWord.ContainsKey(tmp)) { keyValuePairWord.Add(tmp, 1); } else { keyValuePairWord[tmp]++; } } var res = from pair in keyValuePairWord orderby pair.Value descending, pair.Key ascending select pair; Dictionary<string, int> result = new Dictionary<string, int>(); int j = 0; foreach (var i in res) { if (j == n) { break; } result.Add(i.Key, i.Value); j++; Console.WriteLine(i.Key + ":" + i.Value); } sr.Close(); fs.Close(); return result; }
```PhraseStat 传入文件路径和参数m,一行一行读取,判断每行是否有m个符合条件的单词组,并用字符串表示单词组存入字典中,返回字典
```///
/// 统计词组
///
///
///
public Dictionary<string, int> PhraseStat(string path, int m)
{
Dictionary<string, int> keyValuesPairPhrase = new Dictionary<string, int>();string tool1 = @"\b[a-zA-z]\w{0,}"; FileStream fs = new FileStream(path, FileMode.Open); StreamReader sr = new StreamReader(fs); string Line = ""; while ((Line = sr.ReadLine()) != null) { MatchCollection mc = Regex.Matches(Line, tool1); for (int i = 0; i < mc.Count - m + 1; i++) { string tmp = ""; for (int j = i; j < i + m; j++) { if (mc[j].Length < 4) { goto tick; } tmp += mc[j].ToString() + " "; } if (!keyValuesPairPhrase.ContainsKey(tmp)) { keyValuesPairPhrase.Add(tmp, 1); } else { keyValuesPairPhrase[tmp]++; } tick:; } } Dictionary<string, int> result = new Dictionary<string, int>(); foreach (var i in keyValuesPairPhrase) { Console.WriteLine(i.Key + ":" + i.Value); result.Add(i.Key, i.Value); } sr.Close(); fs.Close(); return result; }
```
7.总结
这次作业收获挺大,首先我巩固了C#的文件读取、正则表达式、Linq、字典的使用,以前只是学习过,直到在这次实践中反复使用才熟练掌握。其次在结对编程中,两人的思路不同,在设计中能很好地启发我。当然合作必须对自己要求严格,函数功能要注释说明,命名也得规范。最后代码互审很好地发现每个人的设计盲区,毕竟有些设计错误自己会理所当然的忽视,多个伙伴能很好的捉虫。这次结对我认为是1+1>2的。