cs14dotnet10书笔记
创建项目
- 创建一个
sln文件:dotnet new sln --name slnName - 创建控制台项目:
dotnet new console --output folderName,可以用-f net6.0指定 .NET SDK 版本 - 将项目添加到解决方案
sln文件:dotnet sln add folderName - 导航到
folderName,执行dotnet run运行项目
不使用顶级语句创建项目:
dotnet new console -o projectName --use-program-main
在vscode中运行时,启动位置在<projectname>,而在vs中,启动文件夹在<projectname>\bin\Debug\net<x.x>,可以通过Environment.CurrentDirectory验证
- 获取帮助信息:
dotnet help <command>或者dotnet <command> -?|-h|--help - 允许
unsafe块:dotnet run -p:AllowUnsafeBlocks=true - 热重载功能:
dotnet watch - 获取当前安装的
.NET SDK和运行时:dotnet --info - 查看项目模板列表:
dotnet new list dotnet help: 显示命令行帮助。dotnet new: 创建一个新的.NET项目或文件。dotnet tool: 安装或管理扩展.NET体验的工具。dotnet workload: 管理可选的工作负载,例如.NET MAUI。dotnet restore: 为项目下载依赖项。dotnet build: 构建(也称编译)一个.NET项目。.NET 8引入了一个新开关--tl(即终端日志记录器),它提供了现代化的输出方式。例如,它可以实时显示构建正在进行的操作。您可以在以下链接中了解更多信息:https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-build#options。dotnet build-server: 与由构建启动的服务器进行交互。dotnet msbuild: 运行MS Build引擎命令。dotnet clean: 移除构建过程中的临时输出。dotnet test: 构建后运行项目的单元测试。dotnet run: 构建后运行项目。dotnet pack: 为项目创建NuGet包。dotnet publish: 构建并发布项目,可以包含依赖项,也可以作为自包含应用程序发布。在.NET 7及更早版本中,默认发布的是调试配置;而在.NET 8及更高版本中,默认发布的是发布配置。dotnet add: 向项目添加对包或类库的引用。dotnet remove: 从项目中移除对包或类库的引用。dotnet list: 列出项目所引用的包或类库。dotnet package search: 允许您在一个或多个包源中搜索与搜索词匹配的包。命令格式为:dotnet package search[搜索词][选项]。您可以在以下链接中了解更多信息:https://devblogs.microsoft.com/nuget/announcing-nuget-6-9/#support-for-dotnet-search-command。
发布:- 自包含:
dotnet publish -c Release -r win-x64 --self-contained - 单文件:
dotnet publish -r win-x64 -c Release --no-self-contained -p:PublishSingleFile=true - 修剪以减小程序大小
- 开启修剪/完全修剪:
dotnet publish ... -p:PublishTrimmed=true - 部分修剪:
TrimMode=partial
- 开启修剪/完全修剪:
语法
- 语句:
decimal totalPrice = subtotal + salesTax; - 单行注释:
// Sales tax must be added to the subtotal. - 多行注释
/*
This is a
multi-line comment.
*/
多行注释也可以用在行内,比如
var a = /*一些注释*/ 1 + 2;
引入namespace
可以在cs文件顶部引入特定namesapce,例如
using System;
using System.Linq;
using System.Collections.Generic;
当有多个cs文件,有些namesapce经常使用,需要在很多文件内都使用,可以新建一个GlobalUsings.cs文件,然后使用global关键词
global using System;
global using System.Linq;
global using System.Collections.Generic;
全局静态引入类型
对于经常使用的类型,比如Console,无需在每个文件中引入,操作.csproj文件,在PropertyGroup下方,写入类似下面的内容
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
...
</PropertyGroup>
<ItemGroup Label="Simplify console app">
<Using Include="System.Console" Static="true" />
</ItemGroup>
</Project>
如此便可在项目的所有文件内直接使用ReadLine()、WriteLine()等方法
变量命名规则
- 驼峰命名法:局部变量、私有字段,
- 帕斯卡命名法:类型、非私有字段、其他成员(如方法)
| 命名法 | 用于 | 含义 | 示例 |
|---|---|---|---|
| 驼峰 | 局部变量、私有字 | 除首字母外的其他单词开头大写 | cost, orderDetail |
| 帕斯卡 | 类型、非私有字段、其他成员(如方法) | 所有单词首字母大写 | String, Int32, Cost |
字符串字面量
- 普通字符串
string filePath = "C:\televisions\sony\bravia.txt"; - 带转义的
string fullNameWithTabSeparator = "Bob\tSmith"; - 不带转义的
string filePath = @"C:\televisions\sony\bravia.txt"; - 原始的字符串字面量,可以方便的定义包含其他语言的内容,可以使用3个及以上的双引号,例如:如果内容里面也有3个双引号,则整体可以使用4个双引号包裹。结果中并不会完整保留每行前方的空格,编译器会删除与最后3个双引号等量的缩进。
string xml = """
<person age="50">
<first_name>Mark</first_name>
</person>
""";
- 原始字符串字面量与字符串插值的结合,前面几个
$则差值匹配几对{}
var person = new { FirstName = "Alice", Age = 56 };
string json = $$"""
{
"first_name": "{{person.FirstName}}",
"age": {{person.Age}},
"calculation": "{{{ 1 + 2 }}}"
}
""";
Console.WriteLine(json);
输出结果为
{
"first_name": "Alice",
"age": 56,
"calculation": "{3}"
}
数字
int、uint、float、double、decimal
float类型的字面量需要在末尾加fdecimal类型字面量需要在末尾加M- 数字中可以在任意位置添加下划线,使其更易读,例如
10_00_000 - 十六进制数:以
0x开头 - 二进制数:以
0b开头
永远不要使用==来判断两个double类型是否相等,decimal类型内存储的是整数,所以是精确的
System.Console.WriteLine(Compare((double)0.1+(double)0.2, (double)0.3));
System.Console.WriteLine(Compare((float)0.1+(float)0.2, (float)0.3));
System.Console.WriteLine(Compare((decimal)0.1+(decimal)0.2, (decimal)0.3));
string Compare<T>(T expression, T expected)
{
return EqualityComparer<T>.Default.Equals(expression, expected) ? "PASS" : "FAIL";
}
运行结果为
FAIL
PASS
PASS
特殊的数字
NaN:not-a-number,例如 0/0Epsilon:可以被float或double存储的最小的正数PositiveInfinity:正无穷NegativeInfinity:负无穷
检查方法IsInfinity:检测是否是无穷IsNaN:检测是否为NaN
System.Console.WriteLine(double.Epsilon);
System.Console.WriteLine(double.PositiveInfinity);
System.Console.WriteLine(double.NegativeInfinity);
输出结果为
5E-324
∞
-∞
新引入的数据类型
System.Half:存储实数,通常使用2个字节System.Int128:存储有符号整数,通常使用16字节ystem.UInt128:存储无符号整数,通常使用16字节
验证,对于新类型,使用sizeof需要在unsafe块内,编译命令为dotnet run -p:AllowUnsafeBlocks=true
unsafe
{
System.Console.WriteLine(sizeof(Half));
System.Console.WriteLine(sizeof(Int128));
System.Console.WriteLine(sizeof(UInt128));
}
object 类型
有性能损失,应避免使用
object height = 1.88;
object name = "BOS";
Console.WriteLine($"{name} is {height}m tall.");
//int length1 = name.Length; // error
int length2 = ((String)name).Length;
Console.WriteLine($"{name} has {length2} characters.");
自动推断类型
编译时进行推断,不会影响运行时的性能,使用方法为var varName = value;
- 对于没有小数点的数字,默认为
int类型,除非有特定后缀
| 后缀 | L | UL | M | D | F |
|---|---|---|---|---|---|
| 推断类型 | long | ulong | decimal | double | float |
- 对于带小数点的数字,默认推断类型为
double,除非有特定后缀
| 后缀 | M | F |
|---|---|---|
| 推断类型 | decimal | float |
- 双引号推断为
string - 单引号推断为
char,注意char可能是2个字节 true|false推断为bool
new
//在栈中分配空间,指向堆中的对象,默认为 null
Person bob;
//在堆中分配空间以存储Person对象,bob 不为 null
bob = new Person();
// 在堆中分配空间并且初始化状态
bob = new Person("Bob", "Smith", 45);
target-typed new
在c# 9及以后,在new时可以不重复类型名
XmlDocument xml3 = new();
Person kim = new();
kim.BirthDate = new(1967, 12, 26); // new DateTime(1967, 12, 26)
class Person
{
public DateTime BirthDate;
}
在 arrays和 collections中更有用
List<Person> people = new() // Instead of: new List<Person>()
{
new() { FirstName = "Alice" }, // Instead of: new Person() { ... }
new() { FirstName = "Bob" },
new() { FirstName = "Charlie" }
};
值类型的默认值
default(<type>),比如default(int)int number; number = default;
格式化字符串
格式化字符串的格式为{ index [, alignment ] [ : formatString ] }
index 表示索引序号,alignment 表示对其方向,负数左对齐,正数右对齐
formatString:
N0:有千位分隔符且没有小数点的数字C:当地钱币符号
格式化字符串通常用于表格对齐
string applesText = "Apples";
int applesCount = 1234;
string bananasText = "Bananas";
int bananasCount = 56789;
Console.WriteLine();
Console.WriteLine(format: "{0,-10} {1,6}", "Name", "Count");
Console.WriteLine(format: "{0,-10} {1,6:N0}", applesText, applesCount);
Console.WriteLine(format: "{0,-10} {1,6:N0}",
arg0: bananasText, arg1: bananasCount);
自定义数字格式字符串
标准数字格式字符串
自定义日期和时间格式字符串
标准日期和时间格式字符串
格式化字符串示例:
decimal value = 0.325M;
Console.WriteLine("Currency: {0:C}, Percentage: {0:0.0%}", value);
可空类型与 null-forgiving 操作符
string firstName = Console.ReadLine();
编译器会显示警告将 null 文本或可能的 null 值转换为不可为 null 类型
通常情况下,ReadLine并不会返回null,有以下几种消除警告的方法
- 声明变量为可空类型
string? firstName = Console.ReadLine(); - 告诉编译器不会返回空值
string firstName = Console.ReadLine()!;,即在末尾加!
获取用户输入
注意,在编辑器内置终端中执行,某些组合键可能会被编辑器捕获
获取用户按下的组合键
Console.Write("按下任意键组合");
ConsoleKeyInfo key = Console.ReadKey();
Console.WriteLine();
Console.WriteLine($"Ket: {key.Key}, Char: {key.KeyChar}, Modifiers: {key.Modifiers}");
输出
按下任意键组合a
Ket: A, Char: a, Modifiers: Alt
控制台项目的输入参数
传递参数的方式为dotnet run <参数1 参数2 ...>,只有空格是有效分隔符,如果参数中有空格,则需要用引号包裹
WriteLine($"传入的参数数量为{args.Length}");
foreach (var arg in args)
{
WriteLine(arg);
}
控制流程、类型转换、处理异常
操作符
三元操作符,应当多用
var result = boolean_expression ? value_if_true : value_if_false;
注意
/运算时,如果两个操作数都是int,则返回值也是int类型
组合操作符:+=, -=, *=, /=
空合并操作符:
?.:对象null返回null,不为空则返回成员值??:左侧表达式为null则返回右侧值,否则什么也不做??=:左侧表达式为null,使用右侧的值赋值,否则什么也不做,用于对null变量赋值
特殊操作符nameof:返回变量、类型、成员的名称字符串sizeof:返回简单类型占用的字节数,需要unsafe块。但是像int、double这样的大小被编译器硬编码为常量,不需要unsafe块
(条件)逻辑操作
&:与,左右都是true才返回true|:或,有true则返回ttue^:异或,两侧不相同才返回true
条件逻辑操作符(短路特性),$$,||$$:左侧为false则不执行右侧,直接返回false||:左侧为true则不执行右侧,直接返回true
逐位操作
按位与、或、异或:$, |, ^,注意操作数是int类型,如果是bool类型则是逻辑操作
移位:<<, >>
模式匹配
if语句中的模式匹配
在if语句中varName is Type newVarName,起2个作用
- 检查是否为指定的类型
- 如果
treu,则将其转换为指定类型并赋值给指定的变量
object o = 3;
int j = 4;
if (o is int i)
{
Console.WriteLine($"{i} x {j} = {i * j}");
}
else
{
Console.WriteLine("o is not an int so it cannot multiply!");
}
在C# 9+,可以使用 is not表示非
switch语句中的模式匹配
case后的值不一定是字面量,还可以跟when表达式精确匹配。
注意在switch中,无论default在哪,都是最后匹配,但通常总是把default放在最后
常规switch模式匹配示例如下
var animals = new Animal?[]
{
new Cat{Name="Karen",
Born = new(2025, 8, 20),
Legs=4,
IsDomestic=true},
null,
new Cat{Name="Mufasa", Born=new(2020, 5, 1)},
new Spider{Name= "Peter", Born=DateTime.Today, IsVenomous=true},
new Spider{Name="Miles", Born=DateTime.Today}
};
foreach (Animal? animal in animals)
{
string message;
switch (animal)
{
case Cat fourLegsCat when fourLegsCat.Legs == 4:
message = $"猫{fourLegsCat.Name}有四条腿";
break;
case Cat wildCat when wildCat.IsDomestic == false:
message = $"{wildCat.Name}是野生猫";
break;
case Cat cat:
message = $"{cat.Name}是猫";
break;
default:
message = $"{animal.Name} 是 {animal.GetType().Name}";
break;
case Spider spider when spider.IsVenomous == true:
message = $"{spider.Name} 是有毒蜘蛛,快跑!";
break;
case null:
message = "这是个空对象";
break;
}
System.Console.WriteLine($"switch的输出为{message}");
}
class Animal
{
public string? Name { get; set; }
public DateTime Born { get; set; }
public byte Legs { get; set; }
}
class Cat : Animal
{
public bool IsDomestic { get; set; }
}
class Spider : Animal
{
// 是否有毒的
public bool IsVenomous { get; set; }
}
其中case Cat fourLegsCat when fourLegsCat.Legs == 4:还可以表述为case Cat { Legs: 4 } fourLegsC
当switch语句的作用是在不同情况下为变量赋值,且语句较简单,则可以简化:
foreach (Animal? animal in animals)
{
string message = animal switch
{
Cat fourLegsCat when fourLegsCat.Legs == 4
=> $"猫{fourLegsCat.Name}有四条腿",
Cat wildCat when wildCat.IsDomestic == false
=> $"{wildCat.Name}是野生猫",
Cat cat => $"{cat.Name}是猫",
Spider spider when spider.IsVenomous == true
=> $"{spider.Name} 是有毒蜘蛛,快跑!",
null => "这是个空对象",
_ => $"{animal.Name} 是 {animal.GetType().Name}",
};
System.Console.WriteLine($"switch的输出为{message}");
}
其中_表示default的情况
增强的模式匹配
先定义一些类
public class Passenger
{
public string? Name { get; set; }
}
public class BusinessClassPassenger : Passenger
{
public override string ToString()
{
return $"商务仓乘客:{Name}";
}
}
public class FirstClassPassenger : Passenger
{
public int AirMiles { get; set; }
public override string ToString()
{
return $"头等仓乘客:{Name}, 里程数:{AirMiles}";
}
}
public class EconomyClassPassenger : Passenger
{
public double CarryOnKG { get; set; }
public override string ToString()
{
return $"经济仓乘客:{Name}, 行李重量:{CarryOnKG}KG";
}
}
再使用模式匹配,计算乘客的票价应该是多少
Passenger[] passengers =
{
new FirstClassPassenger { Name = "BOS", AirMiles = 1_000 },
new FirstClassPassenger { Name = "Lucy", AirMiles = 1_562 },
new BusinessClassPassenger { Name = "Bella"},
new EconomyClassPassenger { Name = "Dave", CarryOnKG = 23.6 },
new EconomyClassPassenger { Name = "Amit", CarryOnKG = 0 }
};
C# 8语法的模式匹配
// 遍历
foreach (var passenger in passengers)
{
decimal flightCost = passenger switch
{
FirstClassPassenger p when p.AirMiles > 35_000 => 1_500M,
FirstClassPassenger p when p.AirMiles > 15_000 => 1_750M,
FirstClassPassenger _ => 2_000M,
BusinessClassPassenger _ => 1_000,
EconomyClassPassenger p when p.CarryOnKG < 10.0 => 500M,
EconomyClassPassenger _ => 650M,
_ => 800M
};
System.Console.WriteLine($"{passenger} 的航班价格是 {flightCost:C}");
}
System.Console.WriteLine();
C# 9+语法 嵌套的switch
foreach (var passenger in passengers)
{
decimal flightCost = passenger switch
{
FirstClassPassenger p => p.AirMiles switch
{
> 35_000 => 1_500M,
> 15_000 => 1_750M,
_ => 2_000M
},
BusinessClassPassenger => 1_000,
EconomyClassPassenger p when p.CarryOnKG < 10.0 => 500M,
EconomyClassPassenger => 650M,
_ => 800M
};
System.Console.WriteLine($"{passenger} 的航班价格是 {flightCost:C}");
}
如果想避免嵌套switch,可以结合使用关系匹配和属性匹配
decimal flightCost = passenger switch
{
FirstClassPassenger {AirMiles: > 35_000} => 1_500M,
FirstClassPassenger {AirMiles: > 15_000} => 1_750M,
FirstClassPassenger => 2_000M,
BusinessClassPassenger => 1_000,
EconomyClassPassenger {CarryOnKG: < 10.0}=> 500M,
EconomyClassPassenger => 650M,
_ => 800M
};
数组
1维数组
有2种声明方法
string[] names = new string[4];
string[] names2 = { "Kate", "Jack", "Rebecca", "Tom" };
取某个元素names[<index], index从0开始
2维数组
声明,若在声明时初始化,则需要定义每个元素,空值可以定义为string.Empty,如果可空的话string?[]还可以定义为null()
// 声明时初始化
string[,] grid1 =
{
{ "Alpha", "Beta", "Gamma", "Delta" },
{ "Anne", "Ben", "Charlie", "Doug" },
{ "Aardvark", "Bear", "Cat", "Dog" }
};
// 声明时不初始化
string[,] grid2 = new string[3,4];
访问grid1[row, col]
嵌套不规则数组(交错数组)
string[][] jagged =
[
[ "Alpha", "Beta", "Gamma" ],
[ "Anne", "Ben", "Charlie", "Doug" ],
[ "Aardvark", "Bear" ]
];
列表模式匹配
| 示例 | 说明 |
|---|---|
[] |
匹配空数组或空集合。 |
[..] |
匹配包含任意数量项(包括零项)的数组或集合;若需同时匹配空与非空,[..] 必须放在 [] 之后。 |
[7, 2] |
精确匹配包含两个指定值且顺序一致的列表。 |
[_] |
匹配包含任意单个元素的列表。 |
[_, _] |
匹配包含任意两个元素的列表。 |
[_, _, _] |
匹配包含任意三个元素的列表。 |
[int item1] 或 [var item1] |
匹配包含任意单个元素的列表,并可在返回表达式中通过引用 item1 使用该值。 |
[var item1, var item2] |
匹配包含任意两个元素的列表,并可在返回表达式中通过引用 item1 和 item2 使用这两个值。 |
[var item1, ..] |
匹配包含一个或多个元素的列表。可在返回表达式中通过引用 item1 使用第一个元素的值。 |
[var firstItem, .., var lastItem] |
匹配包含两个或更多元素的列表。可在返回表达式中通过引用 firstItem 和 lastItem 使用首尾元素的值。 |
[.., var lastItem] |
匹配包含一个或多个元素的列表。可在返回表达式中通过引用 lastItem 使用最后一个元素的值。 |
类型转换
强制类型转换
(<type>)varName
可能会导致溢出
使用System.Convert转换
System.Convert不会导致溢出,而是产生异常
Convert.ToInt32:四舍五入,舍入原则为
- 若小数部分<0.5,则向0舍入
- 若小数部分>0.5,则向远离0的方向舍入
- 若小数部分是0.5
- 当整数部分是奇数,则向远离0的方向舍入
- 当整数部分是偶数,则向0舍入
控制舍入规则
Math.Round(value: n, digits: 0, mode: MidpointRounding.AwayFromZero));
转换溢出异常
捕获溢出异常
使用checked检查转换异常System.OverflowException
checked // 去掉 checked 就没有异常了
{
int a = int.MaxValue;
a += 1;
}
可以用try-catch捕获异常
try
{
checked
{
int a = int.MaxValue;
a += 1;
}
}
catch (System.OverflowException)
{
System.Console.WriteLine("Overflow");
}
关闭溢出异常检查
unchecked //直接写 int y = int.MaxValue + 1; 编译器会报错
{
int y = int.MaxValue + 1;
System.Console.WriteLine(y);
}
抛出异常
基础异常抛出
.NET已经定义了很多异常类型,一般无需再自行定义。
double count = 0;
if (count <= 0)
{
throw new ArgumentOutOfRangeException(nameof(count));
}
卫语句
不用实例化一个异常,而是使用异常的静态方法,可以简化代码
double count = 0;
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value: count, paramName: nameof(count));
重新抛出异常
某些情况下在捕获异常后还需重新抛出异常,有3种方法
- 抛出带有原始调用堆栈的异常
try {...}
catch { throw; }
- 将异常在当前级别抛出,这是一种不好的做法,除非需要剔除敏感信息。可能会损失信息,编译器也会产生警告
try
{
}
catch(IOException ex)
{
throw ex;
}
- 将捕获的异常包装在另一个异常之中
try
{
}
catch (IOException ex)
{
throw new InvalidOperationException(message:"补充的异常信息", innerException:ex);
}
编写、调试和测试函数
XUnitTest单元测试
通常单元测试分为3步
arrange:准备输入输出变量,act:执行要测试的方法assert:断言,比较实际输出与预期输是否一致。
通常,期望的输出命名为expected,测试方法得到的输出为actual,执行的断言为Assert.Equal(expected, actual);
测试一种参数
[Fact]
public void TestTheMethod()
{
// Arrange
double a = ;
double b = ;
double expected = ;
// Act
double actual = Method(a, b);
// Assert
Assert.Equal(expected, actual);
}
同时测试多种参数
InlineData的参数需要与函数参数类型一致
[Theory]
[InlineData(1,2,3)]
[InlineData(0,2,5)]
[InlineData(8,0,0)]
[InlineData(...,...,...)]
public void TestTheMethod(double a, double b, double expected)
{
// Arrange
MyClass my = new();
// Act
double actual = Method(a, b);
// Assert
Assert.Equal(expected, actual);
}
面向对象
- C#中使用
calss,record,struct定义类型 - 类可以嵌套,即一个类可以定义在另一个类内部
- 可以用
partial拆分类,public partial class MyClass
文件范围的 namespace
传统上,需要把类包裹在 namespace 的花括号中
namespace MyNamespace
{
public class MyClass
{...}
}
在C#10+,可以直接在namespace 后加分号;,表示整个文件都在该namespace下,可以减少嵌套
namespace MyNamespace;
public class MyClass
{...}
namespace 的别名
当两个namesapce中有相同的个类型时,单纯地using两个namespace并使用重复的类型,编译器会报错,但可以给其中一个namesapce设置别名来避免冲突
namespace France
{public class Paris {} }
namespace Texas
{public class Paris {} }
在其他文件中
using France;
using Texas;
Paris p = new(); // 错误
设置别名
using France;
using Tx = Texas;
Paris p = new(); // France 命名空间
Tx.Paris p2 = new(); // Texas 命名空间
类型的别名
类型的别名和namespace的别名设定一样
using Env = System.Environment;
System.Console.WriteLine(Env.OSVersion);
字段
字段:在类内部定义的变量,字段通常不是public的,因为无法控制其内容,然后创建public属性来对其控制
class Person
{
public string? Name;
}
静态字段
只能通过类名访问,即BankAccount.InterestRate,不能通过实例访问
public class BankAccount
{
public string? AccountName;
public decimal Balance;
public static decimal InterestRate;
}
常量字段
使用const关键字,在编译时分配字面量,不可更改该值,只能
public const string? Speices = "Homo Sapines"
只读字段
只读字段可以在运行时计算,可以通过构造函数或字段赋值来设置,通常应使用只读字段而不是常量字段
public readonly string HomePlanet = "Earth"
静态只读字段
值在实例之间共享
require字段
只有.NET 7+和C# 11+才支持,所以.NET standard 2.0不支持
使用required的字段必须在实例化时设定值,否则编译器会报错
如果在构造函数中设置了required字段的值,则必须在该构造函数上方使用[SetRequiredMembers],否则使用该构造函数时编译器仍会报错未设定required成员
属性
字段会提供数据的地址,因此外部功能可以随意修改它的值,但属性不提供内存地址,当读取或设置值时,属性会执行控制语句,决定如何响应。
只读属性
删除set即成为只读属性
// 适用于 C# 1~5的只读属性
public string Origin { get { return string.Format("{0}出生于{1}", arg0: Name, arg1: Born); } }
// 数用于 C# 6+ , 使用 lambda 表达式
public string Greeting => $"{Name} 说:你好!";
可读写属性
代码片段:prop
属性都有对应的字段用于存储数据,即使没有手动创建,编译器也会自动创建
public string? FavoriteIceCream { get; set; }
完整属性
代码片段:propfull
private string? _favoritePrimaryColor;
public string? FavoritePrimaryColor
{
get { return _favoritePrimaryColor; }
set
{
_favoritePrimaryColor = value switch
{
"red" or "green" or "blue" => value,
_ => throw new ArgumentOutOfRangeException(nameof(value), "请输入正确的主要颜色。范围在 red, green, blue 之间。"),
};
}
}
field属性
在C# 14中,可以通过关键字field访问编译器自动创建的支持字段,无需手动创建完整的属性
public string? FavoritePrimaryColor
{
get { return field; }
set
{
field = value?.ToLower() switch
{
"red" or "green" or "blue" => value,
_ => throw new ArgumentOutOfRangeException(nameof(value), "请输入正确的主要颜色。范围在 red, green, blue 之间。"),
};
}
}
枚举
public enum Wonders
{
GreatWall,
ChichenItza,
}
使用枚举存储多个值
每个枚举的某一位是1,其余为0,这样可以实现组合,需要使用[Flags]修饰,确保每项的位不重叠。
枚举允许继承整数类型,byte, sbyte, Int16, Int32, Int64, UInt16, UInt32, UInt64,但Int128和UInt128不受支持
[Flags]
public enum Wonders: byte
{
None = 0b_0000_0000,
GreatWall = 0b_0000_0001,
ChichenItza = 0b_0000_0010,
TajMahal = 0b_0000_0100,
}
使用时可以用|运算符设置多个值,返回以,分隔的字符串
方法
- 方法可以有/无返回值,也可以重载
- 一个方法可以在另一个方法内部定义,成为局部函数
- 和
partial类一样,也有partial方法, - 一部分为方法的声明(签名),另一部分为方法的具体实现。
- 如果未实现,编译器会移除对该方法的调用,并且不会抛出错误
- 必须返回
void,不能有out参数 - 不能是
virtual
可选参数
必须位于参数列表的末尾public string MyMethod(par1, par2, par3 = "Run"){...}
调用时指定参数名
当有多个可选参数时,可以指定要设置的某些可选参数。当指定参数名时,也可以不按顺序填写参数
MyMethod("BOS", createDate: new DateOnly(2026, 1, 9));
参数的传递方式
- 值传递:默认情况,传递的是值,而不是变量自身,方法不会更改变量的值
in: 将变量的引用传递到方法,如果变量值更改,在方法内部也会体现出来,不允许在方法内部对变量进行修改,因为是只读的ref:变量的引用被传递到方法中,会改变原始变量的值out:传入的参数原始值被舍弃,并使用方法内运算后的值
public static void PassingParameters(int w, in int x, ref int y, out int z)
{
z = 100;
w++;
y++;
z++;
// x++; // 会报错
System.Console.WriteLine($"方法内部:w: {w}, x: {x}, y: {y}, z: {z}");
}
// 调用
int a = 10;
int b = 20;
int c = 30;
System.Console.WriteLine($"方法之前:a: {a}, b: {b}, c: {c}, d: {null}");
PassingParameters(a, b, ref c, out int d);
System.Console.WriteLine($"方法之后:a: {a}, b: {b}, c: {c}, d: {d}");
可变数量的参数
C# 13+可以传递collection表达式,例如List<T>, IEnumberable<T>
params关键字,注意在方法中只能定义一次,必须位于最后
- 可以传递独立的参数,编译器会将其打包为
array, - 可以传递
array或者collection表达式 - 可以在调用方法时不传递任何参数,此时会传递一个空
array
public static void ParamsPass(string text, params int[] numbers)
{...}
元组
元组可以将多个值组合为一个单元,可以用于方法返回多个值
元组变量的基础定义
var thing1 = ("BOS", 100);
Person bos = new() { Name = "BOS", Born = new DateTimeOffset(2026, 1, 9, 21, 28, 0, TimeSpan.FromHours(8)) };
var thing2 = (bos.Name, bos.Born);
// 注意这里的参数名
System.Console.WriteLine($"thing2: {thing2.Name}, {thing2.Born}");
元组作为方法返回值
元组有命名和不命名的使用方式
- 基础调用方式
public static (string, int) GetFruit()
{
return ("Apple", 10);
}
// 或者
public static (string Name, int Number) GetFruit()
{
return ("Apple", 10);
}
// 或者
public static (string Name, int Number) GetFruit()
{
return (Name: "Apple", Number: 10);
}
// 调用
(string, int) fruit = GetFruit();
System.Console.WriteLine($"tuple fruit: {fruit.Item1}, {fruit.Item2}");
- 给返回的参数命名
public static (string Name, int Number) GetFruit()
{
return (Name: "Apple", Number: 10);
}
// 调用
var fruit = GetFruit();
System.Console.WriteLine($"tuple fruit: {fruit.Name}, {fruit.Number}");
- 方法中没有命名返回值,但调用时命名
public static (string, int) GetFruit()
{
return ("Apple", 10);
}
共有2种调用方式
// 调用方式1
(string Name, int Number) fruit = GetFruit();
System.Console.WriteLine($"tuple fruit: {fruit.Name}, {fruit.Number}");
// 也可以继续使用 Item1 和 Item2 来访问
System.Console.WriteLine($"tuple fruit: {fruit.Item1}, {fruit.Item2}");
// 调用方式2
(string Name, int Number) = GetFruit();
System.Console.WriteLine($"tuple fruit2: {Name}, {Number}");
元组的别名
c# 12+可以给元组创建别名
using UnnamedParameters = (string, int);
// 首字母应大写,例如 Name,Number
using Fruit = (string Name, int Number);
Fruit fruit = GetFruit();
使用元组解构其他类型
任何类型都有一个Deconstruct的方法,多个Deconstruct的参数数量必须不同,该方法会自动调用,只需将对象分配给元组变量
// 在 Person 类内定义
public void Deconstruct(out string? name, out DateTimeOffset born)
{
name = Name;
born = Born;
}
public void Deconstruct(out string? name, out DateTimeOffset born, out Wonders fav)
{
name = Name;
born = Born;
fav = favouriteWonder;
}
// 调用
Person bos = new() { Name = "BOS", Born = new DateTimeOffset(2026, 1, 9, 21, 28, 0, TimeSpan.FromHours(8)), favouriteWonder = Wonders.Petra };
var (name1, born1) = bos;
System.Console.WriteLine($"bos: {name1}, {born1}");
var (name2, born2, fav2) = bos;
System.Console.WriteLine($"bos: {name2}, {born2}, {fav2}");
自定义索引器[, ]
索引器是类似访问数组元素的操作,例如myArray[0]
注意此处this[]指代索引器,而不是当前对象
public List<Person> Children = new();
// 整数索引器 bos[0]
public Person this[int index]
{
get => Children[index];
set => Children[index] = value;
}
// 设置一个字符串索引器 bos["Bella"]
public Person this[string name]
{
get => Children.Find(p => p.Name == name);
}
init-only属性
.NET Standard 2.0不支持
只能在初始化器或者构造函数中对这类属性赋值,之后不可更改该值
即使在创建对象时未对该属性赋值,以后也不能再赋值,如有必要可添加required关键字要求必须对其初始化
public class ImmutablePerson
{
public string? FirstName { get; init; }
public string? LastName { get; init; }
}
// 调用
ImmutablePerson bos = new()
{
FirstName = "B",
LastName = "OS"
};
bos.FirstName = "Geoff"; // 报错
record类型
with创建一个某些状态改变后的副本,即使 car被销毁,repaintedCar也会保持存在,即实现非破坏性修改。
record类型通过属性值判断相等性
public record ImmutableVehicle
{
public int Wheels { get; init; }
public string? Color { get; init; }
public string? Brand { get; init; }
}
ImmutableVehicle car = new()
{
Brand = "xiao mi",
Color = "Green",
Wheels = 4
};
ImmutableVehicle repaintedCar = car with { Color = "Red" };
System.Console.WriteLine($"原始颜色为{car.Color},修改后的颜色为{repaintedCar.Color}");
record也可以自动生成构造函数和解构函数
public record ImmutableAnimal
{
public string Name { get; init; }
public string Species { get; init; }
public ImmutableAnimal(string name, string species)
{
Name = name;
Species = species;
}
public void Deconstruct(out string name, out string species)
{
name = Name;
species = Species;
}
}
等价于public record ImmutableAnimal(string Name, string Species);
// 调用
ImmutableAnimal oscar = new("Oscar", "Labrador");
var (who, what) = oscar; // 调用解构函数
System.Console.WriteLine($"{who}是{what}");
主构造函数
初始化非只读private字段/属性,并且不需要执行其他语句,才用主要构造函数
public class Headset(string manufacturer, string productName);
// 调用
Headset vp = new("Apple", "Vision Pro");
// 这句会报错,因为默认是 private 的,类外不能访问
System.Console.WriteLine($"{vp.productName} 由{vp.manufacturer}制造");
修改为下面这样,就可以正常使用了
public class Headset(string manufacturer, string productName)
{
public string Manufacturer { get; set; } = manufacturer;
public string ProductName { get; set; } = productName;
}
使用默认无参构造函数调用主要构造函数
public class Headset(string manufacturer, string productName)
{
public string Manufacturer { get; set; } = manufacturer;
public string ProductName { get; set; } = productName;
// 当默认构造函数被调用时,传递2个参数
public Headset() : this("Microsoft", "HoloLens") { }
}
实现接口、继承类
可以写具有相同或类似功能的静态方法和实例方法,以便使用。实例方法可以调用静态该方法,反之不行
运算符重载
返回类型不能是void
// +
public static bool operator +(Person p1, Person p2)
{
Marrry(p1, p2);
return p1.Married && p2.Married;
}
// *
public static Person operator *(Person p1, Person p2)
{
return Procreate(p1, p2);
}
输出对象图
使用的nuget库为Dumpify,所需namesapce为Dumpify
using Dumpify;
lamech.Dump();
输出类似这样的图像
Person
┌──────────┬─────────────────────────────────────────────────────┐
│ Name │ Value │
├──────────┼─────────────────────────────────────────────────────┤
│ Name │ "Lamech" │
│ Born │ [0001/1/1 0:00:00 +00:00] │
│ Children │ ┌────────────────────────────────────────────┐ │
│ │ │ List<Person> │ │
│ │ ├────────────────────────────────────────────┤ │
│ │ │ Person │ │
│ │ │ ┌──────────┬─────────────────────────────┐ │ │
│ │ │ │ Name │ Value │ │ │
│ │ │ ├──────────┼─────────────────────────────┤ │ │
│ │ │ │ Name │ "Jabal" │ │ │
│ │ │ │ Born │ [2026/1/10 20:31:08 +08:00] │ │ │
│ │ │ │ Children │ ┌──────────────┐ │ │ │
│ │ │ │ │ │ List<Person> │ │ │ │
│ │ │ │ │ └──────────────┘ │ │ │
│ │ │ │ Spouses │ ┌──────────────┐ │ │ │
│ │ │ │ │ │ List<Person> │ │ │ │
泛型
非泛型的例子,不推荐使用非泛型,因为没有任何检查,类型转换也会损失性能
Person harry = new() { Name = "Harry" };
System.Collections.Hashtable lookupObject = new();
lookupObject.Add(1, "Alpha");
lookupObject.Add(2, "Beta");
lookupObject.Add(3, "Gamma");
// 此处使用一个 Person 对象作为 key
lookupObject.Add(harry, "Delta");
// 查找
int key = 2;
Console.WriteLine($"{key}: {lookupObject[key]}");
Console.WriteLine($"{harry}: {lookupObject[harry]}");
使用泛型
除非因为使用旧的非泛型库,否则永远不要使用可以存储任何类型的非泛型代码
通常,只有1个类型时,命名为T,有多个类型时,命名为T开头的名称,例如TKey, TValue
Dictionary<int, string> lookupIntString = new();
lookupIntString.Add(1, "alpha");
lookupIntString.Add(2, "beta");
lookupIntString.Add(3, "gamma");
// lookupIntString.Add(harry, "delta"); // 类型检查报错,保证安全
lookupIntString.Add(4, "gamma");
// 查找值
key = 3;
Console.WriteLine($"{key}: {lookupIntString[key]}");
泛型约束
有些情况,泛型不能为任意类型,而是需要进行某些限制,使用 where 约束
where T : struct: 非空值类型where T : class: 引用类型,如类、接口、数组、委托where T : new(): 必须有public无参构造函数,和其他约束组合时必须放在最后where T: 基类名称: 必须继承指定类where T: 接口名称: 必须实现指定接口- 多个约束之间用
,分隔 - 多个泛型类型,可以写多个
where约束
委托和事件
委托
委托类似与函数指针public delegate <return type> <delegate-name> <parameter list>
委托可以绑定多个函数,并按添加顺序依次调用,类似函数列表
// 声明
public delegate int MathOperation(int x, int y);
// 实例化
MathOperation m1 = new MathOperation(Add);
m1 += Add2;
// 调用
m1(6, 5);
int Add(int x, int y)
{
return x+y;
}
int Add2(int x, int y)
{
return x+y + 1;
}
内置的还有 Func, Action, Predicate 泛型委托
事件
事件的本质是委托。两个预定义事件
// 用于不需要额外传递参数的方法
public delegate void EventHandler(object? sender, EventArgs e);
// 使用泛型传递额外参数值
public delegate void EventHandler<TEventArgs>(object? sender, TEventArgs e);
使用示例
// 定义事件
public event EventHandler? Shout;
// 调用委托
Shout(this, EventArgs.Empty); // 方式1
Shout.Invoke(this, EventArgs.Empty); // 方式2
// 异步调用
IAsyncResult result = Shout.BeginInvoke(this, EventArgs.Empty, null, null);
public event EventHandler? Shout;:
是事件,外部只能+=/-=订阅,不能直接调用或赋值,安全且符合规范。public EventHandler? Shout;:
是普通委托字段,外部可随意赋值、清空、调用,不安全,不应用于事件。
lambda匿名函数
结合委托,使lambda表达式作为函数的参数,以增强灵活性
namespace TEST;
public class Program
{
public delegate bool MyDelegate(Person a, Person b);
public static void Main(Person person)
{
List<Person> list =
new() {
// 一些 Person 对象
};
// 调用
MySort(list, (a,b)=>a.Id>b.Id);
void MySort(List<Person> list, MyDelegate func)
{
// 根据 func(Person1, Person2) 的结果排序
}
}
}
public class Person(int age, int id)
{
public int Age { get; set; } = age;
public int Id { get; set; } = id;
}
接口
接口即约定
注意:C# 8+、 .NET 5+、.NET Core 3+、.NET Standard 2.1+还支持在接口中使用默认实现,以便于以后对接口添加新内容
常用接口
| 接口 | 方法 | 描述 |
|---|---|---|
| IComparable | CompareTo(other) | 此方法定义了类型实现的比较方式,用于对其实例进行排序或排列。 |
| IComparer | Compare(first, second) | 此方法定义了次级类型实现的比较方式,用于对初级类型的实例进行排序或排列。 |
| IDisposable | Dispose() | 此方法定义了释放非托管资源的方式,比等待终结器更高效。 |
| IFormattable | ToString(format, culture) | 此方法定义了基于文化敏感的格式化方式,可将对象的值转换为字符串表示形式。 |
| IFormatter | Serialize(stream, object) Deserialize(stream) |
此方法定义了将对象与字节流相互转换的方法,以便存储或传输。 |
| IFormatProvider | GetFormat(type) | 此方法定义了根据语言和地区对输入进行格式化的机制。 |
CompareTo: <0排前面,0相等,>0排后面 |
接口的隐式实现与显式实现
当继承的多个接口有相同名称的方法,则在实现和调用时需要显式指定来自哪个接口
- 隐式实现的接口方法访问修饰符必须与接口内定义一致
- 显示实现的接口方法不能加任何访问修饰符
public interface IGamePlayer { void Lose(); } // 默认 public
public interface IKeyHolder { void Lose(); }
public class Human : IGamePlayer, IKeyHolder
{
// 隐式实现接口
public void Lose() { cw("IKeyHolder lose"); }
// 显示实现另一个接口
void IGamePlayer.Lose(){ cw("IGamePlayer lose"); }
}
Human human = new();
human.Lose(); // 调用隐式实现的方法
((IGamePlayer)human).Lose(); // 显示调用接口方法
// 显示调用接口的另一种方法
IGamePlayer player = human as IGamePlayer;
player.Lose();
重写父类方法
new和override区别
override:重写父类的virtual方法,支持多态(通过基类引用调用子类实现)。new:隐藏父类方法(无论是否virtual),不支持多态(基类引用仍调用父类方法)。⚠️ 仅在无法修改父类且必须同名时使用。
优先用
override;new会破坏多态,慎用。
抽象类
抽象类和接口有些类似,但抽象类C# 1+ 即可使用,其中abstract修饰的方法必须被子类实现,virtual表示可以被子类重写
public abstract class MyClass
{
public abstract void Gamma(); // 抽象方法,必须被子类 override 实现
public virtual void Delta() { ... } // 可以被重写
}
阻止继承和重写
sealed关键字,可以作用于class实现不能继承,也可以作用于方法,实现不能重写
sealed只能应用于重写的方法,即sealed override一起用
多态与类型转换
Person aliceInPerson = aliceInEmplotee;
aliceInEmplotee.WriteToConsole(); // 这个方法是 new 关键字修饰的,非多态
aliceInPerson.WriteToConsole();
Console.WriteLine(aliceInEmplotee.ToString()); // ToString具有多态特性
Console.WriteLine(aliceInPerson.ToString());
// 类型转换
Employee explicitAlice = (Employee)aliceInPerson; // 不够安全
// 更安全的做法
if(aliceInPerson is Employee)
{
Console.WriteLine($"{nameof(aliceInPerson)} 是 Employee");
Employee explicitAlice2 = (Employee)aliceInPerson; // 不够安全
}
// 更好的做法
if (aliceInPerson is Employee explicitAlice3)
{
Console.WriteLine($"{nameof(aliceInPerson)} 是 Employee");
}
as类型转换
无法转换则返回null,不会抛出异常
Employee? aliceAsEmployee = aliceInPerson as Employee;
自定义异常
public class PersonException:Exception
{
public PersonException() : base() { }
public PersonException(string message) : base(message) { }
public PersonException(string? message, Exception? innerException) : base(message, innerException)
{ }
}
扩展方法
扩展某个类型的功能,需要定义在一个静态类的静态方法中,要扩展的类型需用this修饰,C# 3+支持
using System.Text.RegularExpressions;
public class StringExtensions
{
public static bool IsValidEmail(string input)
{
return Regex.IsMatch(input, @"^([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+|\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$");
}
}
调用时就像普通方法一样,但像一般方法那样调用也是可以的
string email = "bos@test.com";
// 未使用扩展方法
Console.WriteLine($"{email}是有效的电子邮件: {StringExtensions.IsValidEmail(email)}");
// 使用扩展方法
Console.WriteLine($"{email}是有效的电子邮件: {email.IsValidEmail()}");
链式调用
将传入的参数返回,即可实现链式调用
car.SetColor(xxx).SetModel(xxx).Start(xxx);
.NET常用类型
数字
| 命名空间 | 示例类型 | 描述 |
|---|---|---|
| System | SByte、Int16、Int32、Int64、Int128 | 整数;即零以及正负整数。 |
| System | Byte、UInt16、UInt32、UInt64、UInt128 | 自然数;即零和正整数。 |
| System | Half、Single、Double | 实数;即浮点数。 |
| System | Decimal | 精确实数;即用于科学、工程或金融场景。 |
| System.Numerics | BigInteger、Complex、Quaternion | 任意大的整数、复数和四元数。 |
大整数乘法
Console.WriteLine("大整数乘法");
int n1 = 2_000_000_000;
int n2 = 2;
Console.WriteLine($"第一个数字:{n1:N0}");
Console.WriteLine($"第二个数字:{n2:N0}");
Console.WriteLine($"n1*n2: {n1 * n2:N0}");
Console.WriteLine($"Math.BigMul(n1,n2): {Math.BigMul(n1, n2):N0}");
Console.WriteLine($"int.BigMul(n1,n2): {int.BigMul(n1, n2):N0}");
使用复数
Complex c1 = new(real: 4, imaginary: 2);
Complex c2 = new(real: 3, imaginary: 7);
Complex c3 = c1 + c2;
// 使用默认的 ToString 方法输出
Console.WriteLine($"{c1} + {c2} = {c1 + c2}");
随机数
.NET 6引入共享静态Random实例,以减小内存消耗
Next: 返回随机int类型,有两个参数minValue和maxValue,取值范围最大为maxValue-1NextInt64: 返回long类型NextDouble: 返回[0.0, 1.0)之间的小数NextSingle: 返回float类型NextBytes: 用随机byte类型填充任意大小的array
Random r = Random.Shared; // 访问共享对象
// 随机 int [1, 7)
int dieRoll = r.Next(1, 7);
Console.WriteLine($"Random die roll: {dieRoll}");
// 随机 double [0, 1)
double randomReal = r.NextDouble();
Console.WriteLine($"Random double: {randomReal}");
// 随机 byte 填充 array
byte[] arrayOfBytes = new byte[256];
r.NextBytes(arrayOfBytes); // 填充256个byte随机数
Console.Write($"Random bytes: ");
foreach (var item in arrayOfBytes)
{
Console.Write($"{item:X2} ");
}
.NET 8+ 引入了新的随机方法
GetItems<T>从中随机选出指定个数的对象Shuffle<T>: 将传入的内容随机打乱
string[] beatles = r.GetItems(choices: new[] { "John", "Paul", "George", "Ringo" }, length: 10);
Console.Write("随机10个beatles: ");
foreach (var item in beatles)
{
Console.Write($"{item} ");
}
Console.WriteLine();
r.Shuffle(beatles);
Console.Write("Shuffled beatles: ");
foreach (var item in beatles)
{
Console.Write($"{item} ");
}
GUID
globally unique identifier: 全局唯一标识符,128位的字符串
- 类型:`System.Guid
- 从字符串解析:
Parse、TryParse - 生成:
NewGuid方法
Console.WriteLine();
Console.WriteLine($"空的GUID: {Guid.Empty}");
Guid guid = Guid.NewGuid();
Console.WriteLine($"生成的GUID: {guid}");
byte[] guidBytes = guid.ToByteArray();
Console.Write("GUID as byte array: ");
foreach (var item in guidBytes)
{
Console.Write($"{item: X2}");
}
Console.WriteLine();
Console.WriteLine("生成3个 v7 GUID:");
for (int i = 0; i < 3; i++)
{
Guid guidV7 = Guid.CreateVersion7(DateTimeOffset.UtcNow);
Console.WriteLine($" {guidV7}");
}
文本
常用文本格式
| 命名空间 | 类型 | 描述 |
|---|---|---|
| System | Char | 用于存储单个文本字符 |
| System | String | 用于存储多个文本字符 |
| System.Text | StringBuilder | 高效操作字符串 |
| System.Text.RegularExpressions | Regex | 高效进行字符串模式匹配 |
常用操作
- 获取长度:
.Length - 获取一个字符:
<str>[<index>] - 切分:
Split - 获取字符串的一部分:
Substring(startIndex, length)、Substring(startIndex) - 获取字符的索引:
IndexOf - 检查内容:
StartsWith,EndsWith,Contains - 比较:
string.Compare
// 字符串数值比较
// 排序 windows version .NET 10 +支持
StringComparer numericStringComparer = StringComparer.Create(CultureInfo.CurrentCulture, CompareOptions.NumericOrdering);
Console.WriteLine($"07 == 7?: {numericStringComparer.Equals("07", "7")}");
string[] oses = { "Windows 10", "Windows 8", "Windows 11" };
foreach (var item in oses.Order(numericStringComparer))
{
Console.WriteLine(item);
}
- 查找
// .NET 8+
string vowels = "AEIOUaeiou"; // 元音
SearchValues<char> vowelsSearchValues = SearchValues.Create(vowels);
string text = "BOS";
Console.WriteLine($"元音:{vowels}");
Console.WriteLine($"文本:{text}");
Console.WriteLine($"text.IndexOfAny(vowelsSearchValues): {text.IndexOfAny(vowelsSearchValues)}");
// .NET 9+ 支持搜索子字符串
string[] names = ["Cassian", "Luthen", "Mon Mothma", "Dedra", "Syril", "Kino"];
SearchValues<string> namesSearchValues = SearchValues.Create(names, StringComparison.OrdinalIgnoreCase);
string sentence = "In Andor, Diego Luna returns as the titular character, Cassian Andor, to whom audiences were first introduced in Rogue One.";
Console.WriteLine($"姓名:{string.Join(' ', names)}");
Console.WriteLine($"句子:{sentence}");
Console.WriteLine($"sentence.IndexOfAny(namesSearchValues): {sentence.IndexOfAny(namesSearchValues)}");
正则表达式
普通使用方法
private const string DigitsOnlyText = @"^\d+$";
private const string CommaSeparatorText = "(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)";
// 调用
string films = """
"Monsters, Inc.","I, Tonya","Lock, Stock and Two Smoking Barrels"
""";
Regex csv = new(CommaSeparatorText);
MatchCollection filmsSmart = csv.Matches(films);
Console.WriteLine("正则表达式匹配结果");
foreach (Match item in filmsSmart)
{
Console.WriteLine(item.Groups[2].Value);
}
给字符串添加颜色
[StringSyntax(StringSyntaxAttribute.Regex)]
private const string DigitsOnlyText = @"^\d+$";
[StringSyntax(StringSyntaxAttribute.Regex)]
private const string CommaSeparatorText = "(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)";
使用源生成器提高正则表达式性能
[StringSyntax(StringSyntaxAttribute.Regex)]
private const string DigitsOnlyText = @"^\d+$";
[StringSyntax(StringSyntaxAttribute.Regex)]
private const string CommaSeparatorText = "(?:^|,)(?=[^\"]|(\")?)\"?((?(1)[^\"]*|[^,\"]*))\"?(?=,|$)";
[GeneratedRegex(DigitsOnlyText, RegexOptions.IgnoreCase)]
private static partial Regex DigitsOnly { get; }
[GeneratedRegex(CommaSeparatorText, RegexOptions.IgnoreCase)]
private static partial Regex CommaSeperator { get; }
// 调用
Regex ageChecker = DigitsOnly;
Regex csv = CommaSeperator;
collections
用于在变量中存储多个值,所有的collections都实现了ICollection接口
拥有AsReadOnly方法创建ReadOnlyCollection<T>实例,引用原始的collection
| 命名空间 | 示例类型 | 描述 |
|---|---|---|
| System.Collections | IEnumerable, IEnumerable |
集合所使用的接口和基类。 |
| System.Collections.Generic | List Queue Stack Dictionary<TKey, TValue> |
于 C# 2 中随 .NET Framework 2.0 引入。这些集合允许您通过泛型类型参数指定要存储的类型(更安全、更快、更高效)。字典需要指定两种类型:一种用于键,另一种用于值。 |
| System.Collections.Concurrent | BlockingCollection, ConcurrentDictionary, ConcurrentQueue |
这些集合在多线程场景中使用是安全的。 |
| System.Collections.Immutable | ImmutableArray, ImmutableDictionary, ImmutableList, ImmutableQueue |
专为原始集合的内容永远不会改变的场景设计,尽管它们可以创建修改后的集合作为新实例。 |
| 好的实践 |
- 一些
collection类型具有.EnsureCapacity方法,可以预先分配容量,以减小扩展大小时的性能开销 - 使用
collection作为函数的参数,形参类型为IEnumerable,能方便处理多种类型,但会损失性能 - 返回
collection类时,不要返回null,否则对返回值迭代可能会报错
初始化方法
注意:并不是所有collection都支持初始化表达式,比如字典和多维数组就不行
C# 11+
int[] numbersArray11 = { 1, 3, 5 };
List<int> numbersList11 = new() { 1, 3, 5 };
Span<int> numbersSpan11 = stackalloc int[] { 1, 3, 5 };
C# 12 可以使用方括号
int[] numbersArray12 = [ 1, 3, 5 ];
List<int> numbersList12 = [ 1, 3, 5 ];
Span<int> numbersSpan12 = [ 1, 3, 5 ];
List
IReadOnlyList可以避免内容被修改
List的使用示例
List<string> cities = new List<string>();
cities.Add("Shanghai");
cities.Add("Wuhan");
cities.Add("Beijing");
OutputCollection("初始值", cities);
Console.WriteLine($"第一个城市是: {cities[0]}");
//Console.WriteLine($"最后一个城市是: {cities[^1]}");
Console.WriteLine($"最后一个城市是: {cities[cities.Count-1]}");
Console.WriteLine($"所有的城市字符>4 : {cities.TrueForAll(city=> city.Length>4)}");
Console.WriteLine($"所有城市包含字符 `e`: {cities.TrueForAll(city => city.Contains('e'))}");
cities.Insert(0, "Shijia Zhuang");
OutputCollection("插入 石家庄 后", cities);
cities.RemoveAt(1);
cities.Remove("Beijing");
OutputCollection("删除2个城市 后", cities);
// 定义方法
private static void OutputCollection<T>(string title, IEnumerable<T> collection)
{
Console.WriteLine($"{title}: ");
foreach (var item in collection)
{
Console.WriteLine($" {item}");
}
}
Set
每个元素必须唯一
| 方法 | 描述 |
|---|---|
| Add | 如果集合中尚不存在该元素,则将其添加。如果元素成功添加,此方法返回 true;如果元素已存在于集合中,则返回 false。 |
| ExceptWith | 此方法会从当前集合中移除作为参数传入的集合中的元素。 |
| IntersectWith | 此方法会从当前集合中移除那些既不在参数集合中也不在当前集合中的元素。 |
| 是真子集、是真超集、是子集、是超集IsProperSubsetOf, IsProperSupersetOf, IsSubsetOf, IsSupersetOf |
子集是指其所有元素都包含于另一个集合中的集合。真子集是指其所有元素都包含于另一个集合中,但另一个集合中至少存在一个元素并不属于该集合。超集是指包含另一个集合所有元素的集合。真超集是指包含另一个集合所有元素且至少多出一个不属于该集合的元素的集合。 |
| Overlaps | 该集合与另一个集合至少有一个共同元素。 |
| SetEquals | 该集合与另一个集合包含完全相同的元素。 |
| 对称差集SymmetricExceptWith | 此操作会从当前集合中移除参数集合中不存在的元素,并添加任何缺失的元素。 |
| 并集UnionWith | 此操作会将参数集合中所有尚未存在于当前集合中的元素添加到当前集合中。 |
栈和队列
Stack: 后入先出
Queue: 先入先出
- 入队:
.Enqueue - 出队:
.Dequeue - 在不出队的情况下查看值:
.Peek
.NET 6引入PriorityQueue,队中的每个元素还有优先级,优先级最低的先出队
.NET 9为PriorityQueue引入Remove方法,移除1个队列中指定项,如果有多个重复,则随机移除1个
自动排序的 collection
.NET 9引入OrderedDictionary<TKey, TValue>,可以自动排序 ,有TryAdd 和 TryGetValue用于添加和检索
| 集合 | 描述 |
|---|---|
| SortedDictionary<TKey, TValue> | 这是一个按键排序的键值对集合。内部使用二叉树来维护这些项。 |
| SortedList<TKey, TValue> | 这是一个按键排序的键值对集合。名称容易引起误解,因为它实际上并不是一个列表。与SortedDictionary<TKey, TValue>相比,它的查找性能相似,但占用内存更少,而对于未排序的数据,插入和删除操作的速度较慢。如果该集合是由已排序的数据填充的,则其性能会更快。内部使用一个已排序的数组,并配合二分查找来定位元素。 |
| SortedSet |
这是一个以排序方式维护的唯一对象集合。 |
特殊的 collection
System.Collections.BitArray: 管理一个紧凑的bit arraySystem.Collections.Generics.LinkedList<T>: 双向链表,每个项都有前后项的引用,在频繁向列表中间插入/删除时,性能好于List<T>System.Collections.Immutable:Add方法会返回一个新的immutable collection,可以调用方法.ToImmutableDictionary()frozen collections:.NET 8只有2种,FrozenDictionary<TKey, TValue>和FrozenSet<T>,
不同collection对Add方法的操作结果不同
List<T>:此操作会将新项添加到现有列表的末尾。Dictionary<TKey, TValue>:此操作会将新项添加到现有字典中,具体位置由其内部结构决定。ReadOnlyCollection<T>:此操作会抛出不支持的异常。ImmutableList<T>:此操作会返回一个包含新项的新列表,且不会影响原列表。ImmutableDictionary<TKey, TValue>:此操作会返回一个包含新项的新字典,且不会影响原字典。FrozenDictionary<TKey, TValue>:此类型不存在。
扩展元素 spread element (operator)
..,可用于任何可迭代对象,即可以用foreach的对象,注意不要与range operator混淆
int[] row0 = [1, 2, 3];
int[] row1 = [4,5];
int[] row2 = [6,7,8,9];
int[] combinedRows = [..row0, ..row1, ..row2, 10];
foreach (int number in combinedRows)
{
Console.Write($"{number} ");
}
处理文件、流和序列化
Spectre.Console包可以美化控制台输出效果
文件夹处理
- 文件夹是否存在:
Path.Exists,Directory.Exists - 路径组合:
Path.Combine - 特殊文件夹:
Environment.SpecialFolder - 创建文件夹:
Directory.CreateDirectory - 删除文件夹:
Directory.Delete
文件
- 文件是否存在:
File.Exists - 创建空白文件并写入一行内容,如果已存在则覆盖
StreamWriter writer = File.CreateText(textFile);
writer.WriteLine("Hello BOS!");
writer.Close(); // 释放资源
- 复制文件:
File.Copy - 删除文件:
File.Delete - 打开已存在的文件并读取内容
StreamReader reader = File.OpenText(backupFile);
Console.WriteLine(reader.ReadToEnd());
reader.Close();
路径
- 从文件路径中获取文件夹路径:
Path.GetDirectoryName - 获取完整的文件名:
Path.GetFileName - 获取不带扩展名的文件名:
Path.GetFileNameWithoutExtension - 获取扩展名:
Path.GetExtension - 创建并返回临时文件夹:
Path.GetTempFileName()
获取文件信息
FileInfo, DirectoryInfo
控制文件处理的方式
File.Open可以设定 FileMode FileAccess FileShare
XML streams
- 有子元素:
WriteStartElementWriteEndElement - 无子元素:
WriteElementString
Compressing streams
FileStream file = File.Create(filePath);
Stream compressor;
GZipStream
compressor = new GZipStream(file, CompressionMode.Compress);
`BrotliStream
compressor = new BrotliStream(file, CompressionMode.Compress);
压缩XML流:
using (compressor)
{
using (XmlWriter xml = XmlWriter.Create(compressor))
{
xml.WriteStartDocument();
xml.WriteStartElement("callsigns");
foreach (string item in Viper.Callsigns)
{
xml.WriteElementString("callsign", item);
}
}
} // Also closes the underlying stream.
解压缩
Stream decompressor;
decompressor = new GZipStream(file, CompressionMode.Decompress);
// decompressor = new BrotliStream(file, CompressionMode.Decompress);
using (decompressor)
using (XmlReader reader = XmlReader.Create(decompressor))
{ ... }
.NET 10 还支持操作 ZIP 文件
对流编解码
StreamReader reader = new(stream, Encoding.UTF8);
StreamWriter writer = new(stream, Encoding.UTF8);
XML JSON序列化
序列化为XML
注意:using System.Xml.Serialization;可以使用 XmlSerializer 类,也可以使用一些 [XmlAttribute]
序列化时只有public的字段和属性才会包含,反序列化时对象必须有无参构造函数
using System.Xml.Serialization;
List<Person> people = new(){ ... } // 要序列化的对象
XmlSerializer xs = new XmlSerializer(people.GetType());
// 创建一个文件
string path = Path.Combine(Environment.CurrentDirectory, "people.xml");
Console.WriteLine($"创建文件的路径: {path}");
using (FileStream stream = File.Create(path))
xs.Serialize(stream, people);
使用修饰属性可以有效减小文件大小
public class Person
{
public Person()
{
}
public Person(decimal initialSalary)
{
Salary = initialSalary;
}
[XmlAttribute("fname")]
public string? FirstName { get; set; }
[XmlAttribute("lname")]
public string? LastName { get; set; }
[XmlAttribute("dob")]
public DateTime BirthDay { get; set; }
public HashSet<Person>? Children { get; set; }
protected decimal Salary { get; set; }
}
XML反序列化
using (FileStream xmlLoad = File.Open(path, FileMode.Open))
{
List<Person>? loadedPeople = xs.Deserialize(xmlLoad) as List<Person>;
}
JSON 序列化
Newtonsoft.JSON nuget包,System.Text.Json 性能更好,但功能并不如前面那个丰富
string josnFilePath = Path.Combine(Environment.CurrentDirectory, "people.json");
using (StreamWriter jsonStream = File.CreateText(josnFilePath))
{
Newtonsoft.Json.JsonSerializer jss = new();
jss.Serialize(jsonStream, people);
}
Json 反序列化
await using (FileStream jsonLoad = File.OpenRead(jsonFilePath))
{
List < Person >? loadPeople = await System.Text.Json.JsonSerializer.DeserializeAsync(jsonLoad, typeof(List<Person>)) as List<Person>;
}
JSON schema exporter
Console.WriteLine(System.Text.Json.Schema.JsonSchemaExporter.GetJsonSchemaAsNode(System.Text.Json.JsonSerializerOptions.Default, typeof(Person)));
得到类似这样的模式
{
"type": [
"object",
"null"
],
"properties": {
"FirstName": {
"type": [
"string",
"null"
]
JSON 补丁
表示对json文件的操作,例如增加、删除、替换、移动、复制、测试(test),可以减小载荷,提高效率
.NET 10 提供了 json patch 的新实现,要启用 System.Text.Json的json patch 支持,需要安装nuget包 Microsoft.AspNetCore.JsonPatch.SystemTextJson, 但不能完全代替 Newtonsoft.Json, 比如不支持动态类型
点击查看代码
using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
Person person = new()
{
FirstName = "B",
LastName = "OS",
BirthDay = new(1990, 5, 23)
};
// 输出原始信息
Console.WriteLine($"before: {System.Text.Json.JsonSerializer.Serialize(person)}");
// 定义 json patch 文档
string jsonPatch = """
[
{"op":"replace", "path":"/FirstName", "value": "Varian"},
{"op":"replace", "path":"/LastName", "value":"Skye"},
{"op":"remove", "path" :"/BirthDay"}
]
""";
// 反序列化 json patch
JsonPatchDocument<Person>? patchDoc = System.Text.Json.JsonSerializer.Deserialize<JsonPatchDocument<Person>>(jsonPatch);
// 应用补丁
patchDoc.ApplyTo(person);
// 输出修改后的对象信息
Console.WriteLine($"after: {System.Text.Json.JsonSerializer.Serialize(person)}");
TODO Entity Framework Core 处理数据库
数据库提供程序
| NuGet 包 | 支持的数据库引擎 | 维护商/供应商 | 备注/要求 | 针对 EF Core | 有用链接 |
|---|---|---|---|---|---|
| Microsoft.EntityFrameworkCore.SqlServer | Azure SQL、SQL Server 2012 及更高版本、Azure Synapse Analytics | EF Core 项目 (Microsoft) | 8, 9, 10 | docs | |
| Microsoft.EntityFrameworkCore.Sqlite | SQLite 3.46.1 及更高版本 | EF Core 项目 (Microsoft) | 8, 9, 10 | docs | |
| Microsoft.EntityFrameworkCore.InMemory | EF Core 内存中数据库 | EF Core 项目 (Microsoft) | Limitations | 8, 9, 10 | docs |
| Microsoft.EntityFrameworkCore.Cosmos | Azure Cosmos DB SQL API | EF Core 项目 (Microsoft) | 8, 9, 10 | docs | |
| Npgsql.EntityFrameworkCore.PostgreSQL | PostgreSQL | Npgsql 开发团队 | 8, 9 | docs | |
| Pomelo.EntityFrameworkCore.MySql | MySQL、MariaDB | Pomelo Foundation 项目 | 8, 9 | readme | |
| MySql.EntityFrameworkCore | MySQL | MySQL 项目 (Oracle) | 8, 9 |
简单的日志系统
免费书Practical Debugging for .NET Developers
file权限修饰符
Refactor your code using alias any type
成员访问修饰符
创建record类型
主要构造函数
主要构造函数重构
拆分与重组partial类
模式匹配
运算符重载
反编译程序集
源生成器
源生成器示例
在数据模型中使用GUID v7
string builder
frozen collections doc
spread element
using https://github.com/markjprice/cs14net10/blob/main/docs/ch06-memory.md#ensuring-that-dispose-is-called
using https://github.com/markjprice/cs14net10/blob/main/docs/ch06-memory.md#releasing-unmanaged-resources
using https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/language-specification/statements#1314-the-using-statement
压缩等级
XmlAttributeAttribute
数据库入门
遗留问题:
- SafeHandles

浙公网安备 33010602011771号