65.9K
CodeProject 正在变化。 阅读更多。
Home

如何编写代码解决问题:初学者指南

starIconstarIconstarIconstarIconstarIcon

5.00/5 (32投票s)

2020 年 10 月 6 日

CPOL

7分钟阅读

viewsIcon

64229

开发的第一步:分解,再次分解!

引言

这里的代码都是 C# 编写的,但都很简单,而且过程在任何语言中都是一样的。如果你不理解括号和分号,就“哔”一声忽略它们吧!

最近,有一个问题涉及到一部分家庭作业:编写一个方法,读取一个文件并返回所有后面跟着包含三个星号的行的行。代码写得太糟糕了,甚至对初学者来说也是如此,差到我不会为了尴尬任何人而链接它。

引用

大家好,我不知道在 return 方法之后是否还有继续的选项,或者我应该如何构造代码。我希望在 return 之后继续读取 txt 文件的其余部分,并让 return 返回更多值。

public static string FindLineAboveAsterisks(TextReader reader)
        {
            StringBuilder sbBuilder = new StringBuilder();
            string result = reader.ReadLine();
            string line = String.Empty;          

            while  (result is object && (line = reader.ReadLine()) is object)
        {
                int startIndex = 21;
                int length = 9;

            if (line.Contains("***"))
            {
                sbBuilder.AppendLine(result);
                return result;
                    
                }
                {
                    result = line.Substring(startIndex, length);
                }
                
            }
            return string.Empty;

你看了那段代码,你就会开始想……为什么?那个缩进是什么?那是什么意思?为什么这样做?你期望它怎么工作?

当然,它行不通。它不可能行得通——原因在于作者根本没有思考任务,就随意地组合了代码。

我的回答(稍作扩展)

这看起来就像是毫无思考地随意组合出来的,根本没考虑你要做什么!

我知道,这很直白。但我想引起他的注意。

把它扔掉,然后思考你的任务:读取一个文件,找到所有在星号行上面的那些行,并返回它们。

依然直白,但我们来思考一下。

所以,让我们从头开始:你需要返回不止一行——所以显而易见的事情是返回一个字符串集合而不是一个单独的字符串。因为虽然你可以将它们作为单个字符串返回,但这会使调用你方法的代码的生活变得更加艰难——它必须“再次将其分解”才能使用这些信息。
让我们来改变一下

public static List<string> FindLineAboveAsterisks(TextReader reader)

现在,它返回一个字符串集合,以便外部世界可以处理它。

思考你想让这个方法做什么:不要让调用代码变得复杂——因为你将调用它一次或多次,而你只写一次。如果你让外部世界更辛苦,那么每次使用该方法时,你都会增加自己的工作量。
所以,如果你需要一个项目集合,就返回一个集合——不要通过其他方式处理,让外部世界每次调用你时都要做更多处理!

但是……你为什么要传递一个 TextReader 呢?这意味着每次调用它时,外部世界都必须完成创建、打开、传递和关闭读取器的所有工作——这太愚蠢了。传递路径,让方法自己处理。

public static List<string> FindLineAboveAsterisks(string filePath)

现在,调用者看起来更容易使用了。

同样,让自己生活轻松点:你想读取一个文件?传递路径,让方法自己决定如何处理。如果你传递一个 TextReader 或 Stream,那么你就限制了外部世界能做什么,并强迫代码采用一个“形状”,而这个形状可能不是最容易或最高效的工作方式。
你使参数越“通用”,你的代码就越灵活——这意味着它可以被重用——这可以节省你编写另一个相似的方法来做同样的事情。

让我们开始填充方法:我们需要一个 List 来返回,并处理文件中的每一行。如果我们想使用每一行,那就获取所有行,让系统来处理!这很简单。

        public static List<string> FindLineAboveAsterisks(string filePath)
            {
            List<string> lines = new List<string>();
            foreach (string line in File.ReadLines(filePath))
                {
                // ... 
                }
            return lines;
            }
还有什么比这更简单的呢?我们知道必须做两件事:返回一个行集合,并处理文件中的所有行。所以,在方法顶部创建集合;在底部返回它。添加一个简单的循环,一次给我们一行。结果:代码很简单,而且容易编写。如果容易编写,它很可能就会起作用……

现在,我们必须对这些行做什么?
很简单;我们需要收集所有后面一行包含三个星号的行。
所以我们需要知道上一行是什么。

稍微思考一下:在循环内部,我们怎么知道下一行包含什么?实际上,我们做不到(除非我们让代码复杂化并使用不同的循环结构,但那很麻烦)。但我们确实知道上一行是什么——因为我们已经处理了它,并且可以保留一份副本供下次使用。
所以,换个角度思考问题,把它看作是“找到所有包含三个星号的行,并为每一行返回上一行”。稍作思考就知道这样可以得到相同的结果,并且意味着我们可以处理“历史数据”,即我们已经查看过的数据,而不是“未来数据”,即我们尚未查看过的数据。

让我们添加这个

        public static List<string> FindLineAboveAsterisks(string filePath)
            {
            List<string> lines = new List<string>();
            string lastLine = "";
            foreach (string line in File.ReadLines(filePath))
                {
                // ... 
                lastLine = line;
                }
            return lines;
            }
每次,我们都添加一点点简单的代码——没有什么复杂的,所以出问题的可能性更小。

我们需要检查当前行是否包含“***”。如果是,就将上一行添加到集合中。这也很简单——一个快速的 if 测试就可以做到。

        public static List<string> FindLineAboveAsterisks(string filePath)
            {
            List<string> lines = new List<string>();
            string lastLine = "";
            foreach (string line in File.ReadLines(filePath))
                {
                if (line.Contains("***"))
                    {
                    lines.Add(lastLine);
                    }
                lastLine = line;
                }
            return lines;
            }

等等……这不就完成了,对吧?

现在我们只需要调用它并测试它。

            string path = @"D:\Test Data\List of hats.txt";
            foreach (string line in FindLineAboveAsterisks(path))
                {
                Console.WriteLine(line);
                }
我可以向你展示那段原始代码……但你可能刚吃完饭……

哦,看——它工作了!

那么我们做了什么?

基本上,我们所做的就是把一个完整的任务分解成更小的任务。

引用

编写一个方法,读取一个文件并返回所有后面跟着包含三个星号的行的行。

  1. 决定它需要返回什么。
  2. 决定它需要哪些参数。
  3. 创建可返回值,并准备返回它。
  4. 添加一个循环来查看每一行。
  5. 在循环结束时,当我们完成查看当前行后,将其保存以备下次使用。
  6. 检查该行是否包含星号。
  7. 如果是,则将我们上次循环保存的行添加到输出集合中。

这些任务都不难:它们只需要一两行代码,而且代码也相当简单。

这就是秘诀:大任务是由小任务组成的,而小任务又由更小的任务组成。

你已经习惯了这一点:你每天都在使用它!

任务:“吃早餐。”

更小的任务

  1. 去厨房。
  2. 决定早餐吃什么。
  3. 准备好。
  4. 吃掉它。
  5. 饭后收拾。

这些任务中的每一个可能都很复杂。

子任务:“去厨房”

  1. 确定你的位置。
  2. 计算如何从这里到厨房。
  3. 移动过去。

这些可能还有子-子任务。

子-子任务:“确定你的位置”

  1. 醒来。
  2. 睁开眼睛。
  3. 环顾四周:我在哪?我认识这个房间吗?我昨晚到底干了什么?
  4. ...

摘要

关键在于,每个任务都可以分解成更小的部分,直到你达到一个你能够完成,或者知道如何找到方法去完成的任务。如果你在一个陌生的房间醒来,那么你需要检查是否还有其他人,也许问问他们厨房在哪里——依此类推。

软件任务也是如此;将任务细化成更小的部分,其中一些——可能全部——都可以轻松完成,并逐步完成那些听起来不可能的、更复杂的大任务。

与其直接上手写代码,不如先思考:五分钟的规划可以为你节省数小时的工作!

历史

  • 2020年10月6日:原始版本
  • 2020年10月6日:拼写错误。总是有拼写错误……
  • 2020年10月8日:修复了几个小于号和大于号:它们显示为 HTML 等效字符:“&lt;” 和 “&gt;”
© . All rights reserved.