CSharp入门

C# 又称“C Sharp”,是微软发布的一种简单、安全、稳定、通用的面向对象编程语言。

C# 是从 C/C++ 衍生出来的,它在继承 C/C++强大功能的同时,抛弃了 C/C++ 的一些复杂特性。C# 还和 Java 非常类似,仅仅在一些细节上有差别。

C# 运行在 .NET Framework 上,借助 C# 可以开发不同类型的应用程序:

  • 桌面应用程序;
  • 网络应用程序;
  • 分布式应用程序;
  • Web 服务应用程序;
  • 数据库应用程序等。

.NET Framework

.NET Framework 主要由四个部分构成,如下所示:

  • 公共语言运行库(CLR);
  • 框架类库(FCL);
  • 核心语言(WinForms、ASP.NET 和 ADO.NET);
  • 其他模块(WCF、WPF、WF、Card Space、LINQ、Entity Framework、Parallel LINQ、Task Parallel Library 等)。

4-220I0133321X5

CLR:公共语言运行库

CLR 全称为“Common Language Runtime”,它为 .NET 应用程序提供了一个托管的代码执行环境(类似 Java 中的虚拟机),是整个 .NET 框架的核心。实际上 CLR 是驻留在内存里的一段代码,负责程序执行期间的代码管理工作,例如内存管理、线程管理、安全管理、远程管理、即时编译等。

  • Base Class Library Support(基础类库):一个类库,为 .NET 应用程序提供了一些类;
  • Thread Support(线程支持):用来管理多线程应用程序的并行执行;
  • COM Marshaler(COM 封送处理程序):提供 COM 对象与应用程序之间的通信;
  • Type Checker(类型检查器):检查应用程序中使用的类型,并验证它们是否与 CLR 提供的标准类型匹配;
  • Code Manager(代码管理器):在程序运行时管理代码;
  • Garbage Collector(垃圾回收器):释放未使用的内存,并将其分配给新的应用程序;
  • Exception Handler(异常管理器):在程序运行时处理异常,避免应用程序运行失败;
  • Class Loader(类加载器):在运行时加载所有的类。

FCL:框架类库

FCL 全称为“Framework Class Library”,它是一个标准库,其中包含了成千上万个类,主要用于构建应用程序。FCL 的核心是 BCL(Base Class Library:基础类库),BCL 提供了 FCL 的基本功能。FCL 的基本组成如下所示:

WinForms

WinForms 是 Windows Forms 的简称,它是一种 .NET Framework 的智能客户端技术,用来开发可以在电脑中运行的应用程序,经常使用的记事本就是使用 WinForms 技术开发的。

ASP.NET

ASP.NET 是一个微软设计和开发的 Web 框架,于 2002 年 1 月首次发布,ASP.NET 中完美的集成了 HTML、CSS 和 JavaScript。可以使用 ASP.NET 来开发网站、Web 应用程序和 Web 服务

ADO.NET

ADO.NET 一个是 .Net Framework 的模块,由可用于连接、检索、插入和删除数据的类组成,主要用来开发能够与 SQL Server、Oracle 等数据库进行交互的应用程序。

WPF

WPF 全称为“Windows Presentation Foundation”,是微软推出的基于 Windows 的用户界面框架,主要用来设计 Windows 应用程序的用户界面。WPF 以前也叫“Avalon”,集成在 .NET Framework 中,2006 发布的 .NET Framework 3.0 是最早支持 WPF 的。

WCF

WCF 全称为“Windows Communication Foundation”,是由微软开发的支持数据通信的应用程序框架,中文翻译为 Windows 通讯开发平台。与 WPF 相同,WCF 最早也是集成在 .NET Framework 3.0 中,WCP、WPF 和 WF 被统称为新一代 Windows 操作系统以及 WinFX(Windows Vista 的托管代码编程模型)的三个重大应用程序开发类库。

WCF 整合了 Windows 通讯中的 .net Remoting、WebService、Socket 机制,并融合了 HTTP 和 FTP 的相关技术,因此尤其适合 Windows 平台上分布式应用的开发

WF

WF 全称为“Windows Workflow Foundation”,是微软提供的一项技术,其中提供 API、进程内工作流引擎和可重新托管的设计器,用来将长时间运行的进程实现为 .NET 应用程序中的工作流。

LINQ

LINQ 技术在 2007 年跟随 .NET Framework 3.5 一同发布,其全称为“Language Integrated Query”,是微软的一项技术,新增了一种自然查询的 SQL 语法到 .NET Framework 的编程语言中,当前支持 C# 以及 Visual Basic .NET 语言。

常用的LINQ方法包括:

  • Select: 投影操作,将集合的每个元素转换为另一个形式。
  • Where: 过滤操作,根据条件筛选集合的元素。
  • OrderBy/OrderByDescending: 排序操作。
  • GroupBy: 分组操作。
  • Aggregate: 聚合操作,可以进行累积计算。
  • Any/All: 用于判断集合中是否有任何一个元素满足条件,或者所有元素都满足条件。
  • First/FirstOrDefault: 获取集合中的第一个元素。
  • SelectMany: 将多个集合扁平化为一个集合。

linq语法在tolist的时候才真正执行代码,属于延迟执行

Parallel LINQ

Parallel LINQ 也叫 PLINQ,是对 LINQ 技术的并行实现,PLINQ 将 LINQ 语法的简洁和可靠性与并行编程的强大功能结合在一起,大大提高了使用 LINQ 时的运行速度。

开发环境搭建

mac版

完整参考此处

  1. 下载.net链接 , dotnet -info查看是否安装完成
  2. vscode安装C# Dev Kit 和 C#拓展
  3. dotnet new console -o 新项目文件夹路径 到新项目去

p.s. launch.json中添加 "console": "integratedTerminal",设置输出到终端

编译命令

1
2
3
4
5
6
7
8
9
10
11
12
13
#项目目录下
#编译发布版
dotnet build -c Release
#编译调试版
dotnet build -c Debug
#使用dotnet build实际上调用的是C#编译器(csc)

#查看已安装的nuget包依赖项
dotnet list package
#添加 NuGet 包。
dotnet add package
#删除 NuGet 包。
dotnet remove package

在Mac上使用NuGet时,您通常会使用dotnet命令行工具来执行NuGet操作。dotnet命令行工具是.NET Core的官方命令行工具,用于构建、运行和管理.NET应用程序。通过dotnet命令行工具,您可以使用NuGet来添加、删除和更新项目的依赖项。

1、列出Nuget本地的路径

1
dotnet nuget locals all --list

2、使用dotnet命令安装引用Nuget包

1
dotnet add package NLog

3、安装引用指版本使用-v

1
dotnet add package NLog -v 4.6.7

4、使用特定源安装引用Nuget包

1
dotnet add package Microsoft.AspNetCore.StaticFiles -s https://dotnet.myget.org/F/dotnet-core/api/v3/index.json

注意:执行命令的目录是要安装的项目的.csproj文件位置

5、指定项目.csproj文件位置

1
dotnet add ToDo.csproj package NLog -v 1.0.0

vscode Nuget Package Manager扩展插件

在VSCode的扩展插件中,搜索并且安装Nuget Package Manager扩展插件

使用ctrl + shift + p或者ctrl + p(mac下将ctrl替换成cmd)

输入> nuget ,在下拉框中选择>Nuget Package Manager:Add Package

输入需要安装的包名(不需要完整的包名,可以模糊搜索),进行搜索

mac配置c#开发环境更详细的参考

nuget包相关

在 .NET Core 和 .NET 5+ 项目中,NuGet 包默认存储在用户的全局包缓存中,而不是项目的 packages 文件夹中。全局包缓存的位置如下:

  • Windows: C:\Users\<username>\.nuget\packages
  • macOS/Linux: /Users/<username>/.nuget/packages/home/<username>/.nuget/packages

.net core下使用nuget包的相关命令要使用dotnet nuget ...这样的形式使用

nuget包的打包与发布

vs在类库项目属性中添加各种信息,记得勾选在构建时生成NuGet包选项,之后生成项目的时候会生成一个nupkg文件,到nuget官网上传页上传该文件就可以了

详细信息可以参考此处

更新

在nuget官网上新建API密钥

注意要在vs设置中确保使用一个未被占用的版本号,以便顺利完成发布

通过命令指定新生成的nupgk文件来更新: dotnet nuget push YourPackage.nupkg --source https://api.nuget.org/v3/index.json --api-key YourApiKey

CSharp概述

C# 之所以能称为一门被广泛应用的编程语言,原因有以下几点:

  • C# 是一种现代的通用的编程语言;
  • C# 是面向对象的;
  • C# 是面向组件的;
  • C# 简单易学;
  • C# 是一种结构化语言;
  • 使用 C# 开发效率很高;
  • C# 可以在各种计算机平台上进行编译;(相对于 Java 的“一次编写,到处运行”,C# 的跨平台性可能稍显不足。)
  • C# 是 .Net Framework 的一部分。

以下是 C# 的一些重要功能的列表:

  • 布尔条件;
  • 自动垃圾回收;
  • 标准库;
  • 组件版本;
  • 属性和事件;
  • 委托和时间管理;
  • 易于使用的泛型;
  • 索引器;
  • 条件编译;
  • 简单的多线程;
  • LINQ 和 Lambda 表达式;
  • 集成 Windows。

借助 C# 编程语言,可以开发不同类型且安全可靠的应用程序,例如:

  • 桌面应用程序;
  • 网络应用程序;
  • 分布式应用程序;
  • Web 服务应用程序;
  • 数据库应用程序等。

C# 中的关键字是编译器预先定义好的一些单词,也可以称为保留字或者保留标识符,这些关键字对编译器有特殊的意义,不能用作标识符。但是,如果您非要使用的话也不是没有办法,只需要在关键字前面加上@前缀即可,例如@if就是一个有效的标识符,而if则是一个关键字。

在 C# 中,有些关键字在代码的上下文中具有特殊的意义,例如 get 和 set,这样的关键字被称为上下文关键字(contextual keywords)。一般来说,C# 语言中新增的关键字都会作为上下文关键字,这样可以避免影响到使用旧版语言编写的 C# 程序。

下图列出了 C# 中的保留关键字(Reserved Keywords)和上下文关键字(Contextual Keywords)

image-20240320132236392

数据类型

C# 语言中内置了一些基本的数据类型,数据类型用来指定程序中变量可以存储的数据的类型,C# 中的数据类型可以大致分为三类:

  • 值类型(Value types);
  • 引用类型(References types);
  • 指针类型(Pointer types)。

在C#中,不需要像C++中函数传参的时候考虑应该用引用传递还是值传递,因为:

在C#中,传递对象时的行为取决于该对象的类型。值类型和引用类型

  • 值类型:当你传递一个值类型的变量时,C#默认进行值复制,也就是说,函数内部得到的是原始变量的一个副本,对这个副本的修改不会影响到原始变量。但是,你可以使用refout关键字来按引用传递值类型,这样函数内部对参数的修改会影响到原始变量。
  • 引用类型:当你传递一个引用类型的变量时,C#默认进行引用传递,也就是说,函数内部得到的是原始对象的引用,对这个引用的修改会影响到原始对象。但是,如果你修改了引用本身(即让它指向一个新的对象),这个修改不会影响到原始变量,因为这个修改只改变了函数内部的引用,而没有改变原始变量的引用。

值类型

C# 中的值类型是从 System.ValueType 类中派生出来的,对于值类型的变量可以直接为其分配一个具体的值。当声明一个值类型的变量时,系统会自动分配一块儿内存区域用来存储这个变量的值,需要注意的是,变量所占内存的大小会根据系统的不同而有所变化。

C# 中的值类型有很多,如下表所示:

image-20240320132506645

引用类型

用类型的变量中并不存储实际的数据值,而是存储的对数据(对象)的引用,换句话说就是,引用类型的变量中存储的是数据在内存中的位置。当多个变量都引用同一个内存地址时,如果其中一个变量改变了内存中数据的值,那么所有引用这个内存地址的变量的值都会改变。

C# 中内置的引用类型包括

  • Object(对象)
  • Dynamic(动态)
  • string(字符串)

对象类型(Object)

对象类型是 C# 通用类型系统(Common Type System:CTS)中所有数据类型的最终基类,Object 是 System.Object 类的别名。任何类型的值都可以分配给对象类型,但是在分配值之前,需要对类型进行转换。

将值类型转换为对象类型的过程被称为“装箱”,反之将对象类型转换为值类型的过程则被称为“拆箱”。注意,只有经过装箱的数据才能进行拆箱。

类型检查在编译时进行的

动态类型(Dynamic)

您可以在动态类型的变量中存储任何类型的值,这些变量的类型检查是在程序运行时进行的。动态类型的声明语法如下所示:

dynamic <variable_name> = value;

例如:

dynamic d = 20;

类型检查在程序运行时进行的

字符串类型(String)

字符串类型的变量允许您将一个字符串赋值给这个变量,字符串类型需要通过 String 类来创建,String 类是 System.String 类的别名,它是从对象(Object)类型中派生的。在 C# 中有两种定义字符串类型的方式,分别是使用" "@" "

1
2
3
4
//使用引号的声明方式
String str = "http://helloworld/";
//使用 @ 加引号的声明形式
@"http://helloworld/";

使用@" "形式声明的字符串称为“逐字字符串”,逐字字符串会将转义字符\当作普通字符对待,例如string str = @"C:\Windows";等价于string str = "C:\\Windows";

另外,在@" "形式声明的字符串中可以任意使用换行,换行符及缩进空格等都会计算在字符串的长度之中。

指针类型

C# 语言中的指针是一个变量,也称为定位器或指示符,其中可以存储另一种类型的内存地址。C# 中的指针与 C 或 C++ 中的指针具有相同的功能。

变量

变量可以理解为是程序可以操作的内存区域的名称,在 C# 中每个变量都有自己特定的类型,这个类型确定了变量所占内存的大小、布局、取值范围以及可以对该变量执行的操作。

可以将变量当作一种通过符号(变量名)表示某个内存区域的方法,变量的值可以更改,并且可以多次重复使用。C# 中的基本变量类型可以归纳为以下几种:

类型 示例
整型(整数类型) sbyte、byte、short、ushort、int、uint、long、ulong、char
浮点型 float、double
十进制类型 decimal
布尔型 true、false
空类型 可为空值的数据类型
1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
namespace helloworld{
class Program {
static void Main(string[] args) {
int a, b;
Console.WriteLine("请输入第一个数字:");
a = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("请输入第二个数字:");
b = Convert.ToInt32(Console.ReadLine());
Console.WriteLine("{0}+{1}={2}", a, b, a+b);
}
}
}

因为使用 Console.ReadLine() 接收的数据是字符串格式的,所以使用 Convert.ToInt32() 函数来将用户输入的数据转换为 int 类型。

C#中的表达式非

  • 左值表达式 Lvalues
  • 右值表达式 Rvalues

CSharp可空类型

在 C# 1.x 的版本中,一个值类型的变量是不可以被赋值为 null(空值)的,否则会产生异常。而在 C# 2.0 中,新增了一个 nullable 类型,可以使用 nullable 类型定义包含 null 值的数据,例如,您可以在 nullable (可为空的 int32 类型)类型的变量中存储 -2147483648 到 2147483647 之间的任何值或者 null。同样,您可以在 nullable (可为空的 bool 类型)类型的变量中存储 true、false 或 null。声明可空类型的语法如下:

data_type? variable_name = null;

其中,data_type 为要声明的数据类型,后面紧跟一个问号;variable_name 则为变量的名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;

namespace helloworld
{
class Demo
{
static void Main(string[] args){
int? num1;
int? num2 = 123;
num1 = null;

double? num3 = new double?();
double? num4 = 3.1415926;
bool? boolval = null;

// 输出这些值
Console.WriteLine("num1 = {0} \r\n num2 = {1} \r\n num3 = {2} \r\n num4 = {3} \r\n boolval = {4}", num1, num2, num3, num4, boolval);
Console.ReadLine();
}
}
}
// num1 =
// num2 = 123
// num3 =
// num4 = 3.1415926
// boolval =

Null 合并运算符(??)

在 C# 中 Null 合并运算符用于定义可空类型和引用类型的默认值。如果此运算符的左操作数不为 null,那么运算符将返回左操作数,否则返回右操作数。例如表达式a??b中,如果 a 不为空,那么表达式的值则为 a,反之则为 b。

需要注意的是,Null 合并运算符左右两边操作数的类型必须相同,或者右操作数的类型可以隐式的转换为左操作数的类型,否则将编译错误。

num3 = num1 ?? 321;

类型转换

隐式类型转换

一种数据类型(类型 A),只要其取值范围完全包含在另一种数据类型(类型 B)的取值范围内,那么类型 A 就可以隐式转换为类型 B。基于这一特性,C# 的隐式类型转换不会导致数据丢失。

显示类型转换

显式类型转换也叫强制类型转换,这种转换需要使用(type)value的形式或者预定义函数显式的完成,显式转换需要用户明确的指定要转换的类型,而且在转换的过程中可能会造成数据丢失

C# 中还提供了一系列内置的类型转换方法,如下表所示:

方法 描述
ToBoolean 将类型转换为布尔型
ToByte 将类型转换为字节类型
ToChar 将类型转换为单个 Unicode 字符类型
ToDateTime 将类型(整数或字符串类型)转换为日期时间的结构
ToDecimal 将浮点型或整数类型转换为十进制类型
ToDouble 将类型转换为双精度浮点型
ToInt16 将类型转换为 16 位整数类型
ToInt32 将类型转换为 32 位整数类型
ToInt64 将类型转换为 64 位整数类型
ToSbyte 将类型转换为有符号字节类型
ToSingle 将类型转换为小浮点数类型
ToString 将类型转换为字符串类型
ToType 将类型转换为指定类型
ToUInt16 将类型转换为 16 位无符号整数类型
ToUInt32 将类型转换为 32 位无符号整数类型
ToUInt64 将类型转换为 64 位无符号整数类型

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;

namespace helloworld{
class StringConversion {
static void Main(string[] args) {
int i = 75;
float f = 53.005;
double d = 2345.7652;
bool b = true;

Console.WriteLine(i.ToString());
Console.WriteLine(f.ToString());
Console.WriteLine(d.ToString());
Console.WriteLine(b.ToString());
Console.ReadKey();
}
}
}
//75
//53.005
//2345.7652
//True

运算符优先级

image-20240321123105179

CSharp foreach循环

除了前面介绍的几种循环语句外,C# 同样也支持 foreach 循环,使用 foreach 可以遍历数组或者集合对象中的每一个元素,其语法格式如下:

1
2
3
foreach(数据类型 变量名 in 数组或集合对象){
语句块;
}

CSharp函数/方法

格式如下:

1
2
3
4
5
6
Access_Specifier Return_Type FunctionName(Parameter List)
{
Function_Body
Return_Statement
}
//需要注意的是,访问权限修饰符是可以省略,省略后默认为private

静态函数

C# 中的静态函数指的是,在一个类中使用 static 修饰的函数,调用静态函数比调用普通函数要简单很多,只需要函数名即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using System;
namespace helloworld
{
class Demo
{
static void Main(string[] args){
string msg = Output("http://helloworld/"); // 调用类中的静态函数
Console.WriteLine(msg);
}
/*
* 定义一个函数,该函数可以接收一个字符串参数,
* 并返回一个字符串
*/
static string Output(string message){
string str = "欢迎访问:" + message;
return str;
}
}
}

CSharp封装

C# 中的访问权限修饰符有以下几种:

  • public:公共的,所有对象都可以访问,但是需要引用命名空间;
  • private:私有的,类的内部才可以访问;
  • internal:内部的,同一个程序集的对象可以访问,程序集就是命名空间;
  • protected:受保护的,类的内部或类的父类和子类中可以访问;
  • Protected internal:protected 和 internal 的并集,符合任意一条都可以访问。

CSharp值传递,引用传递,输出传递

方式 描述
值传递 值传递会复制参数的实际值并赋值给函数的形式参数,实参和形参使用的是两个不同内存位置中的值,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全
引用传递 引用传递会复制参数的内存位置并传递给形式参数,当形参的值发生改变时,同时也会改变实参的值
输出传递 输出传递可以一次返回多个值

引用传递

在 C# 中,需要使用 ref 关键字来使用引用传递

1
2
3
4
5
public void Func(ref int val){
val *= val;
Console.WriteLine("函数内部的值:{0}", val);
}
Obj.Func(ref val);

输出传递

使用 return 语句可以从函数中返回一个值,但是使用输出传递则可以从函数中一次性返回多个值。输出传递与引用传递相似,不同之处在于输出传递是将数据从函数中传输出来而不是传输到函数中。

在 C# 中,需要使用 out 关键字来使用输出传递,下面通过示例来演示一下:

1
2
3
4
5
6
public void getValue(out int x){
int temp = 11;
x = temp;
x *= x;
}
Obj.getValue(out val);

CSharp Array数组

1
2
3
4
5
6
7
8
9
10
11
int[] array1 = new int[10]                // 初始化一个长度为 10 的整型数组
double[] array2 = new double[5] // 初始化一个长度为 5 的浮点型数组

//不指定长度赋值
double[] arr1 = {96.5, 98.0, 99.5, 90.0};
int[] arr2 = {1, 2, 3, 4, 5, 6, 7, 8, 9};
double[] arr1 = new double[]{96.5, 98.0, 99.5, 90.0};
int[] arr2 = new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
//指定长度赋值
double[] arr1 = new double[4]{96.5, 98.0, 99.5, 90.0};
int[] arr2 = new int[10]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};

二维数组

可以使用arr[i, j]的形式来访问二维数组中的每个元素,其中 arr 为数组的名称,而 i 和 j 则是数组元素的索引,类似于表格中的行和列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//初始化二维数组
// 第一种方式
int[,] arr = new int[3,4]{
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
// 第二种方式
int[,] arr = new int[,]{
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
// 第三种方式
int[,] arr = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};

//访问二维数组中的元素
int a = arr[1, 0];

交错数组

C# 中的交错数组其实就是元素为数组的数组,换句话说就是交错数组中的每个元素都可以是维度和大小不同的数组,所以有时交错数组也被称为“数组的数组”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//定义并初始化
int[][] jaggedArray = new int[3][]; // 定义一个交错数组
jaggedArray[0] = new int[5]; // 对数组的第一个元素初始化
jaggedArray[1] = new int[4]; // 对数组的第二个元素初始化
jaggedArray[2] = new int[2]; // 对数组的第三个元素初始化

int[][] jaggedArray = new int[][]{
new int[] {1, 2, 3, 4, 5},
new int[] {6, 7, 8, 9},
new int[] {10, 11}
};
//还可以简写为:
int[][] jaggedArray = {
new int[] {1, 2, 3, 4, 5},
new int[] {6, 7, 8, 9},
new int[] {10, 11}
};

定义一个交错数组,并遍历数组中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;

namespace helloworld
{
class Demo
{
static void Main(string[] args){
int[][] arr = new int[3][]{
new int[]{31, 22, 16, 88},
new int[]{21, 54, 6, 77, 98, 52},
new int[]{112, 25}
};
// 遍历数组
for(int i = 0; i < arr.Length; i++){
for(int j = 0; j < arr[i].Length; j++){
Console.Write(arr[i][j]+" ");
}
Console.WriteLine();
}
Console.ReadLine();
}
}
}

交错数组和多维数组

交错数组中的元素不仅可以是一维数组,还可以是多维数组,例如下面的代码中定义了一个包含三个二维数组元素的一维交错数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int[][,] jaggedArray = new int[3][,]
{
new int[,] {
{1, 1},
{2, 3}
},
new int[,] {
{5, 8},
{13, 21},
{34, 55}
},
new int[,] {
{89, 144},
{233, 377},
{610, 987}
}
};
//访问
int a = jaggedArray[1][1,1] // 变量 a 的值为 21
int b = jaggedArray[2][0,0] // 变量 b 的值为 89

参数数组

某些情况下,在定义函数时可能并不能提前确定参数的数量,这时可以使用 C# 提供的参数数组,参数数组通常用于为函数传递未知数量的参数。

若要使用参数数组,则需要利用 params 关键字,语法格式如下:

访问权限修饰符 返回值类型 函数名(params 类型名称[] 数组名称)

使用参数数组时,既可以直接为函数传递一个数组作为参数,也可以使用函数名(参数1, 参数2, ..., 参数n)的形式传递若干个具体的值作为参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 public string getSum(params int[] arr){
int sum = 0;
string str = "";
foreach(int i in arr){
sum += i;
str += "+ " + i + " ";
}
str = str.Trim('+');
str += "= "+sum;
return str;
}
//调用方式1
string str = Obj.getSum(1, 2, 3, 4, 5, 6);
//调用方式2
int[] arr = {2, 4, 6, 8, 10};
string str2 = Obj.getSum(arr);

CSharp Array类

Array 类是 C# 中所有数组的基类,其中提供了一系列用来处理数组的操作,例如对数组元素进行排序、搜索数组中指定的元素等。

Array 类中提供了一系列属性,通过这些属性可以获取数组的各种信息。Array 类中的常用属性如下表所示:

属性

属性 描述
IsFixedSize 检查数组是否具有固定大小
IsReadOnly 检查数组是否为只读
IsSynchronized 检查是否同步对数组的访问(线程安全)
Length 获取数组中所有维度中元素的总数
LongLength 获取数组中所有维数中元素的总数,并返回一个 64 位整数
Rank 获取数组的秩(维数),例如一维数组返回 1,二维数组返回 2,依次类推
SyncRoot 用来获取一个对象,该对象可以用于同步对数组的访问

方法

方法 描述
Clear(Array, Int32, Int32) 将数组中指定范围内的元素设置为该元素所属类型的默认值
Copy(Array, Array, Int32) 从第一个元素开始拷贝数组中指定长度的元素,并将其粘贴到另一个数组中(从第一个元素开始粘贴),使用 32 位整数来指定要拷贝的长度
CopyTo(Array, Int32) 从指定的目标数组索引处开始,将当前一维数组的所有元素复制到指定的一维数组中,索引使用 32 位整数指定
GetLength 获取数组指定维度中的元素数,并返回一个 32 位整数
GetLongLength 获取数组指定维度中的元素数,并返回一个 64 位整数
GetLowerBound 获取数组中指定维度第一个元素的索引
GetType 获取当前实例的类型(继承自 Object )
GetUpperBound 获取数组中指定维度最后一个元素的索引
GetValue(Int32) 获取一维数组中指定位置的值
IndexOf(Array, Object) 在一个一维数组中搜索指定对象,并返回其首个匹配项的索引
Reverse(Array) 反转整个一维数组中元素的顺序
SetValue(Object, Int32) 设置一维数组中指定元素的值
Sort(Array) 对一维数组中的元素排序
ToString() 返回一个表示当前对象的字符串(继承自 Object)

如果想要了解有关 Array 类中的属性和方法的详细介绍,可以查阅 C# 的官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;

namespace helloworld
{
class Demo
{
static void Main(string[] args)
{
// 创建一个数组并赋值
int[] arr = new int[6] {15, 33, 29, 55, 10, 11 };
// 创建一个空数组
int[] arr2 = new int[6];
// 获取数组的长度
Console.WriteLine("数组 arr 的长度为:"+arr.Length);
// 为数组排序
Array.Sort(arr);
Console.Write("排序后的数组 arr 为:");
// 打印排序后的 arr
PrintArray(arr);
// 查找数组元素的索引
Console.WriteLine("\n数组 arr 中值为 29 的元素的索引为:"+Array.IndexOf(arr,29));
// 拷贝 arr 到 arr2
Array.Copy(arr, arr2, arr.Length);
Console.Write("打印数组 arr2:");
// 打印数组 arr2
PrintArray(arr2);
Array.Reverse(arr);
Console.Write("\n反序排列数组 arr: ");
PrintArray(arr);
}
// 遍历数组元素
static void PrintArray(int[] arr)
{
foreach (Object elem in arr)
{
Console.Write(elem+" ");
}
}
}
}

CSharp String字符串

在 C# 中,string(或 String)关键字是 System.String 类的别名,其中提供了定义字符串以及操作字符串的一系列方法,下面就来详细介绍一下。

1
2
// 使用 System.String.Empty 定义一个空字符串
string str2 = System.String.Empty;

属性

属性 描述
Chars[Int32] 获取指定字符在字符串中的位置
Length 获取当前 String 对象中的字符数(字符串的长度)

方法

方法 描述
Clone() 返回对此 String 实例的引用
Compare(String, String) 比较两个指定的 String 对象,并返回一个指示二者在排序顺序中的相对位置的整数
CompareOrdinal(String, String) 通过比较每个字符串中的字符,来比较两个字符串是否相等
CompareTo(String) 将一个字符串与另一个字符串进行比较
Concat(String, String) 连接两个指定的字符串
Contains(String) 判断一个字符串中是否包含零一个字符串
Copy(String) 将字符串的值复制一份,并赋值给另一个字符串
CopyTo(Int32, Char[], Int32, Int32) 从字符串中复制指定数量的字符到一个字符数组中
EndsWith(String) 用来判断字符串是否以指定的字符串结尾
Equals(String, String) 判断两个字符串是否相等
Format(String, Object) 将字符串格式化为指定的字符串表示形式
GetEnumerator() 返回一个可以循环访问此字符串中的每个字符的对象
GetHashCode() 返回该字符串的哈希代码
GetType() 获取当前实例的类型
GetTypeCode() 返回字符串的类型代码
IndexOf(String) 返回字符在字符串中的首次出现的索引位置,索引从零开始
Insert(Int32, String) 在字符串的指定位置插入另一个字符串,并返回新形成的字符串
Intern(String) 返回指定字符串的内存地址(返回的是引用)
IsInterned(String) 返回指定字符串的内存地址(返回的是可能为空的引用)
IsNormalized() 判断此字符串是否符合 Unicode 标准
IsNullOrEmpty(String) 判断指定的字符串是否为空(null)或空字符串(””)
IsNullOrWhiteSpace(String) 判断指定的字符串是否为 null、空或仅由空白字符组成
Join(String, String[]) 串联字符串数组中的所有元素,并将每个元素使用指定的分隔符分隔开
LastIndexOf(Char) 获取某个字符在字符串中最后一次出现的位置
LastIndexOfAny(Char[]) 获取一个或多个字符在字符串中最后一次出现的位置
Normalize() 返回一个新字符串,新字符串与原字符串的值相等,但其二进制表示形式符合 Unicode 标准
PadLeft(Int32) 返回一个指定长度的新字符串,新字符串通过在原字符串左侧填充空格来达到指定的长度,从而实现右对齐
PadRight(Int32) 返回一个指定长度的新字符串,新字符串通过在原字符串右侧填充空格来达到指定的长度,从而实现左对齐
Remove(Int32) 返回一个指定长度的新字符串,将字符串中超出长度以外的部分全部删除
Replace(String, String) 使用指定字符替换字符串中的某个字符,并返回新形成的字符串
Split(Char[]) 按照某个分隔符将一个字符串拆分成一个字符串数组,返回分割后的字符串数组
StartsWith(String) 判断字符串是否使用指定的字符串开头
Substring(Int32) 从指定的位置截取字符串
ToCharArray() 将字符串中的字符复制到 Unicode 字符数组
ToLower() 将字符串中的字母转换为小写的形式
ToLowerInvariant() 使用固定区域性的大小写规则将字符串转换为小写的形式
ToString() 将其它数据类型转换为字符串类型
ToUpper() 将字符串中的字母转换为大写形式
Trim() 删除字符串首尾的空白字符
TrimEnd(Char[]) 删除字符串尾部的空白字符
TrimStart(Char[]) 删除字符串首部的空白字符

上表中只列举了一些 String 类中常用方法,可以通过查阅 C# 的官方文档来了解 String 类中的全部的方法介绍。

CSharp struct结构体

在 C# 中,结构体也被称为结构类型(“structure type”或“struct type”),它是一种可封装数据和相关功能的值类型,在语法上结构体与类(class)非常相似,它们都可以用来封装数据,并且都可以包含成员属性和成员方法。

1
2
3
4
5
6
struct Books {
public string title;
public string author;
public string subject;
public int book_id;
};

在设计结构体时有以下几点需要注意:

  • 不能为结构体声明无参数的构造函数,因为每个结构体中都已经默认创建了一个隐式的、无参数的构造函数;
  • 不能在声明成员属性时对它们进行初始化,静态属性和常量除外;
  • 结构体的构造函数必须初始化该结构体中的所有成员属性;
  • 结构体不能从其他类或结构体中继承,也不能作为类的基础类型,但是结构类型可以实现接口;
  • 不能在结构体中声明析构函数。

C# 中的结构体与 C/C++ 中的结构体有很大的不同,在 C# 中结构体具有以下功能:

  • 结构体中可以具有方法、字段、索引、属性、运算符方法和事件;
  • 结构体中可以定义构造函数,但不能定义析构函数,需要注意的是,定义的构造函数不能没有参数,因为没有参数的构造函数是 C# 默认自动定义的,而且不能更改
  • 与类不同,结构体不能继承其他结构体或类;
  • 结构体不能用作其他结构体或类的基础结构;
  • 一种结构体可以实现一个或多个接口;
  • 结构体成员不能被设定为 abstract、virtual 或 protected;
  • 与类不同,结构体可以不用 New 操作符来实例化,当使用 New 操作符来实例化结构体时会自动调用结构体中的构造函数;
  • 如果不使用 New 操作符来实例化结构体,结构体对象中的字段将保持未分配状态,并且在所有字段初始化之前无法使用该结构体实例。

类和结构体的主要区别

  • 类是引用类型,结构体是值类型;
  • 结构体不支持继承,但可以实现接口;
  • 结构体中不能声明默认的构造函数。

类是引用类型,结构体是值类型;

在C#中,结构体是值类型,意味着当你创建一个结构体的实例时,实际上在内存中存储的是该实例的实际数据。当你将一个结构体赋值给另一个变量或作为参数传递时,会复制整个结构体的数据。相比之下,类是引用类型,意味着当你创建一个类的实例时,内存中存储的是对该实例的引用,而不是实际数据。当你将一个类的实例赋值给另一个变量或作为参数传递时,实际上是传递了对同一个对象的引用,而不是对象的副本。

CSharp enum枚举类型

枚举类型(也可以称为“枚举器”)由一组具有独立标识符(名称)的整数类型常量构成,在 C# 中枚举类型不仅可以在类或结构体的内部声明,也可以在类或结构体的外部声明,默认情况下枚举类型中成员的默认值是从 0 开始的,然后逐一递增。

在 C# 中可以使用 enum 关键字来声明枚举类型,语法格式如下所示:

1
2
3
enum enum_name{
enumeration list;
}

在使用枚举类型时有以下几点需要注意:

  • 枚举类型中不能定义方法;
  • 枚举类型具有固定的常量集;
  • 枚举类型可提高类型的安全性;
  • 枚举类型可以遍历。

默认情况下,枚举类型中的每个成员都为 int 类型,它们的值从零开始,并按定义顺序依次递增。但是也可以显式的为每个枚举类型的成员赋值,如下所示

1
2
3
4
5
6
7
enum ErrorCode
{
None,
Unknown,
ConnectionLost = 100,
OutlierReading = 200
}

遍历枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//使用 GetValues()  遍历枚举类型中的所有成员:
namespace helloworld
{
class Demo
{
enum Season {
winter = 10,
spring,
summer = 15,
autumn
};
static void Main(string[] args)
{
foreach(Season i in Enum.GetValues(typeof(Season))){
Console.WriteLine("{0} = {1}", i, (int)i);
}
Console.ReadKey();
}
}
}
//winter = 10
//spring = 11
//summer = 15
//autumn = 16

//使用 GetNames() 遍历枚举类型中的所有成员:
namespace helloworld
{
class Demo
{
enum Season {
winter = 10,
spring,
summer = 15,
autumn
};
static void Main(string[] args)
{
foreach(String s in Enum.GetNames(typeof(Season))){
Console.WriteLine(s);
}
Console.ReadKey();
}
}
}
//winter
//spring
//summer
//autumn

CSharp class类

类的定义语法格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<access specifier> class class_name
{
// 成员属性
<access specifier> <data type> variable1;
<access specifier> <data type> variable2;
...
<access specifier> <data type> variableN;
// 成员函数/成员方法
<access specifier> <return type> method1(parameter_list)
{
// 函数体
}
<access specifier> <return type> method2(parameter_list)
{
// 函数体
}
...
<access specifier> <return type> methodN(parameter_list)
{
// 函数体
}
}

语法说明如下:

  • <access specifier> 为访问权限修饰符,用来指定类或类中成员的访问规则,可以忽略不写,如果没有指定,则使用默认的访问权限修饰符,类的默认访问权限修饰符是 internal,类中成员的默认访问权限修饰符是 private;
  • class_name 为类的名称;
  • <data type> 为数据类型,用来指定成员属性的数据类型;
  • variable1、variable2 等为成员属性的名称,类似于变量名;
  • <return type> 为返回值类型,用来指定成员函数的返回值类型;
  • method1、method2 等为成员函数的名称。

对象

类和对象是不同的概念,类决定了对象的类型,但不是对象本身。另外,类是在开发阶段创建的,而对象则是在程序运行期间创建的。可以将对象看作是基于类创建的实体,所以对象也可以称为类的实例。

想要创建一个类的实例需要使用 new 关键字:

1
2
3
4
5
6
7
8
9
Student Object = new Student();

//类是引用类型
//虽然也可以像创建普通变量那样只创建一个 Student 类型的变量,而不使用 new 关键字实例化 Student 这个类
Student Object2;
//不过不建议使用这样的写法,因为此时的 Object2 只是一个 Student 类型的普通变量,它并没有被赋值,所以不能使用 Object2 来访问对象中的属性和方法。如果非要使用 Object2 的话,则可以将一个已经创建的对象赋值给它
Student Object3 = new Student();
Student Object2 = Object3;
//Object2 和 Object3 指向同一个 Student 对象,因此使用 Object3 对 Student 对象的任何操作也会影响到 Object2

构造函数

C# 中的构造函数有三种:

  • 实例构造函数;
  • 静态构造函数;
  • 私有构造函数。

实例构造函数

构造函数是类中特殊的成员函数,它的名称与它所在类的名称相同,并且没有返回值。当使用 new 关键字创建类的对象时,可以使用实例构造函数来创建和初始化类中的任意成员属性

只要创建 Person 类的对象,就会调用类中的实例构造函数

1
2
3
4
5
6
7
8
9
10
11
12
public class Person{
private string name;
private int age;
public Person(string n, int a)
{
name = n;
age = a;
}
// 类中剩余的成员
}
//调用类中的实例构造函数
Person P = new Person("张三", 18);

如果没有为类显式的创建构造函数,那么 C# 将会为这个类隐式的创建一个没有参数的构造函数(无参数构造函数),这个无参的构造函数会在实例化对象时为类中的成员属性设置默认值(关于 C# 中类型的默认值可以查阅《值类型》一节)。在结构体中也是如此,如果没有为结构体创建构造函数,那么 C# 将隐式的创建一个无参数的构造函数,用来将每个字段初始化为其默认值。

静态构造函数

静态构造函数用于初始化类中的静态数据或执行仅需执行一次的特定操作。静态构造函数将在创建第一个实例或引用类中的静态成员之前自动调用

静态构造函数具有以下特性:

  • 静态构造函数不使用访问权限修饰符修饰或不具有参数;
  • 类或结构体中只能具有一个静态构造函数;
  • 静态构造函数不能继承或重载;
  • 静态构造函数不能直接调用,仅可以由公共语言运行时 (CLR) 调用;
  • 用户无法控制程序中静态构造函数的执行时间;
  • 在创建第一个实例或引用任何静态成员之前,将自动调用静态构造函数以初始化类;
  • 静态构造函数会在实例构造函数之前运行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;

namespace helloworld
{
class Demo
{
public static int num = 0;
// 构造函数
Demo(){
num = 1;
}
// 静态构造函数
static Demo(){
num = 2;
}
static void Main(string[] args)
{
Console.WriteLine("num = {0}", num);
Demo Obj = new Demo();
Console.WriteLine("num = {0}", num);
Console.Read();
}
}
}
//当执行上面程序时,会首先执行public static int num = 0,接着执行类中的静态构造函数,此时 num = 2,然后执行 Main 函数里面的内容,此时打印 num 的值为 2,接着初始化 Demo 类,这时会执行类中的构造函数,此时 num 会重新赋值为 1,所以上例的运行结果如下所示:
//num = 2
//num = 1

私有构造函数

私有构造函数是一种特殊的实例构造函数,通常用在只包含静态成员的类中。如果一个类中具有一个或多个私有构造函数而没有公共构造函数的话,那么其他类(除嵌套类外)则无法创建该类的实例。

对于一些类并不需要实例化就用这种方式防止实例化

1
2
3
4
5
6
class NLog
{
// 私有构造函数
private NLog() { }
public static double e = Math.E; //2.71828...
}

上例中定义了一个空的私有构造函数,这么做的好处就是空构造函数可阻止自动生成无参数构造函数。需要注意的是,如果不对构造函数使用访问权限修饰符,则默认它为私有构造函数。

析构函数

与《构造函数》类似,C# 中的析构函数(也被称作“终结器”)同样是类中的一个特殊成员函数,主要用于在垃圾回收器回收类实例时执行一些必要的清理操作。

C# 中的析构函数具有以下特点:

  • 析构函数只能在类中定义,不能用于结构体;
  • 一个类中只能定义一个析构函数;
  • 析构函数不能继承或重载;
  • 析构函数没有返回值;
  • 析构函数是自动调用的,不能手动调用;
  • 析构函数不能使用访问权限修饰符修饰,也不能包含参数。

析构函数的名称同样与类名相同,不过需要在名称的前面加上一个波浪号~作为前缀,如下所示:

1
2
3
4
5
6
7
class Car
{
~Car() // 析构函数
{

}
}

注意:析构函数不能对外公开,所以我们不能在析构函数上应用任何访问权限修饰符。

CSharp this关键字

C# 中,可以使用 this 关键字来表示当前对象,日常开发中我们可以使用 this 关键字来访问类中的成员属性以及函数。不仅如此 this 关键字还有一些其它的用法

  1. 使用 this 表示当前类的对象

  2. 使用 this 关键字串联构造函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class Test
    {
    public Test()
    {
    Console.WriteLine("无参构造函数");
    }
    // 这里的 this()代表无参构造函数 Test()
       // 先执行 Test(),后执行 Test(string text)
    public Test(string text) : this() //此处
    {
    Console.WriteLine(text);
    Console.WriteLine("实例构造函数");
    }
    }
  3. 使用 this 关键字作为类的索引器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //可以理解为c++重载operator[]函数
    public class Test
    {
    int Temp0;
    int Temp1;
    public int this[int index]
    {
    get
    {
    return (0 == index) ? Temp0 : Temp1;
    }

    set
    {
    if (0==index)
    Temp0 = value;//注意这个value也是关键字
    else
    Temp1 = value;
    }
    }
  4. 使用 this 关键字作为原始类型的扩展方法

    扩展方法是对现有类型功能的一种补充,但这种补充并非改变原始类型的行为,而是为它们添加新的行为,使其看起来像是类型本身就具有这些方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Demo
    {
    static void Main(string[] args)
    {
    string str = "你好世界";
    string newstr = str.ExpandString();//调用拓展方法
    Console.WriteLine(newstr);
    }
    }
    public static class Test
    {
    public static string ExpandString(this string name)//定义给string类型的拓展方法
    {
    return name+" hello world";
    }
    }
    //上述代码中,ExpandString是一个拓展方法,它拓展了string类型,使其看起来拥有将字符串转换为标题格式的功能.这里的`this string name`就表示这个方法是拓展string类型的,调用时如同直接在`string`对象上调用该方法一样

    C#的扩展方法并不适用于原始类型,而是适用于任何用户自定义类型或.NET Framework内建的引用类型(如string)。原始类型如intdouble等不支持扩展方法。

CSharp 静态成员

在 C# 中,我们可以使用 static 关键字声明属于类型本身而不是属于特定对象的静态成员,因此不需要使用对象来访问静态成员。在类、接口和结构体中可以使用 static 关键字修饰变量、函数、构造函数、类、属性、运算符和事件。

注意:索引器和析构函数不能是静态的

若在定义某个成员时使用 static 关键字,则表示该类仅存在此成员的一个实例,也就是说当我们将一个类的成员声明为静态成员时,无论创建多少个该类的对象,静态成员只会被创建一次,这个静态成员会被所有对象共享。

静态属性

使用 static 定义的成员属性称为“静态属性”,静态属性可以直接通过类名.属性名的形式直接访问,不需要事先创建类的实例。静态属性不仅可以使用成员函数来初始化,还可以直接在类外进行初始化。

静态函数

除了可以定义静态属性外,static 关键字还可以用来定义成员函数,使用 static 定义的成员函数称为“静态函数”,静态函数只能访问静态属性

CSharp 静态类

C#中的静态类是一种特殊的类,它具备以下几个显著特征:

  1. 静态成员限定:静态类只能包含静态成员(字段、属性、方法、事件和嵌套类型),不能包含实例成员(非静态字段、非静态方法等)。这意味着你不能在静态类中定义构造函数,因为构造函数总是与实例关联的。
  2. 不可实例化:由于静态类仅包含静态成员,所以不能使用new关键字创建该类的实例。也就是说,静态类不能有实例生命周期,因为它不是为了创建对象而设计的。
  3. 静态构造函数:尽管静态类不能有实例构造函数,但可以有一个静态构造函数,它在第一次访问该类的任何静态成员之前自动调用,并且在整个程序中只调用一次。静态构造函数用于初始化静态类的静态数据成员或者执行必要的静态资源初始化。
  4. 单一实例和全局可见性:静态类的所有成员都是全局共享的,意味着对静态成员的任何更改都会影响到整个应用程序的所有使用者。
  5. 密封性:静态类在概念上类似于密封的抽象类,因为它既不能被继承也不能被实例化,仅仅作为一个组织相关静态成员的容器。
  6. 编译时常量:静态类常用于封装与类层次结构无关的全局常量、工具方法或服务,这些内容在整个应用程序域中只需要一份拷贝即可。
  7. 加载时机:.NET Framework公共语言运行库(CLR)会在加载包含静态类的程序集时自动加载此类及其成员。

总结来说,C#静态类的主要目的是组织和管理那些不需要与类实例关联的、全局可用的功能和数据。

CSharp 继承

C# 中只支持单继承,也就是说一个派生类只能继承一个基类

1
2
3
class 派生类 : 基类{
... ...
}

所有的C#类都隐式继承自System.Object类,该类提供了这些可以被子类重写的方法。

CSharp 接口

对应C++中的纯虚基类

接口可以看作是一个约定,其中定义了类或结构体继承接口后需要实现功能,接口的特点如下所示:

  • 接口是一个引用类型,通过接口可以实现多重继承;
  • 接口中只能声明”抽象”成员,所以不能直接对接口进行实例化;
  • 接口中可以包含方法、属性、事件、索引器等成员;
  • 接口名称一般习惯使用字母“I”作为开头(不是必须的,不这样声明也可以);
  • 接口中成员的访问权限默认为 public,所以我们在定义接口时不用再为接口成员指定任何访问权限修饰符,否则编译器会报错;
  • 在声明接口成员的时候,不能为接口成员编写具体的可执行代码,也就是说,只要在定义成员时指明成员的名称和参数就可以了;
  • 接口一旦被实现(被一个类继承),派生类就必须实现接口中的所有成员,除非派生类本身也是抽象类。

在 C# 中声明接口需要使用 interface 关键字,语法结构如下所示:

1
2
3
4
5
public interface InterfaceName{
returnType funcName1(type parameterList);
returnType funcName2(type parameterList);
... ...
}

其中,InterfaceName 为接口名称,returnType 为返回值类型,funcName 为成员函数的名称,parameterList 为参数列表。

在 C# 中,一个接口可以继承另一个接口,例如可以使用接口 1 继承接口 2,当用某个类来实现接口 1 时,必须同时实现接口 1 和接口 2 中的所有成员,下面通过一个示例来演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using System;

namespace helloworld
{
public interface IParentInterface
{
void ParentInterfaceMethod();
}

public interface IMyInterface : IParentInterface
{
void MethodToImplement();
}
class Demo : IMyInterface
{
static void Main(string[] args)
{
Demo demo = new Demo();
demo.MethodToImplement();
demo.ParentInterfaceMethod();
}
public void MethodToImplement(){
Console.WriteLine("实现 IMyInterface 接口中的 MethodToImplement 函数");
}
public void ParentInterfaceMethod(){
Console.WriteLine("实现 IParentInterface 接口中的 ParentInterfaceMethod 函数");
}
}
}
//实现 IMyInterface 接口中的 MethodToImplement 函数
//实现 IParentInterface 接口中的 ParentInterfaceMethod 函数

接口实现的多重继承

与单继承相反,多重继承则是指一个类可以同时继承多个基类,C# 并不支持多重继承,但是可以借助接口来实现多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
using System;
namespace helloworld
{
class Demo
{
static void Main(string[] args)
{
Rectangle oblong = new Rectangle();
oblong.setWidth(3);
oblong.setHeight(4);
int area = oblong.getArea();
int girth = oblong.getGirth();
Console.WriteLine("长方形的面积为:{0}", area);
Console.WriteLine("长方形的周长为:{0}", girth);
}
}
// 基类
class Shape{
protected int width, height;
public void setWidth(int w){
width = w;
}
public void setHeight(int h){
height = h;
}
}
// 定义接口
public interface Perimeter{
int getGirth();
}
// 派生类
class Rectangle : Shape, Perimeter{
public int getArea(){
return width*height;
}
public int getGirth(){
return (width+height)*2;
}
}
}

接口实现的泛型数据结构实例

1
List<IEquipment> equipmentList = new List<IEquipment>();

在C#中,接口是一种抽象类型,无法直接实例化。在这种情况下, List<IEquipment> 并不是实例化了一个接口,而是实例化了一个泛型列表,该列表可以存储实现了 IEquipment 接口的类的实例。因为接口可以被类实现,所以你可以将实现了 IEquipment 接口的类的实例添加到 equipmentList

但要注意:只有接口中定义的方法和属性才能被List对象访问

CSharp 多态

在 C# 中具有两种类型的多态:

  • 编译时多态:通过 C# 中的方法重载和运算符重载来实现编译时多态,也称为静态绑定或早期绑定;
  • 运行时多态:通过方法重载实现的运行时多态,也称为动态绑定或后期绑定。

编译时多态

在编译期间将函数与对象链接的机制称为早期绑定,也称为静态绑定。C# 提供了两种技术来实现编译时多态,分别是函数重载和运算符重载

函数重载

在同一个作用域中,可以定义多个同名的函数,但是这些函数彼此之间必须有所差异,比如参数个数不同或参数类型不同等等,返回值类型不同除外

运算符重载

C# 中支持运算符重载,所谓运算符重载就是我们可以使用自定义类型来重新定义 C# 中大多数运算符的功能。运算符重载需要通过 operator 关键字后跟运算符的形式来定义的,我们可以将被重新定义的运算符看作是具有特殊名称的函数,与其他函数一样,该函数也有返回值类型和参数列表,如下例所示:

1
2
3
4
5
6
7
8
9
// 重载 + 运算符,把两个 Box 对象相加
public static Box operator+ (Box b, Box c)
{
Box box = new Box();
box.length = b.length + c.length;
box.breadth = b.breadth + c.breadth;
box.height = b.height + c.height;
return box;
}
可重载与不可重载的运算符
运算符 可重载性
+、-、!、~、++、-- 这些一元运算符可以进行重载
`+、-、*、/、%、&、 、^、<<、>>、=、!=、<、>、<=、>=`
&&、
(type)var_name 强制类型转换运算符不能重载
`+=、-=、*=、/=、%=、&=、 =、^=、<<=、>>=`
^、=、.、?.、? : 、??、??=、..、->、=>、as、await、checked、unchecked、default、delegate、is、nameof、new、sizeof、stackalloc、switch、typeof 这些运算符无法进行重载

注意:比较运算符必须成对重载,也就是说,如果重载一对运算符中的任意一个,则另一个运算符也必须重载。比如==!=运算符、<>运算符、<=>=运算符。

演示如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
using System;

namespace helloworld
{
class Box
{
private double length; // 长度
private double breadth; // 宽度
private double height; // 高度

public double getVolume()
{
return length * breadth * height;
}
public void setLength( double len )
{
length = len;
}

public void setBreadth( double bre )
{
breadth = bre;
}

public void setHeight( double hei )
{
height = hei;
}
// 重载 + 运算符来把两个 Box 对象相加
public static Box operator+ (Box b, Box c)
{
Box box = new Box();
box.length = b.length + c.length;
box.breadth = b.breadth + c.breadth;
box.height = b.height + c.height;
return box;
}

public static bool operator== (Box lhs, Box rhs)
{
bool status = false;
if (lhs.length == rhs.length && lhs.height == rhs.height && lhs.breadth == rhs.breadth)
{
status = true;
}
return status;
}
public static bool operator!= (Box lhs, Box rhs)
{
bool status = false;
if (lhs.length != rhs.length || lhs.height != rhs.height || lhs.breadth != rhs.breadth)
{
status = true;
}
return status;
}
public override bool Equals(object o)
{
if(o==null) return false;
if(GetType() != o.GetType()) return false;
return true;
}
public override int GetHashCode()
{
return base.GetHashCode();
}
public static bool operator <(Box lhs, Box rhs)
{
bool status = false;
if (lhs.length < rhs.length && lhs.height < rhs.height && lhs.breadth < rhs.breadth)
{
status = true;
}
return status;
}

public static bool operator >(Box lhs, Box rhs)
{
bool status = false;
if (lhs.length > rhs.length && lhs.height > rhs.height && lhs.breadth > rhs.breadth)
{
status = true;
}
return status;
}

public static bool operator <=(Box lhs, Box rhs)
{
bool status = false;
if (lhs.length <= rhs.length && lhs.height <= rhs.height && lhs.breadth <= rhs.breadth)
{
status = true;
}
return status;
}

public static bool operator >=(Box lhs, Box rhs)
{
bool status = false;
if (lhs.length >= rhs.length && lhs.height >= rhs.height && lhs.breadth >= rhs.breadth)
{
status = true;
}
return status;
}
public override string ToString()
{
return String.Format("({0}, {1}, {2})", length, breadth, height);
}
}
class Demo
{
static void Main(string[] args)
{
Box Box1 = new Box(); // 声明 Box1,类型为 Box
Box Box2 = new Box(); // 声明 Box2,类型为 Box
Box Box3 = new Box(); // 声明 Box3,类型为 Box
Box Box4 = new Box();
double volume = 0.0; // 体积

// Box1 详述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);

// Box2 详述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);

// 使用重载的 ToString() 显示两个盒子
Console.WriteLine("Box1: {0}", Box1.ToString());
Console.WriteLine("Box2: {0}", Box2.ToString());

// Box1 的体积
volume = Box1.getVolume();
Console.WriteLine("Box1 的体积: {0}", volume);

// Box2 的体积
volume = Box2.getVolume();
Console.WriteLine("Box2 的体积: {0}", volume);

// 把两个对象相加
Box3 = Box1 + Box2;
Console.WriteLine("Box3: {0}", Box3.ToString());
// Box3 的体积
volume = Box3.getVolume();
Console.WriteLine("Box3 的体积: {0}", volume);

//comparing the boxes
if (Box1 > Box2)
Console.WriteLine("Box1 大于 Box2");
else
Console.WriteLine("Box1 不大于 Box2");
if (Box1 < Box2)
Console.WriteLine("Box1 小于 Box2");
else
Console.WriteLine("Box1 不小于 Box2");
if (Box1 >= Box2)
Console.WriteLine("Box1 大于等于 Box2");
else
Console.WriteLine("Box1 不大于等于 Box2");
if (Box1 <= Box2)
Console.WriteLine("Box1 小于等于 Box2");
else
Console.WriteLine("Box1 不小于等于 Box2");
if (Box1 != Box2)
Console.WriteLine("Box1 不等于 Box2");
else
Console.WriteLine("Box1 等于 Box2");
Box4 = Box3;
if (Box3 == Box4)
Console.WriteLine("Box3 等于 Box4");
else
Console.WriteLine("Box3 不等于 Box4");

Console.ReadKey();
}
}
}
//Box1: (6, 7, 5)
//Box2: (12, 13, 10)
//Box1 的体积: 210
//Box2 的体积: 1560
//Box3: (18, 20, 15)
//Box3 的体积: 5400
//Box1 不大于 Box2
//Box1 小于 Box2
//Box1 不大于等于 Box2
//Box1 小于等于 Box2
//Box1 不等于 Box2
//Box3 等于 Box4 等于 Box4

运行时多态

abstract

C# 允许您使用 abstract 关键字来创建抽象类,抽象类用于实现部分接口。另外,抽象类包含抽象方法,可以在派生类中实现。

下面列举了一些有关抽象类的规则:

  • 不能创建一个抽象类的实例;
  • 不能在一个抽象类外部声明抽象方法;
  • 通过在类定义时使用 sealed 关键字,可以将类声明为密封类,密封类不能被继承,因此抽象类中不能声明密封类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
namespace helloworld
{
abstract class Shape{
public abstract int area();
}
class Rectangle : Shape{
private int width, height;
public Rectangle(int w, int h){
width = w;
height = h;
}
public override int area(){//覆写抽象类中的抽象方法
return (width * height);
}
}
class Demo
{
static void Main(string[] args)
{
Shape r = new Rectangle(12,15);
//Rectangle r = new Rectangle(12,15); //都可以
double a = r.area();
Console.WriteLine("长方形的面积为: {0}",a);
Console.ReadKey();
}
}
}
//运行结果如下:
//长方形的面积为: 180

virtual

C#的运行时多态也通过virtual实现

virtual的几个特点

1、声明了virtual的方法无需去改动类的声明,他只在此方法上起到影响。(与abstract不同,如果一个类中包含抽象成员函数,那么这个类必须被声明为抽象类)

2、只有virtual的方法可以被子类override。

3、子类可以不override父类的virtual方法,这种情况下他就像普通的父类方法一样。

virtual与abstract的区别

  • 抽象类不可以直接实例化,他可以有n个(n>=0)抽象方法,这些抽象方法子类必须实现(强制)
  • virtual关键字就是告诉子类,此方法可以被override,但非强制

virtual在接口和抽象类中

  • 在接口中,所有成员都默认是公共的抽象成员,不包含任何实现,因此不能使用virtual关键字定义虚拟方法。
  • 在抽象类中,抽象方法(abstract method)是用来强制子类实现的,而虚拟方法(virtual method)是可以在抽象类中提供默认实现的方法。因此,抽象类中可以使用virtual关键字定义虚拟方法,但不需要使用abstract关键字。

CSharp与C++多态对比

Charp支持abstract,支持virtual,支持[interface](#CSharp 接口),支持抽象类

C++支持所有类的多重继承,首先这点CSharp就不支持,CSharp中仅接口可以支持多重继承

概念对照

C++ CSharp 主要功能
[[C++基础#纯虚函数和抽象类|纯虚函数]] 抽象函数 必须被重写
[[C++基础#多态|virtual]] virtual 不重写则调用父类虚函数
[[C++基础#纯虚函数和抽象类|抽象类(包含一个纯虚函数就是抽象类)]] 抽象类
全是纯虚函数的抽象类 接口

对应关系大致符合上表,但还有不同:

  • C#中普通方法不能被重写,只有抽象或虚方法可以被重写.
  • C++中普通函数也能被重定义,抽象或虚方法也可以被重写.

override注意点

  • C# 中,如果你想要重写基类中的虚方法(virtual 方法)或抽象方法(abstract 方法),override 关键字是必写的
  • C++中,override只是个标志,增加可读性,可写可不写都能重写父类

对接口和抽象类的理解

接口虽然不可以被实例化,但是接口引用可以指向1个实现该接口的对象.

若A实现了接口B,可以:

1
2
3
4
5
//接口引用实现了他的实例
B b = new A();
//也可以把A的对象强制转换为 接口B的对象
A a = new A();
B b = (B)a;

什么情况下应该使用接口而不用抽象类

总的来说,如果你需要定义一组应该被多个不相关的类实现的行为,或者需要多重继承的特性,接口是更好的选择。如果你的目标是共享代码或定义一个明确的“是一个”(is-a)继承结构,并且需要包含状态或属性,那么抽象类可能是更好的选择。

在实际开发中,经常会同时使用接口和抽象类。接口定义行为的契约,而抽象类提供部分实现。理解它们各自的优势和用途,可以帮助你做出更合适的设计选择。

CSharp namespace:命名空间

在 C# 中,可以将命名空间看作是一个范围,用来标注命名空间中成员的归属,一个命名空间中类与另一个命名空间中同名的类互不冲突,但在同一个命名空间中类的名称必须是唯一的。

在一个简单的 C# 程序中,假如我们要输出某些数据,就需要使用System.Console.WriteLine(),其中 System 就是命名空间,而 Console 是类的名字,WriteLine 则是具体要使用方法。也就是说,如果要访问某个命名空间中的类,我们需要使用namespacename.classname.funcname()的形式。当然也可以使用 using 关键字来引用需要的命名空间,例如using System,这样我们就可以直接使用Console.WriteLine()来输出指定的数据了。

在 C# 中定义命名空间需要使用 namespace 关键字,语法格式如下:

1
2
3
4
5
namespace namespaceName{
// 命名空间中的代码
}
//调用指定命名空间下的成员,需要使用
namespaceName.className.funcName()

using关键字

using 关键字用来引用指定的命名空间,它可以告诉编译器后面的代码中我们需要用到某个命名空间。例如我们在程序中需要使用到 System 命名空间,只需要在程序的开始使用using System引用该命名空间即可,这时我们在使用 System 命名空间下的类时就可以将System.省略,例如Console.WriteLine();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;
using First;
using Second;

namespace helloworld
{
class Demo
{
static void Main(string[] args)
{
firstClass first = new firstClass();
secondClass second = new secondClass();
first.sayHello();//签名相同的函数,要指明哪个命名空间中的函数
second.sayHello();
}
}
}

namespace First{
public class firstClass{
public void sayHello(){
System.Console.WriteLine("First 命名空间下 demoClass 类中的 sayHello 函数");
}
}
}

namespace Second{
public class secondClass{
public void sayHello(){
System.Console.WriteLine("Second 命名空间下 demoClass 类中的 sayHello 函数");
}
}
}

命名空间可以嵌套使用,也就是说我们可以在一个命名空间中再定义一个或几个命名空间,如下所示:

1
2
3
4
5
6
namespace namespaceName1{
// namespaceName1 下的代码
namespace namespaceName2{
// namespaceName2 下的代码
}
}

您可以使用点.运算符来访问嵌套的命名空间成员,例如namespaceName1.namespaceName2

CSharp 预处理器指令

预处理指令的作用主要是向编译器发出指令,以便在程序编译开始之前对信息进行一些预处理操作。在 C# 中,预处理器指令均以#开头,并且预处理器指令之前只能出现空格不能出现任何代码。另外,预处理器指令不是语句,因此它们不需要以分号;结尾。

在 C# 中,预处理指令用于帮助条件编译。不同于 C 和 C++ 中的指令,在 C# 中不能使用这些指令来创建宏,而且预处理器指令必须是一行中唯一的代码,不能掺杂其它。

CSharp 中的预处理器指令

预处理器指令 描述
#define 用于定义一系列字符,可以将这些字符称为符号
#undef 用于取消一个已定义符号
#if 用于测试符号是否为真
#else 用于创建复合条件指令,与 #if 一起使用
#elif 用于创建复合条件指令
#endif 指定一个条件指令的结束
#line 用于修改编译器的行数以及(可选地)输出错误和警告的文件名
#error 用于在代码的指定位置生成一个错误
#warning 用于在代码的指定位置生成一级警告
#region 用于在使用 Visual Studio Code Editor 的大纲特性时,指定一个可展开或折叠的代码块
#endregion 用于标识 #region 块的结束

#define 预处理器

#define 预处理器指令用来创建符号常量,这个符号可以作为传递给 #if 指令的表达式,表达式将返回 true。#define 的语法格式如下:

#define symbol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define PI
using System;

namespace helloworld
{
class Demo
{
static void Main(string[] args)
{
#if (PI)
Console.WriteLine("PI 已定义");
#else
Console.WriteLine("PI 未定义");
#endif
Console.ReadKey();
}
}
}
//PI 已定义

条件指令

您可以使用 #if 来创建条件指令,条件指令可以用于测试一个或多个符号的值是否为 true 。如果符号的值为 true,那么编译器将评估 #if 指令和下一个指令之间的所有代码。在语法上 #if 预处理器语句与 C# 中的 if 条件判断语句比较相似,如下所示:

1
2
3
4
5
6
7
8
#if symbol_1
// 要执行的代码
#elif symbol_2
// 要执行的代码
#else
// 要执行的代码
#endif
//symbol 是要测试的符号的名称

条件指令中仅可以使用运算符==(相等)和!=(不相等)来测试布尔值 true 或 false,例如 true 表示已定义该符号。另外,还可以使用&& (and)|| (or)! (not)运算符来同时测试多个符号,以及使用括号对符号和运算符分组。

CSharp 正则表达式

正则表达式是一种匹配输入文本的模式,可以用于解析和验证给定文本以及模式之间是否匹配,模式可以包含运算符、字符字面值或结构。

参考[[正则表达式]]

Regex类

Regex 类用于使用一个正则表达式,下表中列出了 Regex 类中一些常用的方法:

方法 描述
public bool IsMatch( string input ) 指示 Regex 构造函数中指定的正则表达式是否在指定的输入字符串中找到匹配项
public bool IsMatch( string input, int startat ) 指示 Regex 构造函数中指定的正则表达式是否在指定的输入字符串中找到匹配项,从字符串中指定的位置开始查找
public static bool IsMatch( string input, string pattern ) 指示指定的正则表达式是否在指定的输入字符串中找到匹配项
public MatchCollection Matches( string input ) 在指定的输入字符串中搜索正则表达式的所有匹配项
public string Replace( string input, string replacement ) 在指定的输入字符串中,把所有匹配正则表达式模式的所有匹配的字符串替换为指定的替换字符串
public string[] Split( string input ) 把输入字符串分割为子字符串数组,根据在 Regex 构造函数中指定的正则表达式模式定义的位置进行分割

CSharp 异常

在 C# 中,异常是在程序运行出错时引发的,例如以一个数字除以零,所有异常都派生自 System.Exception 类。异常处理则是处理运行时错误的过程,使用异常处理可以使程序在发生错误时保持正常运行。

C# 中的异常处理基于四个关键字构建,分别是 try、catch、finally 和 throw。

  • try:try 语句块中通常用来存放容易出现异常的代码,其后面紧跟一个或多个 catch 语句块;

  • catch:catch 语句块用来捕获 try 语句块中的出现的异常;

  • finally:finally 语句块用于执行特定的语句,不管异常是否被抛出都会执行;**只要跳出try块一定会被执行到!**try内中途return也会被执行到

    在 C# 中,finally 块的主要作用是确保无论 try 块中的代码是否抛出异常,或者是否在 try 块中执行了 return 语句,finally 块中的代码都会被执行。这使得 finally 块非常适合用于清理资源,比如关闭文件、释放网络连接或释放数据库连接等。

  • throw:throw 用来抛出一个异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
try{
// 引起异常的语句
}catch( ExceptionName e1 ){
// 错误处理代码
}catch( ExceptionName e2 ){
// 错误处理代码
}
...
catch( ExceptionName eN ){
// 错误处理代码
}finally{
// 要执行的语句
}

CSharp中的异常类

C# 中的异常类主要是从 System.Exception 类派生的,比如 System.ApplicationException 和 System.SystemException 两个异常类就是从 System.Exception 类派生的。

  • System.ApplicationException 类支持由程序产生的异常,因此我们自定义的异常都应继承此类;
  • System.SystemException 类是所有系统预定义异常的基类。

下表中列举了一些从 Sytem.SystemException 类派生的预定义异常类:

异常类 描述
System.IO.IOException 处理 I/O 错误
System.IndexOutOfRangeException 处理当方法引用超出范围的数组索引时产生的错误
System.ArrayTypeMismatchException 处理当数组类型不匹配时产生的错误
System.NullReferenceException 处理引用一个空对象时产生的错误
System.DivideByZeroException 处理当除以零时产生的错误
System.InvalidCastException 处理在类型转换期间产生的错误
System.OutOfMemoryException 处理空闲内存不足产生的错误
System.StackOverflowException 处理栈溢出产生的错误

自定义异常类

除了可以使用系统预定义的异常类外,我们还可以自行定义异常类,自定义的异常类都应继承 System.ApplicationException 类。下面通过示例来演示一下自定义异常类的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;

namespace helloworld
{
class Demo
{
static void Main(string[] args)
{
TestUserDefinedException test = new TestUserDefinedException();
try{
test.validate(12);
}catch(InvalidAgeException e){
Console.WriteLine("InvalidAgeException: {0}", e);
}
Console.WriteLine("其它代码");
}
}
}
public class InvalidAgeException : ApplicationException
{
public InvalidAgeException (string message): base(message)
{
}
}
public class TestUserDefinedException
{
public void validate(int age)
{
if(age < 18)
{
throw (new InvalidAgeException("Sorry, Age must be greater than 18"));
}
}
}
//InvalidAgeException: InvalidAgeException: Sorry, Age must be greater than 18
// 在 TestUserDefinedException.validate(Int32 age)
// 在 helloworld.Demo.Main(String[] args)
//其它代码

抛出异常

如果异常是直接或间接派生自 System.Exception 类,则可以在 catch 语句块中使用 throw 语句抛出该异常,所谓抛出异常这里可以理解为重新引发该异常。throw 语句的语法格式如下所示:

1
2
3
4
catch(Exception e) {
...
Throw e
}

异常调用堆栈理解

--- End of stack trace from previous location --- 的作用是:

  • 分隔符:它表示在此行之前的调用堆栈信息是来自于一个不同的上下文或调用路径。也就是说,当前的异常可能是由之前的一些操作引发的,但这些操作是在不同的上下文中进行的。

  • 上下文切换的指示:这条消息提醒开发者,异常的发生与之前的调用堆栈(即在这条消息之前的调用)是分开的。它帮助开发者理解,异常并不是在当前线程的上下文中直接引发的,而是可能与其他线程或异步操作有关。

CSharp 文件读写

文件是存储在磁盘中的具有特定名称和目录路径的数据集合,当我们使用程序对文件进行读取或写入时,程序会将文件以数据流(简称流)的形式读入内存中。我们可以将流看作是通过通信路径传递的字节序列,流主要分为输入流和输出流,输入流主要用于从文件读取数据(读操作),输出流主要用于向文件中写入数据(写操作)。

CSharp 中的 I/O 类

System.IO 命名空间中包含了各种用于文件操作的类,例如文件的创建、删除、读取、写入等等。如下表中所示:

I/O 类 描述
BinaryReader 从二进制流中读取原始数据
BinaryWriter 以二进制格式写入原始数据
BufferedStream 临时存储字节流
Directory 对目录进行复制、移动、重命名、创建和删除等操作
DirectoryInfo 用于对目录执行操作
DriveInfo 获取驱动器的信息
File 对文件进行操作
FileInfo 用于对文件执行操作
FileStream 用于文件中任何位置的读写
MemoryStream 用于随机访问存储在内存中的数据流
Path 对路径信息执行操作
StreamReader 用于从字节流中读取字符
StreamWriter 用于向一个流中写入字符
StringReader 用于从字符串缓冲区读取数据
StringWriter 用于向字符串缓冲区写入数据

FileStream 类

FileStream 类在 System.IO 命名空间下,使用它可以读取、写入和关闭文件。创建 FileStream 类对象的语法格式如下所示:

1
2
3
FileStream <object_name> = new FileStream(<file_name>, <FileMode Enumerator>, <FileAccess Enumerator>, <FileShare Enumerator>);
//例子:
FileStream F = new FileStream("sample.txt", FileMode.Open, FileAccess.Read, FileShare.Read);

参数说明如下:

  • object_name:创建的对象名称;
  • file_name:文件的路径(包含文件名在内);
  • FileMode:枚举类型,用来设定文件的打开方式,可选值如下:
    • Append:打开一个已有的文件,并将光标放置在文件的末尾。如果文件不存在,则创建文件;
    • Create:创建一个新的文件,如果文件已存在,则将旧文件删除,然后创建新文件;
    • CreateNew:创建一个新的文件,如果文件已存在,则抛出异常;
    • Open:打开一个已有的文件,如果文件不存在,则抛出异常;
    • OpenOrCreate:打开一个已有的文件,如果文件不存在,则创建一个新的文件并打开;
    • Truncate:打开一个已有的文件,然后将文件清空(删除原有内容),如果文件不存在,则抛出异常。
  • FileAccess:枚举类型,用来设置文件的存取,可选值有 Read、ReadWrite 和 Write;
  • FileShare:枚举类型,用来设置文件的权限,可选值如下:
    • Inheritable:允许子进程继承文件句柄,Win32 不直接支持此功能;
    • None:在文件关闭前拒绝共享当前文件,打开该文件的任何请求(由此进程或另一进程发出的请求)都将失败;
    • Read:允许随后打开文件读取,如果未指定此标志,则文件关闭前,任何打开该文件以进行读取的请求都将失败,需要注意的是,即使指定了此标志,仍需要附加权限才能够访问该文件;
    • ReadWrite:允许随后打开文件读取或写入,如果未指定此标志,则文件关闭前,任何打开该文件以进行读取或写入的请求都将失败,需要注意的是,即使指定了此标志,仍需要附加权限才能够访问该文件;
    • Write:允许随后打开文件写入,如果未指定此标志,则文件关闭前,任何打开该文件以进行写入的请求都将失败,需要注意的是,即使指定了此标志,仍可能需要附加权限才能够访问该文件;
    • Delete:允许随后删除文件。

FileStream 类中的常用方法

方法 描述
Close() 关闭当前流并释放与之关联的所有资源(如套接字和文件句柄)
CopyTo(Stream) 从当前流中读取字节并将其写入到另一流中
Dispose() 释放由 Stream 使用的所有资源
Equals(Object) 判断指定对象是否等于当前对象
Finalize() 确保垃圾回收器回收 FileStream 时释放资源并执行其他清理操作
Flush() 清除此流的缓冲区,使得所有缓冲数据都写入到文件中
GetHashCode() 默认哈希函数
GetType() 获取当前实例的 Type
Lock(Int64, Int64) 防止其他进程读取或写入 FileStream
Read(Byte[], Int32, Int32) 从流中读取字节块并将该数据写入给定缓冲区中
ReadByte() 从文件中读取一个字节,并将读取位置提升一个字节
ToString() 返回表示当前对象的字符串
Unlock(Int64, Int64) 允许其他进程访问以前锁定的某个文件的全部或部分
Write(Byte[], Int32, Int32) 将字节块写入文件流
WriteByte(Byte) 将一个字节写入文件流中的当前位置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.IO;

namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args)
{
FileStream file = new FileStream("test.txt", FileMode.OpenOrCreate, FileAccess.ReadWrite);

for(int i = 0; i < 20; i++){
file.WriteByte((byte)i);
}
file.Position = 0;

for(int i = 0; i < 20; i++){
Console.Write(file.ReadByte() + " ");
}
file.Close();
Console.ReadKey();
}
}
}

FileStream 是一个较低级别的类,是直接操作文件的字节流的,无论是读取还是写入都是基于字节的,它适用于需要对原始二进制数据进行精确控制的场合,例如读写图像、音频、自定义格式的数据文件等。但如果需要更复杂的数据读取写入操作,那么应该考虑下面的两种方式

二进制文件读写

C# 中的 BinaryReaderBinaryWriter 类可以用于二进制文件的读写。

BinaryReader

BinaryReader 类用于从文件读取二进制数据,类中的常用方法如下所示:

方法 描述
public override void Close() 关闭 BinaryReader 对象和基础流
public virtual int Read() 从基础流中读取字符,并根据所使用的编码和从流中读取的特定字符,将流的当前位置前移
public virtual bool ReadBoolean() 从当前流中读取一个布尔值,并将流的当前位置前移一个字节
public virtual byte ReadByte() 从当前流中读取下一个字节,并将流的当前位置前移一个字节
public virtual byte[] ReadBytes(int count) 从当前流中读取指定数目的字节到一个字节数组中,并将流的当前位置前移指定数目的字节
public virtual char ReadChar() 从当前流中读取下一个字节,并把流的当前位置按照所使用的编码和从流中读取的指定的字符往前移
public virtual char[] ReadChars(int count) 从当前流中读取指定数目的字符,并以字符数组的形式返回数据,并把流的当前位置按照所使用的编码和从流中读取的指定的字符往前移
public virtual double ReadDouble() 从当前流中读取一个 8 字节浮点值,并把流的当前位置前移八个字节
public virtual int ReadInt32() 从当前流中读取一个 4 字节有符号整数,并把流的当前位置前移四个字节
public virtual string ReadString() 从当前流中读取一个字符串,字符串以长度作为前缀,同时编码为一个七位的整数

完整的方法列表查阅 C# 的官方文档

BinaryWriter

BinaryWriter 类用于向文件写入二进制数据,类中的常用方法如下表所示:

方法 描述
public override void Close() 关闭 BinaryWriter 对象和基础流
public virtual void Flush() 清理当前编写器的所有缓冲区,使得所有缓冲数据写入基础设备
public virtual long Seek(int offset,SeekOrigin origin) 设置当前流中的位置
public virtual void Write(bool value) 将一个字节的布尔值写入到当前流中,0 表示 false,1 表示 true
public virtual void Write(byte value) 将一个无符号字节写入到当前流中,并把流的位置前移一个字节
public virtual void Write(byte[] buffer) 将一个字节数组写入到基础流中
public virtual void Write(char ch) 将一个 Unicode 字符写入到当前流中,并把流的当前位置按照所使用的编码和要写入到流中的指定字符往前移
public virtual void Write(char[] chars) 将一个字符数组写入到当前流中,并把流的当前位置按照所使用的编码和要写入到流中的指定字符往前移
public virtual void Write(double value) 将一个 8 字节浮点值写入到当前流中,并把流位置前移八个字节
public virtual void Write(int value) 将一个 4 字节有符号整数写入到当前流中,并把流位置前移四个字节
public virtual void Write(string value) 将一个有长度前缀的字符串按 BinaryWriter 的当前编码写如到流中,并把流的当前位置按照所使用的编码和要写入到流中的指定字符往前移

完整的方法列表请查阅 C# 的官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
BinaryWriter bw;
BinaryReader br;
int i = 25;
double d = 3.14157;
bool b = true;
string s = "你好世界";
// 创建文件
try{
bw = new BinaryWriter(new FileStream("mydata", FileMode.Create));
}catch (IOException e){
Console.WriteLine(e.Message + "\n 文件创建失败!");
return;
}
// 写入文件
try{
bw.Write(i);
bw.Write(d);
bw.Write(b);
bw.Write(s);
}catch (IOException e){
Console.WriteLine(e.Message + "\n 文件写入失败!");
}
bw.Close();
// 读取文件
try{
br = new BinaryReader(new FileStream("mydata", FileMode.Open));
}catch (IOException e){
Console.WriteLine(e.Message + "\n 文件打开失败!");
return;
}
try{
i = br.ReadInt32();
Console.WriteLine("Integer data: {0}", i);
d = br.ReadDouble();
Console.WriteLine("Double data: {0}", d);
b = br.ReadBoolean();
Console.WriteLine("Boolean data: {0}", b);
s = br.ReadString();
Console.WriteLine("String data: {0}", s);
}catch (IOException e){
Console.WriteLine(e.Message + "\n 文件读取失败!.");
}
br.Close();

文本文件的读写

System.IO 命名空间下的 StreamReaderStreamWriter 类可以用于文本文件的数据读写。这些类继承自抽象基类 Stream,Stream 类提供了对文件流读写的功能。

StreamReader

StreamReader 类继承自抽象基类 TextReader,用来从文件中读取一系列字符,下表列出了 StreamReader 类中一些常用的方法:

方法 描述
public override void Close() 关闭 StreamReader 对象和基础流,并释放任何与之相关的系统资源
public override int Peek() 返回下一个可用的字符,但不使用它
public override int Read() 从输入流中读取下一个字符,并把字符位置往前移一个字符

完整的方法列表,可以访问 C# 的官网文档

1
2
3
4
5
6
7
8
// 创建 StreamReader 类的对象
StreamReader file = new StreamReader("test.txt");
string line;
// 从文件中读取内容
while((line = file.ReadLine()) != null){
Console.WriteLine(line);
}
file.Close();
StreamWriter

StreamWriter 类同样继承自抽象类 TextWriter,用来向文件中写入一系列字符,下表列出了 StreamWriter 类中一些常用的方法:

方法 描述
public override void Close() 关闭当前的 StreamWriter 对象和基础流
public override void Flush() 清理当前所有的缓冲区,使所有缓冲数据写入基础流
public virtual void Write(bool value) 将布尔值的文本表示形式写入文本流
public override void Write(char value) 将一个字符写入流
public virtual void Write(decimal value) 将一个小数值的文本表示形式写入文本流
public virtual void Write(double value) 将一个 8 字节浮点值的文本表示形式写入文本流
public virtual void Write(int value) 将一个 4 字节有符号整数的文本表示形式写入文本流
public override void Write(string value) 将一个字符串写入文本流
public virtual void WriteLine() 将行结束符写入文本流

完整的方法列表查阅 C# 的官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
// 要写入文件中的数据
string[] str = new string[]{
"你好世界",
"hello world",
"C#"
};
// 创建 StreamWriter 类的对象
StreamWriter file = new StreamWriter("demo.txt");
// 将数组中的数据写入文件
foreach(string s in str){
file.WriteLine(s);
}
file.Close();

CSharp 目录操作

DirectoryInfo

DirectoryInfo 类派生自 FileSystemInfo 类,其中提供了各种用于创建、移动、浏览目录和子目录的方法。需要注意的是,该类不能被继承。

常用属性

属性 描述
Attributes 获取当前文件或目录的属性
CreationTime 获取当前文件或目录的创建时间
Exists 获取一个表示目录是否存在的布尔值
Extension 获取表示文件存在的字符串
FullName 获取目录或文件的完整路径
LastAccessTime 获取当前文件或目录最后被访问的时间
Name 获取该 DirectoryInfo 实例的名称

常用方法

方法 描述
public void Create() 创建一个目录
public DirectoryInfo CreateSubdirectory(string path) 在指定的路径上创建子目录,指定的路径可以是相对于 DirectoryInfo 类的实例的路径
public override void Delete() 如果为空的,则删除该 DirectoryInfo
public DirectoryInfo[] GetDirectories() 返回当前目录的子目录
public FileInfo[] GetFiles() 从当前目录返回文件列表

完整的方法以及属性介绍,查阅 C# 官方文档

1
2
3
4
5
6
7
8
9
10
//获取当前目录文件列表
// 创建一个 DirectoryInfo 对象
DirectoryInfo mydir = new DirectoryInfo(@"./");
// 获取目录中的文件以及它们的名称和大小
FileInfo[] f = mydir.GetFiles();
foreach (FileInfo file in f)
{
Console.WriteLine("文件名称:{0} 大小:{1}", file.Name, file.Length);
}
Console.ReadKey();

CSharp 文件操作

FileInfo

FileInfo 类派生自 FileSystemInfo 类,其中提供了用于创建、复制、删除、移动、打开文件的属性和方法。与 DirectoryInfo 类相同,FileInfo 类也不能被继承。

常用属性

属性 描述
Attributes 获取当前文件的属性
CreationTime 获取当前文件的创建时间
Directory 获取文件所属目录的一个实例
Exists 获取一个表示文件是否存在的布尔值
Extension 获取表示文件存在的字符串
FullName 获取文件的完整路径
LastAccessTime 获取当前文件最后被访问的时间
LastWriteTime 获取文件最后被写入的时间
Length 获取当前文件的大小,以字节为单位
Name 获取文件的名称

常用方法

方法 描述
public StreamWriter AppendText() 创建一个 StreamWriter,追加文本到由 FileInfo 的实例表示的文件中
public FileStream Create() 创建一个文件
public override void Delete() 永久删除一个文件
public void MoveTo(string destFileName) 移动一个指定的文件到一个新的位置,提供选项来指定新的文件名
public FileStream Open(FileMode mode) 以指定的模式打开一个文件
public FileStream Open(FileMode mode,FileAccess access) 以指定的模式,使用 read、write 或 read/write 访问,来打开一个文件
public FileStream Open(FileMode mode,FileAccess access,FileShare share) 以指定的模式,使用 read、write 或 read/write 访问,以及指定的分享选项,来打开一个文件
public FileStream OpenRead() 创建一个只读的 FileStream
public FileStream OpenWrite() 创建一个只写的 FileStream

完整的方法以及属性介绍,查阅 C# 官方文档

CSharp 特性

通俗而言,特性相当于给代码元素贴标签

1
2
[DbField] // <-这就是特性
pulic string Name{ get; set; }

特性(Attribute)是一种用于在程序运行时传递各种元素(例如类、方法、结构、枚举等)行为信息的声明性代码。使用特性可以将元数据(例如编译器指令、注释、描述、方法和类等信息)添加到程序中。.Net Framework 提供了两种类型的特性,分别是

在 C# 中,特性具有以下属性:

  • 使用特性可以向程序中添加元数据,元数据是指程序中各种元素的相关信息,所有 .NET 程序中都包含一组指定的元数据;
  • 可以将一个或多个特性应用于整个程序、模块或者较小的程序元素(例如类和属性)中;
  • 特性可以像方法和属性一样接受自变量;
  • 程序可使用反射来检查自己的元数据或其他程序中的元数据。

预定义特性

.Net Framework 中提供了三个预定义的属性:

AttributeUsage

预定义特性 AttributeUsage 用来描述如何使用自定义特性类,其中定义了可以应用特性的项目类型。AttributeUsage 的语法格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[AttributeUsage (
validon,
AllowMultiple = allowmultiple,
Inherited = inherited
)]

//如:
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]

参数说明如下:

  • 参数 validon 用来定义特性可被放置的语言元素。它是枚举器 AttributeTargets 的值的组合。默认值是 AttributeTargets.All;
  • 参数 allowmultiple(可选参数)用来为该特性的 AllowMultiple 属性(property)提供一个布尔值,默认值为 false(单用的),如果为 true,则该特性是多用的;
  • 参数 inherited(可选参数)用来为该特性的 Inherited 属性(property)提供一个布尔值,默认为 false(不被继承),如果为 true,则该特性可被派生类继承。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Demo
{
//该特性允许使用别名?来取代Help
[CommandLineSwitchAlias("?")]
public bool Help
{
//...
}

//该特性指出Out是一个必要参数
[CommandLineSwitchRequired]
public string Out
{
//...
}

}

Conditional

预定义特性 Conditional 用来标记一个方法,它的执行依赖于指定的预处理标识符。根据该特性值的不同,在编译时会起到不同的效果,例如当值为 Debug 或 Trace 时,会在调试代码时显示变量的值。

预定义特性 Conditional 的语法格式如下:

1
2
3
4
5
6
7
[Conditional(
conditionalSymbol
)]

//如:
[Conditional("DEBUG")]
//用于在特定的编译条件下包含或排除方法的调用。具体来说,当编译器定义了 DEBUG 符号时,标记了 [Conditional("DEBUG")] 的方法会被包含在编译结果中;否则,这些方法的调用会被忽略。

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define DEBUG
using System;
using System.Diagnostics;

namespace c.biancheng.net
{
class Demo
{
static void function1()
{
Myclass.Message("Function1 函数");
function2();
}
static void function2()
{
Myclass.Message("Function2 函数");
}
static void Main(string[] args)
{
Myclass.Message("Main 函数");
function1();
Console.ReadKey();
}
}
public class Myclass
{
[Conditional("DEBUG")]
public static void Message(string msg)
{
Console.WriteLine(msg);
}
}
}

这个代码中,[Conditional("DEBUG")] 特性作用于 Myclass.Message 方法,它告诉编译器该方法的执行应取决于预处理器符号 DEBUG 是否被定义。在代码顶部,有一个预处理器指令 #define DEBUG,这意味着在编译阶段,DEBUG 符号被定义。当 Message 方法前面加上 [Conditional("DEBUG")] 特性时,意味着在调试版本(即 DEBUG 符号被定义时)编译代码时,Message 方法会被正常编译并执行。而在发布版本(即 DEBUG 符号未定义时),编译器将会移除所有对该方法的调用,就像这些调用从未存在过一样。这样一来,当你在发布版中运行这段代码时,Myclass.Message 方法不会输出任何消息,因为它在编译阶段就被优化掉了。而在调试版中运行时,你将看到所有的消息打印出来。这种方法常用于插入调试代码,以便在调试阶段辅助追踪程序流程,而不影响最终发布产品的性能或体积。

Obsolete

预定义特性 Obsolete 用来标记不应被使用的程序,您可以使用它来通知编译器放弃某个目标元素。例如当您需要使用一个新方法来替代类中的某个旧方法时,就可以使用该特性将旧方法标记为 obsolete(过时的)并来输出一条消息,来提示我们应该使用新方法代替旧方法。

预定义特性 Obsolete 的语法格式如下:

1
2
3
4
5
6
7
8
9
//两种任选一种
[Obsolete (
message
)]

[Obsolete (
message,
iserror
)]

语法说明如下:

  • 参数 message 是一个字符串,用来描述项目为什么过时以及应该使用什么替代;
  • 参数 iserror 是一个布尔值,默认值是 false(编译器会生成一个警告),如果设置为 true,那么编译器会把该项目的当作一个错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
namespace c.biancheng.net
{
class Demo
{
[Obsolete("OldMethod 已弃用,请改用 NewMethod", true)]
static void OldMethod()
{
Console.WriteLine("已弃用的函数");
}
static void NewMethod()
{
Console.WriteLine("新定义的函数");
}
static void Main(string[] args)
{
OldMethod();
}
}
}
//demo.cs(18,10): error CS0619: “c.biancheng.net.Demo.OldMethod()”已过时:“OldMethod 已弃用,请改用 NewMethod”

自定义特性

.Net Framework 允许您创建自定义特性,自定义特性不仅可以用于存储声明性的信息,还可以在运行时被检索。创建并使用自定义特性可以分为四个步骤:

  • 声明自定义特性;
  • 构建自定义特性;
  • 在目标程序上应用自定义特性;
  • 通过反射访问特性。

最后一步涉及编写一个简单的程序来读取元数据以便查找各种符号。元数据是有关数据或用于描述其他数据信息的数据。该程序应在运行时使用反射来访问属性

简单案例

定义了两个特性Attribute1Attribute2

1
2
3
4
5
[AttributeUsage(AttributeTargets.Property)] // 只能贴在属性上
public class Attribute1 : Attribute { } // 空特性

[AttributeUsage(AttributeTargets.Property)]
public class Attribute2 : Attribute { }
  • AttributeUsage:规定这个”特性标签”能贴在什么地方
  • DbFieldAttribute 继承自 Attribute:表示这是一个特性类

声明自定义属性

自定义特性应该继承 System.Attribute 类,如下所示:

1
2
3
4
5
6
7
8
9
10
11
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(
AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]

public class DeBugInfo : System.Attribute
//声明了一个名为 DeBugInfo 的自定义属性

构建自定义特性

让我们构建一个名为 DeBugInfo 的自定义特性,该特性可以存储下面列举的调试信息:

  • bug 的代码编号;
  • 该 bug 的开发人员名字;
  • 上次审查代码的日期;
  • 一个存储了开发人员标记的字符串消息。

DeBugInfo 类中带有三个用于存储前三个信息的私有属性(property)和一个用于存储消息的公有属性(public)。所以 bug 编号、开发人员名字和审查日期将是 DeBugInfo 类的必需的定位( positional)参数,而消息则是一个可选的命名(named)参数。

每个特性都至少有一个构造函数,而且定位( positional)参数需要通过构造函数传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]

public class DeBugInfo : System.Attribute
{
private int bugNo;
private string developer;
private string lastReview;
public string message;

public DeBugInfo(int bg, string dev, string d)
{
this.bugNo = bg;
this.developer = dev;
this.lastReview = d;
}

public int BugNo
{
get {return bugNo;}
}

public string Developer
{
get {return developer;}
}

public string LastReview
{
get {return lastReview;}
}

public string Message
{
get {return message;}
set {message = value;}
}
}

应用自定义特性

通过把特性放置在紧挨着它的目标上面来应用该特性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
using System;

namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args)
{
Rectangle rec = new Rectangle(12, 15);
rec.Display();
}
}

[DeBugInfo(45, "Zara Ali", "12/8/2012", Message = "返回值类型不匹配")]
[DeBugInfo(49, "Nuha Ali", "10/10/2012", Message = "未使用变量")]
class Rectangle
{
// 成员变量
protected double length;
protected double width;
public Rectangle(double l, double w)
{
length = l;
width = w;
}
[DeBugInfo(55, "Zara Ali", "19/10/2012",
Message = "返回值类型不匹配")]
public double GetArea()
{
return length * width;
}
[DeBugInfo(56, "Zara Ali", "19/10/2012")]
public void Display()
{
Console.WriteLine("Length: {0}", length);
Console.WriteLine("Width: {0}", width);
Console.WriteLine("Area: {0}", GetArea());
}
}
// 一个自定义特性 BugFix 被赋给类及其成员
[AttributeUsage(AttributeTargets.Class |
AttributeTargets.Constructor |
AttributeTargets.Field |
AttributeTargets.Method |
AttributeTargets.Property,
AllowMultiple = true)]

public class DeBugInfo : System.Attribute
{
private int bugNo;
private string developer;
private string lastReview;
public string message;

public DeBugInfo(int bg, string dev, string d)
{
this.bugNo = bg;
this.developer = dev;
this.lastReview = d;
}

public int BugNo
{
get
{
return bugNo;
}
}
public string Developer
{
get
{
return developer;
}
}
public string LastReview
{
get
{
return lastReview;
}
}
public string Message
{
get
{
return message;
}
set
{
message = value;
}
}
}
}
/*
Length: 12
Width: 15
Area: 180
*/

通过把特性放置在紧挨着它的目标上面来应用该特性

DeBugInfo 是一个自定义特性(Attribute),它主要用于记录与调试和错误修复相关的信息,这些信息并不是直接参与到程序的运行逻辑中,而是在编译后的元数据中保存,可以通过反射在运行时读取这些信息。这段代码中并未提供如何在运行时读取并利用这些自定义特性所携带的信息。要真正发挥其作用,通常还需要额外编写代码,通过反射API来提取和展示这些元数据。

CSharp 属性

属性(Property)是类(class)、结构体(struct)和接口(interface)的成员,类或结构体中的成员变量称为字段,属性是字段的扩展,使用访问器(accessors)可以读写私有字段的值。

属性没有确切的内存位置,但具有可读写或计算的访问器。例如有一个名为 Student 的类,其中包含 age、name 和 code 三个私有字段,我们不能在类的范围以外直接访问这些字段,但是可以访问这些私有字段的属性。

在C#中,属性提供了一种封装类字段并公开对其访问的方式。属性允许您定义对类成员的访问方法,这样您可以控制对类的数据的访问方式。通过属性,您可以实现对类的字段的读取和写入操作,并在必要时执行其他逻辑(比如验证输入或触发事件)。属性使代码更易读,更易维护,并提供了一种更安全的方式来访问类的数据。

访问器

属性访问器有两种,分别是 get 属性访问器和 set 属性访问器。其中 get 访问器用来返回属性的值,set 访问器用来为属性设置新值。在声明访问器时可以仅声明其中一个,也可以两个访问器同时声明,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 声明 string 类型的 Code 属性
public string Code {
get {
return code;
}
set {
code = value;
}
}

// 声明 string 类型的 Name 属性
public string Name {
get {
return name;
}
set {
name = value;
}
}

// 声明 int 类型的 Age 属性
public int Age {
get {
return age;
}
set {
age = value;
}
}
//学生信息: 编号 = 001, 姓名 = Zara, 年龄 = 9
//学生信息: 编号 = 001, 姓名 = Zara, 年龄 = 10

抽象属性

抽象类中可以拥有抽象属性,这些属性会在派生类中实现,下面就通过一个示例来演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System;

namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args)
{
// 创建一个新的 Student 对象
Student s = new Student();

// 设置 student 的 code、name 和 age
s.Code = "001";
s.Name = "Zara";
s.Age = 9;
Console.WriteLine("学生信息: {0}", s);
// 增加年龄
s.Age += 1;
Console.WriteLine("学生信息: {0}", s);
Console.ReadKey();
}
}
public abstract class Person
{
public abstract string Name
{
get;
set;
}
public abstract int Age
{
get;
set;
}
}
class Student
{
private string code = "N.A";
private string name = "not known";
private int age = 0;

// 声明类型为 string 的 Code 属性
public string Code
{
get
{
return code;
}
set
{
code = value;
}
}

// 声明类型为 string 的 Name 属性
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}

// 声明类型为 int 的 Age 属性
public int Age
{
get
{
return age;
}
set
{
age = value;
}
}
public override string ToString()
{
return "编号 = " + Code +", 姓名 = " + Name + ", 年龄 = " + Age;
}
}
}
//学生信息: 编号 = 001, 姓名 = Zara, 年龄 = 9
//学生信息: 编号 = 001, 姓名 = Zara, 年龄 = 10

CSharp 索引器

索引器(英文名:Indexer)是类中的一个特殊成员,它能够让对象以类似数组的形式来操作,使程序看起来更为直观,更容易编写。索引器与[属性](#CSharp 属性)类似,在定义索引器时同样会用到 get 和 set 访问器,不同的是,访问属性不需要提供参数而访问索引器则需要提供相应的参数。

C# 中属性的定义需要提供属性名称,而索引器则不需要具体名称,而是使用 this 关键字来定义,语法格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
索引器类型 this[int index]
{
// get 访问器
get
{
// 返回 index 指定的值
}

// set 访问器
set
{
// 设置 index 指定的值
}
}

索引器的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;

namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args){
Demo names = new Demo();
names[0] = "C语言中文网";
names[1] = "http://c.biancheng.net/";
names[2] = "C#教程";
names[3] = "索引器";
for ( int i = 0; i < Demo.size; i++ ){
Console.WriteLine(names[i]);
}
Console.ReadKey();
}

static public int size = 10;
private string[] namelist = new string[size];
public Demo(){
for (int i = 0; i < size; i++)
namelist[i] = "NULL";
}
public string this[int index]{//索引器定义
get{
string tmp;

if( index >= 0 && index <= size-1 ){
tmp = namelist[index];
}else{
tmp = "";
}

return ( tmp );
}
set{
if( index >= 0 && index <= size-1 ){
namelist[index] = value;
}
}
}
}
}

索引器重载

索引器可以被重载,而且在声明索引器时也可以带有多个参数,每个参数可以是不同的类型。另外,索引器中的索引不必是整数,也可以是其他类型,例如字符串类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;

namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args){
Demo names = new Demo();
names[0] = "C语言中文网";
names[1] = "http://c.biancheng.net/";
names[2] = "C#教程";
names[3] = "索引器";
// 使用带有 int 参数的第一个索引器
for (int i = 0; i < Demo.size; i++){
Console.WriteLine(names[i]);
}
// 使用带有 string 参数的第二个索引器
Console.WriteLine("“C#教程”的索引为:{0}",names["C#教程"]);
Console.ReadKey();
}

static public int size = 10;
private string[] namelist = new string[size];
public Demo(){
for (int i = 0; i < size; i++)
namelist[i] = "NULL";
}
public string this[int index]{
get{
string tmp;

if( index >= 0 && index <= size-1 ){
tmp = namelist[index];
}else{
tmp = "";
}

return ( tmp );
}
set{
if( index >= 0 && index <= size-1 ){
namelist[index] = value;
}
}
}
public int this[string name]{
get{
int index = 0;
while(index < size){
if(namelist[index] == name){
return index;
}
index++;
}
return index;
}
}
}
}

CSharp 委托

C# 中的委托(Delegate)类似于 C 或 C++ 中的函数指针,是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。委托特别适用于实现事件和回调方法,所有的委托都派生自 System.Delegate 类。在实例化委托时,可以将委托的实例与具有相同返回值类型的方法相关联,这样就可以通过委托来调用方法。另外,使用委托还可以将方法作为参数传递给其他方法,

委托具有以下特点:

  • 委托类似于 C/C++ 中的函数指针,但委托是完全面向对象的。另外,C++ 中的指针会记住函数,而委托则是同时封装对象实例和方法;
  • 委托允许将方法作为参数进行传递;
  • 委托可用于定义回调方法;
  • 委托可以链接在一起,例如可以对一个事件调用多个方法;
  • 方法不必与委托类型完全匹配;
  • C# 2.0 版引入了 匿名函数 的概念,可以将代码块作为参数(而不是单独定义的方法)进行传递。C# 3.0 引入了 Lambda 表达式,利用它们可以更简练地编写内联代码块。匿名方法和 Lambda 表达式都可编译为委托类型,这些功能现在统称为匿名函数。

声明委托

声明委托需要使用 delegate 关键字,语法格式如下:

delegate <return type> delegate-name(<parameter list>)

其中 return type 为返回值类型,delegate-name 为委托的名称,parameter list 为参数列表。

提示:委托可以引用与委托具有相同签名的方法,也就是说委托在声明时即确定了委托可以引用的方法。

实例化委托

委托一旦声明,想要使用就必须使用 new 关键字来创建委托的对象,同时将其与特定的方法关联。如下例所示:

1
2
3
4
public delegate void printString(string s);           // 声明一个委托
...
printString ps1 = new printString(WriteToScreen); // 实例化委托对象并将其与 WriteToScreen 方法关联
printString ps2 = new printString(WriteToFile); // 实例化委托对象并将其与 WriteToFile 方法关联

委托案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
using System;

delegate int NumberChanger(int n); // 定义委托
namespace c.biancheng.net
{
class Demo
{
static int num = 10;
public static int AddNum(int p){//被委托的函数
num += p;
return num;
}

public static int MultNum(int q){//被委托的函数
num *= q;
return num;
}
public static int getNum(){
return num;
}
static void Main(string[] args){
// 创建委托实例
NumberChanger nc1 = new NumberChanger(AddNum);//实例化委托
NumberChanger nc2 = new NumberChanger(MultNum);//实例化委托
// 使用委托对象调用方法
nc1(25);//使用委托
Console.WriteLine("num 的值为: {0}", getNum());
nc2(5);//使用委托
Console.WriteLine("num 的值为: {0}", getNum());
Console.ReadKey();
}
}
}

常用的委托类型

  • Func<T, TResult>: 表示带有一个输入参数和一个返回值的方法

    Func<double>表示的是返回值是double类型

  • Action<T>: 表示没有返回值的方法

    • 异步方法里要通知调用方并安全更新 UI → 用 IProgress 最省心;

    • 只要简单回调,不在意线程切换 → 用 Action<T>;确保在调用方用 Dispatcher 切回 UI 线程。

  • Predicate<T>: 表示用于定义一组条件的方法并返回布尔值

Predicate<T>实际上就等同于Func<T,bool>

Action<int,int, string, int>表示无返回值的四个参数的类型

多播委托(合并委托)

委托对象有一个非常有用的属性,那就是可以通过使用+运算符将多个对象分配给一个委托实例,同时还可以使用-运算符从委托中移除已分配的对象,当委托被调用时会依次调用列表中的委托。委托的这个属性被称为委托的多播,也可称为组播,利用委托的这个属性,您可以创建一个调用委托时要调用的方法列表。

注意:仅可合并类型相同的委托。

所有委托都支持单一回调(换言之,多重性(multiplicity)等于1).然而,一个委托变量可以引用一系列委托,在这一系列委托中,每个委托都顺序指向一个后续的委托,从而形成了一个委托链,或者称为多播委托(multicast delegate).使用多播委托,可以通过一个方法对象来调用一个方法链,创建变量来引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;

delegate int NumberChanger(int n); // 定义委托
namespace c.biancheng.net
{
class Demo
{
static int num = 10;
public static int AddNum(int p){
num += p;
return num;
}

public static int MultNum(int q){
num *= q;
return num;
}
public static int getNum(){
return num;
}
static void Main(string[] args){
// 创建委托实例
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
nc = nc1;
nc += nc2;
// 调用多播
nc(5);
Console.WriteLine("num 的值为: {0}", getNum());
Console.ReadKey();
}
}
}
//num 的值为: 75

自我理解:相当于C++指针,但比指针多了一个组合功能,可以让一个委托为好几个不同的委托的组合

打印案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//定义一个委托 printString,我们使用这个委托来调用两个方法,第一个把字符串打印到控制台,第二个把字符串打印到文件
using System;
using System.IO;

namespace c.biancheng.net
{
class Demo
{
static FileStream fs;
static StreamWriter sw;
// 委托声明
public delegate void printString(string s);

// 该方法打印到控制台
public static void WriteToScreen(string str){
Console.WriteLine("The String is: {0}", str);
}
// 该方法打印到文件
public static void WriteToFile(string s){
fs = new FileStream("./message.txt", FileMode.Append, FileAccess.Write);
sw = new StreamWriter(fs);
sw.WriteLine(s);
sw.Flush();
sw.Close();
fs.Close();
}
// 该方法把委托作为参数,并使用它调用方法
public static void sendString(printString ps)
{
ps("C语言中文网");
}
static void Main(string[] args){
printString ps1 = new printString(WriteToScreen);
printString ps2 = new printString(WriteToFile);
sendString(ps1);
sendString(ps2);
Console.ReadKey();
}
}
}

CSharp 事件

在 C# 中,事件(Event)可以看作是用户的一系列操作,例如点击键盘的某个按键、单击/移动鼠标等,当事件发生时我们可以针对事件做出一系列的响应,例如退出程序、记录日志等等。C# 中线程之间的通信就是使用事件机制实现的

在C#中,事件是一种特殊的委托,它允许类对象通知其他类对象发生的特定动作。在C++中,你可以使用回调函数来实现类似的功能,但是事件在C#中提供了更强大和更易于使用的机制来实现类似的行为。

事件需要在类中声明和触发,并通过委托与事件处理程序关联事件可以分为发布器和订阅器两个部分,其中发布器是一个包含事件和委托的对象,事件和委托之间的联系也定义在这个类中,发布器类的对象可以触发事件,并使用委托通知其他的对象;订阅器则是一个接收事件并提供事件处理程序的对象,发布器类中的委托调用订阅器类中的方法(事件处理程序)。

有关事件我们需要注意以下几点:

  • 发布器确定何时触发事件,订阅器确定对事件作出何种响应;
  • 一个事件可以拥有多个订阅器,同时订阅器也可以处理来自多个发布器的事件;
  • 没有订阅器的事件永远也不会触发;
  • 事件通常用于定义针对用户的操作,例如单击某个按钮;
  • 如果事件拥有多个订阅器,当事件被触发时会同步调用所有的事件处理程序;
  • 在 .NET 类库中,事件基于 EventHandler 委托和 EventArgs 基类。

若要在类中声明一个事件,首先需要为该事件声明一个委托类型,例如:

public delegate void delegate_name(string status);

然后使用 event 关键字来声明事件本身,如下所示:

1
2
// 基于上面的委托定义事件
public event delegate_name event_name;

上例中定义了一个名为 delegate_name 和名为 event_name 的事件,当事件触发的时侯会调用委托。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System;

namespace c.biancheng.net
{
class Demo
{
static void Main(string[] args){
PublisherDemo e = new PublisherDemo(); /* 实例发布器类*/
SubscriberDemo v = new SubscriberDemo(); /* 实例订阅器类 */
e.MyEvent += new PublisherDemo.MyEntrust( v.printf );
e.SetValue("C语言中文网");
}
}
/***********发布器类***********/
public class PublisherDemo{
private string value;

public delegate void MyEntrust(string str);

public event MyEntrust MyEvent;

public void SetValue( string s ){
value = s;
MyEvent(value); // 触发事件
}
}

/***********订阅器类***********/
public class SubscriberDemo{
public void printf(string str){
Console.WriteLine(str);
}
}
}

注意点

A事件添加B事件,B事件添加A事件,会产生事件循环

CSharp 集合

C# 中的集合类(Collection)是专门用于数据存储和检索的类,类中提供了对栈(stack)、队列(queue)、列表(list)和哈希表(hash table)的支持。大多数集合类都实现了相同的接口。

集合类的用途多种多样,例如可以动态的为元素分配内存、根据索引访问列表项等等,这些类创建 Object 类的对象集合,Object 类是 C# 中所有数据类型的基类。

描述和用法
动态数组(ArrayList) 动态数组表示可被单独索引的对象的有序集合。 动态数组基本上与数组相似,唯一不同的是动态数组可以使用索引在指定的位置添加和移除项目,动态数组会自动重新调整自身的大小。 另外,动态数组也允许在列表中进行动态内存分配、增加、搜索、排序等等。
哈希表(Hashtable) 哈希表可以使用键来访问集合中的元素。 哈希表中的每一项都由一个键/值对组成,键用于访问集合中的指定项。
排序列表(SortedList<key,value>) 排序列表是数组和哈希表的组合,可以使用键或索引来访问列表中的各项。 排序列表中包含一个可使用键或索引访问各项的列表,如果您使用索引访问各项,则它是一个动态数组,如果您使用键访问各项,则它就是一个哈希表。 另外,排序列表中的各项总是按键值进行排序的。
SortedSet 自排序,与SortedList一致,键必须唯一
堆栈(Stack) 堆栈代表了一个后进先出的对象集合。 当您需要对各项进行后进先出的访问时,则可以使用堆栈。为堆栈中添加一项称为推入项目,从堆栈中移除一项称为弹出项目。
队列(Queue) 队列代表了一个先进先出的对象集合。 当您需要对各项进行先进先出的访问时,则可以使用队列。为队列中添加项目称为入队,为队列中移除项目称为出队。
点阵列(BitArray) 点阵列代表了一个使用 1 和 0 来表示的二进制数组。 当您需要存储比特位,但是事先不知道具体位数时,则可以使用点阵列。可以使用整型索引从点阵列集合中访问各项,索引从零开始。

ArrayList动态数组

在 C# 中,动态数组(ArrayList)代表了可被单独索引的对象的有序集合。动态数组基本上可以代替数组,唯一与数组不同的是,动态数组可以使用索引在指定的位置添加和移除指定的项目,动态数组会自动重新调整自身的大小。另外,动态数组允许在列表中进行动态内存分配、增加、搜索、排序等操作。

属性 描述
Capacity 获取或设置动态数组中可以包含的元素个数
Count 获取动态数组中实际包含的元素个数
IsFixedSize 判断动态数组是否具有固定大小
IsReadOnly 判断动态数组是否只读
IsSynchronized 判断访问动态数组是否同步(线程安全)
Item[Int32] 获取或设置指定索引处的元素
SyncRoot 获取一个对象用于同步访问动态数组
方法名 描述
public virtual int Add(object value) 将对象添加到动态数组的末尾
public virtual void AddRange(ICollection c) 将 ICollection 的元素添加到动态数组的末尾
public virtual void Clear() 从动态数组中移除所有的元素
public virtual bool Contains(object item) 判断某个元素是否在动态数组中
public virtual ArrayList GetRange(int index, int count) 返回一个动态数组,表示源动态数组中元素的子集
public virtual int IndexOf(object) 搜索整个动态数组,并返回对象在动态数组中第一次出现的索引,索引从零开始
public virtual void Insert(int index, object value) 在动态数组的指定索引处插入一个元素
public virtual void InsertRange(int index, ICollection c) 在动态数组的指定索引处插入某个集合的元素
public virtual void Remove(object obj) 从动态数组中移除指定的对象
public virtual void RemoveAt(int index) 移除动态数组中指定索引处的元素
public virtual void RemoveRange(int index, int count) 从动态数组中移除某个范围的元素
public virtual void Reverse() 逆转动态数组中元素的顺序
public virtual void SetRange(int index, ICollection c) 复制某个集合的元素到动态数组中某个范围的元素上
public virtual void Sort() 对动态数组中的元素进行排序
public virtual void TrimToSize() 将容量设置为动态数组中元素的实际个数

关于 ArrayList 类中的完整属性和方法介绍,可以查阅 C# 官方文档

存double案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using System.Collections;

class Program
{
static void Main()
{
ArrayList arrayList = new ArrayList();

// 添加double类型数据到ArrayList
double number1 = 3.14;
double number2 = 5.67;
arrayList.Add(number1);
arrayList.Add(number2);

// 遍历ArrayList并输出double类型数据
foreach (object obj in arrayList)
{
if (obj is double)
{
double num = (double)obj;
Console.WriteLine(num);
}
}
}
}

Hashtable哈希表

在 C# 中,Hashtable(哈希表) 类表示根据键的哈希代码进行组织的键(key)/值(value)对的集合,可以使用键来访问集合中的元素。也就是说当您需要使用键来访问指定元素时,可以选择使用哈希表。

属性 描述
Count 获取哈希表中包含的键值对的个数
IsFixedSize 获取一个值,用来表示哈希表是否具有固定大小
IsReadOnly 获取一个值,用来表示哈希表是否只读
Item 获取或设置与指定键关联的值
Keys 获取一个 ICollection,其中包含哈希表中的键
Values 获取一个 ICollection,其中包含哈希表中的值
方法名 描述
public virtual void Add(object key, object value) 向哈希表中添加一个带有指定的键和值的元素
public virtual void Clear() 从哈希表中移除所有的元素
public virtual bool ContainsKey(object key) 判断哈希表是否包含指定的键
public virtual bool ContainsValue(object value) 判断哈希表是否包含指定的值
public virtual void Remove(object key) 从哈希表中移除带有指定的键的元素

关于 Hashtable 类中的完整属性和方法介绍,可以查阅 C# 官方文档

SortedList排序列表

在 C# 中,SortedList 类用来表示键/值对的集合,这些键/值对按照键值进行排序,并且可以通过键或索引访问集合中的各个项。

我们可以将排序列表看作是数组和哈希表的组合,其中包含了可以使用键或索引访问各项的列表。如果您使用索引访问各项,那么它就是一个动态数组(ArrayList),如果您使用键访问各项,那么它就是一个哈希表(Hashtable)。另外,集合中的各项总是按键值进行排序。

属性 描述
Capacity 获取或设置排序列表中可包含的元素个数
Count 获取排序列表中的元素个数
IsFixedSize 判断排序列表是否具有固定大小
IsReadOnly 判断排序列表是否只读
Item 获取或设置排序列表中指定键所关联的值
Keys 获取一个包含排序列表中所有键的集合
Values 获取一个包含排序列表中所有值的集合
方法名 描述
public virtual void Add(object key, object value) 向排序列表中添加一个带有指定的键和值的元素
public virtual void Clear() 从排序列表中移除所有的元素
public virtual bool ContainsKey(object key) 判断排序列表中是否包含指定的键
public virtual bool ContainsValue(object value) 判断排序列表中是否包含指定的值
public virtual object GetByIndex(int index) 获取排序列表中指定索引处的值
public virtual object GetKey(int index) 获取排序列表中指定索引处的键
public virtual IList GetKeyList() 获取排序列表中的键
public virtual IList GetValueList() 获取排序列表中的值
public virtual int IndexOfKey(object key) 返回排序列表中指定键的索引,索引从零开始
public virtual int IndexOfValue(object value) 返回排序列表中指定值第一次出现的索引,索引从零开始
public virtual void Remove(object key) 从排序列表中移除带有指定键的元素
public virtual void RemoveAt(int index) 移除排序列表中指定索引处的元素
public virtual void TrimToSize() 将排序列表的容量设置为排序列表中元素的实际个数

关于 SortedList 类中的完整属性和方法介绍,可以查阅 C# 官方文档

Stack堆栈

在 C# 中,堆栈(Stack)类表示一个后进先出的对象集合,当您需要对项目进行后进先出的访问时,则可以使用堆栈。向堆栈中添加元素称为推入元素,从堆栈中移除元素称为弹出元素。

属性 描述
Count 获取堆栈中包含的元素个数
IsSynchronized 判断是否同步对堆栈的访问(线程安全)
SyncRoot 获取可用于同步对堆栈访问的对象
方法名 描述
public virtual void Clear() 从堆栈中移除所有的元素
public virtual bool Contains(object obj) 判断某个元素是否在堆栈中
public virtual object Peek() 返回在堆栈顶部的对象,但不移除它
public virtual object Pop() 移除并返回在堆栈顶部的对象
public virtual void Push(object obj) 向堆栈顶部添加一个对象
public virtual object[] ToArray() 复制堆栈到一个新的数组中

关于 Stack 类中的完整属性和方法介绍,可以查阅 C# 官方文档

Queue队列

在 C# 中,队列(Queue 类)与堆栈类似,它代表了一个先进先出的对象集合,当您需要对项目进行先进先出访问时,则可以使用队列。向队列中添加元素称为入队(enqueue),从堆栈中移除元素称为出队(deque)。

属性 描述
Count 获取队列中包含的元素个数
IsSynchronized 判断是否同步对队列的访问(线程安全)
SyncRoot 获取可用于同步对队列访问的对象
方法名 描述
public virtual void Clear() 从队列中移除所有的元素
public virtual bool Contains(object obj) 判断某个元素是否在队列中
public virtual object Dequeue() 移除并返回在队列开头的对象
public virtual void Enqueue(object obj) 向队列的末尾处添加一个对象
public virtual object[] ToArray() 复制队列到一个新的数组中
public virtual void TrimToSize() 将队列的容量设置为队列中元素的实际个数

BitArray点阵列

在 C# 中,BitArray 类用来管理一个紧凑型的位值数组,数组中的值均为布尔类型,其中 true(1)表示此位为开启,false(0)表示此位为关闭。

当您需要存储位(英文名“bit”数据存储的最小单位,也可称为比特),但事先又不知道具体位数时,就可以使用点阵列。当需要访问点阵列中的元素时,可以使用整型索引从点阵列中访问指定元素,索引从零开始。

属性 描述
Count 获取点阵列中包含的元素个数
IsReadOnly 判断 点阵列是否只读
Item 获取或设置点阵列中指定位置的值
Length 获取或设置点阵列中的元素个数
方法名 描述
public BitArray And(BitArray value) 对当前的点阵列中的元素和指定点阵列中相对应的元素执行按位与操作
public bool Get(int index) 获取点阵列中指定位置的位值
public BitArray Not() 反转当前点阵列中所有位的值,即将 true 设置为 false,将 false 设置为 true
public BitArray Or(BitArray value) 对当前点阵列中的元素和指定点阵列中的相对应的元素执行按位或操作
public void Set(int index, bool value) 把点阵列中指定位置的位设置为指定的值
public void SetAll(bool value) 把点阵列中的所有位设置为指定的值
public BitArray Xor(BitArray value) 对当前点阵列中的元素和指定点阵列中的相对应的元素执行按位异或操作

关于 BitArray 类中的完整属性和方法介绍,可以查阅 C# 官方文档

ConcurrentQueue并发队列

  • ConcurrentQueue<T> 是一个线程安全的队列,适用于多线程环境。
  • 提供了 EnqueueTryDequeueTryPeek 等方法来操作队列。
  • 适合于需要高并发读写的场景,能够有效地避免锁竞争。

在这里插入图片描述

 综合以上两种实现方式,在支持多线程并发出队并发入队的情况下,ConcurrentQueue使用了分段存储的概念(如上图所示),ConcurrentQueue分配内存时以段(Segment)为单位,一个段内部含有一个默认长度为32的数组和执行下一个段的指针,有个和Head和Tail指针分别指向了起始段和结束段(这种结构有点像操作系统的段式内存管理和页式内存管理策略)。这种分配内存的实现方式不但减轻的GC的压力而且调用者也不用显示的调用TrimToSize()方法回收内存(在某段内存为空时,会由GC来回收该段内存)。

Segment内部和用数组实现的普通队列相当,只不过对于入队和出队操作使用了原子操作来防止多线程竞争问题,使用随机退让等技术保证活锁等问题,实现机制和ConcurrentStack差别不大,跟多TryAppend的实现细节在源码注释中已经阐述的非常清楚这里就再做不过多的解释。

随机退让技术说明: 使用随机退让技术时,线程 A 在发现无法获取资源时,不会立即重试,而是会随机等待一段时间。这样,线程 B 可能会在 A 之后成功获取资源,从而打破活锁状态。

引入命名空间并初始化

1
2
3
using System.Collections.Concurrent;

ConcurrentQueue<int> queue = new ConcurrentQueue<int>();

属性盘点

属性 说明
Count 获取 ConcurrentQueue 中包含的元素数。
IsEmpty 获取一个值,该值指示 ConcurrentQueue 是否为空。

方法盘点

  • Clear()
    ConcurrentQueue<T> 中移除所有对象。

  • CopyTo(T[], Int32)
    从指定数组索引开始将 ConcurrentQueue<T> 元素复制到现有一维数组中。

  • Enqueue(T)
    将对象添加到 ConcurrentQueue<T> 的结尾处。

  • Equals(Object)
    确定指定对象是否等于当前对象。

  • GetEnumerator()
    返回循环访问 ConcurrentQueue<T> 的枚举数。

  • GetHashCode()
    作为默认哈希函数。

  • GetType()
    获取当前实例的 Type。

  • MemberwiseClone()
    创建当前对象的浅表副本。

  • ToArray()
    ConcurrentQueue<T> 中存储的元素复制到新数组中。

  • ToString()
    返回表示当前对象的字符串。

  • TryDequeue(T)
    尝试移除并返回并发队列开头处的对象。

  • TryPeek(T)
    尝试返回 ConcurrentQueue<T> 开头处的对象但不将其移除。

主要成员:入队(EnQueue) 、出队(TryDequeue) 、是否为空(IsEmpty)、获取队列内元素数量(Count)。

阻塞集合BlockingCollection

BlockingCollection<T> 是 .NET 中提供的一个线程安全的集合类,主要用于在生产者-消费者模式中进行数据的安全传递。它实现了 IProducerConsumerCollection<T> 接口,支持多线程环境下的并发操作。

BlockingCollection实际上就是一个自带信号量的ConcurrentQueue

BlockingCollectionConcurrentQueue 的基础上,通过内置信号量机制扩展了堵塞功能

主要特性

  1. 线程安全BlockingCollection<T> 是线程安全的,多个线程可以同时添加和取出元素。
  2. 阻塞操作:当集合为空时,尝试取出元素的线程会被阻塞,直到有元素可用;当集合达到最大容量时,尝试添加元素的线程会被阻塞,直到有空间可用。
  3. 可设置的容量:可以指定集合的最大容量,防止内存过度使用。

构造/设置容量

1
BlockingCollection<int> collection = new BlockingCollection<int>(capacity);//设置容量大小,如果不传参表示无限容量

如果集合达到这个容量,尝试添加新元素的线程将会被阻塞,直到有空间可用

主要方法

添加元素

1
2
public void Add(T item);
public void Add(T item, CancellationToken cancellationToken);
  • Add(T item):将元素添加到集合中,如果集合已满,则阻塞直到有空间可用。
  • Add(T item, CancellationToken cancellationToken):支持取消操作。

取出元素

1
2
public T Take();
public T Take(CancellationToken cancellationToken);
  • Take():从集合中取出一个元素,如果集合为空,则阻塞直到有元素可用。
  • Take(CancellationToken cancellationToken):支持取消操作。
GetConsumingEnumerable

用途: GetConsumingEnumerable 返回一个 IEnumerable<T>,消费者可以通过这个枚举器逐个消费集合中的元素。
行为:

  • 当集合中没有元素时,GetConsumingEnumerable 会阻塞当前线程,直到有新的元素被添加到集合中。
  • 一旦消费者从枚举器中获取了一个元素,该元素会从集合中移除。
  • 当 CompleteAdding 被调用并且集合中没有更多元素时,枚举器会结束。

适用场景: 适用于需要持续消费元素的场景,特别是在生产者-消费者模式中,消费者线程需要不断处理新元素。

1
2
3
4
5
6
7
8
9
// 消费者任务1: 使用 GetConsumingEnumerable
Task consumer1 = Task.Run(() =>
{
foreach (var item in collection.GetConsumingEnumerable())
{
Console.WriteLine($"Consumed (GetConsumingEnumerable): {item}");
Thread.Sleep(200); // 模拟消费延迟
}
});

尝试添加元素

1
2
public bool TryAdd(T item);
public bool TryAdd(T item, TimeSpan timeout);
  • TryAdd(T item):尝试添加元素,如果集合已满则返回 false
  • TryAdd(T item, TimeSpan timeout):在指定的时间内尝试添加元素。

尝试取出元素

1
2
public bool TryTake(out T item);
public bool TryTake(out T item, TimeSpan timeout);
  • TryTake(out T item):尝试从集合中取出一个元素,如果集合为空则返回 false
  • TryTake(out T item, TimeSpan timeout):在指定的时间内尝试取出元素。

完成添加

1
public void CompleteAdding();
  • 标记集合为完成添加,之后不再允许添加新元素。

调用后会将IsAddingCompleted设置为true

任何尝试调用 Add 或 TryAdd 方法向集合中添加新元素的操作都会抛出 InvalidOperationException 异常。
调用 CompleteAdding 后,消费者仍然可以继续消费集合中剩余的元素,直到集合为空

检查是否已完成

1
public bool IsAddingCompleted { get; }
  • 检查是否已完成添加。

ConcurrentBag

主要用于存储对象的集合,特别是在多线程环境中能够高效地处理并发操作,以下是ConcurrentBag<T>的一些主要特性和用途

  • 线程安全: 为多线程环境设计,内部实现确保了在多个线程同时访问时不会发生数据竞争或损坏
  • 无序集合: 无序集合,意味着元素的顺序不是固定的,不能保证按照插入顺序进行迭代
  • 高效的添加和取出: 支持高效的元素添加和取出操作,尤其适合在生产者-消费者模式中使用,允许多个线程同时添加和取出元素
  • 适用于短期对象存储: 例如对象池中的对象,它可以快速地将对象放入和取出,减少了锁的开销

由于其线程安全的特性,可以有效地减少锁的开销,提升并发性能

常用方法

  • **Add(T item)**:

    • ConcurrentBag<T> 中添加一个元素。
  • **TryTake(out T result)**:

    • 尝试从集合中取出一个元素。如果成功,返回 true,并将取出的元素赋值给 result
  • **TryPeek(out T result)**:

    • 尝试查看集合中的一个元素,但不将其移除。如果成功,返回 true,并将查看的元素赋值给 result
  • **ToArray()**:

    • ConcurrentBag<T> 中的元素复制到一个数组中。

ConcurrentDictionary

方法/属性 描述
AddOrUpdate(TKey key, TValue addValue, Func<TKey, TValue, TValue> updateValueFactory) 添加指定的键和值,如果键已经存在,则使用提供的函数更新其值
Clear() 从字典中移除所有键/值对
ContainsKey(TKey key) 确定字典中是否包含指定的键
GetOrAdd(TKey key, TValue value) 如果字典中不存在指定的键,则添加键和值,并返回该值;如果键已存在,则返回该键对应的值
TryAdd(TKey key, TValue value) 尝试将指定的键和值添加到字典中
TryGetValue(TKey key, out TValue value) 尝试从字典中获取指定键的值
TryRemove(TKey key, out TValue value) 尝试从字典中移除指定键及其对应的值
TryUpdate(TKey key, TValue newValue, TValue comparisonValue) 尝试更新字典中指定键的值
Keys 获取字典中所有键的集合
Values 获取字典中所有值的集合
Count 获取字典中键值对的数量
this[TKey key] 获取或设置指定键的值

CSharp所有容器总结

盘点所有容器

  1. Array(数组):固定大小的连续内存块,用于存储相同类型的数据。
  2. List(列表):动态大小的数组,支持快速随机访问,但插入和删除操作在中间位置可能比较慢。
  3. Dictionary<TKey, TValue>(字典):使用键值对存储数据,支持快速查找、插入和删除操作,键必须是唯一的。
  4. HashSet(哈希集):存储不重复的元素,支持快速查找、插入和删除操作。
  5. LinkedList(链表):使用节点存储数据,节点之间通过指针链接,支持快速插入和删除操作,但随机访问比较慢。
  6. Queue(队列):先进先出 (FIFO) 的数据结构,支持快速入队和出队操作。
  7. Stack(栈):后进先出 (LIFO) 的数据结构,支持快速入栈和出栈操作。
  8. ObservableCollection(可观察集合):支持通知机制,当集合发生变化时,会通知订阅者。
  9. IEnumerable(可枚举集合):定义了遍历集合的接口,允许使用 foreach 循环访问集合中的元素。
  10. IQueryable(可查询集合):定义了查询集合的接口,允许使用 LINQ 查询语言对集合进行查询。
  11. SortedList(排序列表):存储键值对,根据键进行排序,支持快速查找操作。
  12. SortedDictionary(排序字典):存储键值对,根据键进行排序,支持快速查找、插入和删除操作。
  13. ConcurrentBag(并发背包):线程安全的集合,支持并发添加和删除操作,但不保证元素的顺序。
  14. ConcurrentQueue(并发队列):线程安全的队列,支持并发入队和出队操作。
  15. ConcurrentStack(并发栈):线程安全的栈,支持并发入栈和出栈操作。
  16. ConcurrentDictionary(并发字典):线程安全的字典,支持并发查找、插入和删除操作。
  17. BlockingCollection(阻塞集合):线程安全的集合,支持生产者 - 消费者模式,当集合为空时,消费者会阻塞等待生产者添加元素。
  18. ReadOnlyCollection(只读集合):只读集合,不允许修改集合中的元素。
  19. ReadOnlyDictionary(只读字典):只读字典,不允许修改字典中的键值对。
  20. ImmutableDictionary(不可变字典): 不可变性线程安全机制

CSharp中所有的容器都实现了实现IEnumerable接口

IEnumerable接口

通过实现IEnumerable接口,可以使自定义的集合类能够被foreach语句简单遍历,提供了一种统一的遍历集合元素的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace System.Collections.Generic
{
//
// 摘要:
// 公开枚举器,支持对指定类型的集合进行简单迭代。
//
// 类型参数:
// T:
// 要枚举的对象的类型。
public interface IEnumerable<out T> : IEnumerable
{
//
// 摘要:
// 返回一个枚举器,用于遍历集合。
//
// 返回结果:
// 一个可用于遍历集合的枚举器。
IEnumerator<T> GetEnumerator();
}
}

特别强调一个非泛型接口,使用的时候要指明命名空间,才可以避免使用到泛型版本

System.Collections.IEnumerable 是一个非泛型接口,它允许你在不关心集合内部具体类型的情况下,对集合进行统一的处理。这意味着你可以将任何实现了 IEnumerable 接口的集合(如 ArrayListHashTableQueue 等)作为统一的容器来处理。

遍历方式

  1. foreach

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    //MyCollection实现了IEnumerable
    public class MyCollection : IEnumerable
    {
    private int[] data = { 1, 2, 3, 4, 5 };

    public IEnumerator GetEnumerator()
    {
    return data.GetEnumerator();
    }
    }

    MyCollection collection = new MyCollection();
    foreach (int item in collection)
    {
    Console.WriteLine(item);
    }
  2. 使用IEnumerator接口手动迭代

    • 可以通过手动调用枚举器对象的方法来遍历集合元素,而不是使用foreach语句。
    • 使用IEnumerator接口的方法可以实现更灵活的遍历方式,例如条件遍历、跳过元素等操作。
  3. LINQ查询

    • LINQ(Language Integrated Query)提供了丰富的查询操作符,可以对实现了IEnumerable接口的集合进行查询和筛选。
    • 可以使用LINQ查询来对集合进行排序、过滤、投影等操作。

CSharp 泛型

在 C# 中,泛型(Generic)是一种规范,它允许我们使用占位符来定义类和方法,编译器会在编译时将这些占位符替换为指定的类型,利用泛型的这一特性我们可以定义为

  • 通用类(泛型类)

  • 通用方法(泛型方法)

定义通用类需要使用尖括号<>,这里的尖括号用于将类或方法声明为泛型。

可以将泛型看作是一种增强程序功能的技术,泛型类和泛型方法兼具可重用性、类型安全性和效率,这是非泛型类和非泛型方法无法实现的。泛型通常与集合以及作用于集合的方法一起使用,System.Collections.Generic 命名空间下就包含几个基于泛型的集合类。下面总结了一些关于泛型的特性:

  • 使用泛型类型可以最大限度地重用代码、保护类型的安全性以及提高性能;
  • 泛型最常见的用途是创建集合类;
  • .NET 类库在 System.Collections.Generic 命名空间中包含几个新的泛型集合类,您可以使用这些类来代替 System.Collections 中的集合类;
  • 您可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托
  • 您也可以对泛型类进行约束以访问特定数据类型的方法;
  • 泛型数据类型中所用类型的信息可在运行时通过使用反射来获取

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class GenericClass<T>{
// 泛型方法
public GenericClass(T msg){
Console.WriteLine(msg);
}
}
class Demo
{
static void Main(string[] args){
GenericClass<string> str_gen = new GenericClass<string>("C语言中文网");
GenericClass<int> int_gen = new GenericClass<int>(1234567);
GenericClass<char> char_gen = new GenericClass<char>('C');
Console.ReadKey();
}
}

泛型方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
class Demo
{
static void Swap<T>(ref T lhs, ref T rhs)
{
T temp;
temp = lhs;
lhs = rhs;
rhs = temp;
}
static void Main(string[] args)
{
int a, b;
char c, d;
a = 10;
b = 20;
c = 'I';
d = 'V';
// 在交换之前显示值
Console.WriteLine("调用 swap 之前的 Int 值:");
Console.WriteLine("a = {0}, b = {1}", a, b);
Console.WriteLine("调用 swap 之前的字符值:");
Console.WriteLine("c = {0}, d = {1}", c, d);
// 调用 swap
Swap<int>(ref a, ref b);
Swap<char>(ref c, ref d);
// 在交换之后显示值
Console.WriteLine("调用 swap 之后的 Int 值:");
Console.WriteLine("a = {0}, b = {1}", a, b);
Console.WriteLine("调用 swap 之后的字符值:");
Console.WriteLine("c = {0}, d = {1}", c, d);
Console.ReadKey();
}
}

泛型委托

delegate T NumberChanger<T>(T n);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Demo
{
delegate T NumberChanger<T>(T n);
static int num = 10;
public static int AddNum(int p){
num += p;
return num;
}
public static int MultNum(int q){
num *= q;
return num;
}
public static int getNum(){
return num;
}
static void Main(string[] args){
// 创建委托实例
NumberChanger<int> nc1 = new NumberChanger<int>(AddNum);
NumberChanger<int> nc2 = new NumberChanger<int>(MultNum);
// 使用委托对象调用方法
nc1(25);
Console.WriteLine("Num 的值为: {0}", getNum());
nc2(5);
Console.WriteLine("Num 的值为: {0}", getNum());
Console.ReadKey();
}
}

CSharp 匿名函数/方法

在 C# 中,可以将匿名函数简单的理解为没有名称只有函数主体的函数。匿名函数提供了一种将代码块作为委托参数传递的技术,它是一个“内联”语句或表达式,可在任何需要委托类型的地方使用。匿名函数可以用来初始化命名委托或传递命名委托作为方法参数。

无需在匿名函数中指定返回类型,返回值类型是从方法体内的 return 语句推断出来的。

名函数是通过使用 delegate 关键字创建的委托实例来声明的

1
2
3
4
5
6
7
8
9
10
delegate void NumberChanger(int n);
...
//匿名函数传递给了nc委托
NumberChanger nc = delegate(int x)
{
Console.WriteLine("Anonymous Method: {0}", x);
};

//作为对比,查看命名函数实例化委托(假设这个命名函数名为xxx)
//NumberChanger nc = new NumberChanger(xxx);

委托可以通过匿名函数调用,也可以通过普通有名称的函数调用,只需要向委托对象中传递相应的方法参数即可。注意,匿名函数的主体后面需要使用;结尾

CSharp new Action的方式定义匿名函数

在C#中,Action是一种代表不返回值的委托类型。它可以用来定义一个接受零个到多个参数的方法,并且不返回任何值。下面是一个示例,演示了如何使用C#中的new Action来定义一个没有参数的方法:

1
2
3
4
5
6
7
8
9
10
using System;

class Program
{
static void Main()
{
Action myAction = () => Console.WriteLine("Hello, Action!");
myAction();
}
}

可以使用 Action<T1, T2, ...> 来定义一个带有多个参数的委托。以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
Action<int, string> myAction = (num, str) =>
{
Console.WriteLine($"Number: {num}, String: {str}");
};

myAction(10, "Hello");
//可以直接整合到一块,一步到位调用
new Action<int, string>(
(num, str) => {
Console.WriteLine($"Number: {num}, String: {str}");
}
)(10,"hello world!");

定义了一个带有两个参数(一个整数和一个字符串)的 Action 委托,并创建了一个匿名函数来输出这两个参数的值。

CSharp 指针变量与unsafe

为了保持类型的安全性,默认情况下 C# 是不支持指针的,但是如果使用 unsafe 关键字来修饰类或类中的成员,这样的类或类中成员就会被视为不安全代码,C# 允许在不安全代码中使用指针变量。在公共语言运行时 (CLR) 中,不安全代码是指无法验证的代码,不安全代码不一定是危险的,只是 CLR 无法验证该代码的安全性。因此 CLR 仅会执行信任程序集中包含的不安全代码。

指针变量

在 C# 中,指针同样是一个变量,但是它的值是另一个变量的内存地址,在使用指针之前我们同样需要先声明指针,声明指针的语法格式如下所示:

示例 说明
int* p p 是指向整数的指针
double* p p 是指向双精度数的指针
float* p p 是指向浮点数的指针
int** p p 是指向整数的指针的指针
int*[] p p 是指向整数的指针的一维数组
char* p p 是指向字符的指针
void* p p 是指向未知类型的指针
1
2
3
4
5
6
7
8
9
10
11
class Demo
{
static unsafe void Main(string[] args)
{
double f = 3.1415;
double* p = &f;
Console.WriteLine("数据的内容是: {0} ", f);
Console.WriteLine("数据在内存中的地址是: {0}", (int)p);
Console.ReadKey();
}
}

提示:在编译上述代码时需要在编译命令中添加-unsafe,例如csc -unsafe demo.cs

在 C# 中,我们可以使用 ToString() 来获取指针变量所指向的数据的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo
{
public static void Main()
{
unsafe
{
int var = 123456;
int* p = &var;
Console.WriteLine("变量 var 的值为: {0} " , var);
Console.WriteLine("指针 p 指向的值为: {0} " , p->ToString());
Console.WriteLine("指针 p 的值为: {0} " , (int)p);
}
Console.ReadKey();
}
}

使用指针访问数组元素

在 C# 中,数组和指向该数组且与数组名称相同的指针是不同的数据类型,例如int* pint[] p就是不同的数据类型。您可以增加指针变量 p 的值,因为它在内存中不是固定的,但数组地址在内存中是固定的,因此您不能增加数组 p 的值。如果您需要使用指针变量访问数组数据,使用 fixed 关键字来固定指针。下面通过示例演示一下:

在C#中,固定语句(fixed statement)通常用于与非托管代码进行交互。非托管代码是指直接操作内存或者由其他语言编写的代码,通常不受C#垃圾回收器的管理。当你需要将C#中的托管对象传递给非托管代码时,必须确保这些对象在内存中不会被移动,因为非托管代码可能会持有指向这些对象的指针。通过使用固定语句,你可以确保在固定块内的对象不会被垃圾回收器移动,从而保证非托管代码能够正确地访问这些对象的内存位置。

C#中的垃圾回收器是自动管理的,它负责在程序运行时检测和回收不再被程序使用的内存。垃圾回收器使用一种叫做”标记-清除”的算法来确定哪些内存块是可以回收的。当垃圾回收器检测到某个对象不再被引用时,它会将其标记为可回收,并在适当的时机清除这些对象以释放内存。这种自动内存管理机制让开发人员不必手动管理内存,减少了内存泄漏和其他内存相关的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Demo
{
public unsafe static void Main()
{
int[] list = {10, 100, 200};
fixed(int *ptr = list)//fixed!!!
/* 显示指针中数组地址 */
for ( int i = 0; i < 3; i++)
{
Console.WriteLine("list[{0}] 的内存地址为:{1}",i,(int)(ptr + i));
Console.WriteLine("list[{0}] 的值为:{1}", i, *(ptr + i));
}
Console.ReadKey();
}
}

编译不安全代码

为了编译不安全代码,在编译时必须使用unsafe命令,例如编译包含不安全代码的 demo.cs 程序的命令如下所示:

csc /unsafe demo.cs

csc -unsafe demo.cs

如果您使用的是 Visual Studio,那么您需要在项目属性中启用不安全代码,具体步骤如下:

  • 通过双击资源管理器(Solution Explorer)中的属性(properties)节点,打开项目属性(project properties);
  • 点击 Build 标签页;
  • 选择选项“Allow unsafe code”。

CSharp 多线程

多线程就是多个线程同时工作的过程,我们可以将线程看作是程序的执行路径,每个线程都定义了一个独特的控制流,用来完成特定的任务。如果您的应用程序涉及到复杂且耗时的操作,那么使用多线程来执行是非常有益的。使用多线程可以节省 CPU 资源,同时提高应用程序的执行效率,例如现代操作系统对并发编程的实现就用到了多线程。到目前为止我们编写的示例程序都是单线程的应用程序,这样的应用程序一次只能执行一个任务。

线程生命周期

线程生命周期开始于我们创建 System.Threading.Thread 类对象的时候,当线程被终止或完成执行时生命周期终止。

下面列出了线程生命周期中的各种状态:

  • 未启动状态:当线程实例被创建但 Start 方法未被调用时的状况;
  • 就绪状态:当线程准备好运行并等待 CPU 周期时的状况;
  • 不可运行状态:下面的几种情况下线程是不可运行的:
    • 已经调用 Sleep 方法;
    • 已经调用 Wait 方法;
    • 通过 I/O 操作阻塞。
  • 死亡状态:当线程已完成执行或已中止时的状况。

主线程

在 C# 中,System.Threading.Thread 类用于处理线程,它允许在多线程应用程序中创建和访问各个线程。在多线程中执行的第一个线程称为主线程,当 C# 程序开始执行时,将自动创建主线程,而使用 Thread 类创建的线程则称为子线程,您可以使用 Thread 类的 CurrentThread 属性访问线程。下面通过示例程序演示主线程的执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
using System.Threading;
class Demo
{
static void Main(string[] args)
{
Thread th = Thread.CurrentThread;
th.Name = "主线程";
Console.WriteLine("这是{0}", th.Name);
Console.ReadKey();
}
}
//输出:这是主线程

Thread 类

属性 描述
CurrentContext 获取线程正在执行的上下文
CurrentCulture 获取或设置当前线程的区域性
CurrentPrincipal 获取或设置线程的当前负责人(对基于角色的安全性而言)
CurrentThread 获取当前正在运行的线程
CurrentUICulture 获取或设置资源管理器使用的当前区域性以便在运行时查找区域性特定的资源
ExecutionContext 获取一个 ExecutionContext 对象,该对象包含有关当前线程的各种上下文的信息
IsAlive 获取当前线程的执行状态
IsBackground 获取或设置一个值,该值表示某个线程是否为后台线程
IsThreadPoolThread 获取线程是否属于托管线程池
ManagedThreadId 获取当前托管线程的唯一标识符
Name 获取或设置线程的名称
Priority 获取或设置线程的调度优先级
ThreadState 获取当前线程的状态
方法名 描述
public void Abort() 在调用此方法的线程上引发 ThreadAbortException,以终止此线程
public static LocalDataStoreSlot AllocateDataSlot() 在所有的线程上分配未命名的数据槽,为了获得更好的性能,请改用以 ThreadStaticAttribute 特性标记的字段
public static LocalDataStoreSlot AllocateNamedDataSlot(string name) 在所有线程上分配已命名的数据槽,为了获得更好的性能,请改用以 ThreadStaticAttribute 特性标记的字段
public static void BeginCriticalRegion() 通知主机执行将要进入一个代码区域,在该代码区域内线程中止或未经处理的异常的影响可能会危害应用程序域中的其他任务
public static void BeginThreadAffinity() 通知主机托管代码将要执行依赖于当前物理操作系统线程的标识指令
public static void EndCriticalRegion() 通知主机执行将要进入一个代码区域,在该代码区域内线程中止或未经处理的异常仅影响当前任务
public static void EndThreadAffinity() 通知主机托管代码已执行完依赖于当前物理操作系统线程的标识指令
public static void FreeNamedDataSlot(string name) 为进程中的所有线程消除名称与数据槽之间的关联。为了获得更好的性能,请改用以 ThreadStaticAttribute 特性标记的字段
public static Object GetData(LocalDataStoreSlot slot) 检索当前线程中指定的值。为了获得更好的性能,请改用以 ThreadStaticAttribute 特性标记的字段
public static AppDomain GetDomain() 返回当前线程运行的域
public static AppDomain GetDomainID() 返回应用程序域的唯一标识符
public static LocalDataStoreSlot GetNamedDataSlot(string name) 查找已命名的数据槽。为了获得更好的性能,请改用以 ThreadStaticAttribute 特性标记的字段
public void Interrupt() 中断处于 WaitSleepJoin 状态的线程
public void Join() 在继续执行标准的 COM 和 SendMessage 消息泵处理期间,阻塞调用线程,直到某个线程终止为止。此方法有不同的重载形式
public static void MemoryBarrier() 按如下方式同步内存访问:执行当前线程的处理器在对指令重新排序时不能采用先执行 MemoryBarrier 调用之后的内存存取,再执行 MemoryBarrier 调用之前的内存存取的方式
public static void ResetAbort() 取消为当前线程请求的 Abort
public static void SetData(LocalDataStoreSlot slot, Object data) 在当前正在运行的线程上的指定槽中为此线程的当前域设置数据。为了获得更好的性能,请改用以 ThreadStaticAttribute 特性标记的字段
public void Start() 开始一个线程
public static void Sleep(int millisecondsTimeout) 让线程暂停一段时间
public static void SpinWait(int iterations) 让线程等待一段时间,时间长短由 iterations 参数定义
public static byte VolatileRead(ref byte address) public static double VolatileRead(ref double address) public static int VolatileRead(ref int address) public static Object VolatileRead(ref Object address) 读取字段值。无论处理器的数目或处理器缓存状态如何,该值都是由计算机处理器写入的最新值
public static void VolatileWrite(ref byte address, byte value) public static void VolatileWrite(ref double address, double value) public static void VolatileWrite(ref int address, int value) public static void VolatileWrite(ref Object address, Object value) 立即向字段中写入一个值,并使该值对计算机中的所有处理器都可见
public static bool Yield() 终止当前正在调用的线程并执行另一个准备运行的线程(由操作系统选择将要执行的另一个线程)

创建线程

C# 是通过扩展 Thread 类来创建线程的,然后使用扩展的 Thread 类调用 Start() 方法开始执行子线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using System;
using System.Threading;
class Demo
{
public static void CallToChildThread()
{
Console.WriteLine("执行子线程");
}

static void Main(string[] args)
{
ThreadStart childref = new ThreadStart(CallToChildThread);//这行代码定义了一个 ThreadStart 委托(该委托不支持返回值也不支持传参)类型的变量 childref ,它指向一个名为 CallToChildThread 的方法。
Console.WriteLine("在 Main 函数中创建子线程");
Thread childThread = new Thread(childref);//这行代码创建了一个新的线程对象 childThread ,并将之前定义的委托 childref 作为参数传递给线程对象。
childThread.Start();//启动了子线程
Thread.Sleep(2000);
childThread.Abort();//中止子线程(m1 mac上不支持该函数)
Console.ReadKey();
}
}

CSharp BackgroundWorker

以下是一个简单的示例,演示如何使用BackgroundWorker在C#中执行异步操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.ComponentModel;
using System.Threading;

class Program
{
static BackgroundWorker backgroundWorker = new BackgroundWorker();

static void Main()
{
backgroundWorker.DoWork += DoWork;
backgroundWorker.ProgressChanged += ProgressChanged;
backgroundWorker.RunWorkerCompleted += RunWorkerCompleted;
backgroundWorker.WorkerReportsProgress = true;
backgroundWorker.RunWorkerAsync();

Console.WriteLine("Main thread is not blocked!");

Console.ReadLine();
}

static void DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i <= 100; i++)
{
Thread.Sleep(100); // 模拟耗时操作

// 报告进度
backgroundWorker.ReportProgress(i);
}
}

static void ProgressChanged(object sender, ProgressChangedEventArgs e)
{
Console.WriteLine($"Progress: {e.ProgressPercentage}%");
}

static void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
Console.WriteLine("Async operation completed!");
}
}

在这个示例中,BackgroundWorker在后台线程中执行一个简单的计数操作,每次递增并报告进度。主线程不会被阻塞,可以继续执行其他操作。

与Thread的区别:

  • BackgroundWorker是一个高级别的组件,封装了在后台执行操作的细节,使得在UI线程上执行操作和在后台线程上执行操作更加容易。它提供了事件来报告进度和完成状态,并且可以在UI线程中方便地更新UI元素。
  • Thread是一个更底层的多线程类,可以更灵活地控制线程的创建和管理。但是使用Thread需要手动处理线程的生命周期、线程同步和异常处理等问题,相对复杂一些。

CSharp 获取线程id

1
2
Thread.CurrentThread.ManagedThreadId//不推荐使用这个
Environment.CurrentManagedThreadId.Dump(title);//这种方式更轻量化

打印线程id函数可封装成如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Runtime.CompilerServices;
using System.Threading;

public class Helper
{
private static int index = 1;
//[CallerMemberName]的效果: name会被传入调用者的方法名
public static void PrintThreadId(string? message = null, [CallerMemberName] string? name = null)
{
var title = $"{index}{name}";

if (!string.IsNullOrEmpty(message))
{
title += $" @ {message}";
}

Console.WriteLine($"线程 ID: {Environment.CurrentManagedThreadId}, {title}");
Interlocked.Increment(ref index);
}
}

CSharp 异步编程模型

什么是异步?

异步是一个更广泛的行为,开个线程也叫异步,回调也叫异步

C# 中的 AsyncAwait 关键字是异步编程的核心。 通过这两个关键字,可以使用 .NET Framework、.NET Core 或 Windows 运行时中的资源,轻松创建异步方法(几乎与创建同步方法一样轻松)。

  • async:将方法标记为异步方法,表示该方法可能包含异步操作。

    最终是否采用异步执行(是否真使用了多线程),不决定于是否用await方式调用这个方法,而决定于这个方法内部,是否有await方式的调用。

    只有将方法标记async后,才可以在方法中使用await关键字

  • await:用于等待一个操作完成的结果,然后继续执行下面的代码。await只能在async方法内部使用。

    在异步中,await表达的意思是:当前线程/方法中,await引导的方法出结果前,跳出当前线程/方法,从调用当前线程/方法的位置,去执行其它可能执行的线程/方法,并在引导的方法出结果后,把运行点拉回到当前位置继续执行;直到遇到下一个await,或线程/方法完成返回,跳回去刚才外部最后执行的位置继续执行。(因此await不会堵塞线程,虽然表现上是停止往后运行了,可以理解为堵塞了逻辑,但不堵塞线程,线程实际上去干别的了)

    在通过await函数时,会等待函数执行完才回来继续执行下一句代码,但如果不通过await执行一个内含await的async函数,那么程序才是真正异步执行(存在子线程执行的情况)

注意 await可以作用于[Task任务](#CSharp Task),而不一定要作用于async方法

1
2
3
4
5
6
7
8
async Task Main()
{
await FooAsync();
}
Task FooAsync()
{
return Task.Delay(1000);
}

要真正使用异步:

  • 被调用函数被async标记,被调用函数内部使用了await

  • 调用被调用函数的时候没写await,才是真正使用异步

    如果使用了await方式调用被调用函数,那么实际上是堵塞了代码逻辑

线程切换时机

此处结合使用了[CSharp 获取线程id](CSharp 获取线程id)中的代码,案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
async Task Main()
{
Helper.PrintThreadId("Before");
await FooAsync();
Helper.PrintThreadId("After");
}

async Task FooAsync()
{
Helper.PrintThreadId("Before");
await Task.Delay(1000);
Helper.PrintThreadId("After");
}

打印如下:

1
2
3
4
线程 ID: 1, 1:Main @ Before
线程 ID: 1, 2:FooAsync @ Before
线程 ID: 5, 3:FooAsync @ After
线程 ID: 5, 4:Main @ After

可见在await Task.Delay(1000)之前是同一个线程执行,在其之后包括await Task.Delay(1000)本身是另一个线程执行

使用await Task.Delay(1000).ConfigureAwait(true);可以让等待后回来执行的线程与离开的线程是同一个,即不会切换线程(等待后继续在原来的上下文中执行后续代码).但是注意该效果只在wpf或winform中有效,控制台程序无效

理解 ConfigureAwait(true)

同步上下文

  • 在 WPF 和 WinForms 应用程序中,存在一个“同步上下文”(SynchronizationContext),它负责管理 UI 线程的执行。当你在 UI 线程上运行异步代码时,await 会捕获当前的同步上下文,并在等待完成后返回到同一个上下文中继续执行后续代码。
  • 这意味着如果你在 UI 线程上调用 await Task.Delay(1000).ConfigureAwait(true);,那么在等待结束后,代码会在 UI 线程中继续执行,从而可以安全地更新 UI 控件。

控制台程序

  • 控制台应用程序默认没有同步上下文,这意味着在控制台程序中使用 await 时,ConfigureAwait(true) 不会有任何效果。因为没有上下文可供返回,await 完成后会在线程池线程上继续执行后续代码。
  • 因此,在控制台应用程序中,即使你使用 ConfigureAwait(true),也不会保证后续代码在原来的线程上执行。

WPF/WINFORM中

在 WPF 中,UI 线程会有一个与之关联的 SynchronizationContext。当你在 UI 线程上调用一个 async 方法并使用 await 时,await 会自动捕获当前的同步上下文(即 UI 线程的上下文),并在异步操作完成后返回到同一个上下文中继续执行后续代码。

无需使用 ConfigureAwait:由于 WPF 的 SynchronizationContext 会自动处理上下文切换,因此在 WPF 中通常不需要使用 ConfigureAwait(true)。这意味着你可以安全地在 async 方法中更新 UI 控件,而不必担心线程安全问题。

异步编程的重要思想是不阻塞!

  • await会暂时释放当前线程,使得该线程可以执行其他工作,而不必阻塞线程直到异常操作完成
  • 不应该在异步方法中用任何方式阻塞当前线程(更细地讲就是wpf或winform中await之前一定不能阻塞,此时还在原线程中)

下面罗列常见阻塞线程情形

  • Task.Wait() Task.Result()
  • Thread.Sleep()
  • IO等操作的同步方法
  • 其他繁重且耗时的任务

因此要注意,当这些阻塞线程的行为在async方法中的第一次使用await代码之前调用的时候,实际上还是会被阻塞线程,因为此时还没有切到别的线程

同步上下文的理解

同步上下文是一种管理和协调线程的机制,允许开发者将代码的执行切换到特定的线程

WinForms 与 WPF 拥有同步上下文(UI线程),而控制台程序默认没有

ConfigureAwait

通过Task.ConfigureAwait(false/true)来设置await执行Task后是否回到原线程,true表示回到原线程

一般只有UI线程会采用这种策略:Task.ConfigureAwait(false)

关于同步上下文导致的死锁

此处记录一个关于同步上下文导致的死锁情况的经典案例

1
2
3
4
5
6
7
8
9
10
11
async Task<int> HeavyJob()
{
await Task.Delay(2000);//2.Delay完成后将回到ui线程
return 10;
}
//按钮点击事件绑定的方法
private void Button_click(object sender,RoutedEventArgs e)
{
var res = HeavyJob().Result;//1. 内部调用了Task.Wait(),会导致ui线程堵塞,直到得到HeavyJob的返回结果 3,HeavyJob的结果需要2号代码能返回到ui线程,可ui线程又正在被堵塞中,于是陷入死锁
txt.Text = res.ToString();
}

上面的代码将导致死锁,不是卡住2秒,而是一直卡住

通俗地讲就是 **你托线程带个东西回来,东西不带回来就线程别想回来,还只能当前线程带回来.**于是导致死锁

解决方法是改编号2的代码为:await Task.Delay(2000).ConfigureAwait(false);,可以通俗理解为: 你要求A线程带个东西回来,东西不带回来A线程就别想回来,但可以托别的线程带回来,别的线程把东西带回来了,A线程也就可以回来了

TaskScheduler

控制Task的调度方式和运行线程

用法可参考此处

  • 线程池线程 Default
  • 当前线程 CurrentThread
  • 单线程上下文 STAThread
  • 长时间运行线程 LongRunning

也可以设置优先级,上下文,执行状态等

直接调用async方法

一发即忘 Fire-and-forget

调用一个异步方法,但是不使用await或阻塞的方式去等待它的结束

无法观察任务的状态(是否完成,是否报错等)

返回值相关

使用async关键字标记的函数通常需要返回一个下面类型的结果

  • Task类型: 表示异步操作完成后不返回任何结果
  • Task<T> 类型:表示异步操作完成后返回一个T类型的值
  • void 类型:通常用于事件处理程序等异步操作中,表示异步操作完成后不返回任何结果,但可能通过事件或其他机制通知调用方。

命名规范: 命名异步方法时,可以在方法名后面加上Async后缀,以明确表示它是一个异步方法,例如DownloadDataAsync

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Net.Http;
using System.Threading.Tasks;

class Program
{
static async Task Main(string[] args)
{
await DownloadWebsiteAsync();//堵塞
Console.WriteLine("下载完成!");
}

static async Task DownloadWebsiteAsync()
{
using (HttpClient client = new HttpClient())
{
string website = "https://www.example.com";
string content = await client.GetStringAsync(website);//堵塞
Console.WriteLine("下载内容长度:" + content.Length);
}
}
}

Main方法和DownloadWebsiteAsync方法都被标记为async,在DownloadWebsiteAsync方法内部,通过await等待GetStringAsync方法的异步操作完成。这样,程序能够在等待异步操作的同时,继续执行其他代码,提高了程序的并发性和响应性。

执行流程

image-20240409134922876 image-20240621090250722

调用Func1函数的时候,没有前缀await,Func1又是一个被async标识的函数,因此Func1函数会由子线程执行

1
2
3
4
5
6
7
8
//输出为
Async proccess - start
Async proccess - enter Func1
Func1 proccess - start
Async proccess - out Func1
Async proccess - done
Func1 proccess - end
Main proccess - done

原理相关

async与await组合使用会将方法包装成[[设计模式#状态模式|状态机],MoveNext方法会被底层调用,从而切换状态

查看SharpLab

1
2
3
4
5
public async Task Foo()
{
await Task.Delay(1);
Console.WriteLine();
}

上面代码会被编译器编译的时候包装成一个状态机:(经过一定的变量名修改,以及提取核心代码段后的代码如下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// CompilerGenerated 属性表示下面的类是编译器生成的,通常用于异步方法的状态机。 
[CompilerGenerated]
// 定义一个名为 FooStateMachine 的密封类,它继承自 IAsyncStateMachine。密封类意味着这个类不能被继承,并且所有成员都是私有的,除非声明为公共或受保护。
private sealed class FooStateMachine : IAsyncStateMachine
{
// 定义一个名为 currentState 的公共整数属性,用于存储状态机的当前状态。这个属性没有指定的访问修饰符,默认为 private。
public int currentState;

// 定义一个名为 t__builder 的公共 AsyncTaskMethodBuilder 类型的属性。AsyncTaskMethodBuilder 用于异步方法的启动和完成,并跟踪异步操作的状态。
public AsyncTaskMethodBuilder<> t__builder;

// 定义一个名为 4__this 的公共 C<> 类型的属性。这个属性通常用于存储异步操作的实例(如果有)。
public C<> 4__this;

// 定义一个名为 taskAwaiter 的私有 TaskAwaiter 类型的字段。这个字段用于存储异步操作的等待器,允许状态机在异步操作完成时恢复执行。
private TaskAwaiter taskAwaiter;

// 定义一个名为 MoveNext 的私有方法。这个方法包含了状态机的逻辑,根据当前状态执行相应的操作。
private void MoveNext()
{
// 声明一个名为 num 的整型变量并初始化为当前状态
int num = currentState;
try
{
// 声明一个名为 awaiter 的 TaskAwaiter 类型局部变量
TaskAwaiter awaiter;
// 判断当前状态是否为 0
if (num!= 0)
{
// 如果当前状态不是 0 ,通过 Task.Delay(1) 创建一个新的 awaiter
awaiter = Task.Delay(1).GetAwaiter();
// 判断新创建的 awaiter 是否已完成,如果没有完成,则将当前状态设置为 0,并将新创建的 awaiter 赋给 taskAwaiter,以便之后恢复执行
if (!awaiter.IsCompleted)
{
num = (currentState = 0);
taskAwaiter = awaiter;
// 创建一个名为 stateMachine 的 FooStateMachine 类的实例,以便在之后使用它恢复执行
<Foo>d__1 stateMachine = this;
// 通过引用 t__builder、awaiter 和 stateMachine,启动一个异步等待操作,这样当 awaiter 完成时,状态机可以恢复执行
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
return;
}
}
// 如果当前状态是 0 ,则将 taskAwaiter 的值赋给 awaiter,并将当前状态设置为 -1,标记为完成
else
{
// 等待 taskAwaiter 完成
awaiter = taskAwaiter;
// 重置 taskAwaiter 为默认状态
taskAwaiter = default(TaskAwaiter);
num = (currentState = -1);
}
// 获取 awaiter 的结果,如果有异常则抛出
awaiter.GetResult();
Console.WriteLine();
}
catch (Exception exception)
{
// 如果发生异常,将当前状态设置为 -2,并通过引用 t__builder,设置异常给异步任务
currentState = -2;
<>t__builder.SetException(exception);
return;
}
// 执行结束,将当前状态设置为 -2,表示异步任务已完成
currentState = -2;
// 通过引用 t__builder,设置异步任务完成
<>t__builder.SetResult();
}

// 定义一个名为 Foo 的公共方法,这个方法可能是异步操作的入口点
public Task Foo()
{
// 创建一个 FooStateMachine 类的实例 stateMachine,用于管理异步操作的状态
<Foo>d__1 stateMachine = new <Foo>d__1();
// 获取或创建一个 AsyncTaskMethodBuilder 实例,并将其赋值给 stateMachine.t__builder;这个对象将允许启动和跟踪异步操作的进度
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
// 将当前类的实例 this 赋值给 stateMachine.4__this,可能是为了在异步操作中访问当前类的成员或方法
stateMachine.<>4__this = this;
// 设置 stateMachine 的当前状态为 -1,可能表示异步操作的初始状态,或者是某种特定的起始状态
stateMachine.currentState = -1;
// 启动 stateMachine 的异步操作,通过引用 stateMachine 实例,异步操作会根据 currentState 的值执行相应的逻辑
stateMachine.<>t__builder.Start(ref stateMachine);
// 返回 stateMachine.t__builder 创建的 Task 对象,这个 Task 对象代表了异步操作的执行,可以通过它来跟踪异步操作的进度或等待其完成
return stateMachine.<>t__builder.Task;
}
}

async只是决定是否将其转换为一个状态机,而被包装的方法还是个Task.但是如果async方法中没有await操作,他其实是个同步方法,因为遇到await才可能涉及到线程切换

因此async是给编译器看的,本质上返回的仍然是Task,只不过通过编译器提供了语法糖,也因此接口中无法声明async Task

p.s. async还带另一个语法糖,在其中写返回值可以直接写 Task<T>中的T类型,不用包装成 Task<T>,如下:

1
2
3
4
5
6
7
8
9
10
public Task<int> Foo()
{
return Task.Run(()=>42);//必须返回Task<int>
//return Task.FromResult(42);//同理
}
//加了async后
public async Task<int> Foo()
{
return 42;
}

async void理解

几乎只用于对事件的注册,因为对事件的注册没办法写async Task

与async Task相比,同样会被包装成状态机,但缺少记录状态的Task对象

由于Task还包含了异步任务里面的异常信息,因此async void无法聚合异常(Aggregate Exception),需要谨慎处理异常

下面展示一个异常案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async Task Main()
{
try{
VoidAsync();
}
catch(Exception ex)
{
Console.WriteLine("捕获异常");
}
Console.ReadLine();//等待异步完成
}

async void VoidAsync()
{
await Task.Delay(1000);
Console.WriteLine("ok");
throw new Exception("Something was wrong!");
}

此抛出的异常不会被try,catch块捕获

因此在使用事件注册的时候,需要异步的情况下不得不使用async void的时候,一定注意在事件注册方法中做异常处理

只有返回Task的才能被await等待结果

异步完成后执行的另一种方式

.ContinueWith是用于任务(Task)的延续操作(continuation)的方法,它允许您在一个任务完成后执行另一个任务。.ContinueWith方法接受一个Action<Task>委托作为参数,表示在原始任务完成后要执行的操作。

使用.ContinueWith方法可以实现任务之间的串行执行,确保一个任务完成后再执行另一个任务。您还可以通过.ContinueWith方法指定不同的任务调度选项和取消标记,以满足更复杂的需求。.ContinueWith是异步编程中一个非常有用的方法,可以帮助您构建复杂的任务流程和处理异步操作的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Task originalTask = Task.Run(() =>
{
// 执行一些耗时操作
Thread.Sleep(2000);
Console.WriteLine("原始任务完成");
});

Task continuationTask = originalTask.ContinueWith((task) =>
{
Console.WriteLine("延续任务开始");
// 在原始任务完成后执行的操作
});

// 堵塞等待延续任务完成
continuationTask.Wait();

传染性

异步编程具有传染性(Contagious)

  • 一处async,处处async

  • 几乎所有自带方法都提供了异步的版本

    如果真的遇到了只提供同步的方案,也可以使用Task.Run把他包装成一个异步的方法

内置异步方法非常多,随意罗列一些:

1
2
3
4
HttpClient.GetAsync
File.WriteAllTextAsync
MemoryStream.ReadAsync
Console.Out.WriteLineAsync

同步异步技巧

如何创建异步任务

  • Task.Run()

    1
    2
    3
    4
    5
    6
    int HeavyJob()
    {
    //....
    }

    var res = await Task.Run(HeavyJob);//将同步任务直接包装成异步任务
  • Task.Factory.StartNew()

  • new Task+Task.Start()

如何同时开启多个异步任务

1
2
3
4
5
6
var tasks = new List<Task<int>>();
foreach(var input in inputs)
{
tasks.Add(HeavyJob(input));
}
await Task.WhenAll(tasks);

如何取消任务

使用CancellationTokenSource + CancellationToken取消任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try
{
var cts = new CancellationTokenSource();
var task = await Task.Delay(100000,cts.token);
Thread.Sleep(2000);
cts.Cancel();
await task;
}
catch(TaskCanceledException)
{
Console.WriteLine("任务取消");
}
finally
{
cts.Dispose();
}

推荐所有的异步方法都带上CancellationToken这一传参

CancellationToken/CancellationTokenSource

CancellationToken 是 C# 中处理异步操作取消的重要工具。通过合理使用 CancellationTokenCancellationTokenSource,可以有效地管理长时间运行的任务,并在需要时安全地取消它们。

  1. CancellationToken:表示请求取消操作的标志。它是一个轻量级的结构,通常用于传递给支持取消的异步方法。
  2. CancellationTokenSource:用于创建 CancellationToken 的对象,并提供取消操作的功能。

创建 CancellationTokenSource

1
2
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

CancellationToken也可以用于同步任务,不是只能用于异步任务

注意: CancellationTokenSource在调用 Cancel()之后就不能再复用了,必须cts = new CancellationTokenSource();创建一个新的实例才能再次使用

Cancel()会使调用CancellationTokenSource的任何位置抛出异常

特性 OperationCanceledException TaskCanceledException
继承关系 基类(直接继承自 SystemException 子类(继承自 OperationCanceledException
触发时机 操作中显式检查取消令牌时 任务被取消(尤其任务未开始执行时)
适用范围 任何取消操作(如异步方法、循环等) 仅限 Task 取消
典型场景 在方法内部调用 ThrowIfCancellationRequested() 等待一个已取消的 Task
是否表示任务状态 ❌ 不关联任务状态 ✔️ 关联任务状态(TaskStatus.Canceled

若需处理所有取消操作,捕获 OperationCanceledException(会同时捕获其子类 TaskCanceledException

最佳异步编程框架实现后台可中止模版示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class BackgroundService
{
private CancellationTokenSource _cts;
private Task _backgroundTask;

public void Start()
{
_cts = new CancellationTokenSource();
_backgroundTask = Task.Run(() => RunAsync(_cts.Token), _cts.Token);
}

private async Task RunAsync(CancellationToken token)
{
try
{
while (!token.IsCancellationRequested)
{
// 执行工作单元
await ProcessBatchAsync(token);

// 或使用显式检查
token.ThrowIfCancellationRequested();
}
}
catch (OperationCanceledException)
{
// 正常取消,无需处理
}
finally
{
// 资源清理(数据库连接、文件句柄等)
await CleanupResourcesAsync();
}
}

public async Task StopAsync()
{
if (_cts == null) return;

// 发起取消请求
_cts.Cancel();

try {
// 等待任务优雅终止(设置超时避免永久等待)
await _backgroundTask.WaitAsync(TimeSpan.FromSeconds(30));
}
catch (OperationCanceledException) { /* 已取消 */ }
finally {
_cts.Dispose();
}
}
}

任务超时如何实现

使用Channel实现异步任务之间的通信

参考视频

类似BlockingCollection以及ConcurrentQueue这种内部函数本质都是同步方法

Chanel

Channels 是一个高性能的线程安全库,专为 生产者-消费者场景 设计,用于解耦生产者和消费者任务,提升并发处理能力

命名空间: System.Threading.Channels

核心作用与优势

  • 解耦生产者与消费者:通过异步读写机制,允许生产者和消费者独立工作,互不阻塞,提高吞吐率
  • 动态扩展性:支持按需增加生产者或消费者数量,应对负载不均衡场景
  • 高性能:相比 BlockingCollectionTPL Dataflow,Channel 在单一职责(存储)场景下性能更高
  • 线程安全:内置同步机制,支持多线程并发读写
特性 Channel TPL Dataflow BlockingCollection
核心职责 存储 存储 + 处理流水线 存储
性能 高(线程安全优化)
适用场景 纯生产者-消费者模型 复杂数据处理流水线 简单同步队列
创建
  • 无限容量通道(Unbounded)

    1
    var channel = Channel.CreateUnbounded<string>();

    适用于不确定数据量或瞬时高吞吐场景。

    可配置 SingleWriterSingleReader 限制多线程操作,默认为单线程操作

  • 有限容量通道(Bounded)

    1
    2
    3
    4
    5
    6
    7
    8
    var channel = Channel.CreateBounded<string>(new BoundedChannelOptions(1000)
    {
    FullMode = BoundedChannelFullMode.Wait, // 设置满容策略
    SingleReader= true,//单线程
    SingleWriter=true,//单线程
    //Capacity
    //AllowSynchronousContinuations
    });

    满容策略

    • Wait:阻塞写入直到有空间。
    • DropWrite:丢弃新写入的数据。
    • DropOldest:丢弃最旧数据腾出空间
    • DropNewest:丢弃最新数据腾出空间
读写操作
方法 用途
TryWrite 非阻塞写入(同步)
WriteAsync 异步阻塞写入(参数可以接受CancellationToken)
TryRead 非阻塞读取(同步)
ReadAsync 异步阻塞读取(参数可以接受CancellationToken)
WaitToWriteAsync 异步等待直到允许写入
WaitToReadAsync 异步等待直到允许读取

通过 Writer.WriteAsync() 异步写入 await channel.Writer.WriteAsync("Hello World");

通过 Reader.ReadAsync() 或非阻塞循环读取

1
2
3
4
5
6
7
while (await channel.Reader.WaitToReadAsync())
{
if (channel.Reader.TryRead(out var message))
{
Console.WriteLine(message);
}
}
  • WaitToReadAsync() 异步等待数据到达,避免忙等待。
  • TryRead() 确保并发场景下安全读取
生产完成通知

Chanel.Writer.Complete()告诉Chanel,生产完成了,当消费者消费完剩余量后,将会触发ChannelClosedException异常

虽然使用reader.Completion.IsCompleted也可以判断是否完成了(但是只有读取的那瞬间能确定是否完成),由于程序执行流程的不确定性,建议通过ChannelClosedException来判断已完成

案例

多生产者-多消费者模型

1
2
3
4
5
var channel = Channel.CreateUnbounded<string>(new UnboundedChannelOptions { SingleWriter = false });
// 多线程写入
Parallel.For(0, 10, i => channel.Writer.WriteAsync("Msg_" + i));
// 多线程读取
Parallel.ForEach(channel.Reader.ReadAllAsync().ToEnumerable(), msg => Process(msg));

ReadAllAsync() 返回异步流,支持并行处理

如果是消费者的需要使用Chanel,可以直接传ChannelWriter<T>也可以,而不必要整个Chanel传过去

在异步任务中汇报进度

参考此处

如何在同步方法中调用异步方法

使用Task.Run包裹就可以很简单的将一个同步方法转换为一个异步方法,也就可以使用await等待结果

如何终止一个同步任务

下面方式似乎可行,实则不行,因为下面的两个方法均是只是获得了超时的执行时间点,并没有真正将同步任务给终止

控制被封装为异步任务的同步任务的执行超时时间,可以使用Task.Run封装起来之后,使用使用WaitAsync并传超时时间参数来设置超时时间

还有一种很巧妙的方式

1
2
3
var task1 = Task.Run(LongRunningJob);//LongRunningJob是个同步任务
var task2 = Task.Delay(3000);
await Task.WhenAny(task1,task2);

未完待续…

真正终止同步方法参考此视频

Task.Run和async方法的区别

  • Task.Run 用于将一个同步方法或代码块放到线程池中异步执行。它通常用于将 CPU 密集型的任务移出主线程,以避免阻塞用户界面。
  • async Task 是用于定义一个异步方法,这个方法可以使用 await 操作符来异步等待其他异步任务的完成。它通常用于 IO 密集型的操作,例如网络请求或文件读取。

在 C# 中,使用 Task.Run 启动的任务可以通过 await 关键字异步等待其结果,同时保持主线程不被阻塞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 主线程(如 UI 线程)调用的示例
private async void Button_Click(object sender, EventArgs e)
{
// 使用 Task.Run 将 CPU 密集型任务放到线程池
var result = await Task.Run(() =>
{
// 模拟耗时计算(如复杂数学运算)
Thread.Sleep(1000);
return 42;
});

// 这里会回到主线程上下文(UI 线程)
label.Text = $"Result: {result}"; // 安全更新 UI
}
  • ❌ 错误方式:var result = Task.Run(...).Result;(会阻塞主线程)
  • ✅ 正确方式:var result = await Task.Run(...);(非阻塞) Task.Run的参数可以接受同步函数

CPU 密集型任务才需要 Task.Run,IO 密集型任务应直接使用异步 API(如 File.ReadAllTextAsync

性能注意: 不要对已经异步的方法使用 Task.Run

TaskCompletionSource高级异步

TaskCompletionSource探讨的是如何让一些事务处理变成异步的

虽然Task.Run能让一些同步代码变成一个Task(通过在一个单独的线程上运行),TaskCompletionSource能让一些已经异步的代码变成Task(当然它最重要的功能使能够让一些基于回调的代码变成更好看的async/await)

但如果他已经是异步的,为啥还需要让他变成一个Task呢?这是因为Task只是一个好用的异步模型,已经异步的代码需要转换为Task才能更优雅的被async/await使用

使得异步代码执行的东西可以被等待,如果本身就是async/await实现的异步因为返回本身就是Task,这种情况下使用TaskCompletionSource就没什么意义,但是如果是用其他方式实现的异步就意义非凡

一个很经典的例子在于,对于子线程中更新界面的控件效果,需要通过Dispatcher.Invoke来执行ui改动界面代码,即Dispatcher.Invoke对于子线程中的代码来说是异步的,这样实际上外部的代码是无法等待Dispatcher.Invoke中执行的代码结束才返回的结果的.这时候就可以TaskCompletionSource来实现等待异步结果的效果:案例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//下面代码在子线程中执行
while(true)
{
Thread.Sleep(5000);
TaskCompletionSource<int> tcs = new();
Dispatcher.Invoke(()=>{
ChooseWindow1 chooseWindow1 = new();//供用户选择一边的数字的窗口
chooseWindow1.ShowDialog();

tcs.SetResult(chooseWindow1.Result);
});

Debug.Show("你的选择:"+await tcs.Task);//可以等待用户选择后才返回用户选择的结果!

即使是在异步编程模型使用本身,也可以使用TaskCompletionSource将某些返回值封装成可异步获取的结果,十分方便

wpf异步编程的死锁避免

在默认情况下,await 会尝试在原始线程上下文(如UI线程或ASP.NET请求上下文)中恢复后续代码的执行。但在同步阻塞操作(如 .Result.GetAwaiter().GetResult().Wait()等等)中,如果原始线程正被阻塞等待异步任务完成,而异步任务又尝试返回到该线程继续执行,就会导致死锁

如:在WinForm/WPF的UI线程中调用 task.Result,而异步方法内部未使用 ConfigureAwait(false)。此时,UI线程被阻塞,但异步任务完成后需要返回到UI线程执行后续代码,两者相互等待,形成死锁

即使使用了ConfigureAwait(false)也不能在子线程中通过dispatcher调度到ui线程执行,这样也会导致死锁,如果不使用dispatcher当需要操作界面元素的时候又是不合理的,因此也不行

多线程环境下的进度通知

Progress<T>对象继承自IProgress<T>接口

  • 是.NET框架的标准接口,位于System命名空间中
  • 专门用于异步操作中的进度报告(实际上是可以用于多线程,不仅仅是异步编程)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private async void Button_Click(object sender,RoutedEventArgs e)
{
button.IsEnabled = false;
//value=> progressBar.Value = value 就是每次Report会做的事情
var progress = new Progress<double>(value=> progressBar.Value = value);
await DoJobAsync(progress);
button.IsEnabled = true;
}
//这里传进来的是IProgress接口的意义在于,隐藏除了Report之外的其他没有意义的成员
async Task DoJobAsync(IProgress<double> progress)
{
for(int i=1;i<=100;i++)
{
await Task.Delay(50).ConfigureAwait(false);//执行完成后不会回到ui线程上,也可以正常执行下面的progress.Report(i);
progress.Report(i);
}
}

Report的内部原理是在new他本身的线程上下文中去执行new他时传递的操作参数

除此之外,progress.ProgressChanged+=可以注册事件,在每次被Report的时候,同时也调用这个事件

Progress中存在OnReport函数,可以被重写,该函数是Report被调用的时候首先会执行的函数

IProgress才可以调用Report函数,Progress对象如果需要调用Report函数需要转换为IProgress接口

Invoke与BeginInvoke

可以参考c#窗口ui线程防堵塞,参考特定于Windows窗体控件的线程安全方法,使用方式类似

Delegate.Invoke

Delegate.Invoke是同步的方法,会卡住调用它的UI线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
public delegate void TestDelegateInvoke();

private void DelegateInvokeMethod()
{
Thread.Sleep(5000);
}

private void btn_DelegateInvoke_Click(object sender , EventArgs e)
{
TestDelegateInvoke testDelegate = new TestDelegateInvoke(DelegateInvokeMethod);
testDelegate.Invoke();
//testDelegate();跟这样没什么区别
}

Delegate.Invoke是用于委托的通用调用,而Control.Invoke是一个特定于Windows窗体控件的线程安全方法,用于跨线程操作界面元素。如果你不需要处理多线程的UI更新,那么你可能不会使用到Control.Invoke

Delegate.BeginInvoke

在C#中,委托提供了BeginInvoke方法来启动委托的异步执行。这是.NET Framework中实现异步编程的早期方式之一(在Task并发模型出现之前)。通过委托的BeginInvoke方法,您可以在另一个线程上异步执行委托指向的方法,而不会阻塞当前线程。

简单案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义委托
public delegate int BinaryOperation(int x, int y);

// 委托指向的方法
public int Add(int x, int y)
{
return x + y;
}

// 异步执行委托
BinaryOperation op = new BinaryOperation(Add);
IAsyncResult asyncResult = op.BeginInvoke(2, 3, null, null);

// 在这里可以执行其他工作...

// 等待异步操作完成并获取结果
int result = op.EndInvoke(asyncResult);

尽管委托的Invoke和BeginInvoke方法仍然可用,但在新的开发中,通常建议使用[基于Task的模式](#CSharp 异步编程模型),因为它提供了更简单、更可靠的异步编程体验。

在这个示例中,我们定义了一个名为BinaryOperation的委托,它接受两个int参数并返回一个int结果。接着,我们创建了这个委托的实例op并将其与Add方法绑定。然后,我们使用BeginInvoke来异步调用这个委托。

BeginInvoke方法的参数与委托的签名相匹配,外加两个额外的参数:一个是AsyncCallback委托,用于指定一个回调方法,另一个是一个object类型的状态对象。在这个示例中,我们没有使用回调,所以传递了null

当异步执行完成后,可以通过调用委托的EndInvoke方法来获取结果。EndInvoke方法接受一个IAsyncResult接口作为参数,这个参数是BeginInvoke方法返回的。

CSharp Task

Task是.NET4.0加入的,跟线程池ThreadPool的功能类似,用Task开启新任务时,会从线程池中调用线程,而Thread每次实例化都会创建一个新的线程。

我们可以说Task是一种基于任务的编程模型。它与thread的主要区别是,它更加方便对线程进程调度和获取线程的执行结果。

Task类和Task<TResult>

  • 前者接收的是Action委托类型
  • 后者接收的是Func<TResult>委托类型

任务和线程的区别:

  1. 任务是架构在线程之上的,也就是说任务最终还是要抛给线程去执行。
  2. 任务跟线程不是一对一的关系,比如开10个任务并不是说会开10个线程,这一点任务有点类似线程池,但是任务相比线程池有很小的开销和精确的控制。

什么是异步任务(Task)

  1. 包含了对异步任务的各种状态的一个引用类型
    • 正在运行,完成,结果,报错等
  2. 对于异步任务的抽象
    • 开启异步任务后,当前线程不会被阻塞,而是可以做其他事情
    • 异步任务(默认)会借助线程池在其他线程上运行
    • 获取结果后回到之前的状态

最好使用[CSharp 异步编程模型](#CSharp 异步编程模型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 private void LoadDataAsync()
{
Task.Run(() =>
{
//子线程中执行,可以使用主线程中的变量,但要注意是否需要锁
var allEvents = EventInfoRepository.GetAll();
return allEvents;
}).ContinueWith(task =>
{
//主线程中执行的回调函数,子线程运行完后由主线程回调,并且可以得到子线程执行完成传回来的返回值
var allEvents = task.Result;
ViewSource = CollectionViewSource.GetDefaultView(allEvents.ToList());
ViewSource.Refresh();
},TaskScheduler.FromCurrentSynchronizationContext());
}
//TaskScheduler.FromCurrentSynchronizationContext()可以让ContinueWith在ui线程上执行

async void LoadUsersAsync()
{
var allUsers = await Task.Run(() => UserInfoRepository.GetAll());
// 将代码调度到UI线程上执行
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
UserList = allUsers;
});
}

UI线程相关解释

TaskScheduler.FromCurrentSynchronizationContext()方法返回与当前同步上下文关联的任务调度程序。在理解这个方法在Task.Run中的作用之前,让我们先介绍一下同步上下文和任务调度程序的概念:

  1. 同步上下文

    • 同步上下文是一个抽象概念,用于跟踪和管理代码执行的上下文环境。在UI应用程序中,同步上下文通常与UI线程关联,用于确保UI操作在UI线程上执行。
  2. 任务调度程序

    • 任务调度程序用于管理和调度任务的执行。不同的任务调度程序可以决定任务在何时何地执行,例如在线程池中执行、在UI线程上执行等。

现在,让我们来解释TaskScheduler.FromCurrentSynchronizationContext()Task.Run中的作用:

  • 在UI应用程序中,通常需要确保UI操作在UI线程上执行,以避免UI线程阻塞或出现线程安全问题。
  • 当您在UI线程上启动一个任务(例如使用Task.Run),默认情况下,任务会在线程池中执行,而不是在UI线程上执行。
  • 通过使用TaskScheduler.FromCurrentSynchronizationContext()方法,您可以获取当前同步上下文关联的任务调度程序,从而将任务调度到与UI线程关联的同步上下文中执行。
  • 这样可以确保任务在UI线程上执行,避免了UI操作的线程安全问题。

Application.Current.Dispatcher详解

Application.Current.Dispatcher是在WPF应用程序中用于访问UI线程调度程序(Dispatcher)的静态属性。在WPF中,UI元素通常只能在创建它们的UI线程上进行更新和操作,以确保UI的响应性和避免线程安全问题。Dispatcher类提供了一种在UI线程上执行代码的机制,而Application.Current.Dispatcher允许您访问当前应用程序的UI线程调度程序。

以下是关于Application.Current.Dispatcher的一些详解:

  1. UI线程调度程序(Dispatcher)
    • Dispatcher是一个用于调度和执行代码的类,它负责将操作调度到与其关联的线程上执行。在WPF应用程序中,每个UI线程都有一个关联的Dispatcher对象,用于管理UI元素的更新和操作。
  2. Application.Current
    • Application.Current是一个静态属性,用于获取当前正在运行的应用程序实例。通过Application.Current可以访问当前应用程序的各种属性和方法,包括UI线程的调度程序。
  3. Dispatcher.Invoke和Dispatcher.BeginInvoke
    • Dispatcher类提供了InvokeBeginInvoke等方法,用于将操作调度到与其关联的线程上执行。Invoke是同步执行操作,会阻塞调用线程直到操作完成,而BeginInvoke是异步执行操作,不会阻塞调用线程。

通过使用Application.Current.Dispatcher,您可以在任何地方访问当前应用程序的UI线程调度程序,从而确保在需要在UI线程上执行代码时,能够正确地调度操作。这对于在异步操作完成后更新UI或在后台线程中执行UI操作非常有用。

await Task.Run 可以等待线程执行完再执行

Task相关方法盘点

创建任务

  • Task.Run

    1
    Task task = Task.Run(() => { /* 执行的代码 */ });

    用于在线程池中启动一个新任务。Task.Run 与调用async方法并非马上调用线程切换不同,会导致立即的线程切换,因为它会在不同的线程上执行任务。

  • Task.Factory.StartNew

    1
    Task task = Task.Factory.StartNew(() => { /* 执行的代码 */ });

    也是用于创建并启动一个新任务,具有更多的配置选项。

  • Task.FromResult

    1
    Task<int> task = Task.FromResult(42);

    创建一个已完成的任务,返回指定的结果。

组合任务

  • Task.WhenAll

    1
    Task.WhenAll(task1, task2, task3);

    接受多个任务并返回一个新的任务,该任务在所有传入任务完成时完成。可以用于并行执行多个异步操作。

  • Task.WhenAny

    1
    Task.WhenAny(task1, task2, task3);

    接受多个任务并返回一个新的任务,该任务在任意一个传入任务完成时完成。

任务状态

  • Task.Status

    1
    2
    Task task = Task.Run(() => { /* 执行的代码 */ });
    var status = task.Status; // 获取任务的状态

    返回任务的当前状态(如 RanToCompletion, Faulted, Canceled 等)。

处理异常

  • Task.Exception

    1
    2
    3
    4
    5
    6
    7
    8
    try
    {
    await task;
    }
    catch (AggregateException ex)
    {
    // 处理异常
    }

    如果任务失败,Exception 属性将包含导致失败的异常。

取消任务

  • CancellationToken

    1
    2
    3
    4
    5
    6
    CancellationTokenSource cts = new CancellationTokenSource();
    Task task = Task.Run(() =>
    {
    // 检查是否已请求取消
    cts.Token.ThrowIfCancellationRequested();
    }, cts.Token);

    使用 CancellationToken 允许你在任务中响应取消请求。

继续任务

  • ContinueWith

    1
    2
    Task task = Task.Run(() => { /* 执行的代码 */ });
    task.ContinueWith(t => { /* 继续的代码 */ });

    在任务完成后执行后续操作。

异步等待

  • Task.Delay

    1
    await Task.Delay(1000); // 等待1秒

    创建一个任务,该任务在指定的时间后完成。

    其他有用的方法

  • Task.Wait

    1
    task.Wait(); // 阻塞当前线程,直到任务完成

    等待任务完成。

  • Task.WaitAll

    1
    Task.WaitAll(task1, task2); // 阻塞当前线程,直到所有任务完成

    等待所有任务完成。

需要更细粒度的控制

使用Task.Factory.StartNew

Task.Factory.StartNew提供了更多的灵活性和选项,例如可以指定TaskCreationOptions、TaskScheduler等参数来对任务进行更详细的控制。在一些需要更细粒度控制任务行为的场景下,可能会选择使用Task.Factory.StartNew。

TaskScheduler可以指定任务的调度器,比如可以指定由UI线程调用

TaskCreationOptions是用于指定任务的创建选项的枚举类型。通过指定TaskCreationOptions,可以对任务的创建和执行行为进行一些控制和定制。

以下是一些常用的TaskCreationOptions选项:

  1. None:默认选项,表示不指定任何特殊的创建选项。
  2. PreferFairness:指示任务调度器在选择下一个要执行的任务时,优先考虑公平性。
  3. LongRunning:指示任务是一个长时间运行的任务,可以让任务调度器做出一些优化。
  4. AttachedToParent:指示新任务将作为父任务的子任务运行,父任务完成时,子任务也会被取消。
  5. DenyChildAttach:禁止将新任务作为父任务的子任务运行。

Task.Delay

在大多数情况下,使用 await Task.Delay(200); 都优于使用 Thread.Sleep(200);,无论是在 UI 线程还是其他线程中。这是因为 await Task.Delay(200);Thread.Sleep(200); 在行为上有本质的不同,尤其是在资源管理和线程利用率方面。

  1. 资源利用和效率
  • Thread.Sleep(200);
    • 阻塞线程:当调用 Thread.Sleep(200); 时,当前线程将停止执行 200 毫秒。在这段时间内,线程不会做任何其他工作,只是处于等待状态。
    • 资源浪费:阻塞线程会占用系统资源(如线程栈和上下文信息),尽管它在 Sleep 期间不消耗 CPU 时间。
  • await Task.Delay(200);
    • 非阻塞等待Task.Delay(200); 返回一个未完成的任务,而 await 关键字会使方法异步等待该任务的完成。这种等待不会阻塞线程,允许操作系统将该线程的资源调度给其他任务。
    • 资源释放:因为线程不被阻塞,所以可以释放资源以供其他任务使用,提高资源利用率和系统的整体响应性。
  1. 在 UI 线程中的表现
  • Thread.Sleep(200);
    • 在 UI 线程中使用 Thread.Sleep 会冻结用户界面,因为 UI 线程被阻塞,无法处理其他事件或更新界面。
  • await Task.Delay(200);
    • 在 UI 线程中使用 await Task.Delay 不会阻塞 UI 线程。await 关键字会在任务完成时自动恢复执行上下文(例如 UI 线程),这确保了界面的响应性和流畅性。
  1. 上下文恢复
  • Thread.Sleep(200);
    • 没有上下文恢复的概念,因为它完全阻塞线程。
  • await Task.Delay(200);
    • 当任务完成后,await 会尝试在原始同步上下文中恢复执行,例如在 UI 应用中,这意味着任务会在 UI 线程上继续执行。这对于保持 UI 操作的顺序性和一致性非常重要。

总结

在绝大多数情况下,await Task.Delay(200); 是更好的选择,因为它允许线程在等待期间执行其他操作,提高了应用程序的整体效率和响应性。特别是在 UI 应用中,这种方法能够避免界面冻结,提供更好的用户体验。唯一可能的例外是在极少数需要精确控制线程行为的场景下,但这种情况通常非常罕见且特定。

await Task.Delay(200); 不会面临创建线程的开销,这是它的一个重要优势。要理解这一点,我们需要深入了解 Task.Delay 的内部工作原理以及异步编程模型。

Task.Delay 的工作原理

Task.Delay(200) 返回一个 Task 对象,这个任务会在 200 毫秒后完成。关键点在于,它不创建新的线程来处理这个延迟。相反,Task.Delay 通过以下机制来实现延迟:

  1. 基于定时器的延迟Task.Delay 实际上是使用系统的定时器机制来管理延迟。它会请求操作系统在 200 毫秒后触发一个事件,这个事件会使 Task 状态变为已完成。因此,不需要线程来等待这个时间,只需一个定时器来完成这一操作。
  2. 非阻塞等待: 当你使用 await Task.Delay(200) 时,当前方法的执行会被挂起,但这不会阻塞线程。相反,await 会将控制权返回给调用者,线程可以继续处理其他工作或进入空闲状态。直到 200 毫秒后,系统会发出一个信号,使得 Task 完成,然后 await 会恢复方法的执行。

与线程相关的开销

  • 不创建线程Task.Delay 不涉及线程创建。它利用系统的定时器机制来处理延迟,而不需要额外的线程资源。这与 Thread.Sleep 不同,Thread.Sleep 需要阻塞当前线程,无法利用系统定时器的非阻塞机制。
  • 轻量级操作Task.Delay 的实现是轻量级的,因为它只需注册一个定时器,而不涉及线程调度或上下文切换开销。这使得它非常适合用于异步编程模型中,能够有效地管理延迟而不会增加不必要的开销。

异步编程模型的优势

  • 响应性: 通过使用 awaitTask.Delay,你可以保持应用程序的响应性,特别是在需要处理 I/O 操作或长时间运行的任务时。例如,在 UI 应用程序中,await Task.Delay 允许 UI 线程处理其他用户输入事件,而不是冻结界面。
  • 资源利用: 异步编程模型能够更有效地利用系统资源。与阻塞线程相比,异步方法可以让线程在等待期间处理其他任务,提高了系统的并发性能。

总结

await Task.Delay(200); 是一种非阻塞的延迟实现方式,不涉及额外的线程创建开销。它利用系统定时器机制和异步编程模型来管理延迟,使得在等待期间线程可以自由地处理其他任务。这是它在异步编程中相比 Thread.Sleep 更具优势的原因。

C++新版本中提供了协程机制,这是一种更接近于异步编程模型的机制,能够更优雅地处理异步操作。协程可以暂停执行,直到某个操作完成,类似于 C# 中的 await

也提供了std::asyncstd::future,一种简单的异步编程方式,能够启动异步任务并获得结果。虽然它主要用于并行计算,但也可以用于执行延迟任务.这虽然不是完全类似于 C# 的 async/await,但在许多情况下可以达到类似的效果。

C++有更强大的异步库,如Boost.Asio,但是复杂性比较高

Parallel.For

Parallel.For是.NET Framework中用于并行循环的功能,它允许在多个线程上并行执行循环迭代,从而提高循环的执行效率。以下是关于Parallel.For的详解:

工作原理

  • Parallel.For会将循环的迭代分配给多个线程,每个线程负责处理一部分迭代。
  • 循环迭代的分配和同步由并行任务调度器(Task Scheduler)负责管理。
  • 并行执行的迭代不一定按照顺序执行,因此需要注意并行执行可能会改变迭代的顺序
1
2
3
4
Parallel.For(startIndex, endIndex, (i) =>
{
// 循环体内的代码,i为当前迭代的索引
});
  • startIndex:循环的起始索引。
  • endIndex:循环的结束索引(不包含)。
  • 循环体内的代码会在多个线程上并行执行。

控制并行度

  • 可以通过ParallelOptions类中的MaxDegreeOfParallelism属性来控制并行度,即同时执行的线程数量。
  • 默认情况下,Parallel.For会根据系统的处理器数量来确定并行度。

注意事项

  • 循环体内的代码应该是线程安全的,避免出现竞争条件。
  • 避免在循环体内进行对共享资源的写操作,可以考虑使用lock或其他同步机制来保护共享资源。

适用场景

  • 处理大量数据或计算密集型任务时,可以使用Parallel.For来提高处理效率。
  • 在需要并行执行独立迭代的情况下,可以使用Parallel.For来并行化循环。

通过合理使用Parallel.For,可以充分利用多核处理器的优势,加快循环的执行速度。但需要注意线程安全和并行度控制等问题,以确保并行执行的正确性和效率。

CSharp 异步定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static async Task Main(string[] args)
{
// 创建一个取消令牌源
CancellationTokenSource cts = new CancellationTokenSource();

// 启动异步定时器
var timerTask = StartTimerAsync(1000, cts.Token); // 每1000毫秒(1秒)执行一次

Console.WriteLine("按任意键停止定时器...");
Console.ReadKey();

// 取消定时器
cts.Cancel();
await timerTask; // 等待定时器任务完成

Console.WriteLine("定时器已停止。");
}

async Task StartTimerAsync(int interval, CancellationToken token)
{
try
{
while (true)
{
// 等待指定的时间间隔
await Task.Delay(interval, token);

// 执行定时操作
Console.WriteLine($"当前时间: {DateTime.Now}");
}
}
catch (TaskCanceledException)
{
// 处理任务被取消的情况
Console.WriteLine("定时器任务被取消。");
}
}
}

线程同步

AutoResetEvent

AutoResetEvent是.NET中的一个同步原语,用于线程间的同步和通信。它允许一个线程在某个事件发生时等待,而其他线程可以在事件发生时通知等待的线程继续执行。

AutoResetEvent有两个主要状态:有信号和无信号。

  • 当AutoResetEvent处于有信号状态时,等待的线程可以继续执行;
  • 当AutoResetEvent处于无信号状态时,等待的线程将被阻塞,直到其他线程将其设置为有信号状态。

下面是AutoResetEvent的一些主要成员方法和属性:

  • WaitOne(): 当AutoResetEvent处于无信号状态时,调用此方法会阻塞当前线程,直到AutoResetEvent被设置为有信号状态。
  • Set(): 将AutoResetEvent设置为有信号状态,唤醒一个等待的线程。
  • Reset(): 将AutoResetEvent设置为无信号状态。
  • WaitOne(int millisecondsTimeout): 在指定的超时时间内等待AutoResetEvent的信号。

使用AutoResetEvent可以实现线程间的同步和协作,例如一个线程等待另一个线程完成某个任务后再继续执行。AutoResetEvent是多线程编程中常用的同步机制之一,可以帮助确保线程之间的顺序和协作。

除此之外还有如下用于解决不同情况的存在:

  1. ManualResetEvent:ManualResetEvent与AutoResetEvent类似,但它在被设置为有信号状态后会一直保持该状态,直到调用Reset()方法将其恢复为无信号状态。适用于一次性事件通知的场景。

  2. CountdownEvent:CountdownEvent是一个倒计时事件,可以设置一个初始计数值,每次调用Signal()方法减少计数值,直到计数值为0时触发事件。适用于需要等待多个任务完成后再继续执行的场景。

  3. TaskCompletionSource:TaskCompletionSource是一个用于创建和控制异步任务的类,可以通过SetResult()方法设置任务完成的结果。适用于异步任务的协作和同步。

  4. Monitor类:Monitor类提供了基于锁的同步机制,可以使用lock关键字或Monitor类的方法来实现线程同步。适用于简单的线程同步场景。

  5. Semaphore类:Semaphore类是一个计数信号量,可以控制同时访问共享资源的线程数量。适用于限制并发访问的场景。

CSharp线程同步机制

CSharp读写锁

ReaderWriterLock是C#中用于同步访问共享资源的机制。它允许多个线程同时进行读取操作,但只允许一个线程进行写入操作。这种锁定机制提高了在读取操作远远多于写入操作的场景下的性能。

  • ReaderWriterLock适用于读多写少、写持续时间短的场景,提高了并发读的效率,写入时会阻塞所有读锁 。
  • 它解决了并发读的性能问题,大大提高了数据并发访问的性能,只有在写入时才会阻塞所有读锁 。
  • 在多线程环境下,选择合适的锁机制非常重要,ReaderWriterLock是一种在多读少写场景下非常高效的选择。

缺点:

  1. 不支持递归锁ReaderWriterLock 不支持递归锁,这意味着在同一个线程持有锁时,不允许再次获取锁。这可能在某些情况下导致不便,特别是在需要递归锁的情况下。
  2. 性能相对较慢:相对于一些其他锁的类型,如 MonitorReaderWriterLock 可能在某些情况下速度较慢。有性能测试表明,ReaderWriterLockSlimReaderWriterLock 更快一倍,但它也有自己的限制。
  3. 复杂性和潜在死锁:使用 ReaderWriterLock 可能引入额外的复杂性,需要谨慎使用,因为不正确的使用锁可能导致死锁和性能问题。需要仔细考虑何时以及如何使用这种锁,以确保安全性和性能。
  4. 可能导致写饥饿:如果写操作频繁,读操作也频繁,那么写操作可能会一直等待,因为每次有读锁的线程时,写操作都无法获取写锁。

ReaderWriterLock 类:定义支持单个写线程和多个读线程的锁。

ReaderWriterLockSlim 类:表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问。

ReaderWriterLockSlim

方法 说明
EnterReadLock() 尝试进入读取模式锁定状态。
EnterUpgradeableReadLock() 尝试进入可升级模式读锁定状态。(锁升级指的是从读锁升级为写锁)
EnterWriteLock() 尝试进入写入模式锁定状态。(可升级模式的读锁也使用这个函数升级)
ExitReadLock() 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式。
ExitUpgradeableReadLock() 减少可升级模式的递归计数,并在生成的计数为 0(零)时退出可升级模式。
ExitWriteLock() 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式。
TryEnterReadLock(Int32) 尝试进入读取模式锁定状态,可以选择整数超时时间。
TryEnterReadLock(TimeSpan) 尝试进入读取模式锁定状态,可以选择超时时间。
TryEnterUpgradeableReadLock(Int32) 尝试进入可升级模式锁定状态,可以选择超时时间。
TryEnterUpgradeableReadLock(TimeSpan) 尝试进入可升级模式锁定状态,可以选择超时时间。
TryEnterWriteLock(Int32) 尝试进入写入模式锁定状态,可以选择超时时间。
TryEnterWriteLock(TimeSpan) 尝试进入写入模式锁定状态,可以选择超时时间。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static int lockTarget = 0;
static ReaderWriterLockSlim rwLock= new ReaderWriterLockSlim();
static void threadWriteFunc()
{
while(true)
{
Thread.Sleep(1000);
try
{
rwLock.EnterUpgradeableReadLock();
if (lockTarget > 10)
{
rwLock.EnterWriteLock();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId + ">>get write lock");
lockTarget--;
rwLock.ExitWriteLock();
}
rwLock.ExitUpgradeableReadLock();
}
finally
{
if(rwLock.IsWriteLockHeld)
rwLock.ExitWriteLock();
if(rwLock.IsUpgradeableReadLockHeld)
rwLock.ExitUpgradeableReadLock();
}
}
}

可以参考一个标准做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 写入操作,先以可升级读锁开始
rwLock.EnterUpgradeableReadLock();
try
{
// 检查是否需要写入
if (需要写入的条件)
{
// 升级到写锁
rwLock.EnterWriteLock();
try
{
// 执行写入操作
}
finally
{
rwLock.ExitWriteLock();
}
}
}
finally
{
rwLock.ExitUpgradeableReadLock();
}

可以加上超时机制

ReaderWriterLock

大多数情况下都是推荐 ReaderWriterLockSlim 的,而且两者的使用方法十分接近。

方法 说明
AcquireReaderLock(Int32) 使用一个 Int32 超时值获取读线程锁。
AcquireReaderLock(TimeSpan) 使用一个 TimeSpan 超时值获取读线程锁。
AcquireWriterLock(Int32) 使用一个 Int32 超时值获取写线程锁。
AcquireWriterLock(TimeSpan) 使用一个 TimeSpan 超时值获取写线程锁。
AnyWritersSince(Int32) 指示获取序列号之后是否已将写线程锁授予某个线程。
DowngradeFromWriterLock(LockCookie) 将线程的锁状态还原为调用 UpgradeToWriterLock(Int32) 前的状态。
ReleaseLock() 释放锁,不管线程获取锁的次数如何。
ReleaseReaderLock() 减少锁计数。
ReleaseWriterLock() 减少写线程锁上的锁计数。
RestoreLock(LockCookie) 将线程的锁状态还原为调用 ReleaseLock() 前的状态。
UpgradeToWriterLock(Int32) 使用一个 Int32 超时值将读线程锁升级为写线程锁。
UpgradeToWriterLock(TimeSpan) 使用一个 TimeSpan 超时值将读线程锁升级为写线程锁。

CSharp信号量

lock不能与异步操作一起使用,因为它会阻止同步上下文的释放。应该使用异步锁代替,例如使用SemaphoreSlim

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);//初始化 表示最初只有一个线程可以访问共享资源,并且最大只允许一个线程访问共享资源

//进入临界区
await _semaphore.WaitAsync();
//离开临界区
_semaphore.Release();


//加上超时的做法,还未测试
//添加超时时间1秒
var lockAcquired = await _semaphore.WaitAsync(TimeSpan.FromSeconds(1))
// 离开临界区,确保只有在获取到锁的情况下才释放
if(lockAcquired)
_semaphore.Release();

CSharp 手动垃圾回收

在.NET的环境中,托管的资源都将由.NET的垃圾回收机制来释放,而一些非托管的资源则需要程序员手动地将它们释放。

非托管资源是指在垃圾回收器管理下不能自动释放的资源。这意味着当不再需要非托管资源时,需要手动调用其Dispose()方法来释放它所占用的内存。常见的非托管资源有:

  1. 操作系统资源:如文件句柄、进程句柄、线程句柄等。
  2. COM对象:使用COM互操作时所涉及的对象。
  3. 外部设备资源:如串行端口、网络套接字等。
  4. 非托管代码资源:通过P/Invoke调用的非托管代码或第三方库。
  5. 数据库连接资源:与数据库交互时所使用的连接对象。
  6. 图形设备接口资源:与图形设备交互时所用到的资源。
  7. 其他需要手动分配和释放内存的资源。

但注意这里指的是原生的,C#提供的那些封装库都是托管的,比如说SerialPort,Stream,TcpClient,TcpServer是托管的

.NET提供了主动和被动两种释放非托管资源的方式,即

  • IDisposable接口的Dispose方法
  • 类型自己的Finalize方法。

任何带有非托管资源的类型,都有必要实现IDisposable的Dispose方法,并且在使用完这些类型后需要手动地调用对象的Dispose方法来释放对象中的非托管资源。

如果类型正确地实现了Finalize方法,那么即使不调用Dispose方法,非托管资源也最终会被释放,但那时资源已经被很长时间无谓地占据了。

using 语句用于管理非托管资源(如文件句柄、数据库连接等)的释放

using语句的作用就是提供了一个高效的调用对象Dispose方法的方式。对于任何IDisposable接口的类型,都可以使用using语句,而对于那些没有实现IDisposable接口的类型,使用using语句会导致一个编译错误。

1
2
3
4
using(StreamWriter sw= new StreamWriter())
{
// 中间处理逻辑
}

using语句一开始定义了一个StreamWriter的对象,之后在整个语句块中都可以使用sw,在using语句块结束的时候,sw的Dispose方法将会被自动调用。using语句不仅免除了程序员输入Dispose调用的代码,它还提供了机制保证Dispose方法被调用,无论using语句块顺利执行结束,还是抛出了一个异常。下面的代码演示了using的这一保护机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
using System;

namespace usingDemo
{
class Program
{
static void Main(string[] args)
{
try
{
// 使用using
using (MyDispose md = new MyDispose())
{
md.DoWork();
// 抛出一个异常来测试using
throw new Exception("抛出一个异常");
}
}
catch
{

}
finally
{
Console.ReadKey();
}
}
}

/// <summary>
/// 继承自IDisposable接口,仅仅用来做测试,不使用任何非托管资源
/// </summary>
public class MyDispose : IDisposable
{
public void Dispose()
{
Console.WriteLine("Dispose方法被调用");
//在此处做回收工作
}
public void DoWork()
{
Console.WriteLine("做了很多工作");
}
}
}
//输出:
//做了很多工作
//Dispose方法被调用

事实上,C#编译器为using语句自动添加了try/finally块,所以Dispose方法能够保证被调用到,所以如下两段代码经过编译后内容将完全一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using (MyDispose md = new MyDispose())
{
md.DoWork();
}
//与下面完全一致
MyDispose md;
try
{
md = new MyDispose();
md.DoWork();
}
finally
{
md.Dispose();
}

应该注意一点使用using时常犯的错误,那就是千万不要试图在using语句块外初始化对象

1
2
3
4
5
MyDispose md = new MyDispose();
using (md)
{
md.DoWork();
}

CSharp中的代码块

1
2
3
#region xxxxxx
//代码
#endregion

这样就形成了可以折叠的代码块

visual studio开发环境相关

官方组件

类设计器参考

可以图形化查看类结构

重构技巧盘点

快捷键

展开所有代码块: ctrl+k,ctrl+j

折叠所有代码块: ctrl+k,ctrl+0

上下文菜单妙用

alt+enter可以打开上下文菜单

数值进制转换

vs中可以直接进行数值进制的转换,打开上下文菜单,转换数字

引入常量/引入局部变量/引入参数

可以将某个变量提取到常量/局部变量/参数

提取接口/基类

可以针对一个类打开上下文菜单,可以选择 提取接口/提取基类/移动到命名空间等等

在一个现有类上生成接口,并移动到想让他去的地方

选择性黏贴

编辑-选择性黏贴-将JSON粘贴为类/将XML粘贴为类

显示结构

右键-注释-显示结构

快捷键为: ctrl+K,ctrl+G

可以显示当前的代码结构的所有外部结构的层次结构

在vscode中直接查看大纲

可以使用 ctrl+shift+O 快速在成员中跳转

新建解决方案资源管理器视图

可以在创建一个局部的解决方案资源管理器

插件相关

下载插件位置:visual studio菜单中-拓展-管理拓展-Visual Studio Marketplace

简易插件

  • Viasfora 括号颜色配对
  • VSColorOutput 输出栏多颜色显示
  • CodeNav 代码导航 (实测占用有点高)

CodeRush

插件CodeRush下载地址

  1. Code Template Expansion 按下空格 代码模板展开

    1
    2
    3
    4
    5
    6
    ps//->按下空格   
    //转换为如下:
    public string xxxxxx{
    get{return xxxx;}
    set{xxxxx=value;}
    }
  2. Tab键可以在跳到选中目标的下一个引用

  3. 拼写检查

  4. 调试可视化 可直接在复合表达式上看到他的值

  5. 一键选择嵌入功能 允许选中部分代码,通过按如下键一键用下面框架包裹住选中代码 (默认未启用)

    • b 尖括号
    • c try/catch块
    • f try/finally块
    • t try/catch/finally块
  6. 大写键+空格添加修饰符 (默认未启用)

CodeGeeX

ai补全+聊天插件

插件网页

优势:

  • Visual studio可用
  • 有免费版

保存的时候格式化

Format document On Save插件

vs2022版本使用Format document On Save for VS2022插件

一键展开排列代码

会将函数参数过长的,每个参数排一行,初始化过长的排成多行等等

在vs中展开排列使用动词wrap,翻译为包装每个参数,实际上是把参数排成一行一个展示,这个插件只是帮我们省了点击vs自带的包装每个参数等功能的时间

CSharpier 默认快捷键: ctrl+K,ctrl+y

安装好插件后,进入vs后会有提示安装dotnet工具才能真正使用

xaml代码格式化

XAML Styler for visual studio 2022

翻译插件

Visual-Studio-Translator - Visual Studio Marketplace

windows下vs的NuGet包管理器

通过nuGet可以很方便的安装第三方库

在Visual Studio中使用NuGet包的步骤如下:

  1. 安装NuGet包管理器:在Visual Studio中,选择“工具”>“扩展与更新”,然后搜索并安装NuGet
  2. 打开NuGet包管理器:在解决方案管理器中,右键点击项目,然后选择“管理NuGet程序包”
  3. 搜索并安装NuGet包:在NuGet包管理器中,选择nuget.org作为包源。在“浏览”选项卡中,搜索你需要的包(例如Newtonsoft.Json),在列表中选择它,然后选择“安装”
  4. 使用NuGet包:安装NuGet包后,可以在代码中通过using 语句引用它,其中``是你正在使用的包的名称。创建引用后,就可以通过API调用包

在命令行中使用NuGet的步骤如下:

  1. 下载并安装NuGet CLI你可以从nuget.org下载NuGet CLI。将nuget.exe文件保存到合适的目录,并确保该目录位于PATH环境变量中
  2. 打开命令行窗口:你可以通过在Windows上按Win+R并输入cmd,或在Mac/Linux上打开终端来打开命令行窗口
  3. 运行NuGet命令:在命令行窗口中,你可以运行nuget并后跟你想要执行的命令和相应的选项。例如,你可以运行nuget help pack来查看pack命令的帮助信息

以下是一些常用的NuGet CLI命令

  • nuget install <packageID>:安装指定的NuGet包。
  • nuget restore:还原项目的依赖项。
  • nuget push <packageID>:将包发布到包源。
  • nuget delete <packageID>:从包源中删除或取消列出包。
  • nuget list:显示来自给定源的包。

windows下的winget

win商店下载方式

要在 Windows 11 或 10 上安裝 Windows 包管理器,請使用以下步驟:

  1. 打開應用程序安裝程序頁面
  2. 單擊獲取按鈕。
  3. 單擊打開 Microsoft Store按鈕。
  4. 單擊更新按鈕。

GitHub下载方式

下载地址点击msixbundle后缀文件,下载安装后即可

vs相关设置

vs生成注释

工具-选项-C#-高级-为///生成XML文档注释打上勾

vs函数参数数据类型显示

函数的每一个内联参数的数据类型都在写代码的时候显示了出来,这样我们在编写代码的时候就可以很方便的进行参数的填写,也能避免一些参数位置填错的惨案

image-20240620112651018 image-20240620112617839

vs添加方法的引用计数

vs内置的CodeLens:CodeLens是一个帮助开发人员在代码编辑器中获取代码引用、单元测试、源代码管理等信息的工具

工具-选项-文本编辑器-所有语言-CodeLens - 启用显示C#和Visaul Basic参考

image-20240627085351760

  • 显示 测试状态(C++, C# 和 Visual Basic):

    这一选项启用后,会在代码编辑器中显示代码的测试状态信息,包括哪些测试覆盖了当前代码,测试是否通过等。

  • 显示 C# 和 Visual Basic 参考:

    这一选项启用后,会在代码编辑器中显示C#和Visual Basic代码的引用信息,例如该方法或类被哪些代码引用了。

  • 显示 JavaScript/TypeScript 引用:

    这一选项启用后,会在代码编辑器中显示JavaScript和TypeScript代码的引用信息,帮助开发者了解代码在项目中的引用情况。

  • 显示 测试者(C# 和 Visual Basic):

    这一选项启用后,会在代码编辑器中显示C#和Visual Basic代码的测试者信息,即哪些测试用例涉及了当前代码。

  • 显示 C++ #include 引用:

    这一选项启用后,会在代码编辑器中显示C++代码中的#include引用信息,帮助开发者了解当前文件被哪些其他文件引用了。

  • 显示 C++ 编译时间:

    这一选项启用后,会在代码编辑器中显示C++代码的编译时间信息,帮助开发者优化代码编译效率。

  • 显示 请求(Application Insights):

    这一选项启用后,会在代码编辑器中显示Application Insights收集的请求信息,帮助开发者了解代码在实际运行中的请求情况和性能指标。

  • 显示 异常(Application Insights):

    这一选项启用后,会在代码编辑器中显示Application Insights收集的异常信息,帮助开发者快速定位和解决代码中的异常问题。

重要的快捷键

ctrl+j 获取建议

项目结构

winForm项目结构

  • App.config 应用配置

  • Form1.cs 源码文件(窗口) 双击打开界面设计器(右键可以选择查看源码还是设计器)

    • Form1.Designer.cs 源码文件(界面设计)

      在定义Form1类的时候含有一个关键字partial,这里就不得不说C#语言设计一个重要的特性了,能作为大多数人开发上位机的首选,C#有一个特性就是设计的时候界面与后台分离,但是类名相同

    • Form1.resx 资源文件

  • Program.cs Main入口所在的源码文件

  • 引用 项目依赖的一些系统库

  • Properties 通常包含一些项目的配置和属性信息,用于管理项目的设置和行为

    • Assemblylnfo.cs 应用程序发布时的一些属性设置,版本号,属性,版权之类的
    • 其余两个文件是工具自动生成的一些设置文件
    • Resources.resx
    • Settings.settings

CSharp语法主要版本迭代

  1. C# 1.0:最初的C#版本,包含了基本的面向对象编程特性,如类、接口、继承、多态等。

  2. C# 2.0:引入了泛型、迭代器、匿名方法等新特性,提高了代码的灵活性和可读性。

  3. C# 3.0:引入了LINQ(Language Integrated Query)特性,使查询数据变得更加简洁和直观。(统一查询语法)

  4. C# 4.0:引入了动态类型(dynamic)、命名参数、可选参数等新特性,增强了语言的灵活性。

  5. C# 5.0:引入了异步编程模型(async/await),使异步操作更加简单和直观。

  6. C# 6.0:引入了自动属性初始化、表达式体成员、字符串插值等新特性,提高了开发效率。

  7. C# 7.0:引入了元组、模式匹配、局部函数等新特性,增强了语言的表达能力和功能性。

  8. C# 8.0:引入了nullable引用类型、异步序列等新特性,提高了代码的安全性和可靠性。

与C++的相互调用关系

  • Platform Invocation Services (P/Invoke)
  • 创建C++/CLI封装器来实现。
  • P/Invoke允许您在C#代码中调用原生C++函数,而C++/CLI是一种混合语言,可以让您在同一个项目中同时使用C#和C++代码。

其实差别就两种:

  • 非托管方式(P/Invoke):适用于已有C++ DLL且不打算修改其源码,仅需C#调用其C兼容接口的情况。简单快捷,无需额外工具链支持,但需要处理数据类型转换和内存管理问题。
  • 托管方式(C++/CLI):适用于愿意调整C++代码以支持.NET框架,追求更紧密集成和更易用性的场景。C#可以直接使用封装好的托管类,但可能增加项目复杂度,需要维护C++/CLI中间层。

非托管方式调用(P/Invoke)

这种情况是不需要修改C++的DLL的

c#调用C++开发的DLL

如果是C++编写的dll,如,该函数:

1
2
3
4
5
6
#include <vector>
extern "C" {
__declspec(dllexport) void ProcessVector(int[] arr,int size) {
// 这里是函数的实现,处理传入的整型数组
}
}

可以直接使用下面的方式对接:

注意:只有C#编写的DLL才可以在项目中被引用

1
2
3
4
5
6
7
8
9
10
11
12
13
using System;
using System.Runtime.InteropServices;

public class CppWrapper
{
[DllImport("YourCppLibrary.dll")]
public static extern void ProcessVector(vector<int> arr);

public void ProcessVectorWrapper(int[] vec,int vecSize)
{
ProcessVector(vec,vecSize);
}
}

要使用DllImport,必须使用using System.Runtime.InteropServices;

再给一个更简单的案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//假如C++ 动态DLL库里面有下面两数相加的方法
int Sum(int value1, int value2)
{
int sumValue = value1 + value2;
return sumValue;
}

//----------------------------------------------------------------
//我们在C#里面想调用C++ dll库,方法如下
//先声明方法
[DllImport("myDll.dll")]
static extern int Sum(int value1, int value2);
//之后就可以像C#普通方法一样调用 如下
Sun(5,6);//计算结果为11

在C#通过P/Invoke(平台调用)调用C++ DLL时,数据类型转换和内存管理是需要特别关注的两个方面

数据类型转换

基本类型转换

  • 数值类型:C#与C++的基本整数和浮点数类型通常有一一对应关系,如intlongfloatdouble等。在定义P/Invoke函数签名时,使用对应的C#类型即可。
  • 字符与字符串
    • char:C++的char通常对应C#的bytechar(取决于是否区分字节和字符)。
    • wchar_t:C++的宽字符(wchar_t)通常对应C#的char(在.NET Core 3.0及更高版本中,.NET默认使用UTF-8编码,因此宽字符可能需要转换为字符串)。
    • std::string / std::wstring:C++的字符串类通常需要转换为C风格的字符串(以\0结尾的字符数组)才能通过P/Invoke传递。在C#端使用MarshalAsAttribute指定为UnmanagedType.LPStr(对于ANSI字符串)或UnmanagedType.LPWStr(对于Unicode字符串)。返回字符串时,通常C++函数会返回指向缓冲区的指针,C#需要负责释放该内存(见内存管理部分)。

结构体与枚举

  • 结构体:如果C++ DLL中定义了结构体供C#使用,需要在C#中创建对应的结构体类,并使用StructLayout(LayoutKind.Sequential)(或Explicit)、FieldOffset等特性确保布局与C++端一致。对于嵌套结构体和数组成员,同样需要正确处理。
  • 枚举:C++的枚举类型可以映射为C#的枚举类型,注意保持枚举值的对应关系,并指定适当的底层类型(如intuint等)。

特殊类型

  • 指针与句柄:C++的原始指针通常映射为C#的IntPtr类型。对于需要传递或返回的句柄(如Windows API中的HANDLE),也通常使用IntPtr
  • 数组:C++数组可以通过固定大小的C#数组、IntPtr结合长度参数,或者使用Marshal.Copy等方法进行传递。

C++标准库中的容器(如std::vector)通常包含复杂的内部结构,如动态分配的内存、迭代器、容量信息等,这些都不适合直接通过P/Invoke传递给C#。然而,有几种方法可以间接地在C#与C++之间交换std::vector所存储的数据

传递容器

通过P/Invoke从C++ DLL向C#传递容器数据时,通常不会直接传递容器本身,而是传递容器内容(如通过数组连续内存块自定义数据结构。在C#端,将接收到的数据转换为相应的C#集合类(如List<T>)后使用

通过数组传递容器内容
1
2
3
4
5
6
7
8
extern "C" __declspec(dllexport)
void GetDynamicArray(int* outputArray, int& arraySize)
{
std::vector<int> vec = ...; // 动态长度数组

arraySize = vec.size();
memcpy(outputArray, vec.data(), arraySize * sizeof(int));
}
1
2
[DllImport("MyCppDll.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void GetDynamicArray(out int[] outputArray, out int arraySize);

拿vector来举例:

vector->int* 指针,int& 长度–进入C#->int[] 数组,int& 长度->List

通过连续内存块传递内容
  1. C++端将动态长度数组序列化为字节数组,并提供一个函数返回该数组的指针和长度。

  2. C#端接收字节数组和长度,然后在C#中反序列化。

    示例:

1
2
3
4
5
6
7
8
extern "C" __declspec(dllexport)
void GetDynamicArraySerialized(char* outputBuffer, int& bufferSize)
{
std::vector<std::string> vec = ...; // 动态长度数组

// 序列化vec到outputBuffer中,记录所需的bufferSize
// ...
}
1
2
[DllImport("MyCppDll.dll", CallingConvention = CallingConvention.Cdecl)]
private static extern void GetDynamicArraySerialized(byte[] outputBuffer, out int bufferSize);
使用自定义数据结构
  • 定义一个包含动态长度数组数据的自定义结构体,包括数据缓冲区和长度信息。
  • C++端提供函数填充该结构体。
  • C#端声明对应的结构体,并解析结构体中的数据。

内存管理

手动管理

  • 堆内存分配:当C++ DLL函数返回指向堆上分配的内存的指针时,C#需要负责释放该内存。通常通过Marshal.FreeCoTaskMemMarshal.FreeHGlobal函数释放。务必确保释放时机正确,避免内存泄漏。
  • 字符串缓冲区:如前所述,C++返回的字符串缓冲区通常需要C#释放。可以使用Marshal.PtrToStringAnsiMarshal.PtrToStringUni等方法将指针转换为托管字符串,同时释放原始内存。

自动管理

  • 使用outref参数:对于简单的数据类型或结构体,可以使用outref参数让C#自动管理内存。这样,C++函数可以直接修改传入的变量,而无需返回新分配的内存。
  • 内联缓冲区:对于小尺寸的字符串或数据,可以考虑在C#结构体中预留内联缓冲区,并使用MarshalAsAttribute指定为UnmanagedType.ByValArray。这样,C++函数可以直接写入缓冲区,避免额外分配内存。

COM接口与智能指针

  • COM接口:如果C++ DLL提供了COM接口,C#可以直接通过System.Runtime.InteropServices.ComImportGuid属性等创建COM对象代理,内存管理由COM机制自动处理。
  • 智能指针:如果C++ DLL使用C++11或更高版本的智能指针(如std::unique_ptrstd::shared_ptr),则需要设计合适的接口让C#能够安全地使用这些智能指针所管理的对象,可能涉及原始指针与智能指针的互操作。

最佳实践

  • 遵循约定:确保C++ DLL导出函数遵循特定的调用约定(如__stdcall__cdecl),并在C#的DllImport属性中指定正确的CallingConvention
  • 错误处理:定义并文档化C++ DLL函数返回错误代码的方式,C#端应检查并妥善处理这些错误。
  • 性能优化:对于频繁调用或大数据量传输的情况,考虑使用unsafe代码、fixed语句、预分配的缓冲区等手段提高效率,但需谨慎处理以避免内存安全问题。

原理

平台调用依赖于元数据在运行时查找导出的函数并封送其参数。下图显示了这一过程。

对非托管 DLL 函数的“平台调用”调用

image-20240516195540536

使用自动化工具生成P/Invoke代码

有一些工具可以帮助自动生成P/Invoke函数声明和相关的类型定义,减少手动编写的工作量。例如:

  • SWIG(Simplified Wrapper and Interface Generator):跨语言开发工具,可以生成C#(以及其他多种语言)绑定代码,封装C++库。需要编写SWIG接口文件描述C++接口,然后通过SWIG生成C#绑定代码。
  • ClangSharp.PInvokeGenerator:基于Clang的工具,可以从C/C++头文件生成C# P/Invoke绑定代码。支持自动处理数据类型转换和内存管理。

使用这些工具,您只需提供C++头文件或接口描述,工具会自动生成对应的C# P/Invoke代码。然后在C#项目中引用生成的代码文件,即可调用C++ DLL函数。

总的说,SWIG和ClangSharp.PInvokeGenerator都是用于简化C#调用C++代码过程的工具,它们通过自动化的方式生成封装代码,大大减轻了手动编写P/Invoke绑定的工作量。SWIG具有更广泛的语言支持和丰富的定制化选项,而ClangSharp.PInvokeGenerator则依托于Clang编译器,提供了精准的类型映射和对现代C++特性的良好支持。

各自最大优势

  • SWIG 广泛语言支持
  • ClangSharp.PInvokeGenerator 对于使用现代C++特性的库,尤其是模板、C++11及以后标准特性丰富的库,ClangSharp.PInvokeGenerator可能由于其精准的类型映射和对最新C++标准的良好支持,成为更合适的选择

SWIG

SWIG是一个广泛使用的跨语言接口生成器,它能够自动将C/C++代码包装成其他多种高级编程语言(包括C#)可以调用的形式。SWIG通过解析C/C++头文件(或者用户提供的接口文件),生成对应的封装代码,使得原生C/C++函数、类和数据结构可以被非C/C++语言(如C#)透明地调用。

主要特点与功能

  1. 广泛的语言支持:SWIG除了支持C#外,还支持许多其他语言,如Python、Java、JavaScript、Perl、Ruby、PHP、Lua等,使得同一套C/C++代码可以轻松地为多个平台和编程环境提供接口。
  2. 自动类型映射:SWIG能够识别C/C++的基本类型、结构体、枚举、类等,并自动映射到目标语言的等价类型。对于复杂类型,SWIG还可以生成必要的适配器代码以保证类型兼容性。
  3. 智能指针支持:SWIG可以处理C++中的智能指针(如std::shared_ptrstd::unique_ptr),在生成的C#接口中提供适当的生命周期管理。
  4. 成员函数封装:对于C++类的成员函数,SWIG会生成对应的代理方法,使C#代码可以直接调用这些方法,如同在C#中操作本地对象。
  5. 模板支持:SWIG能够处理部分C++模板,并生成特定实例化的代码。对于模板类或函数,可能需要在SWIG接口文件中显式实例化。
  6. 扩展性与定制化:SWIG提供了丰富的预处理器指令和宏系统,允许用户自定义类型映射规则、控制代码生成细节、添加额外的包装逻辑等。

使用流程

  1. 编写SWIG接口文件.i 文件):定义需要导出的C/C++接口,包括头文件包含、模块定义、类型映射规则等。可以使用 %include 指令包含C/C++头文件。
  2. 运行SWIG:使用SWIG命令行工具,指定接口文件和目标语言(如-csharp),生成对应的接口代码。
  3. 编译生成的代码:SWIG生成的C#代码通常包括两个部分:C#接口库(.cs 文件)和C++包装代码(.cpp 文件)。需要分别编译这两个部分:C#接口库编译成DLL或NET Assembly,C++包装代码与原生C++库一起编译链接。
  4. 在C#项目中引用:将生成的C#接口库添加为C#项目的引用,即可在C#代码中直接调用原生C++函数和类。

ClangSharp.PInvokeGenerator

ClangSharp.PInvokeGenerator是基于Clang编译器前端的一个工具,用于从C/C++头文件生成C# P/Invoke绑定代码。它利用Clang的完整C/C++语法解析能力,提供精确的类型映射和函数签名生成。

主要特点与功能

  1. 基于Clang:得益于Clang强大的C/C++解析能力,ClangSharp.PInvokeGenerator能够准确处理复杂的C/C++特性,包括模板、宏、内联函数、C++11及以上标准的新特性等。
  2. 精准类型映射:ClangSharp.PInvokeGenerator根据C/C++类型生成对应的C# P/Invoke类型,包括指针、引用、数组、结构体、枚举、模板实例等,并处理C++特有的类型修饰符(const、volatile、restrict等)。
  3. 函数签名生成:生成符合C# P/Invoke规范的函数声明,包括名称修饰、参数传递规则、返回值处理等,确保C#代码能够正确调用C++函数。
  4. 支持C++标准库容器:对于C++标准库容器(如std::vectorstd::string等),ClangSharp.PInvokeGenerator可以生成适配器代码,使得C#能够以接近原生的方式使用这些容器。
  5. 代码注释保留:ClangSharp.PInvokeGenerator在生成C#代码时,尽可能保留C++源码中的注释,有助于C#开发者理解封装的C++接口。

使用流程

  1. 安装ClangSharp.PInvokeGenerator:通常通过NuGet包管理器将ClangSharp.PInvokeGenerator工具包添加到项目中。
  2. 配置项目:在项目中指定要解析的C/C++头文件路径,以及生成的C#代码输出路径。
  3. 运行生成:调用ClangSharp.PInvokeGenerator提供的API或命令行工具,指定头文件和输出选项,生成C# P/Invoke绑定代码。
  4. 集成到C#项目:将生成的C#代码(通常是.cs文件)添加到C#项目中,同时确保项目引用了必要的C++ DLL。C#代码现在可以直接调用C++函数。

托管方式调用(CLR/C++/CLI)

如果愿意修改C++ DLL以支持.NET框架,可以创建一个混合模式(Managed/Unmanaged)的C++/CLI DLL,它既包含原生C++代码,又包含托管C++代码,可以直接被C#项目引用。这种方法适用于需要深度集成C++功能,或者希望C++代码能更方便地使用.NET类库的情况。

  1. 创建C++/CLI项目

    • 使用Visual Studio创建一个C++/CLI类库项目。
  2. 封装原生C++代码

    • 在C++/CLI项目中,编写托管类(使用.NET命名空间和关键字如ref class),并在其内部封装对原生C++代码的调用。例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      #pragma once

      using namespace System;

      public ref class ManagedWrapper
      {
      public:
      ManagedWrapper();
      ~ManagedWrapper();

      int Add(int a, int b);
      String^ Say(String^ str);
      };

      // .cpp文件中实现托管类的方法,调用原生C++代码
      ManagedWrapper::ManagedWrapper() {}
      ManagedWrapper::~ManagedWrapper() {}

      int ManagedWrapper::Add(int a, int b)
      {
      return a + b; // 假设这里实际调用了原生C++代码
      }

      String^ ManagedWrapper::Say(String^ str)
      {
      return str; // 同样,这里实际应调用原生C++代码
      }
  3. 编译并生成C++/CLI DLL

    • 编译C++/CLI项目,生成.dll文件。由于这个DLL现在包含托管代码,它可以被C#项目直接引用。
  4. 在C#项目中引用C++/CLI DLL

    • 在C#项目中添加对C++/CLI DLL的引用,如同引用其他托管DLL一样。然后直接实例化并使用其中的托管类:

CSharp winform

C#串口通信测试小工具教程

  • 事件源(EventSource):描述人机交互中事件的来源,通常是一些控件;
  • 事件(ActionEvent):事件源产生的交互内容,比如按下按钮;
  • 事件处理:这部分也在C++中被叫做回调函数,当事件发生时用来处理事件;

这部分在单片机中也是如此,中断源产生中断,然后进入中断服务函数进行响应;

和qt非常像

容器控件Panel

Panel是容器控件,是一些小控件的容器池,用来给控件进行大致分组,要注意容器是一个虚拟的,只会在设计的时候出现,不会显示在设计完成的界面上,这里我们将整个界面分为6个容器池,如图:

文本标签控件(Lable)

用于显示一些文本,但是不可被编辑;改变其显示内容有两种方法:一是直接在属性面板修改“Text”的值,二是通过代码修改其属性,见如下代码;另外,可以修改Font属性修改其显示字体及大小,这里我们选择微软雅黑,12号字体;

1
label1.Text = "串口";    //设置label的Text属性值

下拉组合框控件(ComboBox)

用来显示下拉列表;通常有两种模式

两种模式通过设置DropDownStyle属性选择

  • DropDown模式,既可以选择下拉项,也可以选择直接编辑
  • DropDownList模式,只能从下拉列表中选择
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void Form1_Load(object sender, EventArgs e)
{
int i;
//单个添加for (i = 300; i <= 38400; i = i*2)
{
comboBox2.Items.Add(i.ToString()); //添加波特率列表
}

//批量添加波特率列表
string[] baud = { "43000","56000","57600","115200","128000","230400","256000","460800" };
comboBox2.Items.AddRange(baud);

//设置默认值
comboBox1.Text = "COM1";
comboBox2.Text = "115200";
comboBox3.Text = "8";
comboBox4.Text = "None";
comboBox5.Text = "1";
}

按钮控件(Button)

文本框控件(TextBox)

TextBox控件与label控件不同的是,文本框控件的内容可以由用户修改,这也满足我们的发送文本框需求

需要多行显示,设置:其Multiline属性为true

TextBox的方法中最多的是APPendText方法,它的作用是将新的文本数据从末尾处追加至TextBox中,那么当TextBox一直追加文本后就会带来本身长度不够而无法显示全部文本的问题,此时我们需要使能TextBox的纵向滚动条来跟踪显示最新文本,所以我们将TextBox的属性ScrollBars的值设置为Vertical即可;

CSharp中的窗体事件驱动

new EventHandler是C#中用于创建事件处理程序委托实例的语法。在C#中,事件处理程序委托是一种特殊的委托类型,用于表示事件处理程序的方法签名。EventHandler是.NET Framework中预定义的委托类型,用于处理不包含数据的事件。

当您使用new EventHandler语法时,您正在实例化一个EventHandler委托,并指定事件处理程序的方法作为参数。在这种情况下,您需要指定事件处理程序方法的签名,即接受两个参数:一个object类型的sender参数和一个EventArgs类型的e参数。

下面是一个示例,演示如何使用new EventHandler语法创建事件处理程序委托实例:

1
2
3
4
5
6
7
8
// 定义事件处理程序方法
void MyEventHandler(object sender, EventArgs e)
{
// 事件处理程序逻辑
}

// 创建事件处理程序委托实例
EventHandler eventHandler = new EventHandler(MyEventHandler);

在上面的示例中,我们首先定义了一个名为MyEventHandler的事件处理程序方法,然后使用new EventHandler语法创建了一个EventHandler委托实例,将MyEventHandler方法作为参数传递给该委托实例。这样就创建了一个用于处理不包含数据的事件的事件处理程序委托实例。

CSharp窗口ui线程防堵塞

在 Windows 窗体程序中,UI 组件(比如文本框、按钮等)是不安全的线程资源。这意味着,只能在创建 UI 组件的线程(通常是主线程或 UI 线程)上对这些组件进行操作。当从非 UI 线程(如串口接收事件处理线程)尝试访问 UI 组件时,就必须使用某种同步方法来确保操作的安全性。

Invoke 是委托类型的实例方法,用于调用委托所引用的方法。委托是一种类型,它允许我们将方法作为参数传递并存储在字段或属性中。当委托实例被调用时,它会调用与之关联的方法。可以使用 += 运算符将一个方法添加到委托中,使用 -= 运算符将其从委托中删除。

在具体使用上,委托被定义为一个类实例,其具有与特定方法签名匹配的方法。每个委托实例都与一个特定方法绑定,并且可以通过委托实例调用该方法。

使用event修饰的委托,就变成了事件,在类外部是不能把该委托当做方法直接调用的,这就是用不用event的区别。

Invoke/BeginInvoke

Invoke 方法是一个同步方法,它可以将一个操作的执行权转交给 UI 线程。这样做可以避免因为多线程访问同一个 UI 组件而引起的竞争条件或冲突。Invoke 方法接受一个委托(Delegate)作为参数,该委托指向将要在 UI 线程上执行的方法

Invoke方式这种方式专门被用于解决从不是创建控件的线程访问控件

首先说下,Invoke和BeginInvoke的使用有两种情况:

control.Invoke(参数delegate)方法:在拥有此控件的基础窗口句柄的线程上同步执行指定的委托。

control.BeginInvoke(参数delegate)方法:在创建控件的基础句柄所在线程上异步执行指定委托。

Invoke的含义是:在拥有此控件的基础窗口句柄的现呈上同步执行指定的委托(同步)
BeginInvoke的含义是:在创建控件的基础句柄所在线程上异步执行的委托(异步)

  1. Invoke() 调用时,Invoke会阻止当前主线程的运行,等到 Invoke() 方法返回才继续执行后面的代码,表现出“同步”的概念。
  2. BeginInvoke() 调用时,当前线程会启用线程池中的某个线程来执行此方法,BeginInvoke不会阻止当前主线程的运行,而是等当前主线程做完事情之后再执行BeginInvoke中的代码内容,表现出“异步”的概念。在想获取 BeginInvoke() 执行完毕后的结果时,调用EndInvoke() 方法来获取。而这两个方法中执行的是一个委托。

使用场景

  • 当您需要立即更新UI并且不介意等待时,使用Invoke
  • 当您不需要立即知道操作结果,并且不希望阻塞当前线程时,使用BeginInvoke

返回值和异常

  • Invoke可以返回值,并且如果委托中引发异常,它将被传递回调用线程。
  • BeginInvoke不能直接返回值,如果委托中引发异常,它将不会传递回调用线程。

Control.Invoke

Control的Invoke一般用于解决跨线程访问的问题,比如你想操作一个按钮button,你就要用button.Invoke,你想操作一个文本label,你就要用label.Invoke.但是大家会发现很麻烦,如果我既然想操作button,又操作label,能不能写在一起呢?有没有更简单的方法呢?

其实主窗体使一个Form,Form自然也是继承了Control的,所以Form也有Invoke的方法,如果你想省点事,就可以直接调用Form.Invoke,这就是常见的this.Invoke.

为什么有的Invoke前面啥都没有?其实前面是this,只不过省略了.

对于Control 的Invoke ,更标准的用法是先加判断,再调用

1
2
3
4
if(this.InvokeRequired)
{
this.Invoke(...);
}

InvokeRequired是Control的一个属性

获取一个值,该值指示调用方在对控件进行方法调用时是否必须调用 Invoke 方法,因为调用方位于创建控件所在的线程以外的线程中。如果控件的 Handle 是在与调用线程不同的线程上创建的(说明您必须通过 Invoke 方法对控件进行调用),则为 true;否则为 false。

Invoke方法接受两个参数:一个委托和一个对象数组。委托定义了要在UI线程上执行的操作,对象数组则提供了传递给委托的参数。

与之对比的,在MFC和QT中防止UI线程卡住的方法分别如下:

  • MFC PostMessageSendMessage

    • QT 信号和槽机制

隐式控件

运行于后台的,用户看不见,更不能直接控制,所以也成为组件,接下来我们添加最主要的串口组件;

串口组件(SerialPort)

这种隐式控件添加后位于设计器下面 ,串口常用的属性有两个,一个是端口号(PortName),一个是波特率(BaudRate),当然还有数据位,停止位,奇偶校验位等;串口打开与关闭都有接口可以直接调用,串口同时还有一个IsOpen属性,IsOpen为true表示串口已经打开,IsOpen为flase则表示串口已经关闭。

SerialPort类是相当容易上手的。在进行串口通讯时,一般的流程是设置通讯端口号及波特率、数据位、停止位和校验位,再打开端口连接,发送数据,接收数据,最后关闭端口连接这样几个步骤。

添加了串口组件后,我们就可以通过它来获取电脑当前端口,并添加到可选列表中

1
2
//获取电脑当前可用串口并添加到选项列表中
comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());

编辑串口通信逻辑

开关串口

首先,我们先来控制打开/关闭串口,大致思路是:当按下打开串口按钮后,将设置值传送到串口控件的属性中,然后打开串口,按钮显示关闭串口,再次按下时,串口关闭,显示打开按钮;

 在这个过程中,要注意一点,当我们点击打开按钮时,会发生一些我们编程时无法处理的事件,比如硬件串口没有连接,串口打开的过程中硬件突然断开,这些被称之为异常,针对这些异常,C#也有try..catch处理机制,在try中放置可能产生异常的代码,比如打开串口,在catch中捕捉异常进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//打开串口按钮
private void button1_Click(object sender, EventArgs e) {
try
{
//将可能产生异常的代码放置在try块中
//根据当前串口属性来判断是否打开
if (serialPort1.IsOpen)
{
//串口已经处于打开状态
serialPort1.Close(); //关闭串口
button1.Text = "打开串口";
button1.BackColor = Color.ForestGreen;
comboBox1.Enabled = true;
comboBox2.Enabled = true;
comboBox3.Enabled = true;
comboBox4.Enabled = true;
comboBox5.Enabled = true;
textBox_receive.Text = ""; //清空接收区
textBox_send.Text = ""; //清空发送区
}
else
{
//串口已经处于关闭状态,则设置好串口属性后打开
comboBox1.Enabled = false;
comboBox2.Enabled = false;
comboBox3.Enabled = false;
comboBox4.Enabled = false;
comboBox5.Enabled = false;
serialPort1.PortName = comboBox1.Text;
serialPort1.BaudRate = Convert.ToInt32(comboBox2.Text);
serialPort1.DataBits = Convert.ToInt16(comboBox3.Text);

if (comboBox4.Text.Equals("None"))
serialPort1.Parity = System.IO.Ports.Parity.None;
else if(comboBox4.Text.Equals("Odd"))
serialPort1.Parity = System.IO.Ports.Parity.Odd;
else if (comboBox4.Text.Equals("Even"))
serialPort1.Parity = System.IO.Ports.Parity.Even;
else if (comboBox4.Text.Equals("Mark"))
serialPort1.Parity = System.IO.Ports.Parity.Mark;
else if (comboBox4.Text.Equals("Space"))
serialPort1.Parity = System.IO.Ports.Parity.Space;

if (comboBox5.Text.Equals("1"))
serialPort1.StopBits = System.IO.Ports.StopBits.One;
else if (comboBox5.Text.Equals("1.5"))
serialPort1.StopBits = System.IO.Ports.StopBits.OnePointFive;
else if (comboBox5.Text.Equals("2"))
serialPort1.StopBits = System.IO.Ports.StopBits.Two;

serialPort1.Open(); //打开串口
button1.Text = "关闭串口";
button1.BackColor = Color.Firebrick;
}
}
catch (Exception ex)
{
//捕获可能发生的异常并进行处理

//捕获到异常,创建一个新的对象,之前的不可以再用
serialPort1 = new System.IO.Ports.SerialPort();
//刷新COM口选项
comboBox1.Items.Clear();
comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());
//响铃并显示异常给用户
System.Media.SystemSounds.Beep.Play();
button1.Text = "打开串口";
button1.BackColor = Color.ForestGreen;
MessageBox.Show(ex.Message);
comboBox1.Enabled = true;
comboBox2.Enabled = true;
comboBox3.Enabled = true;
comboBox4.Enabled = true;
comboBox5.Enabled = true;
}
}
发送和接收串口
串口发送

  串口发送有两种方法,一种是字符串发送WriteLine,一种是Write(),可以发送一个字符串或者16进制发送(见下篇),其中字符串发送WriteLine默认已经在末尾添加换行符;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void button2_Click(object sender, EventArgs e)
{
try
{
//首先判断串口是否开启
if (serialPort1.IsOpen)
{
//串口处于开启状态,将发送区文本发送
serialPort1.Write(textBox_send.Text);
}
}
catch (Exception ex)
{
//捕获到异常,创建一个新的对象,之前的不可以再用
serialPort1 = new System.IO.Ports.SerialPort();
//刷新COM口选项
comboBox1.Items.Clear();
comboBox1.Items.AddRange(System.IO.Ports.SerialPort.GetPortNames());
//响铃并显示异常给用户
System.Media.SystemSounds.Beep.Play();
button1.Text = "打开串口";
button1.BackColor = Color.ForestGreen;
MessageBox.Show(ex.Message);
comboBox1.Enabled = true;
comboBox2.Enabled = true;
comboBox3.Enabled = true;
comboBox4.Enabled = true;
comboBox5.Enabled = true;
}
}
串口接受

在SerialPort类中有DataReceived事件,当串口的读缓存有数据到达时则触发DataReceived事件,其中SerialPort.ReceivedBytesThreshold属性决定了当串口读缓存中数据多少个时才触发DataReceived事件,默认为1。
另外,SerialPort.DataReceived事件运行比较特殊,其运行在辅线程,不能与主线程中的显示数据控件直接进行数据传输,必须用间接的方式实现。

使用串口接收之前要先为串口注册一个Receive事件,相当于单片机中的串口接收中断,然后在中断内部对缓冲区的数据进行读取

1
2
3
4
//串口接收事件处理
private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
}

串口接收也有两种方法,一种是16进制方式读(下篇介绍),一种是字符串方式读,在刚刚生成的代码中编写,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
//串口接收事件处理
private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
try
{
//因为要访问UI资源,所以需要使用Invoke方式同步ui
this.Invoke((EventHandler)(delegate
{
textBox_receive.AppendText(serialPort1.ReadExisting());
}
)
);

}
catch (Exception ex)
{
//响铃并显示异常给用户
System.Media.SystemSounds.Beep.Play();
MessageBox.Show(ex.Message);

}
}

//上面是整体读=================
//下面是按字节租步读=================
//串口接收事件处理
private void SerialPort1_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
int num = serialPort1.BytesToRead; //获取接收缓冲区中的字节数
byte[] received_buf = new byte[num]; //声明一个大小为num的字节数据用于存放读出的byte型数据

receive_count += num; //接收字节计数变量增加nun
serialPort1.Read(received_buf,0,num); //读取接收缓冲区中num个字节到byte数组中

//接第二步中的代码
sb.Clear(); //防止出错,首先清空字符串构造器
//遍历数组进行字符串转化及拼接
foreach (byte b in received_buf)
{
sb.Append(b.ToString());
}
try
{
//因为要访问UI资源,所以需要使用Invoke方式同步ui
Invoke((EventHandler)(delegate
{
textBox_receive.AppendText(sb.ToString());
label7.Text = "Rx:" + receive_count.ToString() + "Bytes";
}
)
);
}
//代码省略}

CSharp 绘图

Graphics 对象在 .NET Framework 中非常灵活,可以在多种不同的上下文中进行绘制。除了在打印文档中使用外,它还可以在以下地方进行绘制:

  1. 控件和窗体的自定义绘制

    • 通过覆盖控件或窗体的 OnPaint 方法,可以在控件或窗体的表面上直接进行绘制。
    • 这允许开发者创建自定义的控件外观或动画效果。
  2. 图像对象

    • 可以使用 Graphics 对象在 Bitmap 对象上进行绘制,从而创建或修改图像。
    • 这常用于图像处理和修改,例如添加文字、图形或进行颜色调整。
  3. 内存中的绘图

    • Graphics 可以从一个 Bitmap 创建,并用于在内存中的图像上绘制,而不需要直接绘制到屏幕上。
  4. 打印图形

    • 除了 PrintDocumentGraphics 还可以用于绘制打印预览和打印输出。
  5. 自定义的窗口装饰

    • 通过自定义窗口的非客户区(如窗口边框和标题栏),可以在窗口的这些区域进行绘制。
  6. 图形用户界面(GUI)组件

    • 在自定义 GUI 组件开发中,Graphics 对象可以用于绘制各种视觉元素。
  7. 游戏开发

    • 在简单的游戏开发中,Graphics 可以用于绘制游戏中的图形和动画。
  8. 数据可视化

    • 用于绘制图表、图形和其他数据可视化元素。

在窗口上直接进行绘制的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 假设 this 是窗体或控件的实例
private void CustomDrawMethod()
{
Graphics g = this.CreateGraphics(); // 创建与当前窗体或控件关联的 Graphics 对象
try
{
// 使用 g 对象绘制内容
g.Clear(this.BackColor); // 清除背景
g.DrawString("Hello, World!", this.Font, Brushes.Black, this.ClientRectangle); // 绘制文本
}
finally
{
g.Dispose(); // 释放 Graphics 对象
}
}

在上面的示例中,CreateGraphics 方法用于创建与当前控件或窗体关联的 Graphics 对象,然后使用该对象在控件或窗体上绘制文本。绘制完成后,需要调用 Dispose 方法来释放 Graphics 对象所占用的资源。

需要注意的是,在进行自定义绘制时,应该在 Dispose 方法中正确管理 Graphics 对象的生命周期,以避免资源泄漏和其他问题。此外,绘制操作可能会影响应用程序的性能,特别是在复杂的绘制任务或高频绘制操作中。

获取Graphics对象

演示如何获取一个控件的 Graphics 对象并使用它来绘制控件的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 假设 myControl 是您想要打印的控件
Control myControl = this.myControl;

// 创建一个与 myControl 控件大小相同的 Bitmap 对象
Bitmap bitmap = new Bitmap(myControl.Width, myControl.Height);

// 使用 CreateGraphics 方法从控件获取 Graphics 对象
Graphics graphics = myControl.CreateGraphics();

try
{
// 使用 Graphics 对象绘制控件内容
// 这里我们简单地将控件的图像绘制到 bitmap 上
graphics.CopyFromScreen(myControl.Location, Point.Empty, myControl.Size);

// 其他绘制操作...
}
finally
{
// 释放 Graphics 对象
graphics.Dispose();
}

// bitmap 现在包含了 myControl 控件的图像
// 您可以使用 bitmap 进行进一步的操作,比如打印或保存到文件

如何在现有控件上进行绘图

如果您需要在已经存在的控件上进行绘图,您通常会处理该控件的 Paint 事件。当控件需要重绘时(例如,当控件被覆盖或其 Invalidate 方法被调用时),Paint 事件会被触发。在 Paint 事件的处理程序中,您可以使用传入的 PaintEventArgs 参数中的 Graphics 对象来绘制您的自定义图形。

以下是如何为控件添加自定义绘图的步骤:

  1. 订阅 Paint 事件
    为您想要绘制的控件订阅 Paint 事件。

  2. 创建 Paint 事件处理器
    编写一个事件处理器来响应 Paint 事件。

  3. 使用 Graphics 对象绘制
    在事件处理器中,使用 PaintEventArgsGraphics 对象绘制您的图形。

  4. 触发重绘
    如果需要,可以调用控件的 Invalidate 方法来触发重绘。

以下是示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 假设 myControl 是您想要在其上绘制的控件
Control myControl = this.myControl;

// 订阅 Paint 事件
myControl.Paint += new PaintEventHandler(myControl_Paint);

private void myControl_Paint(object sender, PaintEventArgs e)
{
// 在这里使用 e.Graphics 对象绘制您的自定义图形
Graphics g = e.Graphics;

// 绘制示例:在控件上绘制一个红色的矩形
g.Clear(myControl.BackColor); // 清除背景
g.FillRectangle(Brushes.Red, new Rectangle(10, 10, 100, 100)); // 绘制填充矩形

// 如果需要,绘制文本
g.DrawString("Hello, World!", myControl.Font, Brushes.Black, new PointF(10, 10));

// 使用完 Graphics 对象后,不需要调用 Dispose,因为它是由系统管理的
}

// 如果需要手动触发重绘
myControl.Invalidate(); // 这将导致 Paint 事件被触发

在上面的代码中,myControl_Paint 方法是 Paint 事件的处理器。当控件需要重绘时,会调用此方法,并传入 PaintEventArgs 对象,其中包含 Graphics 对象和绘制区域的 Rectangle 等信息。

请注意,您不应该手动释放 Graphics 对象,因为它是由系统管理的资源。只需确保不要在 Dispose 方法调用之后使用它即可。

使用此方法,您可以在控件上绘制文本、形状、图像等,实现完全自定义的控件外观。

CSharp 窗体相关的事件

在 .NET Framework 中,Windows Forms 应用程序中的窗体(Form)可以触发多种事件。这些事件按照不同的阶段和操作进行排序,以下是一些常见的窗体事件,按时间顺序排列:

  1. 构造和初始化事件
    • Load:在窗体构造后,窗体的控件被创建和布局完成后触发。
  2. 显示和隐藏事件
    • Shown:在窗体首次显示后触发。
    • Hide:在调用 Hide 方法隐藏窗体时触发。
  3. 激活和取消激活事件
    • Activated:在窗体被激活时触发。
    • Deactivate:在窗体失去焦点时触发。
  4. 大小和布局事件
    • Resize:在调整窗体大小时触发。
    • Layout:在控件布局发生更改时触发。
  5. 移动事件
    • Move:在窗体位置发生移动时触发。
  6. 输入和焦点事件
    • Enter:在控件获得焦点时触发。
    • Leave:在控件失去焦点时触发。
    • GotFocusLostFocus:与 EnterLeave 类似,但更常用于控件。
  7. 点击和双击事件
    • MouseEnter:当鼠标进入窗体时触发。
    • MouseLeave:当鼠标离开窗体时触发。
    • MouseDown:当鼠标在窗体内部按下时触发。
    • MouseUp:当释放鼠标按钮时触发。
    • Click:当用户点击窗体时触发(MouseDownMouseUp 之间)。
    • DoubleClick:当用户在窗体上双击时触发。
  8. 键盘事件
    • KeyDown:当键盘上的键被按下时触发。
    • KeyUp:当键盘上的键被释放时触发。
    • KeyPress:当键盘上的键被按下并释放时触发。
  9. 关闭事件
    • FormClosing:在窗体关闭前触发,可以取消关闭操作。
    • FormClosed:在窗体关闭后触发。
  10. 绘图事件
    • Paint:在需要重绘窗体或控件的任何部分时触发。
    • PaintBackground:在绘制窗体背景时触发。
  11. 拖放事件
    • DragEnter:在开始拖动操作时触发。
    • DragOver:在拖动操作过程中触发。
    • DragDrop:在完成拖动操作时触发。
    • DragLeave:当拖动操作离开窗体时触发。
  12. 帮助事件
    • HelpRequested:当用户请求帮助时触发。

这些事件为开发者提供了丰富的交互性,允许对用户的操作做出响应,并执行相应的逻辑处理。开发者可以根据需要在窗体或控件的事件处理器中编写代码,以实现特定的功能。

CSharp DLL开发

创建DLL

在VS中创建项目选择类库

img

封装成DLL时程序集名字要跟程序里的 namespace 命名一致,如下图,否则应用DLL时无法引用成功

img

img

CSharp中调用该DLL

  1. 把DLL放在项目文件夹的bin目录的Debug目录下
  2. 点击项目里的引用添加DLL

img

img

img

添加完后可以在引用里看到DLL

img

使用using包含进去,并调用dll中的函数

1
2
3
4
5
using myDLL;

Class1 test = new Class1(); //新建类
int a = test.add(1,2); //调用DLL的函数
Console.WriteLine(" a = " + a);//查看调用结果

C#封装的DLL是非标准的DLL(托管类),不可以用 DllImport 调用,DllImport是用来调用标准类(非托管类)的,这类DLL一般是用C++写的

DLL中自定义窗口

项目上右键添加-添加-新建项-窗体(Windows 窗体)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
using System.Windows.Forms;

namespace testDll
{
public class Class1
{
public static void ShowWindow()
{
Form1 form = new Form1();
form.Text = "Hello from DLL";
MessageBox.Show("Hello from DLL");//弹消息框
//form.Show();//非模态弹自定义窗口
form.ShowDialog();//模态弹自定义窗口窗口
}
}

}

NuGet程序包管理器

开发过程中几乎不可避免地要使用第三方包,当然可以不用包管理器。对于开源的项目可以直接引用源文件,预先构建好了的库也可以直接引用dll。但是用nuget之类的包管理器可以更方便地进行管理,比如最基本的安装、更新、卸载功能可以直接通过命令行或者IDE来操作。

常用的包管理工具

  • Linux:apt、yum
  • Javascript:npm
  • Java:Maven、Gradle
  • Python:pip

NuGet是.NET平台上的包管理器,可以帮助开发者轻松地安装、更新和卸载第三方库和工具。

NuGet可以提高项目的开发效率和质量,因为它可以让开发者复用已有的优秀的代码,而不需要自己从头编写或者手动管理依赖关系。

NuGet使用方式

  • [NuGet CLI](NuGet CLI)
  • VS图形界面
    (推荐)VS命令行【程序包管理器控制台】

NuGet CLI

安装前要先查看当前包是否支持自己的项目框架(如下)

NuGet程序包管理器_管理工具

查看安装命令,复制到命令行执行

NuGet程序包管理器_asp.net_02

1
2
3
4
5
6
7
8
#大致语法
#下面命令可以直接在终端中运行
#安装:
Install-Package XXX -Version 指定版本。
#卸载:
UnInstall-Package XXX
#更新到最新版:
Update-Package XXX

VS图形界面

NuGet程序包管理器_管理工具_05

CSharp 合并程序集

我们有多种工具可以将程序集合并成为一个。比如 ILMerge、Mono.Merge。前者不可定制、运行缓慢、消耗资源(不过好消息是现在开源了);后者已被弃用、不受支持且基于旧版本的 Mono.Cecil。

用来替代它们的 ILRepack,使用 ILRepack 来合并程序集。

ILRepack

il-repack 是一款开源的 .NET 类库重打包工具,它能够将多个 DLL 文件合并成一个单一的 DLL 或 EXE 文件。这在处理依赖性复杂的问题时非常有用,并且可以提高应用程序的部署效率。

以帮助开发者解决以下问题:

  • 将多个类库合并为一个文件,减少部署所需的文件数量。
  • 合并类库中的类型冲突。il-repack 可以自动解决这些冲突,并确保程序正常运行。
  • 支持 .NET Framework 和 .NET Core。
  • 支持 Windows、Linux 和 macOS 操作系统。

此外,il-repack 还具有以下特点:

  • 易于使用:只需要在命令行中输入简单的指令即可完成操作。
  • 高效:il-repack 使用 IL 指令进行操作,因此速度非常快。
  • 灵活:支持自定义输出目录和输出文件名。

使用 ILRepack 来合并程序集使用方式

1
PM> NuGet\Install-Package ILRepack -Version 2.0.31

项目根目录下出现的packages\ILRepack.2.0.31\tools下会出现ILRepack.exe

使用类似这样的命令

1
2
#将dll1.dll和dll2.dll以及dll3.dll(以dll1.dll为主)合并为一个output.dll
./ILRepack.exe /out:output.dll /target:library dll1.dll dll2.dll dll3.dll

比如说如果dll3.dll还依赖dll4.dll,将dll4.dll一起拉进来合成dll

CSharp 单元测试

为什么要使用单元测试?

  • 大大节约了测试和修改的时间,有效且便于测试各种情况。
  • 能快速定位bug(每一个测试用例都是具有针对性)。
  • 能使开发人员重新审视需求和功能的设计(难以单元测试的代码,就需要重新设计)。
  • 强迫开发者以调用者而不是实现者的角度来设计代码,利于代码之间的解耦。
  • 自动化的单元测试能保证回归测试的有效执行。
  • 使代码可以放心修改和重构。
  • 测试用例,可作为开发文档使用(测试即文档)。
  • 测试用例永久保存,支持随时测试。

对于我个人来说,主要是防止自己犯低级错误的,同时也方便修改(BUG修复)而不引入新的问题。可以放心大胆的重构。简言之,这个简单有效的技术就是为了令代码变得更加完美。

c#常用单元测试框架:MSTest (Visual Studio官方)、XUnit 和 NUnit。

  1. MS Test为微软产品,集成在Visual Studio 2008+工具中。
  2. NUnit为.Net开源测试框架(采用C#开发),广泛用于.Net平台的单元测试和回归测试中,官方网址(www.nunit.org)。
  3. XUnit.Net为NUnit的改进版。

mock技术

在进行单元测试时,如果函数依赖于许多资源而不仅仅是参数,可以考虑使用mocking技术。Mocking是一种在单元测试中模拟依赖项的技术,以确保测试的独立性和可靠性。通过使用mocking框架或手动创建模拟对象,您可以模拟函数所依赖的资源,从而使测试更加可控和可靠。这样,您就可以专注于测试函数的逻辑而不必担心外部资源的影响。希望这可以帮助您进行函数的单元测试。

Visual Studio本身并不提供内置的mocking框架,但您可以使用第三方的mocking框架来进行单元测试。一些流行的C# mocking框架如Moq、NSubstitute和Rhino Mocks可以与Visual Studio集成,并且可以很容易地在Visual Studio中使用这些框架来进行单元测试

自动生成单元测试

参考链接

仅公共类或公共方法中支持创建单元测试

在需要测试的函数内右键创建单元测试,会打开一个窗口供你配置(直接按默认的设置项就可以)

配置好后会新建一个测试类和一个测试函数,在测试函数中编写断言

测试框架可以选择MSTest和MSTestv2

MSTest和MSTestv2区别:

MSTest 和 MSTest v2 是 Microsoft 提供的两代测试框架,用于在 .NET 中编写和运行单元测试。MSTest v2 是 MSTest 的继任者,改进了许多功能,并且增加了更多的灵活性和现代化特性。以下是两者之间的一些主要区别:

1. 包和命名空间

  • MSTest:
    • 包名:Microsoft.VisualStudio.QualityTools.UnitTestFramework
    • 命名空间:Microsoft.VisualStudio.TestTools.UnitTesting
  • MSTest v2:
    • 包名:MSTest.TestFrameworkMSTest.TestAdapter
    • 命名空间:Microsoft.VisualStudio.TestTools.UnitTesting

2. 跨平台支持

  • MSTest: 主要针对 Windows 平台。
  • MSTest v2: 支持跨平台,可以在 Windows、Linux 和 macOS 上运行。

3. 与 .NET Core 的兼容性

  • MSTest: 不支持 .NET Core,只能用于 .NET Framework 项目。
  • MSTest v2: 支持 .NET Core 和 .NET Framework,可以在新的 .NET 平台上使用。

4. NuGet 包管理

  • MSTest: 一般情况下,它是通过 Visual Studio 自带的组件安装的,不使用 NuGet 包管理。
  • MSTest v2: 通过 NuGet 包进行安装和管理,非常方便。

5. 特性 (Attributes)

  • MSTestMSTest v2 都使用类似的特性来标记测试方法和类,例如 [TestMethod], [TestClass], [TestInitialize], [TestCleanup], [ClassInitialize], [ClassCleanup]。不过 MSTest v2 增加了一些新的特性,如 [DataRow], [DataTestMethod],用于数据驱动测试。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    [TestClass]
    public class MyTests
    {
    [DataTestMethod]
    [DataRow(1, 2, 3)]
    [DataRow(2, 3, 5)]
    public void AddTest(int a, int b, int expected)
    {
    Assert.AreEqual(expected, a + b);
    }
    }

6. 改进的 TestContext

  • MSTest: TestContext 用于提供测试信息和数据,但是功能相对有限。
  • MSTest v2: 改进了 TestContext,增加了更多的功能和更好的灵活性。

7. 配置和运行设置

  • MSTest: 使用 .testsettings.runsettings 文件进行配置。
  • MSTest v2: 更加强调使用 .runsettings 文件,同时支持更多的配置选项。

8. 集成和扩展

  • MSTest: 集成和扩展的能力相对有限。
  • MSTest v2: 提供了更好的扩展机制和更强的集成能力,可以更容易地与其他测试工具和框架(如 Azure DevOps)集成。

总结

MSTest v2 是 MSTest 的现代化替代品,提供了更多的功能、更好的跨平台支持和更强的灵活性。如果你开始一个新的项目,或者需要在 .NET Core 或 .NET 5/6+ 上进行单元测试,推荐使用 MSTest v2。而对于已经使用 MSTest 的现有项目,可以考虑逐步迁移到 MSTest v2 以利用其改进和新特性。

一个例子如下:(包含输出打印,数据驱动的MSTestv2的列子)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
namespace Algorithm.Tests
{
[TestClass()]
public class FrechetTests
{
//用于打印数据显示
public TestContext TestContext { get; set; }

public static IEnumerable<object[]> GetData()
{
// 示例数据集
yield return new object[]
{
new List<double> { 1.1, 2.2, 3.3 },
new List<double> { 4.4, 5.5, 6.6 },
new List<double> { 7.7, 8.8, 9.9 },
new List<double> { 10.10, 11.11, 12.12 }
};
yield return new object[]
{
new List<double> { 0.1, 0.2, 0.3 },
new List<double> { 1.4, 1.5, 1.6 },
new List<double> { 2.7, 2.8, 2.9 },
new List<double> { 3.10, 3.11, 3.12 }
};
yield return new object[]
{
new List<double> { 0.1, 0.2 , 0.3 },
new List<double> { 1.4, 1.5 , 1.1},
new List<double> { 2.7, 2.8, 2.9 , 4.5},
new List<double> { 3.10, 3.11, 3.12 , 6.7}
};
yield return new object[]
{
new List<double> { 0, 1, 2, 3, 4 },
new List<double> { 0, 1, 2, 3, 4 },
new List<double> { 0, 1, 2, 3, 4 },
new List<double> { 0, 1, 2, 3, 4 }
};
// 添加更多数据集
}

[TestMethod()]
[DynamicData(nameof(GetData), DynamicDataSourceType.Method)]
public void CalcTest(List<double> list1,List<double> list2,List<double> list3,List<double> list4)
{

double res = Frechet.Calc(list1, list2, list3, list4);
//Assert.AreEqual(res, 0);//Assert中有很多断言方法
TestContext.WriteLine($"曲线1x: {string.Join(", ", list1)}");
TestContext.WriteLine($"曲线1y: {string.Join(", ", list2)}");
TestContext.WriteLine($"曲线2x: {string.Join(", ", list3)}");
TestContext.WriteLine($"曲线2y: {string.Join(", ", list4)}");
TestContext.WriteLine("相似度为" + res.ToString());
//Assert.Fail(e.Message);
}
}
}

异常相关

  • 如果方法在测试过程中抛出异常,而没有使用 [ExpectedException] 特性或 Assert.ThrowsException 方法来捕获异常,测试将失败。
  • 如果方法在测试过程中没有抛出异常,即使不写任何断言,测试也会通过。

CSharp XML操作

此处记录一个vs很实用的工具,在编辑-选择性粘贴-将XML粘贴为类
可以将XML字符串数据转换为类结构

参考链接

CSharp XML操作 与 [CSharp Json处理](#CSharp Json处理) 有很大的不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 创建一个新的 XML 文档对象
XmlDocument xmlDoc = new XmlDocument();

// 创建 XML 声明
XmlDeclaration xmlDecl = xmlDoc.CreateXmlDeclaration("1.0", "UTF-8", null);
//插入XML声明
xmlDoc.AppendChild(xmlDecl);

// 创建根元素 (根元素是必须要有的)
XmlElement rootElement = xmlDoc.CreateElement("Root");
//插入根元素
xmlDoc.AppendChild(rootElement);

// 在根元素下创建子元素并设置属性
//设置xml标签名为Child
XmlElement childElement = xmlDoc.CreateElement("Child");
childElement.SetAttribute("attributeName", "AttributeValue");
//设置Child标签中的值为"Element Text"
childElement.InnerText = "Element Text";
rootElement.AppendChild(childElement);

// 保存到文件
xmlDoc.Save("example.xml");

Console.WriteLine("XML 文件创建成功。");

//读取示例:

// 创建一个XmlDocument对象并加载XML文件
XmlDocument doc = new XmlDocument();
doc.Load("example.xml");

// 使用SelectSingleNode方法选择匹配的第一个节点
XmlNode node = doc.SelectSingleNode("/Root/Child");

// 获取节点的文本内容
string nodeValue = node.InnerText;

生成的xml文件内容如下:

1
2
3
4
<?xml version="1.0" encoding="UTF-8"?>
<Root>
<Child attributeName="AttributeValue">Element Text</Child>
</Root>

也有GetAttribute函数用于读取

XmlSerializer

XmlSerializer 是一个非常重要的类,用于将对象序列化为 XML 格式,或者将 XML 反序列化为对象

C# 的 System.Xml 命名空间提供了多种 XML 操作类,包括 XmlSerializer,XmlSerializer 是这个库的一部分,主要用于对象与 XML 之间的序列化和反序列化操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using System;
using System.IO;
using System.Xml.Serialization;

[XmlRoot("Person")]
public class Person
{
[XmlElement("Name")]
public string Name { get; set; }

[XmlElement("Age")]
public int Age { get; set; }
}

class Program
{
static void Main()
{
var person = new Person { Name = "张三", Age = 30 };

// 序列化
var serializer = new XmlSerializer(typeof(Person));
using (var writer = new StringWriter())
{
serializer.Serialize(writer, person);
string xml = writer.ToString();
Console.WriteLine(xml);
}

// 反序列化
string xmlData = "<Person><Name>张三</Name><Age>30</Age></Person>";
using (var reader = new StringReader(xmlData))
{
var deserializedPerson = (Person)serializer.Deserialize(reader);
Console.WriteLine($"Name: {deserializedPerson.Name}, Age: {deserializedPerson.Age}");
}
}
}

注意,如果是属性的话要使用:XmlAttribute

CSharp 反射

反射的作用: 相当于代码的”自检”能力

反射是框架设计的基础。

反射在系统中另一个重要应用就是与特性的结合使用

C#编程语言中,最常使用的是类和类中的函数和属性。正向调用的方法是,创建类,然后用类创建一个对象。接下来就可以用这个对象调用类中的方法和属性了。

而反射,就是相对于这种正向调用的存在。即,它是反向调用。

反射可以通过类名的字符串来创建类,可以通过函数名的字符串和属性名的字符串,来调用类下的函数和属性。

为什么需要反射?
在不知道一个类型是什么的情况下,可以动态的去操作一个类型
典型的表现就是依赖注入容器,依赖注入容器是怎么做到我们要什么类型,他就给什么类型的?

类反射

两种通过类名称的字符串,反射出类的对象的用法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ReflectionSyntax
{
public static void Excute()
{
//"Syntax.Kiba"是一个完全限定名。什么是完全限定名?完全限定名就是命名空间+类名。在反射的时候,需要我们传递完全限定名来确定到底要去哪个命名空间,找哪个类。
//如果是反射时,只写了一个类名,那么速度就会变慢。因为它要遍历所有的命名空间,去找这个类。
Type type = GetType("Syntax.Kiba");
Kiba kiba = (Kiba)Activator.CreateInstance(type);
Type type2 = GetType2("Syntax.Kiba");
Kiba kiba2 = (Kiba)Activator.CreateInstance(type2);
}

//先进行了加载Assembly(组件),然后再由组件获取类型,即可以解析DLL中的类
public static Type GetType(string fullName)
{
Assembly assembly = Assembly.Load("Syntax");
Type type = assembly.GetType(fullName, true, false);
return type;
}

//只能解析当前命名空间下的类。如果该类存在于引用的DLL中,就解析不了
public static Type GetType2(string fullName)
{
Type t = Type.GetType(fullName);
return t;
}
}
public class Kiba
{
public void PrintName()
{
Console.WriteLine("Kiba518");
}
}

函数反射

普通函数

函数的反射应用主要是使用类MethodInfo类反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void ExcuteMethod()
{
Assembly assembly = Assembly.Load("Syntax");
Type type = assembly.GetType("Syntax.Kiba", true, false);//获取了Syntax命名空间下Kiba这个类的类型
MethodInfo method = type.GetMethod("PrintName");//通过这个类型来获取指定名称的方法
object kiba = assembly.CreateInstance("Syntax.Kiba");//实例化对象
object[] pmts = new object[] { "K iba518" };//定义了一个参数的Object数组
method.Invoke(kiba, pmts);//执行方法
}
public class Kiba
{
public string Name { get; set; }
public void PrintName(string name)
{
Console.WriteLine(name);
}
}

构造函数

1
2
3
var studentType = typeof(Student);
var studentConstructor = studentType.GetConstructors()[0];//获取第一个构造函数
var s = studentConstructor.Invoke(new object[0]);//无参的方式调用第一个构造函数

上面代码要求没有别的构造函数,但是如果想指定性的获取无参构造函数,可以使用LINQ来筛选出无参构造函数,如:

1
2
3
4
5
6
7
8
9
10
11
12
var studentType = typeof(Student);
var constructors = studentType.GetConstructors();
var parameterlessConstructor = constructors.FirstOrDefault(c => c.GetParameters().Length == 0);//通过构造函数的参数列表来判断

if (parameterlessConstructor != null)
{
var s = parameterlessConstructor.Invoke(new object[0]); // 调用无参构造函数
}
else
{
// 处理没有无参构造函数的情况
}

属性反射

属性反射是用PropertyInfo类来实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void ExcuteProperty()
{
Kiba kiba = new Kiba();
kiba.Name = "Kiba518";
object name = ReflectionSyntax.GetPropertyValue(kiba, "Name");
Console.WriteLine(name);
}
public static object GetPropertyValue(object obj, string name)
{
PropertyInfo property = obj.GetType().GetProperty(name);
if (property != null)
{
object drv1 = property.GetValue(obj, null);
return drv1;
}
else
{
return null;
}
}

反射设置值时先进行类型转换

1
2
3
4
5
6
var property = SelectedDeviceConfig.port.GetType().GetProperty(member.Name);
var targetType = property.PropertyType;
var value = member.Value;
// 尝试将字符串转换为目标类型
var convertedValue = Convert.ChangeType(value, targetType);
property.SetValue(SelectedDeviceConfig.port, convertedValue);

通过这种方式,可以确保在设置属性值之前,value 被正确地转换为属性所需的类型。如果转换失败,可以记录错误信息或采取其他适当的措施

反射与特性结合

1
2
3
4
5
6
7
8
9
var properties = SelectedDeviceConfig.port.GetType().GetProperties();
foreach (var property in properties)
{
// 具有LinkPoint特性的属性
if (!Attribute.IsDefined(property, typeof(LinkPoint)))
{
//...
}
}

上面根据是否有特性来执行代码

演示了如何在运行时根据属性的元数据动态修改对象的属性值,如通过反射,将拥有KibaAttribute特性的,且描述为Clear的属性,清空了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 包含演示反射和自定义属性处理的主类
public partial class ReflectionSyntax
{
// 用于演示使用反射执行 ClearKibaAttribute 方法的示例
public void ExcuteKibaAttribute()
{
// 创建 Kiba 类的实例并设置其属性
Kiba kiba = new Kiba();
kiba.ClearName = "Kiba518"; // 带有 "Clear" 自定义属性的属性
kiba.NoClearName = "Kiba518"; // 带有 "NoClear" 自定义属性的属性
kiba.NormalName = "Kiba518"; // 没有任何自定义属性的普通属性

// 调用方法清除带有 "Clear" 自定义属性的属性值
ClearKibaAttribute(kiba);

// 打印属性值以观察更改
Console.WriteLine(kiba.ClearName); // 应输出 null,因为此属性被清除
Console.WriteLine(kiba.NoClearName); // 应输出 "Kiba518",因为此属性未被清除
Console.WriteLine(kiba.NormalName); // 应输出 "Kiba518",因为此属性没有任何自定义属性
}

// 根据自定义属性清除 Kiba 对象的某些属性
public void ClearKibaAttribute(Kiba kiba)
{
// 获取 Kiba 类的所有公共实例属性
List<PropertyInfo> plist = typeof(Kiba)
.GetProperties(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public)
.ToList();

// 遍历每个属性
foreach (PropertyInfo pinfo in plist)
{
// 获取当前属性的所有 KibaAttribute 类型的自定义属性
var attrs = pinfo.GetCustomAttributes(typeof(KibaAttribute), false);

// 检查该属性是否具有 KibaAttribute 自定义属性
if (null != attrs && attrs.Length > 0)
{
// 将第一个自定义属性强制转换为 KibaAttribute 并获取其 Description 属性值
var des = ((KibaAttribute)attrs[0]).Description;

// 如果 Description 为 "Clear",将该属性值设置为 null
if (des == "Clear")
{
pinfo.SetValue(kiba, null); // 清除属性值
}
}
}
}
}

// 表示对象的类,其属性将被操作
public class Kiba
{
[KibaAttribute("Clear")] // 自定义属性,Description 设置为 "Clear"
public string ClearName { get; set; }

[KibaAttribute("NoClear")] // 自定义属性,Description 设置为 "NoClear"
public string NoClearName { get; set; }

public string NormalName { get; set; } // 没有任何自定义属性的普通属性
}

// 自定义属性类,用于为属性添加元数据
[System.AttributeUsage(System.AttributeTargets.All)] // 该属性可以应用于所有程序元素
public class KibaAttribute : System.Attribute
{
public string Description { get; set; } // 用于存储描述信息的属性

// 构造函数,用于初始化自定义属性的描述信息
public KibaAttribute(string description)
{
this.Description = description;
}
}

在用反射,将DataTable转存到Model实体的时候,遍历属性并赋值的时候,就会多遍历那么几次。

如果只是一个实体,那么,多遍历几次也没影响。但,如果是数十万的数据,那这多几次的遍历影响就大了。

而用反射+特性,就可以减少这些额外遍历次数。即只需要遍历一次,根据属性来做不同的操作

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// <summary>
/// 字符串反射instance类对象中的变量设置为value值
/// </summary>
/// <param name="instance">哪个类的实例</param>
/// <param name="variableName">要设置的变量名</param>
/// <param name="value">要设置的值</param>
private void SetVariableValue(object instance, string variableName, object value)
{
variableName = char.ToLower(variableName[0]) + variableName.Substring(1);
// 获取当前实例的类型
Type type = instance.GetType();

// 使用反射获取字段并赋值
FieldInfo field = type.GetField(variableName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
if (field != null)
{
field.SetValue(instance, value);
}
else
{
StackTrace stackTrace = new StackTrace(true);
Console.WriteLine("反射的变量名不存在:" + variableName + " 调用堆栈:" + stackTrace.ToString());//打印调用堆栈信息
}
}

使用案例

1
SetVariableValue(this,varName, double.Parse(control.Text));//设定的值需要类型转化

反射置空案例

在C#中,没有直接的方法可以将一个对象的所有属性都置为空。您需要逐个设置每个属性为null或默认值。这是因为C#中的对象初始化语法通常需要为每个属性提供明确的赋值。

如果您想要一种更通用的方法来将对象的所有属性置为空,您可能需要编写一个通用的方法或使用反射来实现这一目的。以下是一个使用反射的示例代码,可以将一个对象的所有属性置为空:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static void SetPropertiesToNull(object obj)
{
if (obj == null)
{
return;
}

Type type = obj.GetType();
PropertyInfo[] properties = type.GetProperties();

foreach (PropertyInfo property in properties)
{
if (property.CanWrite)
{
property.SetValue(obj, null);
}
}
}

// Usage
MyClass myObject = new MyClass();
SetPropertiesToNull(myObject);

这个示例代码使用反射来获取对象的所有属性,并将它们的值设置为null。请注意,使用反射可能会影响性能,因此请根据实际情况谨慎使用。

动态类型生成

方法 适用场景 动态性 语法友好性 性能
ExpandoObject 简单动态对象,无需强类型约束 ⭐⭐⭐⭐ 较高
自定义动态类型 复杂动态逻辑(如XML/JSON解析) ⭐⭐⭐ 中等
组合模式 保留原始对象强类型特性 ⭐⭐
反射与动态代理 高级场景,需生成全新类型
  • 若需完全动态行为,优先选择 ExpandoObject 或自定义动态对象。
  • 若需与现有对象协作,使用组合模式或包装类。
  • 避免尝试修改已编译类型的元数据(C#语言限制)

CSharp 元组

C#元组是C# 7.0版本引入的一种数据结构,用于组合多个值并将它们作为一个单元一起传递。元组可以包含不同类型的值,并且可以在不创建新的类或结构的情况下使用。以下是一些关于C#元组的介绍:

语法

  • 使用元组的语法是在圆括号中列出要组合的值,用逗号分隔。例如:(int, string)表示包含一个整数和一个字符串的元组
  • 可以为元组的组成部分指定名称,例如:(int Id, string Name)

创建元组

  • 使用Tuple.Create方法创建元组,例如:var person = Tuple.Create(1, "Alice");
  • 使用元组字面量语法创建元组,例如:(1, "Alice")

访问元组元素

  • 可以使用.操作符访问元组中的元素,例如:var id = person.Item1; var name = person.Item2;
  • 在具有命名组件的元组中,可以使用组件名称访问元素,例如:var id = person.Id; var name = person.Name;

返回元组

  • 可以在方法中返回元组作为结果,例如:public (int, string) GetPerson() { return (1, "Alice"); }

解构元组

  • 可以使用解构语法将元组的值分配给多个变量,例如:var (id, name) = GetPerson();

使用场景

异步方法返回多数据

1
2
3
4
5
6
7
8
9
10
//一个方法体,要返回具体的业务数据,如果里面逻辑校验不通过或者出现了异常,则也要返回响应的错误信息。通过 ref 或者 out 等关键字,来实现多个数据回传
public int Method(int id,ref string errMsg)
{
....//具体的逻辑
}
//但是从async/await普及后,异步方法不支持ref,in或out参数,元组就有用了,写法变成了
public async Task<(int data,string errMsg)> MethodAsync(int id)
{
....//具体的逻辑
}

临时数据

只是想临时性记录一下某个对象的部分属性,比如一个包裹的体积(长,宽,高),或者某个点的平面坐标,或者空间坐标等。 如果单独为其创建类,写起来其实挺费力的,但是如果使用元组,那就简单太多了

变量值互换

1
2
3
int a= 2;
int b= 3;
(a,b)=(b,a);

CSharp dump

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
[Flags]
public enum MINIDUMP_TYPE : uint
{
MiniDumpNormal = 0x00000000,
MiniDumpWithDataSegs = 0x00000001,
MiniDumpWithFullMemory = 0x00000002,
MiniDumpWithHandleData = 0x00000004,
MiniDumpWithThreadInfo = 0x00001000
}

[DllImport("Dbghelp.dll")]
public static extern bool MiniDumpWriteDump(
IntPtr hProcess,
int processId,
IntPtr hFile,
MINIDUMP_TYPE dumpType,
IntPtr exceptionParam,
IntPtr userStreamParam,
IntPtr callbackParam);

public static void CreateDump(string dumpFileName)
{
string dumpsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "dumps");
if (!Directory.Exists(dumpsDirectory))
{
Directory.CreateDirectory(dumpsDirectory);
}
string dumpFilePath = Path.Combine(dumpsDirectory, dumpFileName);
using (var process = Process.GetCurrentProcess())
{
using (FileStream fs = new FileStream(dumpFilePath, FileMode.Create))
{
bool result = MiniDumpWriteDump(
process.Handle,
process.Id,
fs.SafeFileHandle.DangerousGetHandle(),
MINIDUMP_TYPE.MiniDumpWithFullMemory | MINIDUMP_TYPE.MiniDumpWithHandleData | MINIDUMP_TYPE.MiniDumpWithThreadInfo,
IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero);
}
}
}

//带文件数量限制的版本:
public static void CreateDump(string dumpFileName, int maxNum = 3)
{
string dumpsDirectory = Path.Combine(Directory.GetCurrentDirectory(), "dumps");
if (!Directory.Exists(dumpsDirectory))
{
Directory.CreateDirectory(dumpsDirectory);
}
else
{
//获取当前目录下所有dump文件,按创建时间排序
string[] dumpFiles = Directory.GetFiles(dumpsDirectory).OrderBy(f => File.GetCreationTime(f)).ToArray();
if (dumpFiles.Length >= maxNum)
{
//删除最早的dump文件
File.Delete(dumpFiles[0]);
}
}
string dumpFilePath = Path.Combine(dumpsDirectory, dumpFileName);
using (var process = Process.GetCurrentProcess())
{
using (FileStream fs = new FileStream(dumpFilePath, FileMode.Create))
{
bool result = MiniDumpWriteDump(
process.Handle,
process.Id,
fs.SafeFileHandle.DangerousGetHandle(),
MINIDUMP_TYPE.MiniDumpWithFullMemory | MINIDUMP_TYPE.MiniDumpWithHandleData | MINIDUMP_TYPE.MiniDumpWithThreadInfo,

IntPtr.Zero,
IntPtr.Zero,
IntPtr.Zero);

if (result)
{
Debug.WriteLine($"Dump file created at {dumpFilePath}");
}
else
{
Debug.WriteLine("Failed to create dump file.");
}
}
}
}

//使用方式
string dumpFileName = $"IonImplantationSystem_{DateTime.Now:MM-dd_HH-mm-ss-ms}.dmp";
CreateDump(dumpFileName);

常用的其他 MiniDump 选项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
public enum MINIDUMP_TYPE : uint
{
// 基础选项
MiniDumpNormal = 0x00000000,
// 仅包含最基本的崩溃信息:
// - 异常信息
// - 调用堆栈
// - 线程列表
// - 模块列表

MiniDumpWithDataSegs = 0x00000001,
// 包含数据段:
// - 加载的模块的数据段
// - 有助于查看全局变量

MiniDumpWithFullMemory = 0x00000002,
// 包含进程的完整内存转储:
// - 包括所有可读写的内存页
// - 可以查看完整的堆内存
// - 文件较大

MiniDumpWithHandleData = 0x00000004,
// 包含句柄信息:
// - 进程打开的所有句柄
// - 文件句柄
// - 事件句柄
// - 互斥体等

MiniDumpFilterMemory = 0x00000008,
// 过滤内存数据:
// - 仅包含有用的内存页
// - 减小文件大小

MiniDumpScanMemory = 0x00000010,
// 扫描内存:
// - 查找特定的内存模式
// - 用于内存分析

MiniDumpWithUnloadedModules = 0x00000020,
// 包含已卸载的模块信息:
// - 帮助诊断模块加载问题
// - 分析模块依赖关系

MiniDumpWithIndirectlyReferencedMemory = 0x00000040,
// 包含间接引用的内存:
// - 包括指针引用的内存
// - 有助于追踪内存关系

MiniDumpFilterModulePaths = 0x00000080,
// 过滤模块路径:
// - 仅包含必要的模块路径信息
// - 减小文件大小

MiniDumpWithProcessThreadData = 0x00000100,
// 包含进程和线程数据:
// - 详细的线程状态
// - 线程本地存储

MiniDumpWithPrivateReadWriteMemory = 0x00000200,
// 包含私有读写内存:
// - 进程私有的可读写内存页
// - 不包含共享内存

MiniDumpWithoutOptionalData = 0x00000400,
// 排除可选数据:
// - 最小化转储大小
// - 仅保留核心信息

MiniDumpWithFullMemoryInfo = 0x00000800,
// 完整的内存信息:
// - 内存管理信息
// - 内存分配状态
// - 保护属性

MiniDumpWithThreadInfo = 0x00001000,
// 额外的线程信息:
// - 线程时间
// - 线程优先级
// - 线程亲和性

MiniDumpWithCodeSegs = 0x00002000,
// 包含代码段:
// - 所有模块的代码段
// - 用于反汇编分析

MiniDumpWithoutAuxiliaryState = 0x00004000,
// 排除辅助状态:
// - 不包含辅助数据结构
// - 减小文件大小

MiniDumpWithFullAuxiliaryState = 0x00008000,
// 完整的辅助状态:
// - 包含所有辅助数据结构
// - 用于深入分析

MiniDumpWithPrivateWriteCopyMemory = 0x00010000,
// 私有写时复制内存:
// - 包含写时复制页面
// - 用于分析内存修改

MiniDumpIgnoreInaccessibleMemory = 0x00020000,
// 忽略不可访问的内存:
// - 跳过无法读取的内存页
// - 避免转储失败

MiniDumpWithTokenInformation = 0x00040000,
// 包含令牌信息:
// - 安全令牌
// - 权限信息

MiniDumpWithModuleHeaders = 0x00080000,
// 包含模块头信息:
// - PE头信息
// - 导入导出表

MiniDumpFilterTriage = 0x00100000,
// 筛选关键信息:
// - 仅包含故障排除必需信息
// - 优化文件大小

MiniDumpWithAvxXStateContext = 0x00200000,
// AVX状态上下文:
// - AVX寄存器状态
// - SIMD相关信息

MiniDumpWithIptTrace = 0x00400000,
// Intel处理器追踪:
// - 处理器执行追踪信息
// - 性能分析数据

MiniDumpScanInaccessiblePartialPages = 0x00800000,
// 扫描不可访问的部分页面:
// - 尝试读取部分可访问的页面
// - 最大化信息收集

// 组合标志
MiniDumpWithFullMemoryEx = MiniDumpWithFullMemory |
MiniDumpWithFullMemoryInfo |
MiniDumpWithHandleData |
MiniDumpWithThreadInfo |
MiniDumpWithTokenInformation,
// 常用的完整内存转储组合:
// - 包含完整内存映像
// - 内存管理信息
// - 句柄数据
// - 线程信息
// - 安全令牌信息
}

组合盘点

  • 初步分析MiniDumpNormal | MiniDumpWithDataSegs | MiniDumpWithThreadInfo

    适用于生成较小的 Dump 文件,用于基本调试,如快速分析崩溃原因、线程状态等

  • 一般调试MiniDumpWithFullMemory | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithDataSegs

    适合大多数问题分析,如内存泄漏、资源泄漏和多线程问题。

  • 全面诊断MINIDUMP_TYPE.MiniDumpWithFullMemory | MINIDUMP_TYPE.MiniDumpWithHandleData | MINIDUMP_TYPE.MiniDumpWithThreadInfo | MINIDUMP_TYPE.MiniDumpWithDataSegs | MINIDUMP_TYPE.MiniDumpWithPrivateReadWriteMemory | MINIDUMP_TYPE.MiniDumpWithUnloadedModules | MINIDUMP_TYPE.MiniDumpWithFullMemoryInfo;

    全面分析复杂问题,包括内存和线程异常、模块加载/卸载等。

  • 性能调优MiniDumpWithFullMemory | MiniDumpWithThreadInfo | MiniDumpWithCodeSegs

    性能调优,检查内存和代码段的使用情况。

分析方式

visual studio

Visual Studio 可以直接加载 .dmp 文件。打开 Visual Studio 后,选择 “文件” > “打开” > “文件…”,选择 .dmp 文件即可查看堆栈信息、异常上下文和托管对象。

dotnet-dump

超级详细使用方案实践参考

dotnet-dump工具也可用于分析

安装方式: dotnet tool install --global dotnet-dump

进入交互式调试模式: dotnet-dump analyze dump文件路径

进入交互式调试模式,可以执行类似 WinDbg 的命令,比如 clrstack、dumpheap、gcr 等

其他工具还有:

  • 用于列出进程的 dotnet-trace
  • 用于检查托管内存使用情况的 dotnet-counters
  • 用于收集和分析转储文件的 dotnet-dump

死锁排查

参考链接

CPU占用率排查

参考链接

CSharp 拓展方法

扩展方法自C# 3.0引入以来,就一直是C#语言及其运行环境(包括所有.NET版本)的一个重要特性

它允许你向现有的类型添加新方法,而无需修改这些类型的定义或创建子类。这对于增强不可修改的类(例如在.NET库中的类)或者避免使用继承来扩展类的功能是非常有用的

使用场景

  • 不修改源码的情况下扩展功能:无须更改类的基础结构或继承,直接扩展功能。
  • 提供与实例方法类似的调用语法:在实际使用中,扩展方法看起来就像是实例方法。
  • LINQ实现的基础:LINQ的查询操作符(如SelectWhere等)都是通过扩展方法实现的。

实现扩展方法的关键点

  1. 静态方法所在的静态类
    扩展方法必须定义在一个静态类中。
  2. 第一个参数使用this关键字
    扩展方法的第一个参数必须用this关键字来修饰,且紧跟着要扩展的类型。这样可以让编译器知道该方法是扩展方法。
  3. 名称空间的导入
    如果你要使用某个扩展方法,必须在适用代码的文件中使用using指令来导入包含该扩展方法的命名空间。

案例

下面是一个简单的扩展方法示例,它为string类型添加了一个方法来计算字符串内的单词数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using System;

namespace ExtensionMethodsDemo
{
public static class StringExtensions
{
// 用 this 关键字在第一个参数前来定义为 string 的扩展方法
public static int WordCount(this string str)
{
if (string.IsNullOrEmpty(str))
return 0;

string[] words = str.Split(new[] { ' ', '\t', '\n' }, StringSplitOptions.RemoveEmptyEntries);
return words.Length;
}
}

class Program
{
static void Main()
{
string sentence = "Hello, this is a simple sentence.";

// 通过扩展方法,像调用实例方法一样使用 WordCount
int count = sentence.WordCount();
Console.WriteLine($"Word count: {count}");
}
}
}

工作原理

当你调用sentence.WordCount()时,编译器实际上是在字符串对象sentence上调用StringExtensions.WordCount(sentence),把sentence传递给扩展方法的第一个参数str

注意:

  • 扩展方法不可以覆盖已有的实例方法。
  • 过度使用扩展方法可能导致代码可读性下降,因此扩展方法应该用于增强已有类的功能而非替代更具可读性、明确的设计模式

CSharp中的一些通用接口

比较器接口

IEqualityComparer

IEqualityComparer<T>是 C# 中的一个接口,定义了比较两个对象是否相等的功能。它位于 System.Collections.Generic 命名空间中,通常用于集合(如 HashSet<T>Dictionary<TKey, TValue> 等)中,以便自定义对象的相等性比较行为

IEqualityComparer<T> 接口包含两个主要方法:

  1. **Equals(T x, T y)**:比较两个对象是否compa相等。
  2. **GetHashCode(T obj)**:返回对象的哈希代码,通常用于哈希表中。

当需要在集合中使用自定义对象,并希望定义自定义的相等性比较逻辑时,可以实现 IEqualityComparer<T> 接口

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Collections.Generic;

public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

public class PersonComparer : IEqualityComparer<Person>
{
public bool Equals(Person x, Person y)
{
// 比较两个 Person 对象的 Name 和 Age 属性
if (x == null && y == null) return true;
if (x == null || y == null) return false;
return x.Name == y.Name && x.Age == y.Age;
}

public int GetHashCode(Person obj)
{
// 计算 Person 对象的哈希代码
if (obj == null) return 0;
int hashName = obj.Name == null ? 0 : obj.Name.GetHashCode();
int hashAge = obj.Age.GetHashCode();
return hashName ^ hashAge; // 使用异或运算组合哈希代码
}
}

class Program
{
static void Main()
{
var person1 = new Person { Name = "Alice", Age = 30 };
var person2 = new Person { Name = "Alice", Age = 30 };
var person3 = new Person { Name = "Bob", Age = 25 };

var comparer = new PersonComparer();

Console.WriteLine(comparer.Equals(person1, person2)); // 输出: True
Console.WriteLine(comparer.Equals(person1, person3)); // 输出: False

HashSet<Person> people = new HashSet<Person>(comparer);
people.Add(person1);
Console.WriteLine(people.Contains(person2)); // 输出: True,因为 person2 与 person1 相等
}
}

CSharp LINQ

LINQ: 语言集成查询(Language Integrated Query),发音”link”,是微软的一项技术,新增一种自然查询的SQL语法到C#的编程语言中,当前可支持C#以及Visual Basic .NET语言.2007年11月18日随.NET Framework 3.5发布了LINQ技术,其中主要包括

简单的理解为在C#中添加了一种类似SQL的语法,可以方便开发者进行数据的查询操作

• LINQ to Objects(数组、集合)
• LINQ to SQL
• LINQ to Datasets
• LINQ to Entities
• LINQ to Data Source
• LINQ to XML/XSD 参考

表现形式主要有以下两种

  • 查询表达式
  • 链式编程

查询表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
int[] nums={1,2,3,4,5,6,7,8,9,10};
//查询表达式,查询数组中大于4的奇数并降序排列
var query = from num in nums
where num> 4 && num%2==1
orderby num descending
select num;
//遍历结果集(上面的查询语句实际上并没有查询,到这里遍历的时候才真正执行了查询操作)
foreach(var item in query)
{
Console.Write(item+",");
};
Console.WriteLine();

//交集查询
int[] nums={1,2,3,4,5,6,7,8,9,10};
int[] datas={5,6,7,8,9,10,11,12,13,14};
var query2 = from num in nums
where datas.Contains(num)
select num;

//按照字符长度分组排序
string[] languages = { "Java", "XML", "C#", "PHP", "HTML", "JavaScript", "C", "C++" };
var query3 = from language in languages
group language by language.Length into g
orderby g.Key
select g;
foreach(var item in query3)
{
Console.WriteLine($"长度为{item.Key}的语言有:");
foreach(var lan in item)
{
Console.Write(lan+",");
};
Console.WriteLine();
};
/*
长度为1的语言有:
C,
长度为2的语言有:
C#,
长度为3的语言有:
XML,PHP,C++,
长度为4的语言有:
Java,HTML,
长度为10的语言有:
JavaScript,
*/

注意:遍历结果集的时候才真正执行查询

链式编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//链式编程进行查询
var chain = nums.Where(num=>num>4 && num%2==1).OrderByDescending(num=>num).Select(num=>num);
//遍历结果集(上面的查询语句实际上并没有查询,到这里遍历的时候才真正执行了查询操作)
foreach(var item in chain)
{
Console.Write(item+",");
};
Console.WriteLine();

//交集查询
int[] nums={1,2,3,4,5,6,7,8,9,10};
int[] datas={5,6,7,8,9,10,11,12,13,14};
var chain2 = nums.Intersect(datas).Select(num=>num);

//按照字符长度分组排序
string[] languages = { "Java", "XML", "C#", "PHP", "HTML", "JavaScript", "C", "C++" };
var chain3 = languages.
foreach(var item in chain3)
{
Console.WriteLine($"长度为{item.Key}的语言有:");
foreach(var lan in item)
{
Console.Write(lan+",");
};
Console.WriteLine();
};

//按照字符长度分组排序
string[] languages = { "Java", "XML", "C#", "PHP", "HTML", "JavaScript", "C", "C++" };
var chain3 = languages.GroupBy(language=>language.Length).OrderBy(g=>g.Key).Select(g=>g);
foreach (var item in chain3)
{
Console.WriteLine($"长度为{item.Key}的语言有:");
foreach (var lan in item)
{
Console.Write(lan + ",");
};
Console.WriteLine();
};
/*
长度为1的语言有:
C,
长度为2的语言有:
C#,
长度为3的语言有:
XML,PHP,C++,
长度为4的语言有:
Java,HTML,
长度为10的语言有:
JavaScript,
*/

LINQ查询结果

LINQ查询结果(如IEnumerable<T>IQueryable<T>等)也实现了IEnumerable接口,可以通过foreach循环对查询结果进行遍历。

提供的高阶函数盘点

  • Select

    对集合中的每个元素应用一个函数,生成由结果组成的集合。

    1
    var squaredNumbers = numbers.Select(x => x * x);
  • Where

    过滤集合中的元素,只保留符合条件的元素

    1
    var evenNumbers = numbers.Where(x => x % 2 == 0);
  • Aggregate

    通过累积函数将集合元素规约为单一结果

    1
    int sum = numbers.Aggregate((total, next) => total + next);
  • Any

    检查集合中是否有任何元素满足给定条件

    1
    bool hasEven = numbers.Any(x => x % 2 == 0);
  • All

    检查集合中的所有元素是否都满足给定条件

    1
    bool allEven = numbers.All(x => x % 2 == 0);
  • FirstOrDefault

    查找集合中第一个满足条件的元素,如果不存在则返回类型的默认值(int则返回0,string则返回null)。

    1
    int firstEven = numbers.FirstOrDefault(x => x % 2 == 0);

CSharp 断言

  1. Debug.Assert:这是在调试模式下使用的断言。它会在条件为假时引发异常,并且默认情况下在发布模式下会被忽略。
1
Debug.Assert(condition, message);
  1. Contract.Assert:这是在生产环境中使用的断言,可以在调试和发布模式下都生效。需要引用System.Diagnostics.Contracts命名空间。
1
Contract.Assert(condition, message);

在使用断言时,您可以指定一个条件和一个可选的消息。如果条件为假,断言会引发异常并显示消息。断言是一种在开发过程中帮助验证代码逻辑的有用工具,但应当注意,在生产环境中不要过度依赖断言。

CSharp 网络通信

[[网络编程#CSharp网络通信|CSharp网络通信参考此处]]

CSharp 计时

Stopwatch

Stopwatch是C#中用于精确计时的核心工具,适用于性能调优、代码段耗时分析等场景。使用时需注意其非线程安全特性和环境稳定性,结合多次测量以提高结果可靠性

基本用法

命名空间: System.Diagnostics

计时操作盘点如下:

  • 开始计时: Start() 启动计时,支持多次调用以累积时间(需先停止计时)
  • 停止计时: Stop() 暂停计时,可通过再次Start()继续累积
  • 重启计时: Restart() 方法等同于Stop() + Reset() + Start(),清空历史时间后重新开始
  • 重置计时器: Reset() 方法清空累积时间并停止计时

获取时间结果操作盘点如下:

  • Elapsed:返回TimeSpan对象,表示总时间(如00:00:02.1234567)。
  • ElapsedMilliseconds:返回总时间的毫秒数(如2123)。
  • ElapsedTicks:返回计时器刻度数,需结合Frequency属性转换(高精度场景使用)

状态检查操作: IsRunning:布尔属性,判断计时器是否正在运行

简化初始化:使用Stopwatch.StartNew()直接创建并启动实例

最高效的Stopwatch使用方式

1
2
3
4
var start = Stopwatch.GetTimestamp();
//需要计算的执行过程....
var elapsed = Stopwatch.GetElapsedTime(start);
//此时elapsed就是计算出来的执行过程的时间

CSharp 转换器

Converter

在C#中,Convert类是一个提供了各种数据类型之间转换的静态方法的类。Convert类提供了一系列用于将一种数据类型转换为另一种数据类型的方法,包括基本数据类型、日期时间类型、字符串等。以下是Convert类的一些常用方法:

  1. ToBoolean:将其他数据类型转换为布尔类型。

    1
    2
    string str = "true";
    bool boolValue = Convert.ToBoolean(str);
  2. ToInt32:将其他数据类型转换为32位有符号整数。

    1
    2
    string str = "42";
    int intValue = Convert.ToInt32(str);
  3. ToDouble:将其他数据类型转换为双精度浮点数。

    1
    2
    string str = "3.14";
    double doubleValue = Convert.ToDouble(str);
  4. ToString:将其他数据类型转换为字符串。

    1
    2
    int intValue = 42;
    string str = Convert.ToString(intValue);
  5. ToDateTime:将其他数据类型转换为日期时间类型。

    1
    2
    string str = "2022-01-01";
    DateTime dateTimeValue = Convert.ToDateTime(str);

Convert类提供了一种方便的方式来进行数据类型之间的转换,避免了手动编写转换逻辑的繁琐。通过使用Convert类,您可以快速、安全地将不同数据类型之间进行转换,适用于各种数据处理和转换的场景。

BitConverter

BitConverter是C#中用于基本数据类型与字节数组之间相互转换的工具类。它提供了一系列静态方法,可以将各种基本数据类型(如整数、浮点数、布尔值等)转换为字节数组,或将字节数组转换为对应的基本数据类型。

  1. GetBytes:将各种基本数据类型转换为字节数组。

    1
    2
    int intValue = 42;
    byte[] byteArray = BitConverter.GetBytes(intValue);
  2. ToInt32:将4个字节的字节数组转换为32位有符号整数。

    1
    2
    byte[] byteArray = { 0x2A, 0x00, 0x00, 0x00 };
    int intValue = BitConverter.ToInt32(byteArray, 0);
  3. ToSingle:将4个字节的字节数组转换为单精度浮点数。

    1
    2
    byte[] byteArray = { 0x40, 0x48, 0xF5, 0xC3 };
    float floatValue = BitConverter.ToSingle(byteArray, 0);
  4. GetBoolean / GetChar / GetDouble / GetUInt16 / GetUInt32 等:用于其他基本数据类型的转换。

BitConverter类提供了方便的方法来进行基本数据类型与字节数组之间的转换,适用于处理二进制数据、网络通信、文件操作等场景。

Encoding

Encoding类是C#中用于字符编码和解码的类,位于System.Text命名空间中。它提供了一系列静态方法和属性,用于将文本数据转换为字节数组(编码)或将字节数组转换为文本数据(解码),支持多种字符编码方式,如UTF-8、UTF-16、ASCII等。

以下是Encoding类的一些常用方法和属性:

  1. GetBytes:将字符串转换为字节数组,使用指定的编码方式。

    1
    2
    string str = "Hello, World!";
    byte[] byteArray = Encoding.UTF8.GetBytes(str);
  2. GetString:将字节数组转换为字符串,使用指定的编码方式。

    1
    2
    byte[] byteArray = { 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33 };
    string str = Encoding.UTF8.GetString(byteArray);
  3. UTF8 / UTF16 / ASCII:表示常用的编码方式,如UTF-8编码、UTF-16编码、ASCII编码等。

  4. GetEncoding:根据编码名称获取对应的Encoding对象。

    1
    Encoding encoding = Encoding.GetEncoding("GBK");

Encoding类提供了丰富的功能和灵活的接口,可以帮助您在C#应用程序中处理不同编码方式的文本数据。通过使用Encoding类,您可以进行字符编码和解码操作,确保数据在不同系统和环境中的正确传输和处理。

CSharp Json处理

此处记录一个vs很实用的工具,在编辑-选择性粘贴-将JSON粘贴为类
可以将Json字符串数据转换为类结构

C# 中,处理 JSON 数据通常使用 System.Text.JsonNewtonsoft.Json(也称为 Json.NET)库。这两个库都非常流行且功能强大。以下是这两个库的详细介绍和使用示例。

二者差异

性能

  • System.Text.Json 在性能上通常更快,尤其是在处理大量数据时。这是因为它是为 .NET Core 设计的,采用了更高效的内存管理策略。
  • Newtonsoft.Json 在某些情况下可能会稍慢,但它的灵活性和功能丰富性弥补了这一点。

功能

  • Newtonsoft.Json 提供了更多的功能,例如支持 LINQ to JSON、动态对象、复杂的自定义序列化和反序列化等。
  • System.Text.Json 的功能相对较少,但在 .NET 5 及更高版本中,微软正在逐步添加更多功能。

配置选项

  • Newtonsoft.Json 提供了丰富的配置选项,可以通过 JsonSerializerSettings 自定义序列化和反序列化的行为。
  • System.Text.Json 的配置选项相对较少,但也在不断改进。

兼容性

  • Newtonsoft.Json 可以在 .NET Framework 和 .NET Core 中使用,适用范围更广。
  • System.Text.Json 是 .NET Core 3.0 及以上版本的内置库,适用于现代 .NET 应用程序。

System.Text.Json

System.Text.Json 是 .NET Core 3.0 和更高版本中内置的 JSON 处理库,性能较高且使用简单。

序列化(对象转 JSON)

将 C# 对象转换为 JSON 字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Text.Json;

public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}

class Program
{
static void Main()
{
Person person = new Person { Name = "张三", Age = 30 };
string jsonString = JsonSerializer.Serialize(person);
Console.WriteLine(jsonString); // 输出: {"Name":"张三","Age":30}
}
}

反序列化(JSON 转对象)

将 JSON 字符串转换为 C# 对象:

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Text.Json;

class Program
{
static void Main()
{
string jsonString = "{\"Name\":\"张三\",\"Age\":30}";
Person person = JsonSerializer.Deserialize<Person>(jsonString);
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); // 输出: Name: 张三, Age: 30
}
}

处理复杂类型

对于复杂类型(如嵌套对象、集合等),System.Text.Json 也支持:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Company
{
public string Name { get; set; }
public List<Person> Employees { get; set; }
}

class Program
{
static void Main()
{
Company company = new Company
{
Name = "ABC公司",
Employees = new List<Person>
{
new Person { Name = "张三", Age = 30 },
new Person { Name = "李四", Age = 25 }
}
};

string jsonString = JsonSerializer.Serialize(company);
Console.WriteLine(jsonString);
}
}

更复杂的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
public class DeviceConfigs
{
public ObservableDictionary<string, DeviceConfig> model { get; set; } = new();
}

public class DeviceConfig
{
[JsonConverter(typeof(PortConverter))]
public DevicePort port { get; set; }
}

public abstract class DevicePort
{
public abstract string Type { get; }
}

public class SwitchPort : DevicePort
{
public override string Type => "SwitchMatch";
public string firstCtl { get; set; } = "";
public string secondCtl { get; set; } = "";
public string firstFbk { get; set; } = "";
public string secondFbk { get; set; } = "";
}

public class AnalogMonitorPort : DevicePort
{
public override string Type => "AnalogMonitorMatch";
public string port { get; set; } = "";
}

public class ControllerPort : DevicePort
{
public override string Type => "ControllerMatch";
public string controlSet { get; set; } = "";
public string controlFdbk { get; set; } = "";
}

public class DigitalPort : DevicePort
{
public override string Type => "DigitalMatch";
public string port { get; set; } = "";
}

public class PowerPort : DevicePort
{
public override string Type => "PowerMatch";
public string openSet { get; set; } = "";
public string openFdbk { get; set; } = "";
public string closeSet { get; set; } = "";
public string closeFdbk { get; set; } = "";
public string enableSet { get; set; } = "";
public string enableFdbk { get; set; } = "";
public string alarmFdbk { get; set; } = "";
public string controlSet { get; set; } = "";
public string controlFdbk { get; set; } = "";
public string loadSet { get; set; } = "";
public string loadFdbk { get; set; } = "";
}

public class TurboPumpPort : DevicePort
{
public override string Type => "TurboPumpMatch";
public string openSet { get; set; } = "";
public string normal { get; set; } = "";
public string alarm { get; set; } = "";
}

public class FaradayCupPort : DevicePort
{
public override string Type => "FaradayCupMatch";
public string range1 { get; set; } = "";
public string range2 { get; set; } = "";
public string range3 { get; set; } = "";
public string current { get; set; } = "";
}

// 定义一个自定义 JSON 转换器,用于处理 DevicePort 及其子类的序列化和反序列化
public class PortConverter : JsonConverter<DevicePort>
{
// 反序列化方法:将 JSON 数据转换为具体的 DevicePort 子类对象
public override DevicePort? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
// 使用 JsonDocument 解析 JSON 数据,using 确保及时释放资源
using var jsonDoc = JsonDocument.ParseValue(ref reader);

// 获取 JSON 根元素以便访问属性
var root = jsonDoc.RootElement;

// 从 JSON 中获取必须的 Type 属性,用于确定具体子类类型
var type = root.GetProperty("Type").GetString();

// 根据 Type 字段值选择对应的子类进行反序列化
return type switch
{
// 当 Type 为 "SwitchMatch" 时,反序列化为 SwitchPort 对象
"SwitchMatch" => JsonSerializer.Deserialize<SwitchPort>(root.GetRawText()),

// 当 Type 为 "AnalogMonitorMatch" 时,反序列化为 AnalogMonitorPort 对象
"AnalogMonitorMatch" => JsonSerializer.Deserialize<AnalogMonitorPort>(root.GetRawText()),

"ControllerMatch" => JsonSerializer.Deserialize<ControllerPort>(root.GetRawText()),

"DigitalMatch" => JsonSerializer.Deserialize<DigitalPort>(root.GetRawText()),

"PowerMatch" => JsonSerializer.Deserialize<PowerPort>(root.GetRawText()),

"TurboPumpMatch" => JsonSerializer.Deserialize<TurboPumpPort>(root.GetRawText()),

"FaradayCupMatch" => JsonSerializer.Deserialize<FaradayCupPort>(root.GetRawText()),

// 在此处可以继续添加其他设备端口类型的处理逻辑...
// 示例:
// "DigitalInputMatch" => JsonSerializer.Deserialize<DigitalInputPort>(...),

// 遇到未知类型时抛出异常,防止无效数据
_ => throw new JsonException($"未知的设备端口类型: {type}")
};
}

// 序列化方法:将 DevicePort 及其子类对象写入 JSON
public override void Write(Utf8JsonWriter writer, DevicePort value, JsonSerializerOptions options)
{
// 将对象转换为基类 object 类型进行序列化,以支持多态序列化
// 这会触发系统默认的多态序列化处理,保留子类特有的属性
JsonSerializer.Serialize(writer, (object)value, options);
}
}

//序列化:
DeviceConfigs deviceConfigs = new();
deviceConfigs.model.Add("device1", new DeviceConfig
{
port = new SwitchPort
{
firstCtl = "xxxx.yyyy.zzz",
secondCtl = "",
firstFbk = "",
secondFbk = ""
}
});
deviceConfigs.model.Add("device2", new DeviceConfig
{
port = new AnalogMonitorPort
{
port = ""
}
});
deviceConfigs.model.Add("device3", new DeviceConfig
{
port = new PowerPort
{
openSet = "",
openFdbk = "",
closeSet = "",
closeFdbk = "",
enableSet = "",
enableFdbk = "",
alarmFdbk = "",
controlSet = "",
controlFdbk = "",
loadSet = "",
loadFdbk = ""
}
});

string jsonString2 = JsonSerializer.Serialize(deviceConfigs);

中文json序列化

JSON序列化时默认将Unicode字符转义了,要让JSON文件中正确显示中文,我们需要修改JsonSerializerOptions的设置,如下:

原本JSON中显示中文为:\u805A\u7126\u676F

1
2
3
4
5
6
private readonly JsonSerializerOptions _options = new()
{
WriteIndented = false,
Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
JsonSerializer.Serialize(obj, _options);

修改后JSON中的中文为:聚焦杯

Newtonsoft.Json默认就支持正确显示中文

Newtonsoft.Json

Newtonsoft.Json 是一个非常流行的第三方库,功能强大,支持更多的功能和灵活性。

通过 NuGet 包管理器安装 Newtonsoft.Json

序列化(对象转 JSON)

1
2
3
4
5
6
7
8
9
10
11
using Newtonsoft.Json;

class Program
{
static void Main()
{
Person person = new Person { Name = "张三", Age = 30 };
string jsonString = JsonConvert.SerializeObject(person);
Console.WriteLine(jsonString); // 输出: {"Name":"张三","Age":30}
}
}

反序列化(JSON 转对象)

1
2
3
4
5
6
7
8
9
10
11
using Newtonsoft.Json;

class Program
{
static void Main()
{
string jsonString = "{\"Name\":\"张三\",\"Age\":30}";
Person person = JsonConvert.DeserializeObject<Person>(jsonString);
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}"); // 输出: Name: 张三, Age: 30
}
}

处理复杂类型

System.Text.Json 类似,Newtonsoft.Json 也支持复杂类型的序列化和反序列化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Company
{
public string Name { get; set; }
public List<Person> Employees { get; set; }
}

class Program
{
static void Main()
{
Company company = new Company
{
Name = "ABC公司",
Employees = new List<Person>
{
new Person { Name = "张三", Age = 30 },
new Person { Name = "李四", Age = 25 }
}
};

string jsonString = JsonConvert.SerializeObject(company);
Console.WriteLine(jsonString);
}
}

设置JsonSerializerSettings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using Newtonsoft.Json;
using System;
using System.Collections.Generic;

public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime BirthDate { get; set; }

// 用于演示的属性,设置为忽略
[JsonIgnore]
public string Secret { get; set; }
}

class Program
{
static void Main()
{
var person = new Person
{
Name = "张三",
Age = 30,
BirthDate = new DateTime(1993, 1, 1),
Secret = "这是一个秘密"
};

// 创建 JsonSerializerSettings 实例
var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented, // 设置缩进格式
DateFormatString = "yyyy-MM-dd", // 设置日期格式
NullValueHandling = NullValueHandling.Ignore, // 忽略 null 值
DefaultValueHandling = DefaultValueHandling.Ignore // 忽略默认值
};

// 序列化
string jsonString = JsonConvert.SerializeObject(person, settings);
Console.WriteLine(jsonString);

// 反序列化
var deserializedPerson = JsonConvert.DeserializeObject<Person>(jsonString);
Console.WriteLine($"Name: {deserializedPerson.Name}, Age: {deserializedPerson.Age}, BirthDate: {deserializedPerson.BirthDate.ToString("yyyy-MM-dd")}");
}
}
  1. Formatting:

    Formatting.Indented 设置 JSON 字符串的格式为缩进格式,更易于阅读。

  2. DateFormatString:

    DateFormatString 用于指定日期的格式。在这个例子中,日期格式设置为 "yyyy-MM-dd",这样序列化后的日期将以这种格式输出。

  3. NullValueHandling:

    NullValueHandling.Ignore 表示在序列化时忽略值为 null 的属性。如果 Secret 属性为 null,它将不会出现在序列化后的 JSON 中。

  4. DefaultValueHandling:

    DefaultValueHandling.Ignore 表示在序列化时忽略默认值的属性(如整数的默认值为 0,布尔值的默认值为 false)。

  5. JsonIgnore:

    JsonIgnore 特性用于标记在序列化和反序列化过程中应被忽略的属性。在这个例子中,Secret 属性不会出现在序列化的 JSON 中。

LINQ to JSON

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;

class Program
{
static void Main()
{
// 示例 JSON 字符串
string jsonString = @"
{
'Name': '张三',
'Age': 30,
'BirthDate': '1993-01-01',
'Hobbies': ['阅读', '旅行', '游泳'],
'Address': {
'City': '北京',
'ZipCode': '100000'
}
}";

// 解析 JSON 字符串为 JObject
JObject person = JObject.Parse(jsonString);

// 查询属性
string name = (string)person["Name"];
int age = (int)person["Age"];
Console.WriteLine($"Name: {name}, Age: {age}");

// 查询数组
JArray hobbies = (JArray)person["Hobbies"];
Console.WriteLine("Hobbies:");
foreach (var hobby in hobbies)
{
Console.WriteLine($"- {hobby}");
}

// 查询嵌套对象
JObject address = (JObject)person["Address"];
string city = (string)address["City"];
Console.WriteLine($"City: {city}");

// 修改属性
person["Age"] = 31; // 更新年龄
person["Hobbies"].Add("跑步"); // 添加新的爱好

// 输出修改后的 JSON
Console.WriteLine("修改后的 JSON:");
Console.WriteLine(person.ToString(Formatting.Indented)); // 格式化输出
}
}

属性盘点

Newtonsoft.Json(也称为 Json.NET)是 .NET 生态中最常用的 JSON 处理库之一。它提供了一系列属性(Attributes)来控制序列化和反序列化的行为。以下是 Newtonsoft.Json 中常用的属性:

JsonProperty

用于修改JSON序列化和反序列化时的属性名称,默认值等

1
2
3
4
5
6
7
8
9
public class Person
{
[JsonProperty("full_name")]
public string Name { get; set; }

//这个选项表示如果属性的值是其默认值,则在序列化时会忽略该属性
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public int Age { get; set; }
}

作用:

  • 设定JSON对应的键名(如full_name)
  • 控制默认值的处理(DefaultValueHandling)

JsonIgnore

用于忽略某个属性,使其不被序列化或反序列化

1
2
3
4
5
6
7
8
public class User
{
public string Username { get; set; }

//Password 不会出现在序列化后的 JSON 中
[JsonIgnore]
public string Password { get; set; }
}

JsonConverter

指定某个属性的自定义 JSON 转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Product
{
public string Name { get; set; }

//允许枚举值序列化为字符串,而不是默认的整数
[JsonConverter(typeof(StringEnumConverter))]
public ProductType Type { get; set; }
}

public enum ProductType
{
Physical,
Digital
}

JsonObject

用于控制类的序列化方式

如: OptIn 模式下,只有标记 [JsonProperty] 的属性才会被序列化。

1
2
3
4
5
6
7
8
[JsonObject(MemberSerialization.OptIn)]
public class Employee
{
[JsonProperty]
public string Name { get; set; }

public int Salary { get; set; } // 该字段不会被序列化
}

其他

  • JsonArray,用于控制数组或集合的序列化行为,如指定数组的元素是否为必选项
  • JsonDictionary,用于自定义字典类型的JSON序列化行为,如允许自定义字典类型的序列化方式
  • JsonExtensionData,用于接收额外的未映射 JSON 数据,如将JSON中不匹配的额外数据存入ExtraData字典
  • JsonRequired,用于强制要求某个属性在JSON数据中存在,否则弹出异常,如:如果MustHave在JSON中缺失,则反序列化时会抛出异常
  • JsonConstructor,用于指定某个构造函数供JSON反序列化时使用,如:指定反序列化时调用的构造函数

JsonSerializerSettings详解

JsonSerializerSettings 是 Newtonsoft.Json(也称为 Json.NET)中的一个类,用于配置 JSON 序列化和反序列化的行为。你可以通过该类来自定义 JSON 格式、控制属性的序列化方式、处理循环引用等。

属性名 类型 作用 可选值
Formatting Formatting 控制 JSON 是否美化输出 None(默认), Indented
NullValueHandling NullValueHandling 是否忽略 null Include(默认), Ignore
DefaultValueHandling DefaultValueHandling 是否忽略默认值 Include(默认), Ignore, Populate, IgnoreAndPopulate
MissingMemberHandling MissingMemberHandling 反序列化时是否抛出异常 Ignore(默认), Error
ReferenceLoopHandling ReferenceLoopHandling 处理循环引用 Error(默认), Ignore, Serialize
TypeNameHandling TypeNameHandling 是否包含类型信息 None(默认), Objects, Arrays, All, Auto
ContractResolver IContractResolver 自定义序列化规则 CamelCasePropertyNamesContractResolver
Converters List<JsonConverter> 自定义 JSON 转换器 new StringEnumConverter()
DateFormatHandling DateFormatHandling 日期格式化方式 IsoDateFormat(默认), MicrosoftDateFormat
DateParseHandling DateParseHandling 解析 JSON 日期字符串方式 None, DateTime(默认), DateTimeOffset
DateTimeZoneHandling DateTimeZoneHandling 处理 DateTime 的时区 Local, Utc, Unspecified, RoundtripKind
DateFormatString string 自定义日期格式 "yyyy-MM-dd HH:mm:ss"
FloatFormatHandling FloatFormatHandling 浮点数格式化方式 String, Symbol(默认), DefaultValue
FloatParseHandling FloatParseHandling 解析浮点数的方式 Double(默认), Decimal
PreserveReferencesHandling PreserveReferencesHandling 处理对象引用 None(默认), Objects, Arrays, All
ObjectCreationHandling ObjectCreationHandling 反序列化时对象创建方式 Auto(默认), Reuse, Replace
MetadataPropertyHandling MetadataPropertyHandling 是否解析 $id$ref Default(默认), ReadAhead, Ignore
ConstructorHandling ConstructorHandling 反序列化时使用的构造函数 Default(默认), AllowNonPublicDefaultConstructor
TypeNameAssemblyFormatHandling TypeNameAssemblyFormatHandling 控制类型信息的程序集格式 Simple(默认), Full
Error EventHandler<ErrorEventArgs> 处理序列化/反序列化错误的事件 new EventHandler<ErrorEventArgs>(...)
CheckAdditionalContent bool 反序列化时检查 JSON 末尾是否有多余内容 false(默认), true
MaxDepth int? 限制 JSON 解析的最大嵌套深度 null(默认),指定整数值

ContractResolver合约解析器

ContractResolver 允许你控制属性的命名规则、序列化行为等,通常用于:

  • 修改属性名称(如 CamelCase 命名)
  • 忽略特定属性
  • 修改访问级别
  • 自定义序列化逻辑
1
2
3
4
var settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
};
  • CamelCasePropertyNamesContractResolver: 首字母大写改成首字母小写

  • DefaultContractResolver: 默认行为,什么也不修改

  • CustomContractResolver: 可以通过 继承 DefaultContractResolver 来自定义 JSON 规则。例如,忽略特定属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    using Newtonsoft.Json;
    using Newtonsoft.Json.Serialization;
    using System;
    using System.Collections.Generic;
    using System.Linq;

    /// <summary>
    /// 自定义的 JSON 合约解析器,用于忽略指定的属性,使其在 JSON 序列化时不被包含。
    /// </summary>
    public class IgnorePropertyResolver : DefaultContractResolver
    {
    /// <summary>
    /// 需要被忽略的属性名称集合。
    /// </summary>
    private readonly HashSet<string> _propertiesToIgnore;

    /// <summary>
    /// 初始化 IgnorePropertyResolver,并指定要忽略的属性名称。
    /// </summary>
    /// <param name="propertiesToIgnore">要在 JSON 序列化时忽略的属性名称列表。</param>
    public IgnorePropertyResolver(IEnumerable<string> propertiesToIgnore)
    {
    _propertiesToIgnore = new HashSet<string>(propertiesToIgnore);
    }

    /// <summary>
    /// 重写 CreateProperties 方法,过滤掉指定忽略的属性。
    /// </summary>
    /// <param name="type">当前正在序列化的对象类型。</param>
    /// <param name="memberSerialization">成员序列化模式(OptIn / OptOut)。</param>
    /// <returns>经过筛选后的 JSON 属性列表。</returns>
    protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
    {
    // 获取默认的所有可序列化属性
    var properties = base.CreateProperties(type, memberSerialization);

    // 过滤掉需要忽略的属性,并返回最终的属性列表
    return properties.Where(p => !_propertiesToIgnore.Contains(p.PropertyName)).ToList();
    }
    }

对比

功能 ContractResolver Converters
修改属性名 ✅(如 CamelCase)
忽略/修改属性 ✅(可忽略特定属性)
处理 DateTime/Enum ✅(如 IsoDateTimeConverter)
处理特定类型 ✅(如 StringEnumConverter)
序列化/反序列化逻辑
案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/// <summary>
/// 自定义解析器
/// </summary>
/// <typeparam name="TAttribute"></typeparam>
public class AttributeContractResolver<TAttribute> : DefaultContractResolver
where TAttribute : Attribute
{
protected override IList<JsonProperty> CreateProperties(
Type type,
MemberSerialization memberSerialization)
{
// 获取默认所有属性
var properties = base.CreateProperties(type, memberSerialization);

// 过滤:只保留有指定特性的属性
return properties.Where(p =>
p.AttributeProvider.GetAttributes(typeof(TAttribute), true).Any()
).ToList();
}
}

public class TestToJson
{
[Attribute1]
public string Name { get; set; }
[Attribute2]
public int Age { get; set; }

}

var settings2 = new JsonSerializerSettings
{
ContractResolver = new AttributeContractResolver<Attribute1>(), // 使用DbField过滤
};

var jsonOutput = JsonConvert.SerializeObject(new TestToJson()
{
Name = "张三",
Age = 18
}, settings2);

Converters转换器

Converters 允许你自定义 JSON 数据的转换方式,可用于:

  • 格式化特定类型数据(如 DateTime、Enum)
  • 加密/解密特定字段
  • 对象与 JSON 之间的自定义转换
使用内置转换器
  • StringEnumConverter: 枚举转字符串(默认情况下是按照整数显示)
  • IsoDateTimeConverter: 日期格式化
1
2
3
4
var settings = new JsonSerializerSettings
{
Converters = new List<JsonConverter> { new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd" } }
};
自定义转换器

可以继承 JsonConverter<T> 来创建自己的转换器

如,日期变成 UNIX 时间戳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using Newtonsoft.Json;
using System;

/// <summary>
/// 自定义 JSON 转换器,将 DateTime 转换为 Unix 时间戳(秒)。
/// </summary>
public class UnixDateTimeConverter : JsonConverter<DateTime>
{
/// <summary>
/// 将 DateTime 对象转换为 Unix 时间戳(秒)并写入 JSON。
/// </summary>
/// <param name="writer">JSON 写入器。</param>
/// <param name="value">要序列化的 DateTime 值。</param>
/// <param name="serializer">JSON 序列化器(未使用)。</param>
public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer)
{
// 将 DateTime 转换为 Unix 时间戳(1970-01-01T00:00:00Z 以来的秒数)
writer.WriteValue(new DateTimeOffset(value).ToUnixTimeSeconds());
}

/// <summary>
/// 从 JSON 读取 Unix 时间戳(秒),并转换回 DateTime。
/// </summary>
/// <param name="reader">JSON 读取器。</param>
/// <param name="objectType">目标对象的类型(通常是 DateTime)。</param>
/// <param name="existingValue">现有的对象值(未使用)。</param>
/// <param name="hasExistingValue">指示是否有现有值(未使用)。</param>
/// <param name="serializer">JSON 反序列化器(未使用)。</param>
/// <returns>转换后的 DateTime 值。</returns>
public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer)
{
// 确保读取的值为 long 类型(即 Unix 时间戳),否则返回 Unix 时间戳 0(1970-01-01T00:00:00Z)
return DateTimeOffset.FromUnixTimeSeconds(reader.Value is long ? (long)reader.Value : 0).DateTime;
}
}
同一个数据结构生成两版不同的JSON字符串

可以通过 Newtonsoft.Json 为同一个数据结构生成两版不同的 JSON 字符串,每版使用不同的 JsonConverter

实现思路

  • 不依赖 [JsonConverter] 属性:确保目标类型(需要自定义转换的类型)没有通过 [JsonConverter] 属性固定绑定某个转换器。这样可以在序列化时动态选择转换器。
  • 通过 JsonSerializerSettings 动态注入转换器:在序列化时,分别为两个版本创建不同的 JsonSerializerSettings,并在其中添加对应的转换器。

具体步骤

假设需要转换的类型为 ComplexType,编写两个转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ConverterV1 : JsonConverter<ComplexType>
{
public override void WriteJson(JsonWriter writer, ComplexType value, JsonSerializer serializer)
{
// 实现版本1的序列化逻辑
}

public override ComplexType ReadJson(JsonReader reader, Type objectType, ComplexType existingValue, bool hasExistingValue, JsonSerializer serializer)
{
// 反序列化逻辑(若需要)
}

public override bool CanRead => false; // 如果不需要反序列化,可以禁用
}

public class ConverterV2 : JsonConverter<ComplexType>
{
public override void WriteJson(JsonWriter writer, ComplexType value, JsonSerializer serializer)
{
// 实现版本2的序列化逻辑
}

// 类似地,处理反序列化...
}

序列化时传入不同的转换器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var data = new MyComplexDataStructure();

// 版本1的配置
var settingsV1 = new JsonSerializerSettings
{
Converters = { new ConverterV1() },
Formatting = Formatting.Indented
};

// 版本2的配置
var settingsV2 = new JsonSerializerSettings
{
Converters = { new ConverterV2() },
Formatting = Formatting.Indented
};

// 生成两版JSON
string jsonV1 = JsonConvert.SerializeObject(data, settingsV1);
string jsonV2 = JsonConvert.SerializeObject(data, settingsV2);

如果类型已绑定 [JsonConverter] 属性

若目标类型已通过 [JsonConverter] 固定了转换器,需通过 自定义 ContractResolver 动态覆盖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IgnoreJsonConverterAttributeResolver : DefaultContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
var contract = base.CreateContract(objectType);
if (contract.Converter != null)
{
// 移除类型上的 [JsonConverter] 属性影响
contract.Converter = null;
}
return contract;
}
}

// 使用自定义Resolver
var settings = new JsonSerializerSettings
{
Converters = { new ConverterV1() },
ContractResolver = new IgnoreJsonConverterAttributeResolver()
};

总结

  • 可行:只要目标类型未固定绑定转换器,通过不同的 JsonSerializerSettings 动态注入不同的 JsonConverter 即可。
  • 不可行时的方案:若类型已通过 [JsonConverter] 指定转换器,需用 ContractResolver 移除其影响,再动态注入新转换器。

复杂案例

结构如图:

image-20250227093423523

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
public DateTime BirthDate { get; set; }

// 新增的喜爱物品属性(使用基类类型)
public Item FavoriteItem { get; set; }

[JsonIgnore]
public string Secret { get; set; }
}

// 物品基类
public abstract class Item
{
public enum ItemType
{
Clothing,
Digital,
Food
}

public ItemType Type { get; protected set; }
}

// 衣服类物品
public class ClothingItem : Item
{
public ClothingItem()
{
Type = ItemType.Clothing;
}

public string Size { get; set; } // 例如:"XL", "M"
public string Color { get; set; } // 例如:"红色", "深蓝"
}

// 数码类物品
public class DigitalItem : Item
{
public DigitalItem()
{
Type = ItemType.Digital;
}

public decimal Price { get; set; } // 价格
public bool UseBattery { get; set; } // 是否使用电池
}

// 食品类物品
public class FoodItem : Item
{
public FoodItem()
{
Type = ItemType.Food;
}

public bool NeedHeating { get; set; } // 是否需要加热
public double WeightGrams { get; set; } // 重量(克)
}

//序列化并保存代码:
var person = new Person
{
Name = "张三",
Age = 30,
BirthDate = new DateTime(1993, 1, 1),
Secret = "这是一个秘密",
FavoriteItem = new ClothingItem
{
Size = "L",
Color = "白色"
}
};
Console.WriteLine((person.FavoriteItem as ClothingItem)?.Size);

// 创建 JsonSerializerSettings 实例
var settings = new JsonSerializerSettings
{
Formatting = Newtonsoft.Json.Formatting.Indented, // 设置缩进格式
DateFormatString = "yyyy-MM-dd", // 设置日期格式
NullValueHandling = NullValueHandling.Ignore, // 忽略 null 值
DefaultValueHandling = DefaultValueHandling.Ignore // 忽略默认值
};

// 序列化
string jsonString = JsonConvert.SerializeObject(person, settings);
Console.WriteLine(jsonString);

// 保存到当前目录
string filePath = Path.Combine(Directory.GetCurrentDirectory(), "person.json");
File.WriteAllText(filePath, jsonString);

最终产出的json

1
2
3
4
5
6
7
8
9
{
"Name": "张三",
"Age": 30,
"BirthDate": "1993-01-01",
"FavoriteItem": {
"Size": "L",
"Color": "白色"
}
}

下面的复杂案例可以与System.Text.Json中的复杂案例对比看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;

public class DeviceConfigs
{
public Dictionary<string, DeviceConfig> model { get; set; } = new();
}

public class DeviceConfig
{
[JsonConverter(typeof(PortConverter))]
public DevicePort port { get; set; }
}

public abstract class DevicePort
{
public abstract string Type { get; }
}

public class SwitchPort : DevicePort
{
public override string Type => "SwitchMatch";
public string firstCtl { get; set; } = "";
public string secondCtl { get; set; } = "";
public string firstFbk { get; set; } = "";
public string secondFbk { get; set; } = "";
}

public class AnalogMonitorPort : DevicePort
{
public override string Type => "AnalogMonitorMatch";
public string port { get; set; } = "";
}

public class ControllerPort : DevicePort
{
public override string Type => "ControllerMatch";
public string controlSet { get; set; } = "";
public string controlFdbk { get; set; } = "";
}

public class DigitalPort : DevicePort
{
public override string Type => "DigitalMatch";
public string port { get; set; } = "";
}

public class PowerPort : DevicePort
{
public override string Type => "PowerMatch";
public string openSet { get; set; } = "";
public string openFdbk { get; set; } = "";
public string closeSet { get; set; } = "";
public string closeFdbk { get; set; } = "";
public string enableSet { get; set; } = "";
public string enableFdbk { get; set; } = "";
public string alarmFdbk { get; set; } = "";
public string controlSet { get; set; } = "";
public string controlFdbk { get; set; } = "";
public string loadSet { get; set; } = "";
public string loadFdbk { get; set; } = "";
}

public class TurboPumpPort : DevicePort
{
public override string Type => "TurboPumpMatch";
public string openSet { get; set; } = "";
public string normal { get; set; } = "";
public string alarm { get; set; } = "";
}

public class FaradayCupPort : DevicePort
{
public override string Type => "FaradayCupMatch";
public string range1 { get; set; } = "";
public string range2 { get; set; } = "";
public string range3 { get; set; } = "";
public string current { get; set; } = "";
}



public class PortConverter : JsonConverter<DevicePort>
{
public override bool CanWrite => false;

public override void WriteJson(JsonWriter writer, DevicePort value, JsonSerializer serializer)
{
var jo = new JObject();
// jo.Add("$type", value.GetType().Name);
foreach (var prop in value.GetType().GetProperties())
{
if (prop.CanRead)
{
jo.Add(prop.Name, JToken.FromObject(prop.GetValue(value), serializer));
}
}
jo.WriteTo(writer);
}

public override DevicePort ReadJson(JsonReader reader, Type objectType, DevicePort existingValue, bool hasExistingValue, JsonSerializer serializer)
{
JObject jo = JObject.Load(reader);
string typeName = jo["Type"]?.Value<string>();

DevicePort port = typeName switch
{
"SwitchMatch" => new SwitchPort(),
"AnalogMonitorMatch" => new AnalogMonitorPort(),
"ControllerMatch" => new ControllerPort(),
"DigitalMatch" => new DigitalPort(),
"PowerMatch" => new PowerPort(),
"TurboPumpMatch" => new TurboPumpPort(),
"FaradayCupMatch" => new FaradayCupPort(),
_ => throw new JsonSerializationException("Unknown device port type: " + typeName)
};

serializer.Populate(jo.CreateReader(), port);
return port;
}

// public override bool CanConvert(Type objectType)
// {
// return typeof(DevicePort).IsAssignableFrom(objectType);
// }
}

class Program
{
static void Main()
{
DeviceConfigs deviceConfigs = new();
deviceConfigs.model.Add("device1", new DeviceConfig
{
port = new SwitchPort
{
firstCtl = "xxxx.yyyy.zzz",
secondCtl = "",
firstFbk = "",
secondFbk = ""
}
});
deviceConfigs.model.Add("device2", new DeviceConfig
{
port = new AnalogMonitorPort
{
port = ""
}
});
deviceConfigs.model.Add("device3", new DeviceConfig
{
port = new PowerPort
{
openSet = "",
openFdbk = "",
closeSet = "",
closeFdbk = "",
enableSet = "",
enableFdbk = "",
alarmFdbk = "",
controlSet = "",
controlFdbk = "",
loadSet = "",
loadFdbk = ""
}
});

var settings = new JsonSerializerSettings
{
Formatting = Formatting.Indented,
NullValueHandling = NullValueHandling.Ignore,
TypeNameHandling = TypeNameHandling.None
};
//序列化
string json = JsonConvert.SerializeObject(deviceConfigs, settings);
Console.WriteLine(json);
//反序列化
var deserializedConfigs = JsonConvert.DeserializeObject<DeviceConfigs>(json, settings);
Console.WriteLine("\nDeserialized Configs:");
foreach (var device in deserializedConfigs.model)
{
Console.WriteLine($"Device: {device.Key}, Port Type: {device.Value.port.Type}");
}

}
}

CSharp Entity Framework

EF 和 EF Core的区别:

  • Entity Framework:是一个较早的 ORM 框架,主要用于 .NET Framework,提供了一种简单的方法来与数据库交互。它支持多种数据库,但在性能和灵活性方面有一些限制。

  • Entity Framework Core:是一个跨平台的 ORM 框架,旨在支持 .NET Core 和 .NET 5/6 及更高版本。EF Core 进行了重写,提供了更好的性能、更灵活的 API 和对新功能的支持(如异步编程)。

下面主要都是介绍EF Core

特性 Entity Framework (EF 6) Entity Framework Core (EF Core)
平台 仅支持 .NET Framework 支持 .NET Core、.NET Framework、Xamarin、UWP 等跨平台开发
轻量化 体积较大,依赖较多 轻量化、模块化设计,性能更高
跨平台支持 仅支持 Windows 支持跨平台(Windows、Linux、macOS)
性能 相对较慢,特别是批量操作性能低 性能显著提高,批量操作性能优化
支持的数据库 主要支持 SQL Server 支持 SQL Server、MySQL、PostgreSQL、SQLite、Oracle 等多种数据库
LINQ 支持 支持 LINQ,但有一些局限性 LINQ 支持更广泛,包含一些新操作符
离线开发 支持 EDMX 文件的可视化设计器 不支持 EDMX 文件,基于 Code-First 和 Fluent API 设计
迁移 支持数据迁移(Migrations) 支持数据迁移(更灵活,支持自定义脚本)
注重模块化 较为笨重,依赖于 System.Data 模块化设计,独立的包,如 Microsoft.EntityFrameworkCore.SqlServer
未来支持 停止新功能开发,仅维护 持续迭代和更新,主力框架

概述

Entity Framework (EF) Core 是轻量化、可扩展、开源和跨平台版数据库访问技术

image-20240620150200167

选择EntityFrameworkCore的原因

  • 微软的亲儿子,对接了微软的众多其他需要数据访问的框架,例如ASP.NET Identity,ASPNET WebApi
  • 全面支持[LINQ查询](#CSharp LINQ)
  • EFCore性能提升,已非常接近原生的ADO.NET框架了
  • 数据库迁移功能,可以动态创建数据库或修改数据库

优势:

  1. 能够使用 .NET 对象处理数据库(数据迁移,正式环境慎用)

    备注:不建议使用数据迁移,需要学习处理数据库字段、长度、生成值(不生成、默认、添加或更新时生成)、键、索引等一定量内容,并且需要对数据库很熟悉,既然熟悉了数据库为什么还要在EF中处理呢,专业的任务还是专业的工具去做。
    官方建议:部署到生产数据库的建议方法是生成 SQL 脚本

  2. 无需编写大部分数据访问代码(sql)

三种功能

  • 已经有数据表了,程序中没有对应的类
  • 程序中有类了,的设计了,但是没有数据库和程序中的类
  • 已经有数据模型的设计了,但是没有数据库和程序中的类

Entity Framework从4.1版本开始由NuGet发布,从5版本开始开源,开源地址

版本 发行情况 主要特性
EF 3.5 .NET 3.5 SP1 基本ORM, Database First workflow
EF 4.0 .NET 4.0 POCO, Lazy Loading, testability, custom code generation, Model First workflow
EF 4.1+ NuGet DBContext API, Code First workflow
EF 5 NuGet Open Source, Enum support, table-valued functions, spatial data types, multiple-diagrams per model, etc
EF 6+ NuGet It includes many new features related to Code First & EF designer like asynchronous query & save, etc

开发模式

三种模式

对于EF Core,使用模型执行数据访问,模型由实体类和表示数据库会话的上下文对象构成.上下文对象允许查询并保存数据,EFCore支持以下开发模式

  • DBFirst 数据库优先

    从现有数据库生成类和属性

  • ModelFirst 模型优先 (基本不使用)

  • CodeFirst 代码优先 (最常用)

    现有类和属性,从类和属性生成数据库对应的表和字段

    创建模型后,使用EF迁移从模型创建数据库.模型发生变化时,迁移可让数据库不断演进

基本框架

image-20240620150915472

EF注意事项

  • 若要在高性能生产应用中构建、调试、分析和迁移数据,必须具备基础数据库服务器的中级知识或更高级别的知识。例如,有关主键和外键、约束、索引、标准化、DML 和 DDL 语句、数据类型、分析等方面的知识。
  • 功能和集成测试:请务必尽可能严密地复制生产环境,以便:
    • 查找仅在使用特定版本的数据库服务器时应用才出现的问题。
    • 在升级 EF Core 和其他依赖项时捕获中断性变更。例如,添加或升级 ASP.NET Core、OData 或 AutoMapper等框架。这些依赖项可能以多种意外方式影响 EF Core。
  • 通过代表性负载进行性能和压力测试。某些功能的不成熟用法缩放性不佳。例如,多项集合包含内容、大量使用延迟加载、对未编制索引的列执行条件查询、对存储生成的值进行大规模更新和插入、缺乏并发处理、大型模型、缓存策略不充分。
  • 安全评审:例如,连接字符串和其他机密处理、非部署操作的数据库权限、原始 SQL 的输入验证、敏感数据加密。
  • 确保日志记录和诊断充足目可用。例如,适当的日志记录配置、查询标记和 Application Insights。
  • 错误恢复。为常见故障场景(如版本回退、回退服务器、横向扩展和负载平衡、DoS 缓解和数据备份)准备应急计划。
  • 生成的迁移的详细检查和测试。将迁移应用于生产数据前,应对其进行全面测试。若表中包含生产数据,架构的形状和列类型就不能轻易更改。例如,在 SQL Server 上,对于映射到字符串和十进制属性的列,nvarchar(max)和 decima1(18, 2)极少成为最佳类型,但这些是 EF 使用的默认值,因为 EF 不了解你的具体情况。

安装

Nuget安装Microsoft.EntityFrameworkCore

请务必安装Microsoft提供的所有EF Core包的同一版本.例如,如果安装5.0.3版本的Microsoft.EntityFrameworkCore.SqlServer,则所有其他Microsoft.EntityFrameworkCore.*包也必须为5.0.3版本

数据库提供程序

EF Core 通过使用“数据库提供程序“支持不同的数据库系统。每个系统都有自己的数据库提供程序,而提供程序以 NuGet 包的形式提供。应用程序应安装其中一个或多个提供程序包
下表列出了常贝的数据库提供程序。有关可用提供程序的更全面列表,请参阅官方网址

数据库系统 配置示例 NuGet 程序包
SQL Server 或 Azure SQL UseSqlServer(connectionString) Microsoft.EntityFrameworkCore.SqlServer
Azure Cosmos DB UseCosmos(connectionString, databaseName) Microsoft.EntityFrameworkCore.Cosmos
SQLite UseSqlite(connectionString) Microsoft.EntityFrameworkCore.Sqlite
EF Core 内存中数据库 UseInMemoryDatabase(databaseName) Microsoft.EntityFrameworkCore.InMemory
PostgreSQL UseNpgsql(connectionString) Npgsql.EntityFrameworkCore.PostgreSQL
MySQL/MariaDB UseMySql(connectionString) Pomelo.EntityFrameworkCore.MySql
Oracle* PLSQL UseOracle(connectionString) Oracle.EntityFrameworkCore

如执行:dotnet add package Microsoft.EntityFrameworkCore.Sqlite

将会自动安装EntityFrameworkCore的本体等一切依赖

如果需要支持sqlite的数据迁移,还需要安装包: dotnet add package Microsoft.EntityFrameworkCore.Design

命令行工具

不安装也可以,可以通过vs自带的程序包管理器控制台,来执行数据迁移

像rider和vscode等ide没有程序包管理器控制台就只可以通过下面工具来执行数据迁移

使用用于EF Core迁移的工具需要安装相应的工具包

1
dotnet tool install -g --version 8.0.203 dotnet-ef

版本与dotnet --version运行时的版本一致最稳妥

可以使用dotnet ef来判断是否安装成功

dotnet ef等同于dotnet-ef

可以使用dotnet ef --help来查看帮助文档

数据库上下文

Dbcontext的生命周期从创建实例时开始,并在释放实例时结束。Dbcontext 实例旨在用于单个工作单元。这意味着 Dbcontext实例的生命周期通常很短。

引用Martin Fowler 的话,“工作单元将持续跟踪在可能影响数据库的业务事务中执行的所有操作。当你完成操作后,它将找出更改数据库作为工作结果时需要执行的所有操作。”

  • 创建DbContext实例
  • 根据上下文跟踪实体实例.实体将在以下情况下被跟踪
    • 正在从查询返回
    • 正在添加或附加到上下文
  • 根据需要对所跟踪的实体已进行更改以实现业务规则
  • 调用SaveChangesSaveChangesAsync.EF Core检测所做的更改,并将这些更改写入数据库
  • 释放DbContext实例

重要的部分

  • 使用后释放 DbContext 非常重要。这可确保释放所有非托管资源,并注销任何事件或其他挂钩,以防止在实例保持引用时出现内存泄漏。

  • DbContext 不是线程安全的。不要在线程之间共享上下文。请确保在继续使用上下文实例之前,等待所有异步调用。

    最简单的解决方式就是使用await context.SaveChangesAsync();,可以避免线程安全问题

  • EF Core 代码引发的 InvalidOperationException 可以使上下文进入不可恢复的状态。此类异常指示程序错误,并且不旨在从其中恢复。

数据迁移

EF Core提供两种主要方法来保持EF Core模型和数据库架构同步.至于我们应该选用哪个方法,请确定你是希望以EF Core模型为准还是以数据库为准

  1. 如果希望以EF Core模型为准,请使用迁移.对于EF Core模型进行更改时,此方法会以增量方式将相应架构更改应用到数据库,以使数据库保持与EF Core模型兼容(CodeFirst,小项目用这种)
  2. 如果希望以数据库架构为准,请使用反向工程.使用此方法,可通过将数据库架构反向工程到EF Core模型来生成相应的Dbcontext和实体类型(DbFirst,中大型模型建议使用)

迁移

EF与EF Core的差异

对比项 EF 6 EF Core
是否需要设计包 内置于Entityframework本身❌ 需要 Microsoft.EntityFrameworkCore.Design ✅
迁移工具 Visual Studio 包管理器控制台 dotnet CLI (dotnet ef)/Visual Studio 包管理器控制台
迁移命令 Enable-Migrations, Add-Migration, Update-Database dotnet ef migrations add, dotnet ef database update
操作平台 仅支持 .NET Framework 支持 .NET Core / .NET 5+
依赖项 EntityFramework 包已集成所有功能 需要安装 Microsoft.EntityFrameworkCore,Microsoft.EntityFrameworkCore.Design 和 Microsoft.EntityFrameworkCore.Tools
历史记录存储 __MigrationHistory 表 __EFMigrationsHistory 表

迁移的常见操作及数据保留情况

操作类型 对表数据的影响 详细说明
新增表 创建新表,不会影响现有表中的数据。
新增列 新增的列会为所有现有行赋默认值(null 或默认值)。
删除列 删除列后,列中的数据无法恢复,需小心操作。
修改列类型 ⚠️ 可能会删除或截断数据,视更改的类型而定(如 int -> string 不丢数据,但 int -> bool 可能出错)。
重命名列 使用 RenameColumn,数据会保留。
新增索引 索引的增加不会丢失数据。
删除索引 删除索引不会删除表中的数据。
重命名表 使用 RenameTable,数据不会丢失。
删除表 删除表会丢失所有数据,无法恢复。
更改主键 视操作而定,有时需要删除并重建表,数据可能丢失。
外键变更 ⚠️ 视操作而定,如果外键约束被删除,可能会导致约束不满足。

因此,使用重命名列迁移后再删除列再迁移等方式替代直接删除列迁移导致的数据丢失

使用dotnet-ef

dotnet ef migrations add 数据迁移的名字 添加数据迁移

执行语句

1
2
3
4
5
6
7
#添加数据迁移,数据迁移名为Init
dotnet ef migrations add Init
#返回Build succeeded表示成功,如下
Build started...
Build succeeded.
The Entity Framework tools version '8.0.2' is older than that of the runtime '9.0.0'. Update the tools for the latest features and bug fixes. See https://aka.ms/AAc1fbw for more information.
Done. To undo this action, use 'ef migrations remove'

然后执行dotnet ef database update

1
2
3
4
5
6
7
#返回如下:
Build started...
Build succeeded.
The Entity Framework tools version '8.0.2' is older than that of the runtime '9.0.0'. Update the tools for the latest features and bug fixes. See https://aka.ms/AAc1fbw for more information.
Acquiring an exclusive lock for migration application. See https://aka.ms/efcore-docs-migrations-lock for more information if this takes too long.
Applying migration '20241216083634_Init'.
Done.

删除迁移可以使用dotnet ef migrations remove,但如果迁移已被应用(database update),则无法使用remove

列出所有迁移: dotnet ef migrations list

列出更新数据库的脚本: dotnet ef migrations script,可以使用-o重定向到一个sql文件中

在 SQL Server Management Studio 中执行 SQL 文件

  1. **打开 SQL Server Management Studio (SSMS)**。
  2. 连接到您的 SQL Server 实例
  3. 打开 SQL 文件
    • 在 SSMS 中,选择菜单中的 文件 -> 打开 -> 文件,然后选择您刚刚生成的 migration_script.sql 文件。
  4. 执行 SQL 脚本
    • 打开文件后,您可以查看生成的 SQL 脚本。点击工具栏上的 执行 按钮(或者按 F5),以执行 SQL 脚本。

使用vs自带的程序包管理器控制台

上面两个语句分别对应如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#EntityFramework才需要执行这句,即EF Core不需要执行此命令
Enable-Migrations #用于启用迁移功能并为数据库创建迁移文件。该命令会在项目中生成一个 Migrations 文件夹以及一个 Configuration.cs 文件。

#新建迁移
Add-Migration Init
#更新数据库
Update-Database

#删除迁移
Remove-Migration

#列出所有迁移
Get-Migrations

#数据库更新脚本(默认生成整个数据库的sql语句)
Update-Database -Script

#生成增量sql修改文件
Update-Database -Script -From <源迁移名称> -To <目标迁移名称>

EntityFramework的自动迁移

注意,不是EntityFrameworkCore的自动迁移

自动迁移示例:

  1. 创建DbContext

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    using System.Data.Entity;

    public class YourDbContext : DbContext
    {
    public DbSet<User> Users { get; set; }

    public YourDbContext() : base("name=DefaultConnection")
    {
    }
    }

    public class User
    {
    public int Id { get; set; }
    public string Name { get; set; }
    }
  2. 配置自动迁移

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    using System.Data.Entity.Migrations;

    internal sealed class Configuration : DbMigrationsConfiguration<YourDbContext>
    {
    public Configuration()
    {
    AutomaticMigrationsEnabled = true; // 启用自动迁移
    AutomaticMigrationDataLossAllowed = false; // 不允许数据丢失,自动迁移过程中,EF会阻止删除列等危险操作
    }

    protected override void Seed(MyContextEntity context)
    {
    // This method will be called after migrating to the latest version.

    // You can use the DbSet<T>.AddOrUpdate() helper extension method
    // to avoid creating duplicate seed data.
    //该方法可以用于在迁移完成后填充数据库,常用于初始化数据库的种子数据(例如,添加一些默认记录)。
    }
    }
  3. 在代码中调用迁移

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    using System.Data.Entity.Migrations;

    public class Program
    {
    public static void Main(string[] args)
    {
    var configuration = new Configuration();
    var migrator = new DbMigrator(configuration);

    // 运行所有未应用的迁移
    migrator.Update();

    // 继续执行其他程序逻辑
    }
    }

重置所有迁移

在某些极端情况下,可能需要删除所有迁移并重新开始,这可以通过删除Migration文件夹并删除数据库来轻松完成;此时,你可以创建一个新的初始迁移,其中将包含当前整个架构

你还可以重置所有迁移并创建单个迁移,而不会丢失数据.此操作有时称为”更正”,涉及一些手动操作

  • 删除Migrations文件夹
  • 创建新迁移并为其生成SQL脚本 dotnet ef migrations scriptUpdate-Database -Script
  • 在数据库中,删除迁移历史记录表(_EFMigrationsHistory)中的所有行
  • 执行dotnet ef migrations add xxx后,手动在迁移历史记录中插入一行,以记录第一个迁移已经应用,因为表已经存在.(注意,不执行database update)

反向工程

EF Core使用方法

案例参考

BlogContext.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.IO;

namespace testChart
{
//必须继承DbContext
//数据库上下文:C#中的类与数据库中的表进行一个映射关系
public class BlogContext:DbContext
{
#region 声明表
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
#endregion

//连接字符串:.db文件的完整路径
//配置一些连接字符串,日志的输出方式 等等
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
//使用 Entity Framework Core 配置一个 SQLite 数据库连接
//AppDomain.CurrentDomain.BaseDirectory表示始终放在启动目录中,而不是当前工作目录
//此处使用Directory.GetCurrentDirectory()的话是不对的
string databasePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "test.db");
Console.WriteLine($"Database Path: {databasePath}");
optionsBuilder.UseSqlite($"Data Source={databasePath}");
}

}

public class Blog
{
//自增主键
public int Id { get; set; }
public string? Url { get; set; }
//一个文章有多个帖子 (外键)
public List<Post> Posts { get; } = new();
}

public class Post
{
public int PostId { get; set; }
public string? Title { get; set; }
public string? Content { get; set; }
public int BlogId { get; set; }
public Blog? Blog { get; set; }
}
}

数据库操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//Entity Framework Core测试
Console.WriteLine("Entity Framework Core测试");
BlogContext context = new();
//添加一条数据:
Blog blog = new(){Url="https://www.hello.com"};
context.Blogs.Add(blog);//insert into
context.SaveChanges();//执行,提交

//查询:select * from Blogs offset limit 1
var first = context.Blogs.FirstOrDefault();

//删除:delete from Blogs Where Id=1
context.Blogs.Remove(first);
context.SaveChanges();//执行,提交


//查询:select * from Blogs offset limit 1
first = context.Blogs.FirstOrDefault();

//更新: update blog set url="https://www.baidu.com/test" where id = first.id
if (first!=null)
{
first.Url = "https://www.hello.com/test";
context.SaveChanges();//执行,提交

}


//查询:select * from Blogs
var list = context.Blogs.ToList();

foreach (var item in list)
{
Console.WriteLine(item.Url);
}

// 释放上下文
context.Dispose();

上述连接数据库是通过写死连接字符串在代码中实现的,但也可以将连接字符串写成如下形式:

连接字符串配置

EF Core 连接数据库的最常见方式是通过 连接字符串 配置,这些字符串通常存储在 appsettings.json 或 app.config(用于桌面程序)/web.config(用于web程序) 文件中。

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<connectionStrings>
<add name="MyDbContext"
connectionString="Data Source=.;Initial Catalog=MyDatabase;Integrated Security=True;"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>

或json格式文件演示:

1
2
3
4
5
{
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Database=MyDatabase;User Id=myuser;Password=mypassword;"
}
}

开发者需要根据实际情况手动编辑这些配置文件,确保数据库连接字符串正确无误,通常配置项包括:

  • 服务器地址(例如,Data Source=.; 表示使用本地数据库服务器)

  • 数据库名称(例如,Initial Catalog=MyDatabase;)

  • 身份验证信息(例如,使用 Windows 身份验证 Integrated Security=SSPI; 或 SQL Server 身份验证 User ID=admin;Password=123;)

EF使用方法

这里对EF的各种增删改查方法做一次系统的总结和巩固

EF上下文对象

1
JXC_SJKEntities entities = new JXC_SJKEntities();//实例化EF上下文对象

以此表为例:

img

查询

下面的变量名详解:

  • entities:这是DbContext的一个实例。DbContext是EF中用于与数据库交互的主要类。它管理实体对象的生命周期,包括对象的查询、保存、删除等。
  • T_Spb:这里指的是DbContext中的一个DbSet属性。DbSet<T>代表了数据库中的一个集合,用于操作指定类型的实体。

在Entity Framework中,DbSet是表示数据库中表的集合的类。DbSet是Entity Framework中的一种泛型集合类,用于表示数据库上下文中的实体集合。每个DbSet对应数据库中的一个表,可以用于查询、插入、更新和删除表中的数据。

查询全表数据

这里为了查看结果方便,都使用ToList()结束了延迟加载

延迟加载(Lazy Loading)是Entity Framework(EF)中的一个特性,指的是在实体属性被访问时才加载相关联的实体数据。这意味着,当你查询并获取一个实体对象时,并不会立即加载它的导航属性指向的关联实体。只有在你访问这些导航属性时,EF才会执行必要的查询操作,从数据库中加载这些关联实体的数据。

三种方式

  • Linq语句

    1
    2
    var list = entities.T_Spb.Select(s=>s).ToList();
    var list = entities.T_Spb.ToList();
  • Linq方法

    1
    2
    var list = (from item in entities.T_Spb
    select item).ToList();
  • SQL语句

    1
    2
    var list = entities.T_Spb.SqlQuery("select * from T_Spb").ToList();
    var list = entities.Database.SqlQuery("select * from T_Spb").ToList();

三者种方法结果相同,但是ToList()之前的返回值类型是不一样的。方法一、方法二(使用Linq)返回值类型为IQueryable<T>,方法三中DbSet.SqlQuery()返回值类型为DbSqlQuery<T>database.SqlQuery()返回值类型为DbRawSqlQuery<T>

查询单个数据

Find方法根据要查找的实体的主键值查对象。若通过主键查,用Find()比较方便

1
var model = entities.T_Spb.Find(30);
  • Linq语句

    1
    2
    var model = entities.T_Spb.Where(s => s.SpID == 30).FirstOrDefault();
    var model = entities.T_Spb.FirstOrDefault(s => s.SpID == 30);//上面的简写形式

    查询单个数据时,可以使用First()FirstOrDefault()Single()SingleOrDefault()

    • First 返回第一条数据 结果为空时出异常
    • FirstOrDefault 返回序列第一条数据 数据为空返回null
    • Single/SingleOrDefault 返回序列第一条数据 多条结果时出异常 (SingleOrDefault若序列为空返回默认值)
  • Linq方法

    1
    2
    3
    var model = (from s in entities.T_Spb
    where s.SpID == 30
    select s).FirstOrDefault();
  • SQL语句

    1
    2
    var model = entities.T_Spb.SqlQuery("select * from T_Spb where spid=30").FirstOrDefault();
    var model = entities.Database.SqlQuery<T_Spb>("select * from T_Spb where spid=30").FirstOrDefault();

查询部分字段

  • Linq语句

    1
    var list = entities.T_Spb.Select(s => new { id = s.SpID, name = s.Spmc}).ToList();
  • Linq方法

    1
    2
    3
    4
    5
    6
    var list1 = (from s in entities.T_Spb
    select new
    {
    id = s.SpID,
    name = s.Spmc
    }).ToList();
  • SQL语句

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //因为查询部分字段要返回一个匿名类对象,调用SQL语句要指明类型
    public class SpbModel
    {
    public int Id { get; set; }
    public string Name { get; set; }
    }

    // 然后使用这样的代码执行SQL查询并返回结果
    var list = context.Database.SqlQuery<SpbModel>("SELECT SpID AS Id, Spmc AS Name FROM T_Spb").ToList();

增加

主要是三种方法

  • Add()

    1
    2
    3
    var model = new T_Spb() { Spmc = "防脱发神器", Spgg = "ml", Spdw = "瓶", Splb = 2, Sptxm = "0000000000", Spbz = "程序员必备" };//此处定义的model是T_Spb表中一行数据的实例
    entities.T_Spb.Add(model);
    entities.SaveChanges();//通知上下文将实体的变化保存到数据库中
  • 修改State状态

    EF中的实体的状态值有Detached、Unchanged、Added、Deleted、Modified五种,可以通过修改上下文中实体的State值来实现 相应增删改操作

    1
    2
    3
    var model= new T_Spb() { Spmc = "防脱发神器1", Spgg = "ml", Spdw = "瓶", Splb = 2, Sptxm = "0000000001", Spbz = "程序员必备" };
    entities.Entry(model).State = System.Data.Entity.EntityState.Added;
    entities.SaveChanges();
  • 执行SQL语句

    1
    entities.Database.ExecuteSqlCommand("具体sql语句");//返回受影响行数

删除

  • Remove()【先查找再删除,操作数据库两次】

    1
    2
    3
    var model = entities.T_Spb.Find(50);
    entities.T_Spb.Remove(model);
    entities.SaveChanges();

    若直接实例化一个实体对象【不是查出来的】,直接使用Remove()方法,则会引发异常。

    解释:Remove()操作的对象必须在EF上下文中,直接new的实体对象是不在上下文中的。

    解决办法:通过Attach()方法 将实例化的model添加到EF上下文对象即可

    1
    2
    3
    4
    var model1 = new T_Spb() { SpID = 59 };
    entities.T_Spb.Attach(model1); //将model添加到EF上下文对象中
    entities.T_Spb.Remove(model1);
    entities.SaveChanges();
  • 修改State状态【直接删除操作数据库一次】

    1
    2
    3
    var model = new T_Spb() { SpID = 50 };
    entities.Entry(model1).State = System.Data.Entity.EntityState.Deleted;
    entities.SaveChanges();

    若不指定主键值,直接修改State值去删除也会引发异常

    解释:EF上下文中通过主键对实体对象进行跟踪,因此修改State值删除时要指明主键。

    1
    2
    3
    4
    //var model1 = new T_Spb() {  Spmc = "aaa" };//❌异常
    var model1 = new T_Spb() { SpID = 1, Spmc = "aaa" };//✅正确
    entities.Entry(model1).State = System.Data.Entity.EntityState.Deleted;
    entities.SaveChanges();
  • 执行SQL语句:略(同增加)

修改

  • 修改部分字段 (先查再修改)

    1
    2
    3
    var model = entities.T_Spb.FirstOrDefault(x=>x.SpID==30);
    model.Sptxm = "10241024";
    entities.SaveChanges();
  • 修改全部字段【设置State状态跟删除一样,要指明主键】

    1
    2
    3
    var model2 = new T_Spb { SpID = 42,Sptxm="10241024" };
    entities.Entry(model2).State = System.Data.Entity.EntityState.Modified;//全部更新
    entities.SaveChanges();

    注意:这种更新会将实例化的新对象中的值全部更新到数据库中,未赋值的字段为更新为null值

    解决方法:可以先将实例化model添加到上下文中,设定某一字段的IsModified属性来实现只修改部分字段

    1
    2
    3
    4
    var model2 = new T_Spb { SpID = 45,Sptxm="10241024" };
    entities.T_Spb.Attach(model2);//将model2对象添加到EF管理容器(EF上下文对象)
    entities.Entry(model2).Property("Sptxm").IsModified = true;
    entities.SaveChanges()
  • 执行SQL语句:略(同增加)

真正执行sql的时机

只有调用以下方法时才会真正执行sql

  • ToListAsync()
  • CountAsync()
  • FirstOrDefaultAsync()
  • AnyAsync()
  • SumAsync()

权限安全相关

在Entity Framework中,通过DbContext统一封装权限逻辑是典型的多租户架构和行级安全实现思路。以下是基于生产实践的六种实现方案及技术细节

方案 适用场景 技术实现 优势 局限性
全局查询过滤 (Global Query Filters) 行级数据隔离(如多租户、用户部门过滤) OnModelCreating中配置过滤条件,如HasQueryFilter(o => o.UserId == CurrentUser.Id) 自动作用于所有查询,业务代码无感知
利用数据库索引优化性能
需确保CurrentUser注入正确
无法动态调整条件
重写SaveChanges 写操作权限校验(如仅创建者可修改) SaveChanges中拦截变更条目,校验用户权限 集中控制数据修改权限
可自动填充审计字段
无法处理查询权限
高并发下可能影响性能
动态表达式树 复杂多维度权限规则(如动态租户+角色条件) 通过Expression动态生成查询条件,如ApplySecurityPolicy()扩展方法 灵活组合条件
支持按实体类型差异化策略
实现复杂度高
需处理表达式树合并
拦截器(Interceptors) 底层SQL注入权限条件(如强制追加TenantId) 继承DbCommandInterceptor,在ReaderExecuting中改写SQL 适用于遗留系统改造
支持字段级脱敏
需解析SQL语法
维护成本高
DbContext工厂模式 多租户上下文隔离 通过工厂注入用户上下文,如SecuredDbContext 解耦权限与数据层
支持单元测试模拟用户
需管理工厂生命周期
需结合全局过滤使用
组合式策略引擎 企业级RBAC/ABAC权限模型 定义ISecurityPolicy接口,加载多策略(如角色+数据范围) 支持复杂权限模型
易于扩展新策略
架构复杂度高
需设计策略优先级

总结

  • 中小型系统:优先采用全局查询过滤+SaveChanges校验,简单高效。
  • 大型复杂系统:引入组合式策略引擎+动态表达式树,支持RBAC/ABAC混合模型。

全局过滤查询

实现原理: 在OnModelCreating中为实体配置动态查询条件,自动附加权限约束到所有查询语句
典型场景:用户仅能访问所属部门数据

1
2
3
4
5
6
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 根据用户角色动态附加部门过滤条件
modelBuilder.Entity<Order>().HasQueryFilter(o =>
CurrentUser.IsAdmin || o.DepartmentId == CurrentUser.DepartmentId);
}

优势

  • 自动作用于所有DbSet<T>查询,业务代码无需感知权限逻辑
  • 条件表达式直接参与SQL生成,利用数据库索引优化性能
    缺陷
  • 需确保CurrentUser在DbContext生命周期内可用(需依赖注入)

重写SaveChanges进行权限校验

实现原理: 在SaveChanges阶段拦截实体变更,校验当前用户是否有权操作目标数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public override int SaveChanges()
{
var entries = ChangeTracker.Entries()
.Where(e => e.State == EntityState.Added
|| e.State == EntityState.Modified);

foreach (var entry in entries)
{
// 校验修改权限:仅允许创建者或管理员修改数据
if (entry.Entity is IAuditableEntity auditable
&& auditable.CreatedBy != CurrentUser.Id
&& !CurrentUser.IsAdmin)
{
throw new UnauthorizedAccessException("无修改权限");
}
}
return base.SaveChanges();
}

适用场景

  • 数据修改权限的动态校验
  • 审计字段(如CreatedBy)的自动填充

动态修改查询表达式树

实现原理: 通过扩展方法动态附加权限条件,避免硬编码全局过滤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static IQueryable<T> ApplySecurityPolicy<T>(this IQueryable<T> query) 
where T : class
{
if (typeof(ITenantEntity).IsAssignableFrom(typeof(T)))
{
return query.Where(e =>
((ITenantEntity)e).TenantId == CurrentUser.TenantId);
}
return query;
}

// 业务层调用
var orders = dbContext.Orders
.ApplySecurityPolicy()
.Where(o => o.Amount > 1000);

生成SQL

1
2
SELECT * FROM Orders 
WHERE TenantId = @tenantId AND Amount > 1000

优势

  • 支持条件组合,灵活应对多维度权限规则
  • 可针对不同实体类型定义差异化策略

基于拦截器的字段级权限控制

实现原理: 通过DbCommandInterceptor拦截原始SQL,动态注入权限条件

  1. 创建拦截器类继承DbCommandInterceptor
  2. ReaderExecuting方法中解析并改写SQL
1
2
3
4
5
6
7
8
9
10
11
12
public override InterceptionResult<DbDataReader> ReaderExecuting(
DbCommand command, CommandEventData eventData,
InterceptionResult<DbDataReader> result)
{
// 解析原始SQL并追加TenantId条件
if (command.CommandText.Contains("FROM Orders"))
{
command.CommandText += " WHERE TenantId = @tenantId";
command.Parameters.Add(new SqlParameter("@tenantId", CurrentUser.TenantId));
}
return base.ReaderExecuting(command, eventData, result);
}

适用场景

  • 遗留系统改造,无法修改现有查询逻辑
  • 需要实现字段级别的动态脱敏(如隐藏敏感列)

DbContext工厂模式统一注入权限上下文

实现原理: 通过自定义DbContext工厂,在实例化时注入用户身份信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SecuredDbContext : DbContext
{
private readonly IUserContext _userContext;

public SecuredDbContext(DbContextOptions options, IUserContext userContext)
: base(options)
{
_userContext = userContext;
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Document>().HasQueryFilter(d =>
d.OwnerId == _userContext.UserId || d.IsPublic);
}
}

// 注册服务时传递用户上下文
services.AddDbContext<SecuredDbContext>((sp, options) =>
{
var userContext = sp.GetRequiredService<IUserContext>();
options.UseSqlServer(connectionString);
});

优势

  • 解耦权限逻辑与数据访问层
  • 支持在单元测试中模拟用户上下文

组合式权限策略引擎

架构设计:

1
2
3
4
5
6
7
8
9
10
11
               +----------------+
| Policy Engine | // 定义RBAC/ABAC规则
+----------------+

+----------------+ ↓ +------------------+
| Query Filters |←----------→| SaveChanges Hook |
+----------------+ ↓ +------------------+

+-----------------+
| DbContext | // 统一执行策略
+-----------------+

实现要点

  1. 定义策略接口ISecurityPolicy<T>
  2. 在DbContext中加载所有策略实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PolicyDrivenDbContext : DbContext
{
private readonly IEnumerable<ISecurityPolicy> _policies;

public override int SaveChanges()
{
foreach (var entry in ChangeTracker.Entries())
{
foreach (var policy in _policies.Where(p => p.CanApply(entry.Entity)))
{
policy.Apply(entry, CurrentUser);
}
}
return base.SaveChanges();
}
}

实施建议

  1. 性能优化:

    • 为权限字段(如TenantId)建立组合索引
    • 使用AsNoTracking()减少变更跟踪开销
  2. 测试策略:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    [Fact]
    public void Should_Filter_Orders_By_Tenant()
    {
    // 模拟用户上下文
    var user = new MockUser { TenantId = 1 };
    var db = CreateDbContext(user);

    var query = db.Orders.ToQueryString();
    Assert.Contains("WHERE TenantId = 1", query);
    }
  3. 监控手段:

    启用EF Core的查询标签

    1
    .TagWith("Secured by tenant policy")

通过以上方案,可在DbContext层实现企业级权限控制,确保业务代码无需重复处理安全逻辑,同时保持EF Core的开发和性能优势。

以使用PostgreSQL数据库优先为例

因环境为.net6,因此下面版本号均为.net6.0适用

1
2
3
4
Install-Package Microsoft.EntityFrameworkCore  #版本为7.0.20
Install-Package Microsoft.EntityFrameworkCore.Tools # 用于执行脚手架命令,版本为7.0.20
#Install-Package Pomelo.EntityFrameworkCore.MySql #根据数据库类型选择提供程序,这里是mysql,版本为7.0.0
Install-Package Npgsql.EntityFrameworkCore.PostgreSQL #这里使用的是PostgreSQL,该nuget包版本为7.0.18,其支持的PostgreSQL为12-15
特性 Pomelo.EntityFrameworkCore.MySql MySql.EntityFrameworkCore
EF Core 版本支持 支持最新版本(如 EF Core 8.0) 通常滞后于 EF Core 主版本(如支持 EF Core 5.0/6.0)
高级功能 支持 JSON 类型、全局查询过滤器、批量操作优化等 基础功能完善,但缺乏高级扩展(如 JSON 支持较弱)
云原生集成 集成 .NET Aspire 组件,支持健康检查、连接池、观测性 无原生云服务集成,需手动配置
性能优化 提供异步操作优化、分库分表插件(如 ShardingCore) 性能稳定,但优化手段有限

PostgreSQL安装与配置

安装

PostgreSQL下载地址

为了兼容7.0.18的EntityFrameworkCore插件,选择安装15版本的PostgreSQL,这里具体选择的是15.12版本

安装过程中必选安装项:

  • PostgreSQL Server(数据库服务)
  • pgAdmin4(图形化管理工具)
  • Command Line Tools(命令行工具)

设置密码后,保持默认端口号,完成安装(安装好后的超级用户名是postgres,密码是你设置的)

完成后,通过pgAdmin4连接数据库验证: 打开 pgAdmin 4,右键 Servers > Create > Server,填写连接信息(主机名、端口、用户名、密码)

pgAdmin4设置中文: File - Preferences - Miscellaneous - User Interface - Language 设置为中文

或命令行验证:psql -U myuser -d mydb -h 127.0.0.1 -p 5432 # 输入密码后进入交互界面

配置

创建专用用户

1
2
3
4
5
-- 创建非超级用户,强制密码加密(适配 Npgsql 的 Password 认证模式)
CREATE USER app_user WITH
LOGIN
NOSUPERUSER
ENCRYPTED PASSWORD 'xxxxxxx';

回收默认权限

PostgreSQL 默认允许所有用户访问 public schema,需先回收潜在危险权限:

1
2
3
4
-- 禁止在 public schema 中创建对象(防止恶意表/函数注入)
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- 禁止普通用户访问默认数据库模板
REVOKE CONNECT ON DATABASE template1 FROM PUBLIC;

赋予需要的权限

开发模式 需赋权的对象 权限要求
数据库优先 所有用户表、系统表 SELECT(逆向工程)
代码优先 实体对应表、迁移历史表 完整 CRUD、CREATE(迁移)
数据库优先开发模式
1
2
-- 允许所有用户在 public 模式创建对象(默认行为)
GRANT CREATE ON SCHEMA public TO PUBLIC;

所有用户表:通过 Scaffold-DbContext 命令生成模型时,EF 默认映射所有用户表(非系统表)

1
2
3
4
GRANT CONNECT ON DATABASE postgres TO app_user;#这里使用自动生成的默认数据库postgres
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO app_user; -- 允许逆向工程读取表结构
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO app_user; -- 未来新表的默认权限
代码优先开发模式

模型对应的表:EF 根据实体类生成表结构,用户需对生成的表拥有完整的 CRUD 权限

迁移历史表(__EFMigrationsHistory):EF 自动创建此表记录迁移版本,用户需拥有该表的读写权限

1
2
3
4
5
GRANT CONNECT ON DATABASE postgres TO app_user;
GRANT CREATE ON DATABASE postgres TO app_user; -- 允许执行迁移创建表
GRANT USAGE ON SCHEMA public TO app_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO app_user;
GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO app_user; -- 支持自增主键

PostgreSQL操作日志记录

类Unix系统下可以通过Logrotate工具来配置日志轮转记录,更实用方便

找到 postgresql.conf

  • Linux:默认路径为/etc/postgresql/<版本>/main/postgresql.conf(Debian/Ubuntu)或/var/lib/pgsql/<版本>/data/postgresql.conf(RHEL/CentOS)
  • Windows:位于 PostgreSQL 安装目录的 data 文件夹(如 C:\Program Files\PostgreSQL\15\data
  • Docker 容器:路径为 /var/lib/postgresql/data/postgresql.conf

使用文本编辑器(如 nanonotepad)修改以下参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 启用日志收集器(必需)
logging_collector = on

# all表示记录所有 SQL 操作,这里只记录修改不记录查询使用mod
log_statement = 'mod'

# 日志文件存储配置
log_directory = 'log' # 日志存放目录(相对路径基于数据目录)

# 增强日志可读性(可选但建议)
log_line_prefix = '%t [%p]: user=%u,db=%d,app=%a,client=%h '
log_connections = off # 不记录连接建立
log_disconnections = off # 不记录连接断开
log_duration = on # 记录 SQL 执行时间

#控制日志保留时长
log_filename = 'postgresql-%d.log' # %d 表示月份中的第几天(01~31) 使用这个命名可以实现记录一个月的数据,每天一次
log_rotation_age = 1d # 每天生成新日志文件[5,7](@ref)
log_rotation_size = 0 # 禁用基于文件大小的轮转(这个机制局限太大,使用有缺陷)
log_truncate_on_rotation = on # 覆盖旧日志(循环保留)

设置完成后需要重启PostgreSQL服务

  • Linux 系统sudo systemctl restart postgresql
  • Windows 系统: 通过服务管理器重启 PostgreSQL服务
  • Docker容器: docker restart <容器名称>

PostgreSQL创建表

执行sql语句创建完所有表后,在pgAdmin4的对应服务器下 - 架构 - public - 表 中可查看到新建的表

数据库优先生成类代码

使用 EF Core Power Tools 扩展(Visual Studio)可图形化生成代码,支持更高级的反向工程功能

生成DbContext和实体类
1
dotnet-ef dbcontext scaffold "Host=localhost;Port=5432;Database=postgres;Username=app_user;Password=your_password" Npgsql.EntityFrameworkCore.PostgreSQL --output-dir Models --context-dir Data --context PostgresDbContext --data-annotations --force
  • 连接字符串:替换HostPortUsername等为实际数据库信息
  • 提供程序Npgsql.EntityFrameworkCore.PostgreSQL为PostgreSQL的EF Core提供程序
  • 输出目录:
    • --output-dir Models:实体类生成到Models文件夹。
    • --context-dir Data:DbContext生成到Data文件夹。
  • 其他选项
    • --data-annotations:使用数据注解(如[Required])而非Fluent API配置
    • --force:覆盖已存在的文件。

但是注意,这样生成的代码中会存在硬编码的数据库连接字符串,即包含密码明文
应该是连接字符串是从配置文件中读出的,包括连接用户名,端口,数据库名,Host,以及密码,并且密码在配置文件中还应该是加密后的, 读取出来解密后才连接

如何编写各种转换服务: 略
反正是设置好依赖注入容器,就可以直接使用dbContext来操作数据库了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try
{
await context.SaveChangesAsync();//提交保存数据库
}
catch (DbUpdateConcurrencyException ex)
{
// 重试逻辑或合并冲突数据
foreach (var entry in ex.Entries)
{
var databaseValues = await entry.GetDatabaseValuesAsync();
entry.OriginalValues.SetValues(databaseValues);
await context.SaveChangesAsync(); // 重试
}
}

CSharp拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <summary>
/// 克隆当前对象
/// </summary>
/// <returns>返回当前对象的深拷贝</returns>
public IHCData Clone()
{
var clone = (IHCData)this.MemberwiseClone();//浅拷贝

// 如果有引用类型字段或属性,请在此处进行深拷贝
// 例如:
// clone.SomeList = this.SomeList != null ? new List<Type>(this.SomeList) : null;

return clone;
}

延迟实例化

延迟实例化 是一种将对象的创建延迟到第一次需要用时的技术

在 C# 中,lazy 相关的功能是通过 Lazy<T> 类引入的。Lazy<T> 是在 .NET Framework4.0 中引入的,这个类用于实现延迟加载(lazy loading),即在需要时才创建对象的实例,从而提高性能和资源利用率。并且lazy<T>是线程安全的

Lazy<T> 可以用于在访问对象的属性或方法时延迟实例化,也称为懒加载(Lazy Initialization)

当使用 Lazy<T> 的时候,这里的 T 就是你要延迟的集合,那如何做到按需加载呢?调用 Lazy<T>.Value 即可,下面的代码片段展示了如何使用 Lazy<T>

1
2
3
4
5
public class Example
{
private Lazy<SomeClass> _lazyInstance = new Lazy<SomeClass>(() => new SomeClass());
public SomeClass Instance => _lazyInstance.Value;//即访问Instance的时候才真正初始化实例
}

可以使用lazy配合static实现单例类

1
2
private static readonly Lazy<SharedDispatcher> _instance = new Lazy<SharedDispatcher>(() => new SharedDispatcher());
public static SharedDispatcher Instance => _instance.Value;

只有将 Lazy 的实例声明为 static,才能确保在整个应用程序中只有一个 Lazy<T> 实例,从而实现单例模式.如果去掉static,每次创建 SharedDispatcher 实例时都会创建一个新的 Lazy<T> 实例,无法实现单例的效果

原理

具体原理是,Lazy<T> 内部使用了一个委托,该委托负责创建目标对象。当你第一次访问Lazy<T>Value属性时,该委托会执行,实例化目标对象,并将其保存下来。随后的访问会直接返回已经创建好的对象,而不会再次执行委托。

Lazy<T> 类提供了内建的线程安全机制,确保在多线程环境下也能正常工作。通过Lazy<T>,你可以实现延迟加载,而且无需担心线程安全性。Lazy<T> 内部使用了一种双重检查锁(Double-Check Locking)的机制,确保在多线程环境下只有一个线程会执行被延迟加载的对象的初始化操作。这意味着,Lazy<T> 会保证在多线程环境下只有一个线程会调用目标对象的构造函数,避免了竞态条件(Race Condition)的发生。

具体来说,Lazy<T> 使用了双重检查锁机制来保证线程安全:

  1. 第一次检查(Without Lock): 在没有锁的情况下,检查是否已经初始化了对象。如果对象已经初始化,直接返回它,否则进入第二个步骤。
  2. 加锁(Locking): 确保只有一个线程能够进入临界区域。在进入临界区域后,再次检查对象是否已经初始化。如果没有初始化,进行初始化操作。

这种双重检查锁机制在Lazy<T> 类内部实现,确保了延迟加载的对象在多线程环境下的线程安全性。

缺点

  1. 性能开销: 在第一次访问Lazy<T>对象时,需要进行初始化操作,这可能会引入一定的性能开销,特别是在初始化逻辑较复杂或耗时的情况下。
  2. 线程安全性: 默认情况下,Lazy<T>是线程安全的,但如果需要在多线程环境下共享实例,可能需要额外的线程同步措施,这会增加复杂性。
  3. 内存占用: 虽然Lazy<T>可以延迟对象的创建,但在对象创建后,它将一直占用内存,即使后续不再需要该对象。
  4. 不适用于某些场景:Lazy<T>适用于需要延迟初始化的场景,但并不适用于所有情况。在某些情况下,可能需要即时创建对象或使用其他设计模式。
  5. 引入额外复杂性: 在某些情况下,使用Lazy<T>可能会引入额外的复杂性,使代码变得难以理解和维护。

Lazy的线程安全模式

Lazy<T>类提供了一些线程安全的模式,例如LazyThreadSafetyMode.PublicationOnlyLazyThreadSafetyMode.ExecutionAndPublication。你可以根据需求选择适当的模式,确保在多线程环境下实例的安全共享。

1
Lazy<T> lazyInstance = new Lazy<T>(() => CreateInstance(), LazyThreadSafetyMode.None);
  1. None:

    • 描述: 不进行任何线程安全保护。
    • 适用场景: 只有单线程访问时使用,或者你负责在多线程环境中确保线程安全。
    • 注意: 如果多个线程同时访问 Value 属性,可能会导致多个实例的创建。
  2. PublicationOnly:

    • 描述: 允许多个线程同时初始化实例,但只会有一个线程的初始化结果被保留。其他线程在访问 Value 时会返回已经初始化的实例。
    • 适用场景: 当你希望提高性能并允许并行初始化,但不关心哪个线程的初始化结果被保留时使用。
    • 注意: 这种模式下,如果多个线程同时访问,可能会导致多个实例的创建,但最终只会有一个实例被保留。
  3. ExecutionAndPublication: (默认情况)

    • 描述: 这是最安全的模式。只有一个线程能够初始化实例,其他线程在访问 Value 时会等待初始化完成,并共享同一个实例。
    • 适用场景: 当你需要确保在多线程环境中只创建一个实例,并且希望其他线程在访问时能够安全地共享该实例时使用。
    • 注意: 这种模式下,性能可能会受到影响,因为它需要确保线程安全。

C++中的实现

C++ 中可以通过 std::mutex 或 C++11 的 std::call_once 实现线程安全的延迟初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <iostream>
#include <functional>
#include <memory>
#include <mutex>

template<typename T>
class Lazy {
public:
// 构造函数接受一个创建对象的工厂函数
explicit Lazy(std::function<T()> factory)
: valueFactory(std::move(factory)), isValueCreated(false) {}

// 获取实例化的对象,延迟创建
T& Value() {
std::call_once(initFlag, [&]() {
value = std::make_unique<T>(valueFactory());
isValueCreated = true;
});
return *value;
}

// 检查值是否已经初始化
bool IsValueCreated() const {
return isValueCreated;
}

private:
std::function<T()> valueFactory; // 工厂函数,用于创建对象
std::unique_ptr<T> value; // 存储延迟实例化的对象
std::once_flag initFlag; // 保证线程安全的单次初始化
bool isValueCreated; // 记录是否已经初始化
};

// 示例
class ExpensiveObject {
public:
ExpensiveObject() {
std::cout << "ExpensiveObject created!" << std::endl;
}

void DoWork() {
std::cout << "Working..." << std::endl;
}
};

int main() {
Lazy<ExpensiveObject> lazyObject([] {
return ExpensiveObject();
});

std::cout << "Lazy object created, but not initialized yet." << std::endl;

// 首次访问触发初始化
lazyObject.Value().DoWork();

// 再次访问,不会重新初始化
lazyObject.Value().DoWork();

std::cout << "IsValueCreated: " << lazyObject.IsValueCreated() << std::endl;

return 0;
}

CSharp 命名规范

$$
不要给别人和将来的自己添麻烦
$$

遵守相同的习惯与风格,可以提高效率,避免误会

变量命名规范

camelCase 私有字段,局部变量,入参

  • _camelCase _CamelCase 使用依赖注入时更为推荐
  • s_camelCase,m_camelCase 一般不推荐
  • @bool,@object 使用关键字作为名称

PascalCase: 命名空间,类名,方法名

  • IPascalCase 接口名
  • TPascalCase 泛型类型名
  • PascalCaseAttribute 特性
  • PascalCaseProperty 依赖属性

特殊情形

  • XAML中的x:Key名及x:Name

    • camelCase或PascalCase均可
    • <Button Name="btn"/>
    • <Style Key="Button.Common.Light">
  • XAML中的xmlns命名空间名: lowercase 尽量简单

    b:Interaction,cv:BoolConverter

  • 预编译器指令#define UPPER_CASE

    DEBUG,NET_472

  • 控件的事件注册 允许下划线 Button_Click

方法命名规范

名称遵守PascalCase命名规范

  • 无论是否为公共方法,均首字母大写
  • 选择合适的动词(+名词)
  • 非公共方法可为名称添加Internal,Impl等字眼

例外方法

  • 局部方法 可以小写开头
  • 用于注册事件的方法 Window_Loaded

异步方法

异步方法以Async结尾

  • 与同名的同步方法进行区分
  • 便于快速判断调用的方法是否需要等待

例外情况

  • 人尽皆知的方法(Task.Delay,Task.WhenAll)

  • 控制器(Controller)中的方法(ASP.NET中才有)

    虽然是异步的,但是不添加async,因为不会被开发者直接调用,而是框架调用

合理选择单词

选择最合适且被广泛接受的单词描述某个意思

  • Order应该用于排序(orderby),而不是用于命令
  • Apply用于表达应用,而不是申请(Request)
  • Command常用于名词而非动词

避免使用过于宽泛或与标准库重名的词汇

Core,Main,Action,Math

布尔类型的成员一般以Is,Has,Can开头

IsValid,IsActive,HasErrors,CanExecute

使用偏正式的单词,而非偏口语化的单词

Visibility✅ Seen❌

Selection✅ Option✅ Choose❌

语法与时态

一般使用第三人称单数(可以考虑使用复数形式)

Equals,IsEqual,AreEqual,DependsOn

尽量不要使用单复数不符合常见形式(可适当未被词汇或语法)

Persons✅ PersonList✅ People❌(使用orm会很痛苦,需要加一些新的特性)

Infos ✅(不符合语法但可以)

Datas ✅(不符合语法但可以)

考虑时态习惯

OnPropertyChanged 对于事件可以以On开头

Closing 正在关闭 Closed 已经关闭

更多细节

名称写清晰且完整(例外情形遵守普通习惯)

CancellationTokenSource,OperationCanceledException

IsCompletedSuccessfully

SendCaches < SendAllCachedUserData(<指语义上)

拓展方法尽量简洁且清晰

this byte[]: BytesToInt ==> ToInt32 由于调用的时候已经知道转换的主体了,因此后者在保持语义清晰的同时更简洁

杜绝C/C++,MATLAB,JAVA等命名习惯

itoa❌,num2str❌,get_value❌

nuget包盘点

SkiaSharp绘图

下载SkiaSharp包: dotnet add package SkiaSharp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
using System;
using System.Collections.Generic;
using System.Linq;
using SkiaSharp;

public class Program
{
public static void Main()
{
List<double> data = new List<double> { 10, 50, 30, 80, 60, 90, 40 };
DrawLineChart(data, "line_chart.png");
Console.WriteLine("Line chart saved as line_chart.png");
}

public static void DrawLineChart(List<double> data, string filename)
{
int width = 800;
int height = 600;
int padding = 50;

using (var bitmap = new SKBitmap(width, height))
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.White);

double maxData = data.Max();
double minData = data.Min();
double range = maxData - minData;

// 避免除以零的情况
if (range == 0)
{
range = 1;
}

float scaleX = (width - 2 * padding) / (float)data.Count;
float scaleY = (height - 2 * padding) / (float)range;

float zeroY = height - padding - (float)(0 - minData) * scaleY;

// 绘制 x 轴和 y 轴
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
paint.StrokeWidth = 2;
paint.Color = SKColors.Black;
paint.Style = SKPaintStyle.Stroke;

// 绘制 y 轴
canvas.DrawLine(padding, padding, padding, height - padding, paint);

// 绘制 x 轴在零的位置
canvas.DrawLine(padding, zeroY, width - padding, zeroY, paint);
}

var path = new SKPath();
for (int i = 0; i < data.Count; i++)
{
float x = padding + i * scaleX;
float y = height - padding - (float)(data[i] - minData) * scaleY;

if (i == 0)
{
path.MoveTo(x, y);
}
else
{
path.LineTo(x, y);
}
}

using (var paint = new SKPaint())
{
paint.IsAntialias = true;
paint.StrokeWidth = 2;
paint.Color = SKColors.Blue; // 改变折线颜色以区别于轴线
paint.Style = SKPaintStyle.Stroke;

canvas.DrawPath(path, paint);
}

SaveBitmap(bitmap, filename);
}
}

private static void SaveBitmap(SKBitmap bitmap, string filename)
{
using (var image = SKImage.FromBitmap(bitmap))
using (var data = image.Encode(SKEncodedImageFormat.Png, 100))
using (var stream = System.IO.File.OpenWrite(filename))
{
data.SaveTo(stream);
}
}
}

绘制多条曲线图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
public static void DrawMultiLineChart(List<List<double>> dataSets, List<string> labels, string filename)
{
int width = 1920;
int height = 1080;
int padding = 50;
int legendWidth = 200;
int legendHeight = 20;
float pointRadius = 4.0f; // 圆点半径

using (var bitmap = new SKBitmap(width, height))
using (var canvas = new SKCanvas(bitmap))
{
canvas.Clear(SKColors.White);

// Find the global max and min values
double maxData = dataSets.SelectMany(data => data).Max();
double minData = dataSets.SelectMany(data => data).Min();
double range = maxData - minData;

if (range == 0)
{
range = 1;
}

float scaleX = (width - padding - legendWidth) / (float)dataSets.Max(data => data.Count);
float scaleY = (height - 2 * padding) / (float)range;

float zeroY = height - padding - (float)(0 - minData) * scaleY;

// Draw x and y axes
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
paint.StrokeWidth = 2;
paint.Color = SKColors.Black;
paint.Style = SKPaintStyle.Stroke;

// Draw y axis
canvas.DrawLine(padding, padding, padding, height - padding, paint);

// Draw x axis at zero position
canvas.DrawLine(padding, zeroY, width - padding - legendWidth, zeroY, paint);
}

// Define colors for different lines
var colors = new SKColor[] { SKColors.Red, SKColors.Blue, SKColors.Green, SKColors.Purple, SKColors.Orange };

// Draw each line and legend
for (int j = 0; j < dataSets.Count; j++)
{
var data = dataSets[j];
var path = new SKPath();

for (int i = 0; i < data.Count; i++)
{
float x = padding + i * scaleX;
float y = height - padding - (float)(data[i] - minData) * scaleY;

// Draw circle at data point
using (var pointPaint = new SKPaint())
{
pointPaint.IsAntialias = true;
pointPaint.Color = colors[j % colors.Length];
canvas.DrawCircle(x, y, pointRadius, pointPaint);
}

if (i == 0)
{
path.MoveTo(x, y);
}
else
{
path.LineTo(x, y);
}

// //虚线绘制不全,还未修正
// // Draw dashed line to x axis
// using (var dashPaint = new SKPaint())
// {
// dashPaint.Style = SKPaintStyle.Stroke;
// dashPaint.Color = colors[j % colors.Length];
// dashPaint.StrokeWidth = 0.1f;
// dashPaint.PathEffect = SKPathEffect.CreateDash(new float[] { 3, 10 }, 0);
// var dashPath = new SKPath();
// dashPath.MoveTo(x, y);
// dashPath.LineTo(x, zeroY);
// canvas.DrawPath(dashPath, dashPaint);
// }

// // Draw dashed line to y axis
// if (data[i] != 0) // Avoid drawing line at y = 0 (x axis)
// {
// using (var dashPaint = new SKPaint())
// {
// dashPaint.Style = SKPaintStyle.Stroke;
// dashPaint.Color = colors[j % colors.Length];
// dashPaint.StrokeWidth = 0.1f;
// dashPaint.PathEffect = SKPathEffect.CreateDash(new float[] { 3, 10 }, 0);
// var dashPath = new SKPath();
// dashPath.MoveTo(x, y);
// dashPath.LineTo(padding, y); // Draw line to y axis
// canvas.DrawPath(dashPath, dashPaint);
// }
// }
}

// Draw legend for each line
using (var legendPaint = new SKPaint())
{
legendPaint.Style = SKPaintStyle.Stroke;
legendPaint.Color = colors[j % colors.Length];
legendPaint.StrokeWidth = 1;
legendPaint.IsAntialias = true;
canvas.DrawLine(width - padding - legendWidth, padding + j * legendHeight, width - padding, padding + j * legendHeight, legendPaint);
canvas.DrawText(labels[j], width - padding - legendWidth + 10, padding + j * legendHeight + 15, legendPaint);
}

// Draw lines
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
paint.StrokeWidth = 2;
paint.Color = colors[j % colors.Length];
paint.Style = SKPaintStyle.Stroke;

canvas.DrawPath(path, paint);
}
}

// Save bitmap
SaveBitmap(bitmap, filename);
}
}

Dynamic Expresso

Dynamic Expresso 是一个用于动态解析和执行表达式的库,主要用于将字符串形式的表达式解析为可执行的 lambda 表达式。它的主要功能包括:

MIT开源 开源地址

  1. 动态计算:可以在运行时动态地解析和计算表达式,而不需要在编译时确定所有的逻辑。这在某些场景下非常有用,比如需要根据用户输入或配置文件动态生成逻辑的情况。
  2. 简化代码:通过将复杂的逻辑表达式以字符串形式表示,可以简化代码的编写和维护。例如,可以将业务规则或条件逻辑存储为字符串,然后在需要时解析并执行。
  3. 灵活性:允许开发者在运行时创建和调整表达式,使得应用程序能够更灵活地响应变化。例如,在规则引擎或查询语言中,用户可以输入自定义的查询条件,系统则可以解析并执行这些条件

可以评估数学表达式:

1
2
var interpreter = new Interpreter();
var result = interpreter.Eval("8 / 2 + 2");

或者解析带有多个变量或参数的表达式并多次调用它:

1
2
3
4
var interpreter = new Interpreter().SetVariable("service", new ServiceExample());
string expression = "x > 4 ? service.OneMethod() : service.AnotherMethod()";
Lambda parsedExpression = interpreter.Parse(expression, new Parameter("x", typeof(int)));
var result = parsedExpression.Invoke(5);

或者为 LINQ 查询生成委托和 lambda 表达式:

1
2
3
var prices = new [] { 5, 8, 6, 2 };
var whereFunction = new Interpreter().ParseAsDelegate<Func<int, bool>>("arg > 5");
var count = prices.Where(whereFunction).Count();

CsvHelper

CsvWriter 是一个用于在 C# 中生成和读取 CSV(Comma-Separated Values)文件的库。它通常用于将数据导出到 CSV 格式,或者从 CSV 文件中读取数据。CsvWriter 库提供了简单易用的 API,使得处理 CSV 文件变得更加方便。

开源地址

1
dotnet add package CsvHelper

详细使用介绍

简单记录使用方式:

定义一个可以和csv文件格式对应的类

读取方式

1
2
3
4
5
using (var reader = new StreamReader("path\\to\\file.csv"))
using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture))
{
var records = csv.GetRecords<Foo>();//读取自定义的Foo类
}

GetRecords<T>方法将返回一个IEnumerable<T>记录yield。这意味着在迭代记录时一次只返回一条记录。这也意味着只有一小部分文件被读入内存。但要小心。如果您执行任何执行 LINQ 投影的操作(例如调用),则.ToList()整个文件将被读入内存。CsvReader是仅向前的,因此如果您想针对数据运行任何 LINQ 查询,则必须将整个文件拉入内存。只要知道这就是你在做的事情。

如果csv文件中没有标题记录,或者想要改属性名的对应大小写格式都可以参考详细使用介绍

写入方式

1
2
3
4
5
using (var writer = new StreamWriter("path\\to\\file.csv"))
using (var csv = new CsvWriter(writer, CultureInfo.InvariantCulture))
{
csv.WriteRecords(records);//将records对象写入csv文件
}

第三方数据结构库

C5

用于C#/.NET的通用集合库

MIT开源 开源地址

安装方式 dotnet add package C5

线性数据结构

  • ArrayList: 底层使用动态数组实现,支持快速随机访问,但插入和删除操作在中间位置可能比较慢。
  • DoublyLinkedList: 底层使用双向链表实现,支持快速插入和删除操作,但随机访问比较慢。
  • HashIndexedArrayList: 底层使用动态数组和哈希表实现,支持快速随机访问和插入,但删除操作可能比较慢。
  • HashIndexedLinkedList: 底层使用双向链表和哈希表实现,支持快速插入和删除操作,但随机访问比较慢。

集合

  • HashSet: 底层使用哈希表实现,不包含重复元素,支持快速查找、插入和删除操作。
  • HashBag: 底层使用哈希表实现,允许重复元素,支持快速查找、插入和删除操作。
  • SortedArray: 底层使用有序数组实现,支持快速查找、插入和删除操作,但插入和删除操作在中间位置可能比较慢。
  • WrappedArray: 底层使用数组实现,提供对数组的只读访问。
  • TreeSet: 底层使用平衡二叉树 (红黑树) 实现,有序集合,不包含重复元素,支持快速查找、插入和删除操作,并保持元素的排序。
  • TreeBag: 底层使用平衡二叉树 (红黑树) 实现,有序集合,允许重复元素,支持快速查找、插入和删除操作,并保持元素的排序。

栈和队列

  • Stack: 底层使用数组或链表实现,后进先出 (LIFO) 的栈,支持快速入栈和出栈操作。
  • DoubleEndedQueue: 底层使用双向链表实现,支持从两端进行入队和出队操作。
  • CircularQueue: 底层使用数组实现,支持快速入队和出队操作,但容量有限。
  • PriorityQueue: 底层使用间隔堆 (interval heap) 实现,支持快速插入和删除操作,并根据优先级排序元素。

字典

  • HashDictionary: 底层使用哈希表实现,支持快速查找、插入和删除操作。
  • TreeDictionary: 底层使用平衡二叉树 (红黑树) 实现,支持快速查找、插入和删除操作,并保持键的排序。

即使是这样,也没找到一个类似于C++ STL中的MultiMap这样的数据结构

控制反转容器库

Autofac

可以提供自动注入构造函数参数的功能

也可以提供面向切片编程的实现

状态机库

stateless

开源地址

Stateless 是一个轻量级的状态机库,用于在.NET 应用程序中实现状态机模式。它允许你以简单直观的方式定义状态、转换和触发器。

安装方式 Install-Package Stateless

基本概念

  • State(状态): 对象在某一时刻的条件或情况
  • Trigger(触发器): 导致状态改变的事件或动作
  • Transition(转换): 从一个状态到另一个状态的转变

如何使用

  1. 定义状态和触发器

    开始使用之前,你需要定义状态机的状态和触发器。状态表示对象当前所处的阶段,而触发器是导致状态改变的事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public enum State
    {
    Off,
    On
    }

    public enum Trigger
    {
    TurnOn,
    TurnOff
    }
  2. 创建状态机

    使用 StateMachine 类创建一个状态机实例。你需要指定初始状态。

    1
    var machine = new Stateless.StateMachine<State, Trigger>(State.Off);
  3. 配置状态转换

    定义状态和触发器之间的转换关系。你可以使用 Configure 方法为每个状态设置可以接受的触发器。

    1
    2
    3
    4
    5
    6
    7
    //允许状态机在off状态下被turnOn,变化到On状态
    machine.Configure(State.Off)
    .Permit(Trigger.TurnOn, State.On);

    //允许状态机在on状态下被turnOff,变化到O状态
    machine.Configure(State.On)
    .Permit(Trigger.TurnOff, State.Off);
  4. 触发转换

    通过触发器改变状态机的状态。调用 Fire 方法来执行状态转换

    1
    2
    3
    4
    5
    machine.Fire(Trigger.TurnOn);
    Console.WriteLine(machine.State); // 输出: On

    machine.Fire(Trigger.TurnOff);
    Console.WriteLine(machine.State); // 输出: Off
  5. 添加进入和退出动作

    你可以在状态进入或退出时执行特定的动作。使用 OnEntry 和 OnExit 方法

    1
    2
    3
    machine.Configure(State.On)
    .OnEntry(() => Console.WriteLine("Entering On state"))
    .OnExit(() => Console.WriteLine("Exiting On state"));
  6. 使用条件限制转换

    可以使用条件来限制某些状态转换。例如,你可以在某个条件下允许转换。

    1
    2
    3
    bool canTurnOn = true;
    machine.Configure(State.Off)
    .PermitIf(Trigger.TurnOn, State.On, () => canTurnOn);
  7. 异步支持

    Stateless 也支持异步状态转换。你可以使用异步方法来配置和触发状态机

    1
    await machine.FireAsync(Trigger.TurnOn);
  8. 还支持子状态,动态触发器参数等,略

其他:

1
2
3
4
5
6
7
8
// 获取可用触发器
var permitted = machine.GetPermittedTriggers();

// 检查是否可以触发特定转换
bool canFire = machine.CanFire(Trigger.CallDialed);

// 获取当前状态信息
var state = machine.State;

完整案例

下面是一个订单处理系统的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public enum OrderState
{
New,
Pending,
Confirmed,
Processing,
Shipped,
Delivered,
Cancelled
}

public enum OrderTrigger
{
Confirm,
Process,
Ship,
Deliver,
Cancel
}

public class Order
{
private StateMachine<OrderState, OrderTrigger> _machine;
public OrderState State { get; private set; }

public Order()
{
_machine = new StateMachine<OrderState, OrderTrigger>(OrderState.New);
ConfigureStateMachine();
}

private void ConfigureStateMachine()
{
_machine.Configure(OrderState.New)
.Permit(OrderTrigger.Confirm, OrderState.Confirmed)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled)
.OnEntry(() => SendNotification("New order created"));

_machine.Configure(OrderState.Confirmed)
.Permit(OrderTrigger.Process, OrderState.Processing)
.Permit(OrderTrigger.Cancel, OrderState.Cancelled)
.OnEntry(() => SendNotification("Order confirmed"));

_machine.Configure(OrderState.Processing)
.Permit(OrderTrigger.Ship, OrderState.Shipped)
.OnEntry(() => StartProcessing());

_machine.Configure(OrderState.Shipped)
.Permit(OrderTrigger.Deliver, OrderState.Delivered)
.OnEntry(() => UpdateShippingStatus());

_machine.Configure(OrderState.Delivered)
.OnEntry(() => CompleteOrder());

_machine.Configure(OrderState.Cancelled)
.OnEntry(() => HandleCancellation());
}

public void Confirm() => _machine.Fire(OrderTrigger.Confirm);
public void Process() => _machine.Fire(OrderTrigger.Process);
public void Ship() => _machine.Fire(OrderTrigger.Ship);
public void Deliver() => _machine.Fire(OrderTrigger.Deliver);
public void Cancel() => _machine.Fire(OrderTrigger.Cancel);

private void SendNotification(string message)
=> Console.WriteLine(message);

private void StartProcessing()
=> Console.WriteLine("Processing order...");

private void UpdateShippingStatus()
=> Console.WriteLine("Updating shipping status...");

private void CompleteOrder()
=> Console.WriteLine("Order completed");

private void HandleCancellation()
=> Console.WriteLine("Order cancelled");
}

使用方式

1
2
3
4
5
var order = new Order();
order.Confirm();
order.Process();
order.Ship();
order.Deliver();

自动映射库

AutoMappper,Mapster等

但自动映射使用起来要注意,查找所有引用或查找哪里使用了这种操作,无法找到自动映射的使用.这样当清除某些属性的时候,就不好判断是否有别处用了,有可能是在自动映射中使用了.这个问题也有解决方案,有辅助的插件可以生成映射代码

文档操作库

Excel操作库

  • NPOI

    支持xls

  • EPPlus

    不支持xls,但效率更高

  • MiniExcel

    不支持xls,但效率更高

对比维度 EPPlus MiniExcel
核心设计 基于 OpenXML 标准,全文档模型,适合复杂 Excel 操作(如公式、图表) 流式处理(Stream),低内存消耗,专注于基础读写和大数据处理
格式支持 仅支持 .xlsx(Excel 2007+) 主要支持 .xlsx(基于 OpenXML),不原生支持 .xls
性能(100万行) 内存占用:约 1.4GB- 导出耗时:约 22.5 秒 内存占用:约 15MB- 导出耗时:约 11.5 秒
功能特性 支持公式、图表、条件格式、样式模板等高级功能 仅支持基础读写,不支持公式、图表,但提供动态类型映射和模板填充
跨平台兼容性 依赖 .NET Framework/Core,非 Windows 环境可能受限 基于 .NET Standard,支持 Windows/Linux/macOS
API 易用性 接近 Excel 原生对象模型(如 ExcelWorksheet),学习成本较高 API 简洁(如 MiniExcel.SaveAs()),开箱即用
授权与成本 旧版本(≤4.5.3)免费,新版(≥5.0)需商业授权 完全开源免费(MIT 协议)

NPOI

Apache2开源地址

开源,支持.xls和.xlsx格式,无需安装Office,适合跨平台场景,支持格式:xls、xlsx、docx。

支持自动列宽调整

创建Workbook对象

注意: 旧版Excel(.xls)与新格式(.xlsx)的设置方法略有差异,需区分 HSSFWorkbookXSSFWorkbook

但使用下面的方式可以自动识别文件格式

使用FileStream读取文件,并使用WorkbookFactory类自动识别文件格式(需NPOI 2.0+版本)

1
2
3
4
5
6
7
8
using (FileStream fs = new FileStream(filePath, FileMode.OpenOrCreate, FileAccess.ReadWrite))
{
IWorkbook workbook = WorkbookFactory.Create(fs); // 自动判断.xls或.xlsx
ISheet sheet = workbook.GetSheetAt(0);
//任意修改...
// 将修改写回到原文件
workbook.Write(fs);
}
单元格直接赋值

通过模板中固定位置的单元格直接赋值

1
2
3
4
var sheet = workbook.GetSheetAt(0);
IRow row = sheet.GetRow(24) ?? sheet.CreateRow(24);
ICell cell = row.GetCell(2) ?? row.CreateCell(2);
cell.SetCellValue("2023-04-21"); // 替换字段

注意,数字都是从0开始的,如1表示第二行或列

单元格的赋值需要判断行和格是否存在,不存在需要新建

单元格取值

  • cell.CellType 获取单元格类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    switch (cell.CellType)
    {
    case NPOI.SS.UserModel.CellType.String:
    cellValue = cell.StringCellValue;
    break;
    case NPOI.SS.UserModel.CellType.Numeric:
    if (NPOI.SS.Util.DateUtil.IsCellDateFormatted(cell))
    {
    cellValue = cell.DateCellValue.ToString("yyyy-MM-dd HH:mm:ss");
    }
    else
    {
    cellValue = cell.NumericCellValue.ToString();
    }
    break;
    case NPOI.SS.UserModel.CellType.Boolean:
    cellValue = cell.BooleanCellValue.ToString();
    break;
    default:
    cellValue = cell.ToString();
    break;
    }
  • cell.StringCellValue 获取单元格字符串值

  • cell.BooleanCellValue 获取单元格bool值,返回bool变量

  • cell.DateCellValue 获取单元格日期值的类型,会返回一个DateTime对象

  • cell.NumericCellValue 获取单元格数值类型

  • cell.ToString() 会尝试将单元格内容转换为字符串,无论单元格是什么类型(字符串,数字,日期等)

动态列表插入

根据数据源动态生成多行,并保留模板样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 假设 templateRow 是我们要复制格式的行
IRow templateRow = sheet.GetRow(5); // 假设要复制第6行的格式(索引为5)

// 假设 data 是要插入的数据,类型为 DataTable
DataTable data = new DataTable();
// 这里填充 data...

// 首先,移动原有行以腾出插入的空间
sheet.ShiftRows(5, sheet.LastRowNum, data.Rows.Count, true, false);

int rowIndex = 5; // 从第6行开始插入
foreach (DataRow row in data.Rows)
{
IRow newRow = sheet.CreateRow(rowIndex);
newRow.Height = templateRow.Height; // 复制行高

// 复制单元格样式
for (int i = 0; i < templateRow.LastCellNum; i++)
{
ICell newCell = newRow.CreateCell(i);
ICell templateCell = templateRow.GetCell(i);

if (templateCell != null)
{
// 复制单元格的值
newCell.SetCellValue(templateCell.ToString());

// 复制单元格的样式
ICellStyle newCellStyle = workbook.CreateCellStyle();
newCellStyle.CloneStyleFrom(templateCell.CellStyle);
newCell.CellStyle = newCellStyle;
}
}

// 设置新行的值,比如从 DataRow 中获取数据
newRow.GetCell(0).SetCellValue(row["Name"].ToString()); // 假设第一列是 Name
rowIndex++; // 增加行索引
}

自适应内容宽度

支持动态调整列宽

1
2
3
4
5
6
7
8
9
10
11
using (var workbook = new HSSFWorkbook())
{
var sheet = workbook.CreateSheet("Sheet1");
// 插入数据...
for (int i = 0; i < columns.Count; i++)
{
sheet.AutoSizeColumn(i); // 自动调整列宽
sheet.SetColumnWidth(i, sheet.GetColumnWidth(i) + 512); // 附加边距
}
// 保存文件...
}
合并单元格处理

通过模板预定义合并区域,代码动态拓展

1
sheet.AddMergedRegion(new CellRangeAddress(startRow, endRow, startCol, endCol));

注意:动态插入行时,需重新计算合并区域坐标

NPOI打印相关

NPOI支持丰富的打印相关功能,主要通过设置Excel文件的页面属性来实现打印效果的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 设置横向打印与A4纸张
HSSFSheet sheet = workbook.CreateSheet("Sheet1");
sheet.PrintSetup.Landscape = true;
sheet.PrintSetup.PaperSize = 9; // A4

// 设置页边距(单位:英寸)
sheet.SetMargin(MarginType.TopMargin, 0.8 / 3);
sheet.SetMargin(MarginType.BottomMargin, 0.8 / 3);

// 定义打印区域为A1:F20
workbook.SetPrintArea(0, 0, 5, 0, 19);

// 设置每页重复的表头行(第1-2行)
sheet.RepeatingRows = new CellRangeAddress(0, 1, 0, 5);

NPOI仅配置Excel文件的打印属性(等于说只是预配置打印参数),实际打印还需要用户手动触发.需自动化打印,需结合COM组件,或第三方库

MiniExcel

不支持xls,显著降低内存占用,支持跨平台

Apache-2.0 license开源地址,用法也参考此处

安装包: dotnet add package MiniExcel

将数据填充到excel模板

暂时只需要这种用法,遂仅记录此用法

使用excel模板动态生成报表

MiniExcel的模板语法类似于Vue,使用 {{变量}} 占位符动态绑定数据,支持以下形式:

  • 基本变量填充{{Name}},对应单个字段值。
  • 集合渲染{{Items.ColumnName}},用于遍历集合数据(支持 IEnumerableDataTableDapperRow)。
  • 多Sheet填充:同一模板可绑定多个 Sheet,共用同一组参数

标记如下:

图像

导出结果如下:

图像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. By POCO
var value = new
{
Name = "Jack",
CreateDate = new DateTime(2021, 01, 01),
VIP = true,
Points = 123
};
MiniExcel.SaveAsByTemplate(path, templatePath, value);


// 2. By Dictionary
var value = new Dictionary<string, object>()
{
["Name"] = "Jack",
["CreateDate"] = new DateTime(2021, 01, 01),
["VIP"] = true,
["Points"] = 123
};
MiniExcel.SaveAsByTemplate(path, templatePath, value);
集合渲染的标记方法

标记如下:

图像

导出结果如下:

图像
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//1. By POCO
var value = new
{
employees = new[] {
new {name="Jack",department="HR"},
new {name="Lisa",department="HR"},
new {name="John",department="HR"},
new {name="Mike",department="IT"},
new {name="Neo",department="IT"},
new {name="Loan",department="IT"}
}
};
MiniExcel.SaveAsByTemplate(path, templatePath, value);

//2. By Dictionary
var value = new Dictionary<string, object>()
{
["employees"] = new[] {
new {name="Jack",department="HR"},
new {name="Lisa",department="HR"},
new {name="John",department="HR"},
new {name="Mike",department="IT"},
new {name="Neo",department="IT"},
new {name="Loan",department="IT"}
}
};
MiniExcel.SaveAsByTemplate(path, templatePath, value);
打印相关

MiniExcel主要专注于搞笑读写Excel数据,其核心功能是数据导入导出,未提供直接控制打印机的接口

但可以通过 间接打印方案 来实现打印效果

导出需要打印的Excel文件后 ==> 结合Spire.XLS等库读取MiniExcel生成的文件并触发打印

适合仅需导出标准Excel格式,不涉及复杂打印参数配置的场景

Spire.XLS

Spire.XLS 免费版对功能无限制,但生成的 Excel 文件可能包含试用版水印

提供完整的打印设置API(如双面打印,缩放比例)

静默打印

1
2
3
4
Workbook workbook = new Workbook();
workbook.LoadFromFile("Sample.xlsx");
workbook.PrintDocument.PrintController = new StandardPrintController();//避免打印进度弹框,适用于自动化场景
workbook.PrintDocument.Print(); // 直接发送打印命令
  • 优势:无需任何额外配置,适合快速实现基础打印需求。
  • 适用场景:默认打印机、无需调整打印参数(如纸张方向、份数等)

通过打印对话框自定义参数

1
2
3
4
5
6
7
8
9
10
11
12
Workbook workbook = new Workbook();
workbook.LoadFromFile("Sample.xlsx");

PrintDialog dialog = new PrintDialog();
dialog.PrinterSettings.Copies = 2; // 设置打印份数
dialog.PrinterSettings.Duplex = Duplex.Simplex; // 单面打印
workbook.PrintDialog = dialog; // 绑定对话框到工作簿

PrintDocument pd = workbook.PrintDocument;
if (dialog.ShowDialog() == DialogResult.OK) {
pd.Print(); // 用户确认后发送打印命令
}
通过COM接口调用WPS打印预览
  • 创建WPS应用程序对象 通过COM组件启动WPS进程,并获取其应用程序实例。
  • 打开目标文档 使用Documents.Open方法加载需要预览的文档。
  • 调用打印预览 调用PrintPreview方法或设置Application.PrintPreview属性触发预览窗口。
  • 资源释放 操作完成后关闭文档和应用程序实例,避免内存泄漏。

注意,需要以管理员权限运行程序,避免COM组件调用失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Runtime.InteropServices;

public class WpsPreviewHelper
{
public static void ShowPrintPreview(string filePath)
{
dynamic wpsApp = null;
dynamic doc = null;
try
{
// 创建WPS应用程序实例(需安装WPS桌面版)
Type wpsType = Type.GetTypeFromProgID("KWPS.Application");
wpsApp = Activator.CreateInstance(wpsType);
wpsApp.Visible = true; // 显示WPS界面

// 打开文档
doc = wpsApp.Documents.Open(filePath);

// 触发打印预览(方法1:直接调用)
doc.PrintPreview();

// 方法2:通过Application属性设置
// wpsApp.PrintPreview = true;
}
catch (COMException ex)
{
Console.WriteLine($"COM异常: {ex.Message}");
}
finally
{
// 释放资源
if (doc != null) Marshal.ReleaseComObject(doc);
if (wpsApp != null)
{
wpsApp.Quit();
Marshal.ReleaseComObject(wpsApp);
}
GC.Collect();
}
}
}
系统打印对话框

只需触发系统级打印预览(非WPS独占)

1
2
// C#示例:调用系统默认程序打开文档并触发打印对话框
Process.Start(new ProcessStartInfo(filePath) { UseShellExecute = true });

Word操作库

MiniWord

参考视频教程

CSharp 一些好用的控件盘点

Infragistics.Win.UltraWinGrid

CSharp 强制断点

1
System.Diagnostics.Debugger.Break();