网站建设价格与哪些方面,凡客网站设计,自己建设网站怎么盈利,赣州行业网站建设现在开始参考书籍变为#xff1a;《C# 12 and .NET 8 – Modern Cross-Platform Development.Mark Price》
函数
Writing, Debugging, and Testing Functions
写函数Debug运行时 logging单元测试
写函数
一个有着 XML 注释的函数
这里直接举一个例子#xff1a;
Numbe…现在开始参考书籍变为《C# 12 and .NET 8 – Modern Cross-Platform Development.Mark Price》
函数
Writing, Debugging, and Testing Functions
写函数Debug运行时 logging单元测试
写函数
一个有着 XML 注释的函数
这里直接举一个例子
Numbers that are used to count are called cardinal numbers基数, for example, 1, 2, and 3. Whereas numbers that are used to order are ordinal numbers序数, for example, 1st, 2nd, and 3rd.
/// summary
/// Pass a 32-bit integer and it will be converted into its ordinal equivalent.
/// /summary
/// param namenumberNumber is a cardinal value e.g. 1, 2, 3, and so on./param
/// returnsNumber as an ordinal value e.g. 1st, 2nd, 3rd, and so on./returns
static string CardinalToOrdinal(int number)
{switch (number){case 11:case 12:case 13:return ${number}th;default:string numberAsText number.ToString();char lastDigit numberAsText[numberAsText.Length - 1];string suffix string.Empty;switch (lastDigit){case 1:suffix st;break;case 2:suffix nd;break;case 3:suffix rd;break;default:suffix th;break;}return ${number}{suffix};}
}…
留意一下这里初始化字符串
string suffix string.Empty;留意一下上面函数的注释这样可以利用 vscode 的插件 C# XML documentation comments 形成好看的函数提示。
关于 arguments 和 parameters 的简要说明
A brief aside about arguments and parameters
在日常使用中大多数开发人员会互换使用术语 arguments 和 parameters。严格来说这两个术语具有特定且略有不同的含义。但就像一个人可以既是父母又是医生一样这两个术语通常适用于同一件事。
一个 parameter 是函数定义中的变量形参例如下面Hire函数中的 startDate
void Hire(DateTime startDate)
{// Function implementation.
}一旦一个函数被调用。一个 argument 将作为我们传入函数参数的数据实参。例如下面的 when_t
DateTime when_t new(year: 2024, month: 11, day: 5);
Hire(whewhen_tn);可以在传入实参 argument 时指明形参 parameter 如下所示
DateTime when_t new(year: 2024, month: 11, day: 5);
Hire(startDate: when_t);当讨论到这次调用时startDate 是形参 parameter when 是实参 argument 。 当我们阅读官方文档时他们交替使用短语“命名参数和可选参数named and optional arguments”以及“命名参数和可选参数named and optional parameters”链接https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/named-and-optional-arguments 它变得很复杂因为单个对象可以充当形参和shican 具体取决于上下文。例如在 Hire 函数实现中startDate 参数可以作为实参传递给另一个函数如 SaveToDatabase如以下代码所示
void Hire(DateTime startDate)
{...SaveToDatabase(startDate, employeeRecord);...
}命名事物是计算中最困难的部分之一。
总而言之形参parameter定义了函数的输入调用函数时实参argument被传递给函数。 良好实践尝试根据上下文使用正确的术语但如果其他开发人员“误用”某个术语请不要对他们学究气。 …
在函数实现中使用 lambda
Using lambdas in function implementations
在 C# 中使用 表达一个函数的返回值。
例如
static int FibFunctional(uint term) term switch
{0 throw new ArgumentOutOfRangeException(),1 0,2 1,_ FibFunctional(term - 1) FibFunctional(term - 2)
};探索顶级程序、函数和命名空间
Exploring top-level programs, functions, and namespaces
我们之前了解到自从 C#10 与 .NET 6 之后控制台的默认的项目模板使用C#9引入的顶级程序特性。
一旦开始编写函数了解它们如何与自动生成的 Program 类及其 Main$ 方法一起工作非常重要。
在顶层程序中本地函数可以定义在文件底部
using static System.Console;
WriteLine(* Top-level functions example);
WhatsMyNamespace(); // Call the function.void WhatsMyNamespace() // Define a local function.
{WriteLine(Namespace of Program class: {0},arg0: typeof(Program).Namespace ?? null);
}… 函数不需要位于文件的底部但这是一种很好的做法而不是将它们与其他顶级语句混合在一起。类型如类必须在 Program.cs 文件的底部而不是在文件的中间声明否则您将看到编译器错误 CS8803如以下链接所示https://learn.microsoft.com/en-us/dotnet/csharp/languagereference/compiler-messages/cs8803. 最好在单独的文件中定义类等类型。 上述代码的运行结果
* Top-level functions example
Namespace of Program class: null本地函数自动生成什么
What is automatically generated for a local function?
编译器会自动生成一个带有 Main$ 函数的 Program 类然后将语句和函数移至 Main$ 方法内从而使该函数成为本地函数并重命名该函数如以下代码中突出显示的那样
using static System.Console;
partial class Program
{static void Main$(String[] args){WriteLine(* Top-level functions example);Main$g__WhatsMyNamespace|0_0(); // Call the function.void Main$g__WhatsMyNamespace|0_0() // Define a local function.{WriteLine(Namespace of Program class: {0},arg0: typeof(Program).Namespace ?? null);}}
}为了让编译器知道哪些语句需要去哪里必须遵循一些规则
名称空间导入语句using必须在 Program.cs 文件的顶部。Main$ 函数中的语句可以与 Program.cs 文件中间的函数混合。任何函数都将成为 Main$ 方法中的本地函数。
最后一点很重要因为本地函数有局限性比如它们不能用 XML 注释来记录它们。
定义一个有着 static 函数的 partial Program 类
Defining a partial Program class with a static function
更好的方法是将任何函数编写在单独的文件中并将它们定义为 Program 类的静态成员
我们可以在项目新建一个文件名为 Program.Functions.cs并在其中定义 partial Program 类
sing static System.Console;
// 不要定义名称空间所以这个类在默认空名称空间中
partial class Program
{static void WhatsMyNamespace(){WriteLine(Namespace of Program class: {0},arg0: typeof(Program).Namespace ?? null);}
}在 Program.cs 中修改为
using static System.Console;
WriteLine(* Top-level functions example);
WhatsMyNamespace(); // Call the function.输出和之前时一样的。
静态函数自动生成了什么
What is automatically generated for a static function?
当我们使用一个额外的单独的文件来定义 partial Program 类以及静态函数。编译器定义了一个有着 Main$ 函数的 Program 类并将我们的函数作为 Program 类的成员如下所示
using static System.Console;
partial class Program
{static void Main$(String[] args){WriteLine(* Top-level functions example);WhatsMyNamespace(); }static void WhatsMyNamespace() {WriteLine(Namespace of Program class: {0},arg0: typeof(Program).Namespace ?? null);}
}… 良好实践在单独的文件中创建您将在 Program.cs 中调用的任何函数并在部partial Program 类中手动定义它们。这会将它们合并到与 Main$ 方法同一级别的自动生成的 Program 类中而不是作为 Main$ 方法内的本地函数。 值得注意的是命名空间声明的确实。自动生成的 Program 类和显式定义的 Program 类都位于默认的 null 命名空间中。
警告不要为您的partial Program 类定义命名空间。如果这样做它将位于不同的命名空间中因此不会与自动生成的部分 Program 类融合。
可选地Program 类中的所有静态方法都可以显式声明为私有private方法但这其实是默认的。由于所有函数都将在 Program 类本身内调用因此访问修饰符并不重要。
调试
这里需要注意的是 Customizing breakpoints 条件断点。我们可以对设置断点的那个小红点右键选择编辑断点来设置断点发生时的条件包括表达式、命中次数(Hit count)。
另外还有一个东西叫 SharpPad可以用来 Dumping variables
对于经验丰富的 .NET 开发人员来说他们最喜欢的工具之一是 LINQPad。对于复杂的嵌套对象可以方便地将其值快速输出到工具窗口中。 尽管其名称如此LINQPad 不仅适用于 LINQ还适用于任何 C# 表达式、语句块或程序。如果您在 Windows 上工作我推荐它您可以通过以下链接了解更多信息https://www.linqpad.net。 对于跨平台工作的类似工具我们将使用 Visual Studio Code 的 SharpPad 扩展。
为了给项目添加 SharpPad 包我们需要在项目文件夹下的命令行输入
dotnet add package SharpPad点开项目 csproj 后发现该包已经被添加进来了
Project SdkMicrosoft.NET.SdkPropertyGroupOutputTypeExe/OutputTypeTargetFrameworknet7.0/TargetFrameworkImplicitUsingsenable/ImplicitUsingsNullableenable/Nullable/PropertyGroupItemGroupPackageReference IncludeSharpPad Version1.0.4 //ItemGroup/Project…
需要添加导入
using SharpPad;
using System.Threading.Tasks;并将 Main 方法的返回值改为 async Task
using System;
using SharpPad;
using System.Threading.Tasks;
using static System.Console;namespace Dumping
{class Program{static async Task Main(string[] args){var complexObject new{FirstName Petr,BirthDate new DateTime(year: 1972, month: 12, day: 25),Friends new[] { Amir, Geoff, Sal }};WriteLine($Dumping {nameof(complexObject)} to SharpPad.);await complexObject.Dump();}}
}运行后complexObject 变量就会被显示出来。但是注意要在 vscode 安装 SharpPad 插件。
VSCode 在 Debug 时使用内部终端
默认 debug 时 VScode 使用内部 DEBUG CONSOLE不允许使用 ReadLine 这种函数进行交互。
调出左侧调试的侧边栏带年纪 创建一个 launch.json 文件选择 C#。
然后点击 增加配置Add Configuration…按钮选择 .NET:Launch .NET Core Console App进行修改。
注释掉 preLaunchTask在 program 路径添加 the Debugging project folder after the workspaceFolder vari- able.在 program 路径change to net8.0.在 program 路径change project-name.dll to Debugging.dll.更改控制台设置 from internalConsole to integratedTerminal:
{version: 0.2.0,configurations: [{name: .NET Core Launch (console),type: coreclr,request: launch,//preLaunchTask: build,program: ${workspaceFolder}/Debugging/bin/Debug/net8.0/Debugging.dll,args: [],cwd: ${workspaceFolder},stopAtEntry: false,console: integratedTerminal}]
}…
热重载
Hot reloading during development
热重载是一项功能允许开发人员在应用程序运行时对代码应用更改并立即看到效果。这对于快速修复错误非常有用。热重载也称为编辑并继续Edit and Continue。 您可以在以下链接中找到可以进行的支持热重载的更改类型列表https://aka.ms/dotnet/hot-reload。 就在 .NET 6 发布之前一位 Microsoft 高级员工试图将该功能仅限于 Visual Studio从而引起了争议。幸运的是微软内部的开源团队成功推翻了这一决定。使用命令行工具仍然可以使用热重载。 热重载需用以下命令行启动程序
dotnet watch在修改完文件保存后程序即可自动应用新的代码无需重新启动。
例子
/*
* Visual Studio 2022: run the app, change the message, click Hot Reload.
* Visual Studio Code: run the app using dotnet watch, change the
message. */
using static System.Console;
using System.Threading.Tasks;while (true)
{WriteLine(Hello, Hot Reload!);await Task.Delay(2000);
}我们使用 dotnet watch 启动每 2 秒打印一次 “Hello…”我们将代码修改为 “GoodBye…”一会之后我们会发现每 2 秒 打印一次 “GoodBye…”。
日志
一些常见的第三方日志解决方案
Apache log4netNLogSerilog
有两种类型可用于向代码添加简单日志记录调试Debug和跟踪Trace。
Debug 用于添加在开发过程中写入的日志记录。Trace 用于添加在开发和运行时写入的日志记录
还有一对名为 Debug 和 Trace 的类型。 关于 Debug 类的更多信息: https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.debug Debug 和 Trace 类可以写入任何跟踪侦听器trace listener。跟踪侦听器是一种可以配置为在调用 Trace.WriteLine 方法时将输出写入指定位置的类型。 .NET Core 提供了多个跟踪侦听器您甚至可以通过继承 TraceListener 类型来创建自己的跟踪侦听器。 关于 TraceListener 派生的跟踪侦听器列表https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.tracelistener 写到默认的 trace listener
使用 Trace 和 Debug 需要导入名称空间
using System.Diagnostics;DefaultTraceListener 类被自动配置且将会写到 VSCODE 的 DEBUG CONSOLE 调试控制台窗口可以用过代码手动配置。
例如
using System.Diagnostics;
namespace Instrumenting
{class Program{static void Main(string[] args){Debug.WriteLine(Debug says, I am watching!);Trace.WriteLine(Trace says, I am watching!);}}
}启动调试后这两条都会在 “调试控制台” 中打印。如果 dotnet run 的话不会输出任何东西。
配置 trace listeners
配置为写到一个文本文件。
需要导入 System.IO 名称空间。
代码如下
using System.Diagnostics;
using System.IO;
namespace Instrumenting
{class Program{static void Main(string[] args){string logPath Path.Combine(Environment.GetFolderPath(
Environment.SpecialFolder.DesktopDirectory), log.txt);Console.WriteLine($Writing to: {logPath});TextWriterTraceListener logFile new(File.CreateText(logPath));Trace.Listeners.Add(logFile);// text writer 是有缓存的所以可以让 listeners 写完后自动调用 Flush() 刷新缓存。#if DEBUGTrace.AutoFlush true;#endifDebug.WriteLine(Debug says, I am watching!);Trace.WriteLine(Trace says, I am watching!);// Close the text file (also flushes) and release resources.Debug.Close();Trace.Close();}}
}输入文件的缓存机制可以提升性能但是在 dubug 是这种缓存机制可能导致混乱因为我们可能不能立即看到结果所以我们让 AutoFlush 为 true。
我们使用下面的命令运行代码
dotnet run --configuration Release什么都不会在命令行中打印而是默默地输出在了 log.txt 文件中
Trace says, I am watching!如果我们使用下面的命令运行代码
dotnet run --configuration Debug什么都不会在命令行中打印而是默默地输出在了 log.txt 文件中
Debug says, I am watching!
Trace says, I am watching!… 当使用调试配置运行时调试和跟踪都处于活动状态并将在调试控制台中显示其输出。使用发布配置运行时仅显示跟踪输出。因此您可以在整个代码中自由地使用 Debug.WriteLine 调用因为您知道在构建应用程序的发布版本时它们将被自动删除。 选择跟踪级别
Switching trace levels
即使在发布release后Trace.WriteLine 调用仍保留在代码中。因此如果能很好地控制什么时候输出那就太好了。这是我们可以通过跟踪开关trace switch来完成的事情。
跟踪开关trace switch的值可以通过一个数字或词来设置。如下所示
数字 number词 word描述0Off什么也不会输出1Error只输出错误2Warning输出错误和警告3Info输出错误、警告和信息4Verbose输出所有
这里的例子使用了更多的包 package
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Configuration.FileExtensions之后再打开项目 .csproj 文件 ItemGroup 一节中展示了这些添加的包
Project SdkMicrosoft.NET.SdkPropertyGroupOutputTypeExe/OutputTypeTargetFrameworknet7.0/TargetFrameworkImplicitUsingsenable/ImplicitUsingsNullableenable/Nullable/PropertyGroupItemGroupPackageReference IncludeMicrosoft.Extensions.Configuration Version8.0.0 /PackageReference IncludeMicrosoft.Extensions.Configuration.Binder Version8.0.1 /PackageReference IncludeMicrosoft.Extensions.Configuration.FileExtensions Version8.0.0 /PackageReference IncludeMicrosoft.Extensions.Configuration.Json Version8.0.0 //ItemGroup/Project…
我们在项目目录下添加一个文件 appsettings.json内容为
{PacktSwitch: {Value: Info, // Must be set to work with 7.0.3 or later.Level: Info // To work with 7.0.2 or earlier including .NET 6.}
}… 注意在其 IDE 如 VS2022 或 Rider 中需要将这个配置文件配置到输出目录中。 In Visual Studio 2022 and JetBrains Rider, in Solution Explorer, right-click appsettings.json, select Properties, and then in the Properties window, change Copy to Output Directory to Copy always. This is necessary because unlike Visual Studio Code, which runs the console app in the project folder, Visual Studio runs the console app in Instrumenting\bin\Debug\net8.0 or Instrumenting\bin\Release\net8.0. To confirm this is done correctly, review the element that was added to the project file, as shown in the following markup: ItemGroup
None Updateappsettings.jsonCopyToOutputDirectoryAlways/CopyToOutputDirectory
/None
/ItemGroupThe Copy to Output Directory property can be unreliable. In our code, we will read and output this file so we can see exactly what is being processed to catch any issues with changes not being copied correctly. 在代码中导入名称空间
using Microsoft.Extensions.Configuration;在 Main 函数中添加配置相关代码
string settingsFile appsettings.json;
string settingsPath Path.Combine(Directory.GetCurrentDirectory(), settingsFile);Console.WriteLine(Processing: {0}, settingsPath);
Console.WriteLine(--{0} contents--, settingsFile);
Console.WriteLine(File.ReadAllText(settingsPath));
Console.WriteLine(----);ConfigurationBuilder builder new();
builder.SetBasePath(Directory.GetCurrentDirectory());
builder.AddJsonFile(settingsFile,optional: false, reloadOnChange: true);IConfigurationRoot configuration builder.Build();var ts new TraceSwitch(displayName: PacktSwitch,description: This switch is set via a JSON config.);configuration.GetSection(PacktSwitch).Bind(ts);Console.WriteLine($Trace switch value: {ts.Value});
Console.WriteLine($Trace switch level: {ts.Level});Trace.WriteLineIf(ts.TraceError, Trace error);
Trace.WriteLineIf(ts.TraceWarning, Trace warning);
Trace.WriteLineIf(ts.TraceInfo, Trace information);
Trace.WriteLineIf(ts.TraceVerbose, Trace verbose);Debug.Close();
Trace.Close();
Console.WriteLine(Press enter to exit.);
Console.ReadLine();用一下命令行运行
dotnet run --configuration Release命令行输出
Processing: XXX\Instrumenting\appsettings.json
--appsettings.json contents--
{PacktSwitch: {Value: Info, // Must be set to work with 7.0.3 or later.Level: Info // To work with 7.0.2 or earlier including .NET 6.}}
----
Trace switch value: Info
Trace switch level: Infolog.txt
Trace says, I am watching!
Trace error
Trace warning
Trace information…
注意默认的 trace switch 级别为 Off即都不输出。
如果配置文件没有找到将会抛出 System.IO.FileNotFoundException 异常。
最后记得释放资源
Debug.Close();
Trace.Close();
Console.WriteLine(Press enter to exit.);
Console.ReadLine();在使用完Debug或Trace之前请注意不要关闭它们。如果你关闭它们后写的都会被丢弃。
记录关于源代码的信息
当您写入日志时您通常需要包含源代码文件的名称、方法的名称和行号。在 C# 10 及更高版本中您甚至可以将作为参数传递给函数的任何表达式作为字符串值获取以便您可以记录它们。
您可以通过用特殊属性修饰函数参数来从编译器获取所有这些信息如下所示
[CallerMemberName] string member
Sets the string parameter named member to the name of the method or property that is executing the method that defines this parameter.
将名为 member 的字符串参数设置为正在执行定义此参数的方法的方法或属性的名称。
[CallerFilePath] string filepath
Sets the string parameter named filepath to the name of the source code file that contains the statement that is executing the method that defines this parameter.
将名为 filepath 的字符串参数设置为源代码文件的名称该文件包含正在执行定义此参数的方法的语句。
[CallerLinebNumber] int line 0
Sets the int parameter named line to the line number in the source code file of the statement that is executing the method that defines this parameter.
将名为 line 的 int 参数设置为正在执行定义此参数的方法的语句的源代码文件中的行号。
[CallerArgumentExpression(nameof(argumentExpression))] string expression
Sets the string parameter named expression to the expression that has been passed to the parameter named argumentExpression.
将名为 expression 的字符串参数设置为已传递给名为 argumentExpression 的参数的表达式。 您必须通过为这些参数分配默认值来使它们成为可选参数。 示例代码
在项目中创建一个类文件名为 Program.Functions.cs。
内容为
using System.Diagnostics; // To use Trace.
using System.Runtime.CompilerServices; // To use [CallerX] attributespartial class Program
{static void LogSourceDetails(bool condition,[CallerMemberName] string member ,[CallerFilePath] string filepath ,[CallerLineNumber] int line 0,[CallerArgumentExpression(nameof(condition))] string expression ){Trace.WriteLine(string.Format([{0}]\n {1} on line {2}. Expression: {3},filepath, member, line, expression));}
}在主文件 Program.cs 中为类声明前面加 partial 关键字因为我们在另一个文件 Program.Functions.cs中添加了 Program 类的其他部分所以主文件中的类也是个不完整的类当然如果 Program.cs 中没有声明类和名称空间而是直接写的脚本则不用管这些。
关闭 Debug 和 Trace 前添加
int unitsInStock 12;
LogSourceDetails(unitsInStock 10);输出的 log.txt 文件中增加了
[D:\Liyi\coding\C#\StudyCSharp\Chapter04\Instrumenting\Program.cs]Main on line 48. Expression: unitsInStock 10… 我们只是在这个场景中编造一个表达式。在实际项目中这可能是由用户进行用户界面选择以查询数据库等动态生成的表达式。 单元测试
Unit testing
有些开发人员甚至遵循程序员在编写代码之前应该创建单元测试的原则这称为测试驱动开发Test-Driven DevelopmentTDD。
Microsoft 有一个专有的单元测试框架称为 MSTest。还有一个名为 NUnit 的框架。不过我们将使用免费开源的第三方框架 xUnit.net。这三个基本上做同样的事情。 xUnit 是由构建 NUnit 的同一团队创建的但他们修复了他们认为之前犯过的错误。 xUnit 更具可扩展性并且拥有更好的社区支持。
测试的类型
单元测试只是多种测试的一种。
单元测试Unit testing
测试最小的代码单元通常是方法或函数。单元测试是在与其依赖项隔离的代码单元上执行的如果需要可以通过模拟它们来执行。每个单元应该有多个测试一些具有典型输入和预期输出一些具有极端输入值来测试边界一些具有故意错误的输入来测试异常处理。
集成测试Integration testing
测试较小的单元和较大的组件是否可以作为一个软件一起工作。有时涉及与您没有源代码的外部组件集成。
系统测试System testing
测试软件运行的整个系统环境。
性能测试performance testing
测试您的软件的性能例如您的代码必须在 20 毫秒内向访问者返回充满数据的网页。
加载测试Load testing
测试您的软件在保持所需性能的同时可以同时处理多少个请求例如网站有 10,000 个并发访问者
用户验收测试User Acceptance testing
测试用户是否可以愉快地使用您的软件完成他们的工作。
创建需要测试的类库
Creating a class library that needs testing
本节的例子将会创建一个 类库 项目名为 CalculatorLib。
我们将项目下的文件 Class1.cs 重命名为 Calculator.cs内容改为
这里有一个故意的bug【deliberate故意的】
namespace CalculatorLib;public class Calculator
{public double Add(double a,double b){return a * b;}
}编译他
dotnet build然后添加测试项目xUnit Test Project [C#] / xunit project命名为 CalculatorLibUnitTests。
命令行可以这么写
dotnet new xunit -o CalculatorLibUnitTests
dotnet sln add CalculatorLibUnitTests还要添加项目引用之前要测试的项目CalculatorLib中可以选择使用命令行
dotnet add reference ../CalculatorLib/CalculatorLib.csproj或者直接在CalculatorLibUnitTests.csproj中修改配置文件增加要给 item group 来引用要测试的项目
ItemGroupProjectReference Include..\CalculatorLib\CalculatorLib.csproj /
/ItemGroup… 项目引用的路径可以使用正斜杠 / 或反斜杠 \因为路径由 .NET SDK 处理并根据当前操作系统的需要进行更改。 然后构建 CalculatorLibUnitTests 项目。
写单元测试
编写良好的单元测试将由三个部分组成
安排Arrange这部分将声明并实例化输入和输出的变量。执行Act这部分将执行您正在测试的单元。在我们的例子中这意味着调用我们想要测试的方法。断言Assert这部分将对输出做出一个或多个断言。断言是一种信念 belief 如果不正确则表明测试失败。例如当 2 和 2 相加时我们期望结果是 4。
现在我们为 Calculator 类写一些单元测试。
重命名 CalculatorLibUnitTests 项目下的 UnitTest1.cs 为 CalculatorUnitTests.cs 并对应地修改文件中的类名。并在其中导入 CalculatorLib 名称空间然后修改内容
using CalculatorLib;namespace CalculatorLibUnitTests;public class CalculatorUnitTests
{[Fact]public void TestAdding2And2(){// Arrange: Set up the inputs and the unit under test.double a 2;double b 2;double expected 4;Calculator calc new();// Act: Execute the function to test.double actual calc.Add(a, b);// Assert: Make assertions to compare expected to actual results.Assert.Equal(expected, actual);}[Fact]public void TestAdding2And3(){double a 2;double b 3;double expected 5;Calculator calc new();double actual calc.Add(a, b);Assert.Equal(expected, actual);}
}… Visual Studio 2022 仍然使用使用嵌套命名空间的旧项目项模板。上面的代码显示了 dotnet new 和 JetBrains Rider 使用的现代项目项模板该模板使用文件范围的命名空间。也就是不用 namespace 也用一个大括号包裹了 运行
在 VSCode 中如果您最近没有构建测试项目则构建 CalculatorLibUnitTests 项目以确保 C# Dev Kit 扩展中的新测试功能能够识别您编写的单元测试。
然后点击 查看view- 测试Testing然后发现左侧多出一个 TESTING 窗口。
点击刷新然后不断下拉即可找到两个测试函数注意测试函数应该有 [Fact] 特性 /Attribute。
点击上面的运行测试就会自动完成两个测试任务。
我们发现目前的代码会导致一个测试函数没有通过。
修 bug
其实是第二个测试函数 TestAdding2And3 写错了将其中的 expected 改为 6 。
再次运行测试就发现两个测试都通过了。
抛出与捕获异常
在第 3 章“控制流程、转换类型和处理异常”中我们向您介绍了异常以及如何使用 try-catch 语句来处理异常。但是只有当您有足够的信息来缓解问题时您才应该捕获并处理异常。如果不这样做那么您应该允许异常通过调用堆栈传递到更高级别。
使用错误与执行错误
Understanding usage errors and execution errors
使用错误Usage error使用错误是指程序员误用函数通常是将无效值作为参数传递。程序员可以通过更改代码以传递有效值来避免它们。当一些程序员第一次学习 C# 和 .NET 时他们有时认为异常总是可以避免的因为他们假设所有错误都是使用错误。使用错误应该在生产运行之前全部修复。
执行错误Execution errors执行错误是指运行时发生的某些事情无法通过编写“更好”的代码来修复。执行错误可以分为程序错误和系统错误。如果您尝试访问网络资源但网络已关闭您需要能够通过记录异常来处理该系统错误并且可能会暂时退出并重试。但有些系统错误例如内存不足根本无法处理。如果您尝试打开不存在的文件您也许能够捕获该错误并通过创建新文件以编程方式处理它。可以通过编写智能代码以编程方式修复程序错误。系统错误通常无法通过编程方式修复。
函数中常见的抛出异常
Commonly thrown exceptions in functions
您很少应该定义新类型的异常来指示使用错误。 .NET 已经定义了许多您应该使用的内容。
当使用参数定义您自己的函数时您的代码应该检查参数值如果它们的值会阻止您的函数正常运行则抛出异常。
例如如果传给一个函数的一个参数不应该是 null那么就抛出 ArgumentNullException 异常对于其他问题还可以抛出 ArgumentException, NotSupportedException, 或 InvalidOperationException。
对于任何异常请包含一条消息为必须阅读该消息的任何人通常是类库和函数的开发人员或者最终用户描述问题如下所示代码
static void Withdraw(string accountName, decimal amount)
{if (string.IsNullOrWhiteSpace(accountName)){throw new ArgumentException(paramName: nameof(accountName));}if (amount 0){throw new ArgumentOutOfRangeException(paramName: nameof(amount),message: ${nameof(amount)} cannot be negative or zero.);}// process parameters
}… 良好实践如果函数无法成功执行其操作则应将其视为函数失败并通过抛出异常来报告。 使用保护子句抛出异常
Throwing exceptions using guard clauses
其实可以不适用 new 来实例化一个异常我们可以使用用异常本身的静态函数。当在函数实现中用于检查参数值时它们称为保护子句guard clauses。其中一些是在 .NET 6 中引入的更多是在 .NET 8 中添加的。
常见的 guard clauses 如下所示
ArgumentExceptionThrowIfNullOrEmpty, ThrowIfNullOrWhiteSpace
ArgumentNullExceptionThrowIfNull
ArgumentOutOfRangeExceptionThrowIfEqual, ThrowIfGreaterThan, ThrowIfGreaterThanOrEqual, ThrowIfLessThan, ThrowIfLessThanOrEqual, ThrowIfNegative, ThrowIfNegativeOrZero, ThrowIfNotEqual, ThrowIfZero
这样我们就不用 if 判断然后抛出异常了。可以这样
static void Withdraw(string accountName, decimal amount)
{ArgumentException.ThrowIfNullOrWhiteSpace(accountName,paramName: nameof(accountName));ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount,paramName: nameof(amount));// process parameters
}…
调用堆栈
Understanding the call stack
.NET 控制台应用程序的入口点是 Program 类中的 Main 方法如果您已显式定义此类或 Main$如果它是由顶级程序功能为您创建的。
用一个例子展示。先创建一个类库项目 CallStackExceptionHandlingLib 。
重命名 Class1.cs 为 Processor.cs修改其内容为
using static System.Console;namespace CallStackExceptionHandlingLib;public class Processor
{public static void Gamma() // public s所以它能被外部调用{WriteLine(In Gamma);Delta();}private static void Delta() // private 所以它只能被内部调用{WriteLine(In Delta);File.OpenText(bad file path);}
}…
再添加一个控制台项目名为 CallStackExceptionHandling。为该项目添加一个对 CallStackExceptionHandlingLib 类库项目的引用
dotnet add reference ../CallStackExceptionHandlingLib/CallStackExceptionHandlingLib.csproj然后项目文件 .csproj 就会多出
ItemGroupProjectReference Include..\CallStackExceptionHandlingLib\CallStackExceptionHandlingLib.csproj /
/ItemGroup…
然后构建 CallStackExceptionHandling 项目来确保依赖项目被编译而且被复制到本地的 bin 目录中。
在 CallStackExceptionHandling 项目的 Program.cs 中修改内容
using CallStackExceptionHandlingLib; // To use Processor.
using static System.Console;WriteLine(In Main);
Alpha();void Alpha()
{WriteLine(In Alpha);Beta();
}
void Beta()
{WriteLine(In Beta);Processor.Gamma();
}运行后输出为
In Main
In Alpha
In Beta
In Gamma
In Delta
Unhandled exception. System.IO.FileNotFoundException: Could not find file XXX\Chapter04\CallStackExceptionHandling\bad file path.
File name: XXX\Chapter04\CallStackExceptionHandling\bad file path at Microsoft.Win32.SafeHandles.SafeFileHandle.CreateFile(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options)at Microsoft.Win32.SafeHandles.SafeFileHandle.Open(String fullPath, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable1 unixCreateMode) at System.IO.Strategies.OSFileStreamStrategy..ctor(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable1 unixCreateMode) at System.IO.Strategies.FileStreamHelpers.ChooseStrategyCore(String path, FileMode mode, FileAccess access, FileShare share, FileOptions options, Int64 preallocationSize, Nullable1 unixCreateMode)at System.IO.StreamReader.ValidateArgsAndOpenPath(String path, Encoding encoding, Int32 bufferSize)at System.IO.File.OpenText(String path)at CallStackExceptionHandlingLib.Processor.Delta() in XXX\Chapter04\CallStackExceptionHandlingLib\Processor.cs:line 15at CallStackExceptionHandlingLib.Processor.Gamma() in XXX\Chapter04\CallStackExceptionHandlingLib\Processor.cs:line 10at Program.Main$g__Beta|0_1() in XXX\Chapter04\CallStackExceptionHandling\Program.cs:line 15at Program.Main$g__Alpha|0_0() in XXX\Chapter04\CallStackExceptionHandling\Program.cs:line 10at Program.Main$(String[] args) in XXX\Chapter04\CallStackExceptionHandling\Program.cs:line 5请注意调用堆栈是颠倒的从下往上从外入内。从底部开始你会看到
第一个调用是自动生成的 Program 类中的 Main$ 入口点函数。这是字符串数组参数传入的地方。第二次调用是 Main$g__Alpha|0_0 函数。 C# 编译器将其添加为本地函数时将其从 Alpha 重命名。第三次调用是 Beta 函数被重命名为 Main$g__Beta|0_1()第四次调用是 Gamma 函数第五次调用时是 Delta 函数。这里抛出了找不到文件的异常。 良好实践除非您需要单步执行代码来调试它否则您应该始终在不附加调试器的情况下运行代码。在这种情况下不要附加调试器尤其重要因为如果这样做它将捕获异常并将其显示在 GUI 对话框中而不是像书中所示那样输出。 何处捕获异常
Where to catch exceptions
Programmers can decide if they want to catch an exception near the failure point or centralized higher up the call stack. This allows your code to be simplified and standardized. You might know that calling an exception could throw one or more types of exception, but you do not need to handle any of them at the current point in the call stack.
程序员可以决定是否要捕获故障点附近的异常或集中在调用堆栈的较高位置。这使您的代码得以简化和标准化。您可能知道调用异常可能会引发一种或多种类型的异常但您不需要在调用堆栈中的当前点处理任何异常
重新抛出异常
Rethrowing exceptions
有时您想要捕获异常记录它然后重新抛出它。例如如果您正在编写一个将从应用程序调用的低级类库您的代码可能没有足够的信息来以编程方式以智能方式修复错误但调用应用程序可能有更多信息并且能够。您的代码应该记录错误以防调用应用程序没有记录错误然后将其重新抛出调用堆栈以防调用应用程序选择更好地处理它。
有三种方法可以在 catch 块内重新抛出异常如下列表所示
要抛出捕获的异常及其原始调用堆栈请调用 throw。如果像像在调用堆栈中的当前级别抛出一样抛出捕获的异常请使用捕获的异常调用 throw例如 throw ex。这通常是不好的做法因为您丢失了一些可能对调试有用的信息但当您想要故意删除包含敏感数据的信息时这可能很有用。要将捕获的异常包装在另一个异常中该异常可以在消息中包含更多信息以帮助调用者理解问题抛出一个新的异常并将捕获的异常作为innerException参数传递。
如果我们调用 Gamma 函数时可能发生错误那么我们可以捕获异常并执行重新抛出异常的三种技术之一如以下代码所示
try
{Gamma();
}
catch (IOException ex)
{LogException(ex);// 就像他就这这里发生一样抛出异常// 这样将会丢失原本的调用堆栈不推荐throw ex;// 重新抛出捕获的异常并保留其原始调用堆栈。throw;// 抛出一个新的异常并将捕获的异常嵌套在其中。throw new InvalidOperationException(message: Calculation had invalid values. See inner exception for why.,innerException: ex);
}… 这段代码只是说明性的。你永远不会在同一个 catch 块中使用所有三种技术 我们将之前的 CallStackExceptionHandling 项目的 Program.cs 进行修改
void Beta()
{WriteLine(In Beta);try{Processor.Gamma();}catch (Exception ex){WriteLine($Caught this: {ex.Message});throw ex;}
}再次运行
首先编译器给出警告指出这样做会丢失信息
XXX\Chapter04\CallStackExceptionHandling\Program.cs(22,9): warning CA2200: 再次引发捕获到的异常会更改堆栈信息 (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2200) 如果直接用 throw; 就没事了。
相比于上次的输出多出来了这个
Caught this: Could not find file XXX\Chapter04\CallStackExceptionHandling\bad file path.…
实施测试者-执行者和尝试模式
Implementing the tester-doer and try patterns
测试者-执行者模式tester-doer pattern可以避免一些抛出的异常但不能完全消除它们。此模式使用一对函数一个执行测试另一个执行如果测试未通过则失败的操作
.NET 本身实现了这种模式。例如在通过调用 Add 方法将一项添加到集合 collection 之前您可以测试它是否是只读的这会导致 Add 失败从而引发异常
例如在从银行账户提款之前您可能会测试该账户是否没有透支如以下代码所示
if (!bankAccount.IsOverdrawn())
{bankAccount.Withdraw(amount);
}…
测试者-执行者模式会增加性能开销因此您还可以实现 try 模式(try pattern)该模式实际上将测试和执行部分组合到单个函数中正如我们在 TryParse 中看到的那样。
当您使用多个线程时测试者-执行者模式会出现另一个问题。在这种情况下一个线程调用测试函数它返回一个值表明可以继续。但随后另一个线程执行这会改变状态。然后原来的线程继续执行假设一切都很好但其实并不好。这称为条件竞争。这个主题太高级了本书无法介绍如何处理它。 良好实践优先使用尝试模式而不是测试者-执行者模式。 如果您实现自己的 try 模式函数并且失败请记住将 out 参数设置为其类型的默认值然后返回 false如以下代码所示
static bool TryParse(string? input, out Person value)
{if (someFailure){value default(Person);return false;}// Successfully parsed the string into a Person.value new Person() { ... };return true;
}… 更多信息了解更多关于异常详细信息https://learn.microsoft.com/en-us/dotnet/standard/exceptions/ …