教程:在 Visual Studio 中扩展 C# 控制台应用和调试(第 2 部分(共 2 部分)

在本教程系列的第 2 部分中,深入探讨日常开发所需的 Visual Studio 生成和调试功能。 这些功能包括管理多个项目、调试和引用非Microsoft包。

在本教程的第 1 部分中,你将使用在 本教程的第 1 部分 创建的 C# 控制台应用来探索 Visual Studio 集成开发环境(IDE)的功能。 本教程是两部分教程系列的第 2 部分。

在本教程中,你将完成以下任务:

  • 添加第二个项目。
  • 引用库并添加包。
  • 调试代码。
  • 检查已完成的代码。

先决条件

若要完成本文,可以使用以下任一计算器应用:

添加另一个项目

实际的代码涉及多个项目在一个方案中协同工作。 可以向你的计算器应用程序添加一个类库项目,该项目提供一些计算器功能。

在 Visual Studio 中,使用菜单命令 文件>添加新>项目 来添加新项目。 也可以右键单击“解决方案资源管理器”中的解决方案,从上下文菜单中添加项目。

  1. 在 解决方案资源管理器中,右键单击解决方案节点,然后选择“添加>新建项目。

  2. 在“添加新项目 窗口中,在”搜索“框中键入 类库。 选择 C# 类库 项目模板,然后选择“ 下一步”。

    类库项目模板选择的屏幕截图。

  3. 在 “配置新项目 ”屏幕上,输入项目名称 CalculatorLibrary,然后选择“ 下一步”。

  4. 在 “其他信息 ”屏幕上,选择 “.NET 10.0”,然后选择“ 创建”。

    Visual Studio 将创建新项目并将其添加到解决方案。

    解决方案资源管理器的屏幕截图,其中添加了 CalculatorLibrary 类库项目。

  5. 将 Class1.cs 文件重命名为 CalculatorLibrary.cs。 若要重命名文件,可以在 解决方案资源管理器 中右键单击该名称,然后选择“ 重命名”、“选择名称”,然后按 F2 或选择该名称,然后再次选择键入。

    一条消息可能会询问是否要重命名对文件中 Class1 的所有引用。 不管你如何回答,因为你在将来的步骤中替换了代码。

  6. 添加项目引用,该引用允许第一个项目使用新类库公开的 API。 右键单击 计算器 项目中的 依赖项 节点,然后选择 添加项目引用。

    “添加项目引用”菜单项的屏幕截图。

    此时将显示 引用管理器 对话框。 在此对话框中,可以添加对项目所需的其他项目、程序集和 COM DLL 的引用。

  7. 在 “引用管理器 ”对话框中,选中 CalculatorLibrary 的复选框,然后选择“ 确定”。

    “引用管理器”对话框的屏幕截图。

    项目引用显示在解决方案资源管理器的项目 节点下。

    包含项目引用的解决方案资源管理器的屏幕截图。

  8. 在 Program.cs中,选择 Calculator 类及其所有代码,然后按 Ctrl+X 将其剪切。 然后,在 CalculatorLibrary.cs中,将代码粘贴到 CalculatorLibrary 命名空间中。

    此外,在 Calculator 类之前添加 public,以在库外公开它。

    CalculatorLibrary.cs 现在应类似于以下代码:

     // CalculatorLibrary.cs
     namespace CalculatorLibrary
     {
         public class Calculator
         {
             public static double DoOperation(double num1, double num2, string op)
             {
                 double result = double.NaN; // Default value is "not-a-number" if an operation, such as division, could result in an error.
    
                 // Use a switch statement to do the math.
                 switch (op)
                 {
                     case "a":
                         result = num1 + num2;
                         break;
                     case "s":
                         result = num1 - num2;
                         break;
                     case "m":
                         result = num1 * num2;
                         break;
                     case "d":
                         // Ask the user to enter a non-zero divisor.
                         if (num2 != 0)
                         {
                             result = num1 / num2;
                         }
                         break;
                     // Return text for an incorrect option entry.
                     default:
                         break;
                 }
                 return result;
             }
         }
     }
    
  9. Program.cs 也有引用,但有错误指出 调用无法解决Calculator.DoOperation。 错误是因为 CalculatorLibrary 位于不同的命名空间中。 为了获得完全限定的引用,可以将 CalculatorLibrary 命名空间添加到 Calculator.DoOperation中的 调用中。

    // Program.cs
    result = CalculatorLibrary.Calculator.DoOperation(cleanNum1, cleanNum2, op);
    

    或者,可以尝试将 using 指令添加到 Program.cs 文件的开头:

    // Program.cs
    using CalculatorLibrary;
    

    添加 using 指令应允许从调用站点中删除 CalculatorLibrary 命名空间。

    如果 Program.cs 代码位于 Calculator 命名空间中,请将命名空间从 Calculator 重命名为 CalculatorProgram 以删除类名和命名空间名称之间的歧义。

引用 .NET 库:写入日志

可以使用 .NET Trace 类添加所有操作的日志,并将其写入文本文件。 Trace 类也可用于基本打印调试技术。 该 Trace 类位于 System.Diagnostics. 它使用 System.IO 类,如 StreamWriter.

  1. 首先在 using顶部添加 指令:

    // CalculatorLibrary.cs
    using System.Diagnostics;
    
  2. 使用Trace类时,必须保留对类的引用,该引用与文件流相关联。 这一要求意味着计算器作为对象效果更好。 在Calculator类的开头添加构造函数。

    此外,请删除 static 关键字,将静态 DoOperation 方法更改为成员方法。

    // CalculatorLibrary.cs
    public Calculator()
       {
           StreamWriter logFile = File.CreateText("calculator.log");
           Trace.Listeners.Add(new TextWriterTraceListener(logFile));
           Trace.AutoFlush = true;
           Trace.WriteLine("Starting Calculator Log");
           Trace.WriteLine(String.Format("Started {0}", System.DateTime.Now.ToString()));
       }
    
    public double DoOperation(double num1, double num2, string op)
       {
    
  3. 将日志输出添加到每个计算。 DoOperation 现在应类似于以下代码:

    // CalculatorLibrary.cs
    public double DoOperation(double num1, double num2, string op)
    {
         double result = double.NaN; // Default value is "not-a-number" if an operation, such as division, could result in an error.
    
         // Use a switch statement to do the math.
         switch (op)
         {
             case "a":
                 result = num1 + num2;
                 Trace.WriteLine(String.Format("{0} + {1} = {2}", num1, num2, result));
                 break;
             case "s":
                 result = num1 - num2;
                 Trace.WriteLine(String.Format("{0} - {1} = {2}", num1, num2, result));
                 break;
             case "m":
                 result = num1 * num2;
                 Trace.WriteLine(String.Format("{0} * {1} = {2}", num1, num2, result));
                 break;
             case "d":
                 // Ask the user to enter a non-zero divisor.
                 if (num2 != 0)
                 {
                     result = num1 / num2;
                     Trace.WriteLine(String.Format("{0} / {1} = {2}", num1, num2, result));
                 }
                     break;
             // Return text for an incorrect option entry.
             default:
                 break;
         }
         return result;
     }
    
  4. 回到 Program.cs,红色波浪下划线现在标记静态调用。 若要修复此错误,请在 calculator 循环前添加以下代码行来创建 while (!endApp) 变量:

    // Program.cs
    Calculator calculator = new Calculator();
    

    此外,修改 DoOperation 调用位置,使其以小写形式引用名为 calculator 的对象。 代码现在是成员调用,而不是对静态方法的调用。

    // Program.cs
    result = calculator.DoOperation(cleanNum1, cleanNum2, op);
    
  5. 再次运行应用。 完成后,右键单击 计算器 项目节点,然后选择 打开文件资源管理器中的文件夹。

  6. 在文件资源管理器中,导航到 bin/Debug/下的输出文件夹,然后打开 calculator.log 文件。 输出应如下所示:

    Starting Calculator Log
    Started 7/9/2020 1:58:19 PM
    1 + 2 = 3
    3 * 3 = 9
    

此时,CalculatorLibrary.cs 应类似于以下代码:

// CalculatorLibrary.cs
using System.Diagnostics;

namespace CalculatorLibrary
{
    public class Calculator
    {

        public Calculator()
        {
            StreamWriter logFile = File.CreateText("calculator.log");
            Trace.Listeners.Add(new TextWriterTraceListener(logFile));
            Trace.AutoFlush = true;
            Trace.WriteLine("Starting Calculator Log");
            Trace.WriteLine(String.Format("Started {0}", System.DateTime.Now.ToString()));
        }

        public double DoOperation(double num1, double num2, string op)
        {
            double result = double.NaN; // Default value is "not-a-number" if an operation, such as division, could result in an error.

            // Use a switch statement to do the math.
            switch (op)
            {
                case "a":
                    result = num1 + num2;
                    Trace.WriteLine(String.Format("{0} + {1} = {2}", num1, num2, result));
                    break;
                case "s":
                    result = num1 - num2;
                    Trace.WriteLine(String.Format("{0} - {1} = {2}", num1, num2, result));
                    break;
                case "m":
                    result = num1 * num2;
                    Trace.WriteLine(String.Format("{0} * {1} = {2}", num1, num2, result));
                    break;
                case "d":
                    // Ask the user to enter a non-zero divisor.
                    if (num2 != 0)
                    {
                        result = num1 / num2;
                        Trace.WriteLine(String.Format("{0} / {1} = {2}", num1, num2, result));
                    }
                    break;
                // Return text for an incorrect option entry.
                default:
                    break;
            }
            return result;
        }
    }
}

Program.cs 应类似于以下代码:

// Program.cs
using CalculatorLibrary;

namespace CalculatorProgram
{

    class Program
    {
        static void Main(string[] args)
        {
            bool endApp = false;
            // Display title as the C# console calculator app.
            Console.WriteLine("Console Calculator in C#\r");
            Console.WriteLine("------------------------\n");

            Calculator calculator = new Calculator();
            while (!endApp)
            {
                // Declare variables and set to empty.
                // Use Nullable types (with ?) to match type of System.Console.ReadLine
                string? numInput1 = "";
                string? numInput2 = "";
                double result = 0;

                // Ask the user to type the first number.
                Console.Write("Type a number, and then press Enter: ");
                numInput1 = Console.ReadLine();

                double cleanNum1 = 0;
                while (!double.TryParse(numInput1, out cleanNum1))
                {
                    Console.Write("This is not valid input. Please enter an integer value: ");
                    numInput1 = Console.ReadLine();
                }

                // Ask the user to type the second number.
                Console.Write("Type another number, and then press Enter: ");
                numInput2 = Console.ReadLine();

                double cleanNum2 = 0;
                while (!double.TryParse(numInput2, out cleanNum2))
                {
                    Console.Write("This is not valid input. Please enter an integer value: ");
                    numInput2 = Console.ReadLine();
                }

                // Ask the user to choose an operator.
                Console.WriteLine("Choose an operator from the following list:");
                Console.WriteLine("\ta - Add");
                Console.WriteLine("\ts - Subtract");
                Console.WriteLine("\tm - Multiply");
                Console.WriteLine("\td - Divide");
                Console.Write("Your option? ");

                string? op = Console.ReadLine();

                // Validate input is not null, and matches the pattern
                if (op == null || ! Regex.IsMatch(op, "[a|s|m|d]"))
                {
                   Console.WriteLine("Error: Unrecognized input.");
                }
                else
                { 
                   try
                   {
                       result = calculator.DoOperation(cleanNum1, cleanNum2, op); 
                       if (double.IsNaN(result))
                       {
                           Console.WriteLine("This operation will result in a mathematical error.\n");
                       }
                       else Console.WriteLine("Your result: {0:0.##}\n", result);
                   }
                   catch (Exception e)
                   {
                       Console.WriteLine("Oh no! An exception occurred trying to do the math.\n - Details: " + e.Message);
                   }
                }
                Console.WriteLine("------------------------\n");

                // Wait for the user to respond before closing.
                Console.Write("Press 'n' and Enter to close the app, or press any other key and Enter to continue: ");
                if (Console.ReadLine() == "n") endApp = true;

                Console.WriteLine("\n"); // Friendly linespacing.
            }
            return;
        }
    }
}

添加 NuGet 包:写入 JSON 文件

若要以 JSON 输出操作,这是用于存储对象数据的常用可移植格式,可以引用 Newtonsoft.Json NuGet 包。 NuGet 包是 .NET 类库的主要分发方法。

  1. 在 解决方案资源管理器中,右键单击 CalculatorLibrary 项目的 依赖项 节点,然后选择 管理 NuGet 包。

    快捷菜单上“管理 NuGet 包”的屏幕截图。

    NuGet 包管理器随即打开。

  2. 搜索并选择 Newtonsoft.Json 包,然后选择 安装。

    NuGet 包管理器中 Newtonsoft JSON NuGet 包信息的屏幕截图。

    如果系统提示你是否接受更改,请选择“确定”。

    Visual Studio 下载包并将其添加到项目中。 解决方案资源管理器的 包 节点中会出现一个新条目。

    在 CalculatorLibrary.cs 的开头为 using 添加 Newtonsoft.Json 指令。

    // CalculatorLibrary.cs
    using Newtonsoft.Json;
    
  3. 创建 JsonWriter 成员对象,并将 Calculator 构造函数替换为以下代码:

         // CalculatorLibrary.cs
         JsonWriter writer;
    
         public Calculator()
         {
             StreamWriter logFile = File.CreateText("calculatorlog.json");
             logFile.AutoFlush = true;
             writer = new JsonTextWriter(logFile);
             writer.Formatting = Formatting.Indented;
             writer.WriteStartObject();
             writer.WritePropertyName("Operations");
             writer.WriteStartArray();
         }
    
  4. 修改 DoOperation 方法以添加 JSON writer 代码:

         // CalculatorLibrary.cs
         public double DoOperation(double num1, double num2, string op)
         {
             double result = double.NaN; // Default value is "not-a-number" if an operation, such as division, could result in an error.
             writer.WriteStartObject();
             writer.WritePropertyName("Operand1");
             writer.WriteValue(num1);
             writer.WritePropertyName("Operand2");
             writer.WriteValue(num2);
             writer.WritePropertyName("Operation");
             // Use a switch statement to do the math.
             switch (op)
             {
                 case "a":
                     result = num1 + num2;
                     writer.WriteValue("Add");
                     break;
                 case "s":
                     result = num1 - num2;
                     writer.WriteValue("Subtract");
                     break;
                 case "m":
                     result = num1 * num2;
                     writer.WriteValue("Multiply");
                     break;
                 case "d":
                     // Ask the user to enter a non-zero divisor.
                     if (num2 != 0)
                     {
                         result = num1 / num2;
                     }
                     writer.WriteValue("Divide");
                     break;
                 // Return text for an incorrect option entry.
                 default:
                     break;
             }
             writer.WritePropertyName("Result");
             writer.WriteValue(result);
             writer.WriteEndObject();
    
             return result;
         }
    
  5. 添加一种方法,在用户完成输入操作数据后,来完成 JSON 语法。

     // CalculatorLibrary.cs
     public void Finish()
     {
         writer.WriteEndArray();
         writer.WriteEndObject();
         writer.Close();
     }
    
  6. 在 Program.cs结束时,在 return;之前,添加对 Finish的调用:

         // Program.cs
             // Add call to close the JSON writer before return
             calculator.Finish();
             return;
         }
    
  7. 生成并运行应用。 执行一些操作后,输入 n 命令关闭应用。

  8. 在文件资源管理器中打开 calculatorlog.json 文件。 应会看到如下内容:

    {
     "Operations": [
         {
         "Operand1": 2.0,
         "Operand2": 3.0,
         "Operation": "Add",
         "Result": 5.0
         },
         {
         "Operand1": 3.0,
         "Operand2": 4.0,
         "Operation": "Multiply",
         "Result": 12.0
         }
     ]
    }
    

调试:设置并命中断点

Visual Studio 调试器是一个功能强大的工具。 调试器可以单步执行代码,找出出现编程错误的位置。 然后,你可以了解需要进行哪些更正,并进行临时更改,以便可以继续运行应用。

  1. 在 Program.cs,在左边的边缘中,选择如下代码行。

    // Program.cs
    result = calculator.DoOperation(cleanNum1, cleanNum2, op);
    

    还可以在行中选择并选择 F9,或右键单击该行并选择“断点>”。

    显示的红点表示断点。 可以使用断点暂停应用并检查代码。 可以在任何可执行的代码行上设置断点。

    显示设置断点的屏幕截图。

  2. 生成并运行应用。 输入以下计算值:

    • 对于第一个数字,请输入 8。
    • 对于第二个数字,请输入 0。
    • 对于运算符,我们来增添一些趣味。 输入 d。

    应用将暂停创建断点的位置。 左侧的黄色指针和高亮显示的代码显示暂停的行。 突出显示的代码尚未运行。

    显示命中断点的屏幕截图。

    现在,应用暂停后,可以检查应用程序状态。

调试:查看变量

  1. 在突出显示的代码中,将鼠标悬停在变量上,如 cleanNum1 和 op。 这些变量的当前值分别 8 和 d 显示在数据提示中。

    显示查看 DataTip 的屏幕截图。

    调试代码时,检查变量是否保存预期的值通常对于解决问题至关重要。

  2. 在下面的窗格中,查看“局部变量”窗口。 如果关闭,请依次选择“调试”“窗口”>“局部变量”将其打开>。

    局部变量 窗口显示当前处于作用域中的每个变量及其值和类型。

    “局部变量”窗口的屏幕截图。

  3. 了解一下“自动变量”窗口。

    “自动变量”窗口类似于“局部变量”窗口,不同之处在于它显示应用暂停时所处的当前代码行前后紧跟的变量。

    说明

    如果未看到“自动”窗口,请选择“调试”“Windows”>“自动”>将其打开。

接下来,在调试器中逐句运行代码,这称为单步执行。

调试:单步执行代码

  1. 按 F11,或选择“调试”“单步执行”>。

    通过使用“单步执行”命令,应用执行当前语句并前进到下一个可执行语句,通常是下一行代码。 左侧的黄色指针始终指示当前语句。

    单步执行命令的截图。

    你刚刚单步执行到了 DoOperation 类中的 Calculator 方法。

  2. 若要获取程序流的分层查看,请查看 调用堆栈 窗口。 如果关闭,请依次选择“调试”“窗口”>“局部变量”将其打开>。

    调用堆栈窗口的屏幕截图。

    此视图显示由黄色指针指示的当前 Calculator.DoOperation 方法。 第二行显示从 Main中的 方法调用该方法的函数。

    调用堆栈 窗口显示调用方法和函数的顺序。 此外,通过此窗口还可以访问许多调试器功能问,例如从其快捷菜单访问“转到源代码”。

  3. 按 F10(或依次选择“调试”“不进入子函数”)几次,直到应用在 语句处暂停>switch。

    // CalculatorLibrary.cs
    switch (op)
    {
    

    “越过”命令类似于“进入”命令。 对于“单步跳过”命令,如果当前语句调用函数,调试器将在函数中运行代码。 在函数返回之前,它不会暂停执行。 如果对特定函数不感兴趣,"Step Over" 比 "Step Into" 更快。

  4. 再按 F10 一次,使应用在以下代码行上暂停。

    // CalculatorLibrary.cs
    if (num2 != 0)
    {
    

    此代码检查是否存在除以 0 的情况。 如果应用继续,它将引发一般异常(错误),但你可能想要尝试其他操作,例如在控制台中查看实际返回的值。 一个选项是使用名为“编辑并继续”的调试器功能 对代码进行更改,然后继续调试。 但是,暂时修改执行流有不同的技巧。

调试:测试临时更改

  1. 选择当前在 if (num2 != 0) 语句处暂停的黄色指针,然后将它拖到下面的语句:

    // CalculatorLibrary.cs
    result = num1 / num2;
    

    在此处拖动指针会导致应用完全跳过 if 语句,以便你可以看到在除以零时会发生什么情况。

  2. 按 F10 执行代码行。

  3. 如果将鼠标悬停在 result 变量上,则会显示 无穷大的值。 在 C# 中,Infinity 是除以零时的结果。

  4. 按 F5,或选择 调试>继续调试。

    无穷大符号在控制台中显示为数学运算的结果。

  5. 输入 n 命令来正确关闭应用。

代码完成

以下是完成所有步骤后 CalculatorLibrary.cs 文件的完整代码:

// CalculatorLibrary.cs
using Newtonsoft.Json;

namespace CalculatorLibrary
{
    public class Calculator
    {

        JsonWriter writer;

        public Calculator()
        {
            StreamWriter logFile = File.CreateText("calculatorlog.json");
            logFile.AutoFlush = true;
            writer = new JsonTextWriter(logFile);
            writer.Formatting = Formatting.Indented;
            writer.WriteStartObject();
            writer.WritePropertyName("Operations");
            writer.WriteStartArray();
        }

        public double DoOperation(double num1, double num2, string op)
        {
            double result = double.NaN; // Default value is "not-a-number" if an operation, such as division, could result in an error.
            writer.WriteStartObject();
            writer.WritePropertyName("Operand1");
            writer.WriteValue(num1);
            writer.WritePropertyName("Operand2");
            writer.WriteValue(num2);
            writer.WritePropertyName("Operation");
            // Use a switch statement to do the math.
            switch (op)
            {
                case "a":
                    result = num1 + num2;
                    writer.WriteValue("Add");
                    break;
                case "s":
                    result = num1 - num2;
                    writer.WriteValue("Subtract");
                    break;
                case "m":
                    result = num1 * num2;
                    writer.WriteValue("Multiply");
                    break;
                case "d":
                    // Ask the user to enter a non-zero divisor.
                    if (num2 != 0)
                    {
                        result = num1 / num2;
                    }
                    writer.WriteValue("Divide");
                    break;
                // Return text for an incorrect option entry.
                default:
                    break;
            }
            writer.WritePropertyName("Result");
            writer.WriteValue(result);
            writer.WriteEndObject();

            return result;
        }

        public void Finish()
        {
            writer.WriteEndArray();
            writer.WriteEndObject();
            writer.Close();
        }
    }
}

下面是 Program.cs的代码:

// Program.cs
using CalculatorLibrary;

namespace CalculatorProgram
{

    class Program
    {
        static void Main(string[] args)
        {
            bool endApp = false;
            // Display title as the C# console calculator app.
            Console.WriteLine("Console Calculator in C#\r");
            Console.WriteLine("------------------------\n");

            Calculator calculator = new Calculator();
            while (!endApp)
            {
                // Declare variables and set to empty.
                // Use Nullable types (with ?) to match type of System.Console.ReadLine
                string? numInput1 = "";
                string? numInput2 = "";
                double result = 0;

                // Ask the user to type the first number.
                Console.Write("Type a number, and then press Enter: ");
                numInput1 = Console.ReadLine();

                double cleanNum1 = 0;
                while (!double.TryParse(numInput1, out cleanNum1))
                {
                    Console.Write("This is not valid input. Please enter an integer value: ");
                    numInput1 = Console.ReadLine();
                }

                // Ask the user to type the second number.
                Console.Write("Type another number, and then press Enter: ");
                numInput2 = Console.ReadLine();

                double cleanNum2 = 0;
                while (!double.TryParse(numInput2, out cleanNum2))
                {
                    Console.Write("This is not valid input. Please enter an integer value: ");
                    numInput2 = Console.ReadLine();
                }

                // Ask the user to choose an operator.
                Console.WriteLine("Choose an operator from the following list:");
                Console.WriteLine("\ta - Add");
                Console.WriteLine("\ts - Subtract");
                Console.WriteLine("\tm - Multiply");
                Console.WriteLine("\td - Divide");
                Console.Write("Your option? ");

                string? op = Console.ReadLine();

                // Validate input is not null, and matches the pattern
                if (op == null || ! Regex.IsMatch(op, "[a|s|m|d]"))
                {
                   Console.WriteLine("Error: Unrecognized input.");
                }
                else
                { 
                   try
                   {
                       result = calculator.DoOperation(cleanNum1, cleanNum2, op); 
                       if (double.IsNaN(result))
                       {
                           Console.WriteLine("This operation will result in a mathematical error.\n");
                       }
                       else Console.WriteLine("Your result: {0:0.##}\n", result);
                   }
                   catch (Exception e)
                   {
                       Console.WriteLine("Oh no! An exception occurred trying to do the math.\n - Details: " + e.Message);
                   }
                }
                Console.WriteLine("------------------------\n");

                // Wait for the user to respond before closing.
                Console.Write("Press 'n' and Enter to close the app, or press any other key and Enter to continue: ");
                if (Console.ReadLine() == "n") endApp = true;

                Console.WriteLine("\n"); // Friendly linespacing.
            }
            calculator.Finish();
            return;
        }
    }
}

祝贺你完成本教程! 若要了解详细信息,请继续阅读以下内容: