第四次博客作业——结队编程

作业要求地址 https://www.cnblogs.com/harry240/p/11524113.html 
GitHub项目地址  https://github.com/anranbixin/WordCount
结对伙伴的博客  https://www.cnblogs.com/step-enter/p/11644293.html

1、PSP表格

PSP2.1

Personal Software Process Stages

预估耗时(分钟)

实际耗时(分钟)

Planning

计划

 30

 40

· Estimate

· 估计这个任务需要多少时间

 1200

 1440

Development

开发

 1080

 1200

· Analysis

· 需求分析 (包括学习新技术)

 40

 50

· Design Spec

· 生成设计文档

 20

 20

· Design Review

· 设计复审 (和同事审核设计文档)

 20

 30

· Coding Standard

· 代码规范 (为目前的开发制定合适的规范)

 30

 30

· Design

· 具体设计

 60

 50

· Coding

· 具体编码

 700

 960

· Code Review

· 代码复审

 60

 50

· Test

· 测试(自我测试,修改代码,提交修改)

 120

 150

Reporting

报告

 30

 20

· Test Report

· 测试报告

 60

 40

· Size Measurement

· 计算工作量

 30

 20

· Postmortem & Process Improvement Plan

· 事后总结, 并提出过程改进计划

 30

 20

 

合计

 1230

 1480

2、计算模块接口的设计与实现过程

2.1项目分析(项目需求)

(1)应具备的功能

  • 统计文件的字符数

      要求:1、汉字不考虑,空格、空格,水平制表符,换行符,均算字符,

                 2、输出的单词统一为小写格式

  • 统计文件的单词总数

      要求:1、至少以4个英文字母开头,且可以跟上字母数字符号

                 2、单词以分隔符分割,且不区分大小写

  • 统计文件的有效行数

     要求:任何包含非空白字符的行都需要统计

  • 统计文件中各单词的频数

       要求:1、最终只输出频率最高的10个

                  2、频率相同的单词,优先输出字典序靠前的单词。

  • 按照字典序输出到文件txt(默认为output.txt)

       要求:1、字典里面的单词已经过频数排序

                  2、输出的文件可任意指定

                  3、输出的量可控

(2)新功能

  • 词组统计

       要求:能统计文件夹中指定长度的词组的词频 

  • 自定义输出

       要求:能输出用户指定的前n多的单词与其数量

  • 多参数的混合使用

      要求:格式为   -i input路径 -o output路径 -n 参数 -m 参数

(3)附加功能(这个功能我们两个决定选择性的实现)

  • 用户交互界面绘制

2.2项目思路

-- 首先假设给出的路径已知,分别为(便于我们之后要进行指令读入造成的代码修改):

(1)读入文件路径:D:VS_practicewordCountwordCountinDebuginput.txt

(2)写入文件路径:D:VS_practicewordCountwordCountinDebugoutput.txt

注意:1、这里我们程序运行的当前路径为:D:VS_practicewordCountwordCountinDebug

           2、我们只需要调用语句,即可获得当前程序路径,之后加上我们的文件名即可

//获取当前文件路径
string currentpath = Directory.GetCurrentDirectory();

(3)数据的读取上,我们考虑到文件内容如果很大会影响数据的读取,我们采用的是content.ReadLine()进行单行的读取,这里也是会出现问题的,值得注意的一点是:在读入的input.txt文件里面,每一行英文必须是要用空格或者其他符号结尾,不然在后面使用Regex(Regex我会在后面单独提到)进行单词的提取上会出现错误

-- 程序如何才能实现:

(1)我们预计设计两个.cs文件,一个为主函数.cs文件(program.cs),一个为函数.cs文件(port.cs)

(2)数据的存储:使用string 变量来存储字符数,list<> 来存储提取出来的单词,dictionary<string,int> 来存储单词以及其的频率(这里要使用到dictionary的两个参数的使用,key和)

(3)头文件的调用:文件的输入输出需要使用到StreamWriter,因此需要调用IO头文件;提取单词需要使用到System.Text.RegularExpressions.Regex,因此需要调用Text.RegularExpressions头文件

//文件流的输入输出
using System.IO;
//正则表达式
using System.Text.RegularExpressions;

补充:

这里知识点的我参考了其他的博客:https://www.cnblogs.com/liangsetian/archive/2011/07/05/2098280.html

(4)获取字符数、单词数、频数等等,直接使用函数返回即可

(5)指定输出和指定单词组的输出都是涉及函数涉及,在之后会进行详解,这里输出会使用到遍历foreach ( element in list or dictionary)

(6)多参数的混合使用里面涉及到cmd命令,此时需要使用到args[i],通过Main方法中的string[]  args参数来获取

补充:这里我进行了知识点的搜索(args),博客源为:https://blog.csdn.net/eric_k1m/article/details/37518579

-- 程序的流程大致为下图所示:

基础功能模块和新功能模块:

 

2.3接口的设计与代码的实现过程

--  首先进行基础功能的接口设计与实现

这里我使用的是五个函数,首先必须依次调用Getcharacters(path);Withdraword();Tolower();Wordfrequency();这四函数,以实现初始化,中途涉及函数的调用,函数需要进行依次的设计,这四个函数需要依次进行实现,否则出来的结果是不正确的。例如:还没有进行字符的录入,但是你要输出单词这就是无法实现的。以下依次为5个函数的设计(4个基本+1个输出):

(1)进行字符的读取,这里我们必须要将待读取的文件的路径传进来,我们将函数设计为:public void Getcharacters(string path),在里面我们使用了ReadLine()来对path里面的文档进行一行一行的读取,然后将读取的字符直接存储于account_chara字符串里面

//读取文件,string account_chara用于存储字符
        public void Getcharacters(string path)
        {  
            StreamReader content = new StreamReader(path);
            //定义字符临时变量
            string temp = content.ReadLine();
            //读取
            while (temp != null)
            {
                account_chara = account_chara + temp;
                account_line++;
                temp = content.ReadLine();
            }

            //最后一行读入无效,将其删去
            account_line -= 1;
        }
字符读取

(2)接着就是将单词提取出来,这里我们采用的是之间说到的正则表达式,也就是使用Regex来对单词进行提取。由于我们提取单词是有要求的,需要我们(1)至少以4个英文字母开头,且可以跟上字母数字符号,所以我们采用的是:@"([a-zA-Z]{4}w*)" (2)单词以分隔符分割,且不区分大小写,这里就需要把单词全部小写化,我们采用的是ToLower()

//正则表达式匹配英文单词
        public void Withdraword()
        {
            //以字母开头,数字结尾,单词至少4个字符
            MatchCollection mc_word = Regex.Matches(account_chara, @"([a-zA-Z]{4}w*)");
            //临时变量
            int i = 0;
            while (i < mc_word.Count)
            {
                //存储单词
                word.Add(Convert.ToString(mc_word[i]));
                i++;
            }
        }
正则表达式,提取单词
//单词小写    
       foreach (string element in word)
            {
                element.ToLower();
            }
小写化

注意:这里我们是弄的时间比较长,后来在参考博客发现可以使用正则表达式(Regex.Matches),博客的来源:https://www.cnblogs.com/gc2013/p/4071757.html

(3)计算字符的数目,这里需要注意的是我们的字符 (string  account_chara)不包括中文,所以我们首先需要了解中文怎么表示,"[u4e00-u9fa5]" 表示中文。既然我们是把我们的字符存储在一个字符串temp里面(这里是出去中文字符),然后直接输出temp的长度即可得到我们想要的字符数

//字符总数
        public int Characternum()
        {
            //区分是否为中文,中文是@"[u4e00-u9fa5]"     u4E00-u9FA5
            MatchCollection mc_chara = Regex.Matches(account_chara, @"[^u4e00-u9fa5]*");
            string temp = null;
            int i = 0;
            while (i < mc_chara.Count)
            {
                temp = temp + Convert.ToString(mc_chara[i]);
                i++;
            }
            return temp.Length;
        }
字符总数

(4)获取单词以及单词的频数,首先通过d_word.ContainsKey(word[i])获得单词以及单词的频数,然后通过dictionary的key和value进行排序(看你需要如何排序来进行设计)

//单词频数(dictionary,sort)
        public void Wordfrequency()
        {
            //排序之前,将单词存入dictionary
            Dictionary<string, int> d_word = new Dictionary<string, int>();
            int i = 0;
            while (i < word.Count)
            {
                //ContainsKey判断是否存在
                if (d_word.ContainsKey(word[i]))
                {
                    d_word[word[i]]++;
                }
                else
                {
                    d_word[word[i]] = 1;
                }
                i++;
            }
            //通过dictionary的key和value进行排序
            word_num = d_word.OrderByDescending(p => p.Value).ToDictionary(p => p.Key, o => o.Value);
        }
单词频数(dictionary,sort)

注意:这里我的参考博客是:http://www.360doc.com/content/18/0425/18/54584204_748693269.shtml

 (5)写入文档,这里需要使用到字节流的写入,通过FileStream,StreamWriter行文件读写

//写入文件
        public void Writetofile(string path,string outpath)
        {
            //准备(读入文档,单词提取,以及词频的排序)
            Prep(path);
            FileInfo file = null;
            if (outpath == null)
            {
                file = new FileInfo(@"D:VS_practicewordCountwordCountinDebugoutput.txt");
            }
            else
            {
                file = new FileInfo(outpath);
            }
            StreamWriter sw = file.AppendText();
            sw.WriteLine("字符数:" + Characternum());
            sw.WriteLine("单词数:" + Wordnum());
            sw.WriteLine("行数:" + Wordlinenum());
            Console.WriteLine("字符数为:" + Characternum());
            Console.WriteLine("单词数为:" + Wordnum());
            Console.WriteLine("行数为:" + Wordlinenum());
            //统计前10个高频单词
            Writeword(sw,10);
            //关闭文件
            sw.Close();
        }
写入文件

注意:这里文件的写入参考的博客(内含有知识点)是:https://blog.csdn.net/u010159842/article/details/51785613

--  然后是新功能的接口设计与实现

词组统计、自定义输出采用的是2个函数,多参数是在主函数中实现,这里我们使用到了args[i],我只展示多参数的实现:

//筛选出前10个高频单词 
public void Writeword(StreamWriter sw,int n)
//输出指定数量的单词数,并写入文件
public void Wordgroupp(StreamWriter sw, int m)
 for (int i = 0; i < args.Length; i++)
            {
                switch (args[i])
                {
                    case "-i":
                        path = args[i + 1];//输入路径
                        break;
                    case "-o"://-o输出路径
                        outpath = args[i + 1];
                        break;
                    case "-m"://-m输出几个高频词
                        m = args[i + 1];
                        break;
                    case "-n"://-n输出几个单词的个数
                        n = args[i + 1];
                        break;
                    
                }
            }
参数指令

--  运行结果

(1)程序运行:

 (2)cmd输入命令

 

文件的截图不完整,由于数据太大了。

 

2.4接口的封装

由于每次都需要调用函数是容易出错的,所以需要进行接口的封装

例如:我的小伙伴在看我的程序的时候,不知道先后顺序,没有进行单词的排序就直接输出含有单词频数的dictionary变量,结果输出为空。

为此我们进行了函数接口的封装,如下所示:

public void Prep(string path)
        {
            Getcharacters(path);
            Withdraword();
            Tolower();
            Wordfrequency();

        }

这是一个准备函数,直接将需要进行的函数进行封装,在我们开始我们的主程序的时候,只需要先调用这个函数即可完成文件的预处理。

再者,我们将函数写在了一个.cs文件里面,将主函数写在一个.cs文件里面,实现了函数的封装,下次直接调用即可。

3、代码互审过程

代码规范(由我们自己制定的,我们两个的博客中相同)

经过参考C#代码规范,我们制定了我们的代码规范:
我们本着“保持简明,让代码更容易读”的原则,让我们更好地理解和维护程序。
代码风格的原则是:简明,易读,无二义性。
1.缩进:4个空格,在VS2017和其他的一些编辑工具中都可以定义Tab键扩展成为几个空格键。不用 Tab键的理由是Tab键在不同的情况下会显示不同的长度。4个空格的距离从可读性来说正好。
2.括号:在复杂的条件表达式中,用括号清楚地表示逻辑优先级。
3.断行与空白的{ }行:每个“{”和“}”都独占一行
如:

if ( condition)
{
    DoSomething();
}
else
{
    DoSomethingElse();
}

4.分行:不要把多行语句放在一行上。
5.命名:命名方法使用“匈牙利命名法”,在变量面前加上有意义的前缀,就可以让程序员一眼看出变量的类型及相应的语义
例如:
    fFileExist,表明是一个bool值,表示文件是否存在;
    szPath,表明是一个以0结束的字符串,表示一个路径。
6.下划线问题:下划线用来分隔变量名字中的作用域标注和变量的语义
7.大小写问题:由多个单词组成的变量名,如果全部都是小写,很不易读,一个简单的解决方案就是用大小写区分它们 

Pascal——所有单词的第一个字母都大写;
Camel——第一个单词全部小写,随后单词随Pascal格式,这种方式也叫lowerCamel。
一个通用的做法是:所有的类型/类/函数名都用Pascal形式,所有的变量都用Camel形式。
类/类型/变量:名词或组合名词,如Member、ProductInfo等。
函数则用动词或动宾组合词来表示,如gett; RenderPage()。
8.注释:复杂的注释应该放在函数头,养成边写代码边写注释的好习惯

总结:

真的在两个人的项目中我真切的感受到了这个的重要性,刚开始我以为我的代码真的算是规范的了,但是我的小伙伴在看我的代码的时候却还是出现了问题了。

应该是每个人的命名都是会有区别的,我喜欢把关键字写在后面,虽然采用的也是英文命名,但是一个意思的英文单词确实太多了,每次都会有点小差错。

然后我们就自己制定了一个命名规范,并用于我们的小项目当中。

4、接口部分的性能改进

4.1效能分析与改进

首先可以看到我们测出来的代码的性能图,第一个是内存使用率,第二个为CPU使用率:

 

 总结(综合我和我的结对小伙伴):

可以看到我们调用主函数的频率是高的,可以看出Main函数中是CPU占比最多的函数,这样子是不太好的,经过我两的一致讨论结果是把主函数简化,确实相比之后是要优化很多的。

于是我们看到main函数,其中在该函数中,我们读取文件、将最后结果放在文件中所花时间、判断命令行中出现的命令选项、判断并打印结果等所花时间都很多,特别是文件的读取过程很慢,因此我们针对文件读取做了很多分析,最后减少了程序对文件的读取的个数使得性能加快。我们就尽量将主函数在调用port.cs里面的函数都统一在一个函数里面,这样就降低 了他的调用频率,其实对于主函数过于庞大我们两个还是有意见的,她希望将指令单独列成一个函数,我坚持写在主函数里面,可能将其列成函数是有一定的优化效果的。
在另一个类port中,WordFrequency()函数占用率最高,因此我们针对WordFrequency()函数进行了分析,WordFrequency()函数中主要拖垮性能的原因是有大量的判断语句,判断d_word字典中的key值,即单词word[i]是否存在,若存在则执行value值加1的操作,若不存在,则将其作为key存入字典并将其value值令为1,因此我们在函数外将我们需要判断的值优先判断,最后再根据其判断值进行value的值操作,加快了程序性能。

4.2接口性能的改进

将主要的准备工作做成一个函数,到时候直接进行一次调用,就可实现初始化效果,这样子还减少了主函数对于其他函数的调用。但是可以看出将其封装为一个函数的时候(Prep),Prep()的调用率是很高的。

5、异常(错误)处理说明

使用try {} catch {} 来进行异常处理:

首先我们需要了解到哪里是需要进行异常处理的,当我们进行输入的时候,或者是需要进行传参的时候,需要我们来检验那些输入和传入的参数是否有效,如果无效则抛出异常。

(1)在文件进行路径读入的时候(判断路径中是否含有文档,-i 与 -o类似):

try
    {
          path.Contains(".txt");
     }
catch
    {
          Console.WriteLine("输入的路径不含有txt文件!");
     }

(2)指令的输入的时候(判断指令后面的数据是否有效-m 与 -n 的是类似的,这里只列举一个):

try
    {
         int test = int.Parse(args[i + 1]);
    }
catch
    {
         Console.WriteLine("输入的指令无效");
    }

6、单元测试展示

将代码转移到自己的电脑上的时候,出现了一些错误,然后通过调试之后可以运行且无错误。

根据之前的经验我们开始进行单元测试,刚开始的时候还有一些错误(并修改了一些源代码):

 然后通过写单元测试代码,可以看到测试全部都通过了:

 在图片中我们可以看到部分的测试代码,这里就不一一进行解释了。

7、提交项目到GitHub上

将项目git到自己的仓库,再将文件下载下来,再里面创建一个我们两个其中一个的学号的文档(创建的我的),然后将项目放在里面。

通过git bush提交项目(提交命令如下图所示,注意要修改了代码才可以进行提交,每次提交都类似):

 

 之后可以看到我们的提交记录,如下图所示:

三次提交项目:

 

8、结对过程的描述

项目在我们两个人的努力下就这样完成了,刚开始我们最难的也就是写代码。最初的时候不知道怎么开始,在设计好PSP表格之后,就开始设计函数 。设计好基本函数之后,我们就直接输入输入路径来进行测试 (此时发现了一些代码错误,然后 进行了修改),测试良好。然后就进行命令的输入的设计,通过args来读取得到路径(两个人一起查资料,很快就解决了~)。

8、总结

此次的结对编程感觉还不错,两个的代码能力都一般,刚开始的时候还很无措,但是两个人的解决办法就多了,很多问题在短时间就得到了解决。

总体而言两个人的编程1+1>2的,编代码的时间得到了大大的缩短,两个人的代码测试使得代码的结构更加的稳定,而且代码的编写更为规范了。

在实践的过程中还发现了很多自己的不足之处,觉得自己还是有很多需要改进的地方。

原文地址:https://www.cnblogs.com/mylovertomylove/p/11652247.html