我做了一个简单的Mastermind克隆,我想要一些提示,说明如何为已经编写的代码做更好/不同的解决方案。如果您想知道什么是策划者,那么对于原始版本来说,有6种不同的颜色和4种不同的孔。我决定做这件事是因为我已经在其他事情上有所作为,所以我认为这将是C#中的一个很好的入门项目。

评论

“非史密”是什么意思?是法国人吗?

编写此代码的方式就好像作者相信他们只有那么多次才能在死亡之前按回车键! :-)请为每个语句插入换行符。如果从逻辑上来说,您觉得某项功能只有一行,那么将其提取到函数中并调用该函数,嘿,现在只有一行。

#1 楼

初次尝试是很复杂的,但是今天将是摆脱不良习惯的好日子。您的代码充满了他们。

开始于:使用标准格式约定对代码进行格式化。如果代码是垂直的,而您的代码是水平的,那么我们会更容易理解。

让我们逐行进行介绍。

static void Main(string[] args)
{


程序在Main中执行所有操作的程序都是无法轻易重构或重新使用的程序。 Main应该创建一个代表程序的对象,然后执行它。

    Random GenRandom = new Random();


遵循C#命名约定。当地人会获得camelCase名称。应该是

    Random random = new Random();

    int t = 0, r, c1 = random.Next(1, 6), c2 = random.Next(1, 6), c3 = random.Next(1, 6), c4 = random.Next(1, 6);


尽管此样式合法,但被认为是较差的样式。每行声明和初始化一个变量。给变量赋予有意义的名称。如果您花时间输入turn而不是t,您不会死得更年轻。应该是:

    int turn = 0;
    int digit;
    int cell1 = random.Next(1, 6);
    int cell2 = random.Next(1, 6);
    int cell3 = random.Next(1, 6);
    int cell4 = random.Next(1, 6);


继续前进:

    bool w = false;


相同的交易:

    bool hasWon = false;


继续前进:

    string Input, Code = Convert.ToString(c1);
    Code += c2; 
    Code += c3; 
    Code += c4;


同样,每行一个。

现在我们看到在两个地方有相同的数据:不再使用的本地c1,c2,c3,c4和Code。现在是我们进行第一次重构的时候了。我们想要的是:

string code = GenerateCode();


让我们实现:等等,以我们的方法为准。

好吧,我们在哪里?现在,我们有:

static Random random = new Random();
static string GenerateCode()
{
  return random.Next(1, 6).ToString() +
    random.Next(1, 6).ToString()
    random.Next(1, 6).ToString()
    random.Next(1, 6).ToString(); 
}


继续前进:

    string input;
    string code = GenerateCode();


首先,迪斯科舞厅风格的不良形式。第二,八?八个是哪里来的?重写:

    while (t != 8) {


继续前进:

const int maximumTurns = 8;
while (turn != maximumTurns)
{


我发现增量运算符令人讨厌。

  t++;


好多了。

  turn += 1;


疼痛。不要用C#编写非结构化代码;是2018年,而不是1972年。

    Unepic:


我不反对的第一行! :-)但是,正如我们将看到的那样,它应该使用自己的方法。



因为现在将random更改为7,您只需要在一个地方更改代码,而不是很多。

更好的是,创建一个辅助函数:

        Console.Clear();


,然后

        Console.WriteLine("You have {0} turn(s) left.",9-t);


,然后将其移动到帮助器中。

移动上:

        Console.WriteLine($"You have {maximumTurns + 1 - turn} turn(s) left.");


避免拉丁文主义。应该是:

static string Plural(int x, string s)
{
  return x == 1 ? s : s + "s";
} 


现在我们进入需要重构的部分:

        int remainingTurns = maximumTurns - 1 - turn;
        Console.WriteLine($"You have {remainingTurns} {Plural(remainingTurns, "turn")} left.");


到目前为止,还不错...

        Console.WriteLine("Guess the code E.g 1561: ");


不要写重复代码清楚可见的注释。并且不要使用意大利面条逻辑。

        Console.WriteLine("Guess the code (for example, 1561): ");


不要重新发明轮子。 c1已经存在:

        input = Console.ReadLine();


我们应该如何更好地构建整个结构?我们想要的是:

        if (input.Length != 4) goto Unepic; // Checks if input is 4 characters long


这样写:

        try { Convert.ToInt16(Input); Convert.ToString(Input); } catch (FormatException) { goto Unepic; } // Checks if input is only numbers


继续前进:

    int number; // In C# 7 you can use a discard instead.
    bool valid = int.TryParse(input, out number);


结尾处有一个野性半身。再次删除它。

同样,当您编写明显的注释时,请问自己:“我该如何重写代码以使注释不必要?”像这样:

    if (!ValidInput(input)) goto Unepic;


我们稍后再解决。

static bool ValidateInput(string input)
{
  int number;
  return input.Length == 4 && int.TryParse(input, out number);
}


为什么在这里检查?它应该在ValidateInput中。

再放一个小循环:

        if (Input == Code) { w = true; goto End; }; // Checks if you've won


再次,将其放入方法中。

        if (input == code) 
        { 
          hasWon = true; 
          goto End; 
        }


我们编写此循环的通常方法是:

        if (Input.Contains("0") || Input.Contains("7") || Input.Contains("8") || Input.Contains("9")) { goto Unepic; } // Checks if it has any digits that are 0 or above 7


完成游戏循环:

        r = -1;
        while (r != 3)
        {
            r++;
            if (Input[r] == Code[r]) Console.Write(1); else Console.Write(0); // Checks if a digit of the input is equal to a digit of the code
        }


为什么要让用户在这里按下键?但是,可以再次将其放入辅助方法中。

static void WriteHints(string input, string code)
{


我也很好,但是稍后我们将回到goto。 />
  for (int i = 0; i < input.Length; i += 1) 
    Console.Write(input[i] == code[i] ? 1 : 0;
}


maximumTurns是新手代码的肯定标志。 TryParse已经为true或false;您不必说“如果w为真,则为真”,也不必说“如果w为真”,而只需说“如果w”即可。

再说一次,这可能会成为一个帮手。

现在,让我们一起来看一下重构后的主循环

        Console.WriteLine();
        Console.Write("Press any key to continue.");
        Console.ReadKey(true);


这已经容易阅读一百万次了,但我们还没有完成。

我们注意到的第一件事是,我们做了一些工作来确保if (w == true)不会递增如果用户输入错误。那很棒!但是代码的读法不是那样的。让我们重写它吧。到错误的值,以便以后可以增加。

现在我们注意到wturnturngoto Unepic

    }
    End:;
    Console.Clear();


看看与您的版本相比,我的版本更容易理解。任何人,甚至是非编码人员,都可以看一看,就像英语一样阅读。 “当转弯小于或等于最大转弯时,提示用户输入,然后从控制台读取该行,然后验证输入...”

这就是您必须努力的在您的代码中。总是问自己,我该如何使它更容易理解?我该如何使它读起来更像是对我的意图的描述?

我们可以做得更好吗?当然。实际上,我们将两个循环写为一个。有一个循环可以获取有效的用户输入,还有一个可以运行游戏的循环。明确说明这一点:

    if (w == true) { Console.WriteLine("You won! The code you guessed was {0}.", Code); } else { Console.WriteLine("You lost! The code you couldnt guess was {0}.",Code); };


现在,我们看到了另一个使用辅助方法的机会。将内循环移至帮助器!将中奖支票移至帮助者:

static void Main(string[] args)
{
    int turn = 0;
    bool hasWon = false;
    string code = GenerateCode();
    string input;
    const int maximumTurns = 8;
    while (turn != maximumTurns)
    {  
      turn += 1;
    Unepic:
      PromptUserForInput(maximumTurns, turn);
      input = Console.ReadLine();
      if (!ValidInput(input)) goto Unepic;
      if (input == code) 
      { 
        hasWon = true; 
        goto End; 
      }
      WriteHints(input, code);
      PromptUserToPressKey();
    }
    End:;
    PrintWinOrLoseMessage(hasWon, code);
}


如果我从头开始编写您的代码,我将使其比我在这里给您提供的版本更加抽象。我的主要知识是:

    int turn = 1; // not zero!
    while (turn <= maximumTurns) // not !=
    {  
    Unepic:
      PromptUserForInput(maximumTurns, turn);
      input = Console.ReadLine();
      if (!ValidInput(input)) goto Unepic;
      if (input == code) 
      { 
        hasWon = true; 
        goto End; 
      }
      WriteHints(input, code);
      PromptUserToPressKey();
      turn += 1;
    }


尝试从此模板开始编写代码。请注意,当您从代码读取的位置开始时(如工作流的抽象描述),它迫使代码成为多么清晰和逻辑。使每个方法都是这样,您可以在其中从六行代码中读取逻辑工作流。在您还是初学者的时候养成良好的习惯。

评论


\ $ \ begingroup \ $
听这个人的建议。如果我从@ericlippert获得了这样的代码审查,我将感到荣幸!很彻底
\ $ \ endgroup \ $
–史蒂夫
18年8月31日在2:11

\ $ \ begingroup \ $
调用因子分解法的方法是通过转弯,而不是通过maxTurns-1-转弯,也许先分解为剩余的Turns变量会更好,例如剩余转数=最大转数-1-转; Console.WriteLine($“您还剩{remainingTurns} {{Plural(remainingTurns,” turn“)}}。”);
\ $ \ endgroup \ $
–约书亚·韦伯
18年8月31日在7:43



\ $ \ begingroup \ $
您的函数GenerateCode缺少几个+
\ $ \ endgroup \ $
– JAD
18年8月31日在9:52

\ $ \ begingroup \ $
一如既往的出色和透彻,但我认为您忽略了为什么Random应该在类级别而不是给定方法局部的原因。如图所示,它看起来只是为了更清洁的组织。
\ $ \ endgroup \ $
–里克·戴文(Rick Davin)
18年8月31日在13:41

#2 楼

不要使用无意义的名称。 GenRandomtrc1,...这些都不告诉我任何内容,并且会使您的代码变得晦涩难懂。


这不是传统的C#编码风格:

string Input, Code = Convert.ToString(c1); Code += c2; Code += c3; Code += c4;



while (t != 8)中的“ 8”和Console.WriteLine("You have {0} turn(s) left.",9-t);中的“ 9”可能链接在一起,因此我希望其中一个是具有描述性名称的const


goto很少在C#中使用。使用方法来分隔逻辑。


注释应解释原因,而不是原因。例如,// Checks if input is 4 characters long是没有意义的,因为我可以通过阅读代码来看到它。


这两行都检查输入的值:

try { Convert.ToInt16(Input); Convert.ToString(Input); } catch (FormatException) { goto Unepic; }
if (Input.Contains("0") || Input.Contains("7") || Input.Contains("8") || Input.Contains("9")) { goto Unepic; }


为什么不使用简单的正则表达式?


在两行检查输入之间,您将这行放在下面:

if (Input == Code) { w = true; goto End; }; // Checks if you've won


这对我来说毫无意义。在检查输入值是否正确之前,您应该先检查输入的有效性。

评论


\ $ \ begingroup \ $
“ gotos很少用在C#中”-我什至没有意识到它们是该语言的一部分...
\ $ \ endgroup \ $
–马修·菲茨·杰拉德·钱伯兰
18年8月30日在16:24

\ $ \ begingroup \ $
我知道它适用于PHP,但请注意适合goto的卡通-php.net/manual/en/control-structures.goto.php
\ $ \ endgroup \ $
–ggdx
18年8月30日在16:50

\ $ \ begingroup \ $
@ggdx漫画不是为PHP创建的。我认为它实际上是在引用C。原文:xkcd.com/292
\ $ \ endgroup \ $
– jkd
18年8月30日在23:44



\ $ \ begingroup \ $
@jkd可能通过IMO通过这种愚蠢的工具适用于几乎所有语言。
\ $ \ endgroup \ $
–ggdx
18年8月30日在23:46

\ $ \ begingroup \ $
@jkd许多计算机仍然可以使用!他们将其称为VB或VBA,并且它具有许多便捷的现代语言功能,这些功能被完全忽略了,因为该会计师在80年代学习了BASIC,并且不会因为尝试/追赶这样的时尚而改变自己的风格。
\ $ \ endgroup \ $
–莫妮卡基金的诉讼
18年8月31日在19:22

#3 楼

使用具有描述性的变量名,这样它们将有助于为无需阅读注释的任何人编写代码的文档。
例如,代替


bool w = false;



使用

bool HasWon = false;


这立即表明,这与最终获胜状态有关。同样,将“ t”更改为“ ValidTries”等。


从设计的角度来看,如果您确定输入无效,则应向用户编写一条消息以告诉他们为什么这样做是无效的,而不仅仅是再次提示他们。


而不是goto命令和标签,而是在需要重复的任何时候使用while循环或do... while。将您的两个退出条件(他们赢了或他们用尽了尝试)组合在主“保持比赛”循环上,然后尝试do...while循环以寻找有效的尝试。

所以在您想要类似以下的伪代码:

while (not HasWon and ValidTries < 8)

{
    do
    {
        // prompt them to try and read input
        // check validity of input and return error message if there is a problem
    } while (Input is not valid)

    HasWon = //check the answer is correct or not
    if (not HasWon)
    {
       ValidTries ++
       // print message that that try is wrong
     }
}
if (HasWon)
   // print winning message saying how many ValidTries it took
else
   // print message saying they ran out of tries and giving the correct answer