.NET开发:语法笔记

文章发布时间:

最后更新时间:

文章总字数:
6.3k

预计阅读时间:
25 分钟

摘要

本文为自用笔记,包含开发过程中常用的C#与其它语言常用语法、查漏补缺等内容。
知识点内容根据各个教程收集整理。
所有教程版权归各自作者所有。

C#语法&杂项查漏补缺

关键字

init

只能在构造函数中赋值

1
2
3
4
5
6
7
8
9
10
class Person{
public string Name{get;set;}//内外均可赋值
public int Age{get;private set}//同类可以赋值
public string Gender{get;init;}//只能在构造函数赋值,同类其它方法不行
public Person(String name,int age,string gender){
this.Name = name;
this.Age = age;
this.Gender = gender;
}
}

Lock

lock 关键字
虽然 sleep 方法可以控制线程的暂停时间,从而改变多个线程之间的顺序,然而每个线程的执行顺序是随机且不确定的。 线程同步是通过共享线程资源来实现的,允许控制一次执行一个线程并交替执行每个线程。 为了在c#语言中实现线程同步,可以使用lock关键字、monitor类和mutex类。 同步线程最简单的方法之一是使用lock语句,它确保被锁定的线程在完成执行之前不能执行任何其他线程。 lock 的语法形式如下所示。

1
2
3
4
lock(object)
{
//临界区代码
}

lock 后面通常是一个 Object 类型的值,也可以使用 this 关键字来表示。 最好是在 lock 中使用私有的只读的非静态变量或私有的静态成员变量,即使用 Private 或 Private static 修饰的成员。 例如:

1
2
3
4
5
private Object obj = new Object();
lock (obj)
{
//临界区代码
}

来源参考C# 用 lock 线程锁实现对资源的同步访问

sealed

应用于某个类时,sealed 修饰符可阻止其他类继承自该类。
类似于Java中的final
在下面的示例中,类 B 继承自类 A,但没有类可以继承自类 B。

1
2
class A {}
sealed class B : A {}

还可以对替代基类中的虚方法或属性的方法或属性使用 sealed 修饰符。 这可以允许类派生自你的类并防止它们替代特定虚方法或属性。

在下面的示例中,Z 继承自 Y,但 Z 无法替代在 X 中声明并在 Y 中密封的虚函数 F。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class X
{
protected virtual void F() { Console.WriteLine("X.F"); }
protected virtual void F2() { Console.WriteLine("X.F2"); }
}
class Y : X
{
sealed protected override void F() { Console.WriteLine("Y.F"); }
protected override void F2() { Console.WriteLine("Y.F2"); }
}
class Z : Y
{
protected override void F() { Console.WriteLine("Z.F"); }//错误 : Attempting to override F causes compiler error CS0239.
protected override void F2() { Console.WriteLine("Z.F2"); }//可以重写F2
}

在类中定义新方法或属性时,可以通过不将它们声明为虚拟,来防止派生类替代它们。

将 abstract 修饰符与密封类结合使用是错误的,因为抽象类必须由提供抽象方法或属性的实现的类来继承。

应用于方法或属性时,sealed 修饰符必须始终与 override 结合使用。

因为结构是隐式密封的,所以无法继承它们。

来源详见sealed(C# 参考)

readonly

在字段声明中,readonly 指示只能在声明期间或在同一个类的构造函数中向字段赋值。 可以在字段声明和构造函数中多次分配和重新分配只读字段。

构造函数退出后,不能分配 readonly 字段。 此规则对于值类型和引用类型具有不同的含义:

  • 由于值类型直接包含数据,因此属于 readonly 值类型的字段不可变。
  • 由于引用类型包含对其数据的引用,因此属于 readonly 引用类型的字段必须始终引用同一对象。该对象是可变的。readonly 修饰符可防止字段替换为引用类型的其他实例。 但是,修饰符不会阻止通过只读字段修改字段的实例数据。
示例

即使在类构造函数中给字段 year 赋了值,也无法在方法 ChangeYear 中更改其值:

1
2
3
4
5
6
7
8
9
10
11
12
class Age
{
private readonly int _year;
Age(int year)
{
_year = year;
}
void ChangeYear()
{
//_year = 1967; // Compile error if uncommented.
}
}

const

readonly 关键字不同于 const 关键字。 const 字段只能在该字段的声明中初始化。 可以在字段声明和任何构造函数中多次分配 readonly 字段。 因此,根据所使用的构造函数,readonly 字段可能具有不同的值。 另外,虽然 const 字段是编译时常量,但 readonly 字段可用于运行时常量,如下面的示例所示:

1
public static readonly uint timeStamp = (uint)DateTime.Now.Ticks

来源详见readonly(C# 参考)

ref与in

C#中out和ref之间的区别:

首先:两者都是按地址传递的,使用后都将改变原来参数的数值。

其次:ref可以把参数的数值传递进函数,但是out是要把参数清空,就是说你无法把一个数值从out传递进去的,out进去后,参数的数值为空,所以你必须初始化一次。这个就是两个的区别,或者说就像有的网友说的,ref是有进有出,out是只出不进。

ref 关键字使参数按引用传递。其效果是,当控制权传递回调用方法时,在方法中对参数的任何更改都将反映在该变量中。若要使用 ref 参数,则方法定义和调用方法都必须显式使用 ref 关键字

来源详见关于C#中的ref的使用

属性

CallerMemberName属性

记录调用信息.

CallerMemberName:调用者名称。在构造函数,析构函数、属性等特殊的地方调用CallerMemberName属性所标记的函数时,获取的值有所不同。

调用的地方 CallerMemberName获取的结果
方法、属性或事件 方法,属性或事件的名称
构造函数 字符串 “.ctor”
静态构造函数 字符串 “.cctor”
析构函数 字符串 “Finalize”
用户定义的运算符或转换 生成的名称成员,例如, “op_Addition”
特性构造函数 特性所应用的成员的名称

示例:

调用者:

1
2
3
4
public void DoSomething()
{
TraceMessage("Message");
}

被调用者:

1
2
3
4
5
6
7
public static void TraceMessage(string message,
[CallerMemberName] string memberName="",
[CallerFilePath] string sourceFilePath="",
[CallerLineNumber] int sourceLineNumber=0)
{
Console.WriteLine($"message:{message}\nmember name: {memberName}" +$"\nsource file path: {sourceFilePath}\nsource line number: {sourceLineNumber}");
}

输出结果:

1
2
3
4
message:Message
member name: DoSomething
source file path: /ztcly/blog/_posts/DotnetNote.md
source line number: 42

常用方法备查

int.TryParse

C#中针对转换有一个TryParse的方法。
如果转换成功则返回true。否则返回false。

1
int.TryParse(string s,out int i)//尝试将s转换为int

s是要转换的字符串,i 是转换的结果。

来源C# int.TryParse() 方法

ConcurrentBag

ConcurrentBag<T>:表示对象的线程安全的无序集合。
包对于在排序无关紧要时存储对象很有用,与集不同,包支持重复项。 ConcurrentBag<T> 是线程安全包实现,针对同一线程将同时生成和使用存储在包中的数据的方案进行优化。

ConcurrentBag<T> 接受 null 为引用类型的有效值。

其它

_

在部分名称规范中,以下划线_开头表示private

可空引用类型检查

  • 在csproj中开启可空引用类型检查
1
<Nullable>enable</Nullable>
  • 在引用类型后添加”?”代表可空。否则编译器在识别到有可能为Null时会发出警告

  • 在后面添加”!”抑制Null警告

Parallel 类

提供对并行循环和区域的支持

示例
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
using System.Threading.Tasks;   
class Test
{
static int N = 1000;
static void TestMethod()
{
// Using a named method.
Parallel.For(0, N, Method2);
// Using an anonymous method.
Parallel.For(0, N, delegate(int i)
{
// Do Work.
});

Parallel.For(0, N, i => // 使用lambda表达式
{
// Do Work.
});
}

static void Method2(int i)
{
// Do work.
}
}

程序集

.NET 中的程序集

程序集构成了 .NET 应用程序的部署、版本控制、重用、激活范围和安全权限的基本单元。 程序集是为协同工作而生成的类型和资源的集合,这些类型和资源构成了一个逻辑功能单元。 程序集采用可执行文件 (.exe) 或动态链接库文件 (.dll) 的形式,是 .NET 应用程序的构建基块 。 它们向公共语言运行时提供了注意类型实现代码所需的信息。

在 .NET 和 .NET Framework 中,可从一个或多个源代码文件生成程序集。 在 .NET Framework 中,程序集可以包含一个或多个模块。 因此,大型项目可以采用以下规划:由多个开发者单独开发各源代码文件或模块,最后整合所有这些内容以创建一个程序集。

来源详见.NET 中的程序集

NuGet

简介

NuGet 是免费、开源的包管理开发工具,专注于在 .NET 应用开发过程中,简单地合并第三方的组件库。
当需要分享开发的工具或是库,需要建立一个Nuget package,然后把这个package放到Nuget的站点。如果想要使用别人已经开发好的工具或是库,只需要从站点获得这个package,并且安装到自己的Visual Studio项目或是解决方案里。
NuGet能更方便地把一些dll和文件(如jquery)添加到项目中,而不需要从文件中复制拷贝。

来源

使用

VS可视化使用

Pic01

以Newtonsoft.Json为例:

  1. 选中Newtonsoft.Json,在右侧就会出现Newtonsoft.Json的详细信息
  2. 选中要将Newtonsoft.Json下载到的项目,然后点击安装。NuGet会在解决方案目录下创建packages目录并将包下载到该目录
  3. 下载完成后VS会自动将Newtonsoft.Json添加到我们的引用。

使用控制台

打开NuGet控制台管理器

Pic02

  • 通过Install-Package “包名” 这个命令就能完成对目标包的引用。
  • 通过get-help NuGet来获取其他命令的信息
  • Get-Package 获取当前已经安装的包
  • UnInstall-Package 卸载某个包
  • Update-Package 升级某个包

文档

更多使用问题参考官方文档

异步编程

async \ await 简化多线程程序开发,不等于多线程

基本使用

异步方法:使用 async 关键字修饰的方法

  1. 异步方法返回值一般为 Task<T> ,异步方法名惯例以Async结尾。
  2. 异步方法可以没有返回值。但即时没有返回值也应返回非泛型的Task
  3. 调用异步方法一般使用 await ,直接拿到泛型位置指定的类型
  4. 异步方法具有传染性,若在方法中调用 await , 则其自身需要使用 async 进行修饰

异步方法代码参考:

1
2
3
4
5
6
7
8
static async Task Main(String[] args){
string fileName = @"D:\temp\h.txt";
string s = "Hello";
File.Delete(fileName);
await File.WriteAlltextAsync(s);
string result = await File.ReadAllTextAsync(fileName);
Console.WriteLine(result);
}

同步版本:

1
2
3
4
5
6
7
8
static void Main(String[] args){
string fileName = @"D:\temp\h.txt";
string s = "Hello";
File.Delete(fileName);
File.WriteAlltext(s);
string result = File.ReadAllText(fileName);
Console.WriteLine(result);
}
  • 异步代码使用 Task<T> 和 Task ,它们是对后台所完成的工作进行建模的结构。
  • async 关键字将方法转换为异步方法,这使你能在其正文中使用 await 关键字。
  • 应用 await 关键字后,它将挂起调用方法,并将控制权返还给调用方,直到等待的任务完成。
  • 仅允许在异步方法中使用 await。

此部分来源参考异步编程 - C#

编写异步方法

使用 Async 关键字,可直接返回值

无返回值示例

1
2
3
4
5
6
7
//无返回值
static async Task DownloadHtmlAsync(String url,String filename){
using(HttpClient httpClient = new HttpClient()){
String html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(html);
}
}

有返回值示例

1
2
3
4
5
6
7
8
//有返回值
static async Task<int> DownloadHtmlwithReturnAsync(String url,String filename){
using(HttpClient httpClient = new HttpClient()){
String html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(html);
return html.Length;//可直接返回int,会自动包装
}
}

调用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

//调用
static async Task Main(String[] args){
String url = "https://blog.ztcly.top/2023/07/02/DotnetNote/"
String filename = @"E:\temp\h.txt"

//调用无返回值
await DownloadHtmlAsync(url,filename);
Console.WriteLine("Done");

//调用有返回值
await int length = DownloadHtmlwithReturnAsync(url,filename);
Console.WriteLine("Done,HTML Length:"+length);
}

using

using关键字有两个主要的用途:

  • using语句定义了一个作用域,在作用域的尾部将会释放对象
  • using指令为命名空间创建别名或导入其他命名空间中定义的类型
指令

using指令允许使用在命名空间中定义的类型,而无需指定该类型的全名(与C系语言中 include 同)。 using 指令会导入命名空间中的所有类型

1
using System.Text;

using指令可以添加修饰符:

  • global:该修饰符与向项目中每个源文件添加相同的using指令具有相同的效果。该修饰符是在C# 10中引入的。
  • static:该修饰符从单个类型导入静态成员和嵌套类型,而不是导入所有类型。它是C#6.0中引入的。

可以结合使用这两个修饰符,来达到从项目所有源文件中的类型中导入静态成员的目的。
还可以使用using alias directive(别名指令)为命名空间或类型创建别名。示例如下

1
2
// using alias = namespace
using Project = PC.MyCompany.Project;
语句

IDisposable从命名来看就是个接口。
Disposable:可任意使用的、一次性的、用完即丢弃的。
该接口的定义是,提供了一种释放非托管资源的机制。

IDisposable的文档部分非常直白地介绍了该接口的用途:
此接口的主要用途是释放非托管资源。
当管理的对象不再使用时,GC(.NET平台的垃圾回收器)会自动释放分配给该对象的内存。但是呢,到底何时回收垃圾是无法被预测的。而且,GC也不了解非托管资源,如窗口句柄、或打开的文件和流。
使用此接口的Dispose方法可以用GC显式地释放非托管的资源。当某个对象不再被需要时,对象的使用者可以调用此方法。

那么使用实现了IDisposable接口的对象通常有两种方式,
其中一种就是使用一种语言结构,如C#和VB中的using语句,以及F#中的use语句或using函数。当环境支持该语言结构时,你可以使用它来代替显式地调用IDisposable.Dispose。

using(){}:

1
2
3
4
5
6
7
8
//有返回值
static async Task<int> DownloadHtmlwithReturnAsync(String url,String filename){
using(HttpClient httpClient = new HttpClient()){
String html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(html);
return html.Length;//可直接返回int,会自动包装
}
}

using声明,代码在离开作用域之后会自动释放资源(C# 8加入):

1
2
3
4
5
6
7
static async Task<int> DownloadHtmlwithReturnAsync(String url,String filename){
using HttpClient httpClient = new HttpClient()//using声明
String html = await httpClient.GetStringAsync(url);
await File.WriteAllTextAsync(html);
return html.Length;
}
//代码离开方法对象会被释放

此部分来源详见C#中的using关键字

IDisposable

C#提供了方便的垃圾回收机制,使我们几乎不再需要为资源管理费心。可事实上,能被垃圾回收释放掉的只是托管资源,非托管资源还是需要我们手动释放。而为了实现这一目的,C#提供了 IDisposable 接口.

此部分来源详见说说IDisposable

托管资源和非托管资源
  • 托管资源:托管资源是指由垃圾收集器(GC)管理的内存。
  • 非托管资源:非托管资源有文件句柄(类似于文件描述符),网络连接,数据库连接等等。

非托管资源的释放主要有:

  1. 析构函数:与创建资源的构造函数相对应,析构函数可以销毁对象,一般在被垃圾回收器销毁对象之前调用。但C#中析构函数与C++的不同,它不是立即执行。因为要等待GC回收,所以无法确定析构函数的执行时间,这样的不可控在代码中是不能接受的。与此同时,它会延时对象删除的时间,跟没有析构函数的对象相比,它需要调用才能删除。频繁使用析构函数,对性能的影响会变得非常显著。

  2. 实现System.IDisposable:实现IDisposable是C#中推荐的方式,它需要实现一个dispose()方法,以此来显示的释放资源,这样避免了不确定的调用时间。在需要释放资源时,直接调用即可。从规范而言建议在finally中,释放避免异常之后资源不释放的情况。

  3. using语句:using语句不仅可以用于引用namespace,还能在代码中使用。它相当方便,与上面实现IDisposable方法等效。他帮我们实现了IDisposable的方法,并且在代码块的末尾调用Dispose方法。

此部分来源详见托管资源与非托管资源

async背后的线程切换

await调用的等待期间,.NET会将当前线程返回给线程池。待异步方法执行完毕之后另取一个线程执行后面的代码.

async缺点:

  • 异步方法会生成一个类,运行效率没有普通方法高
  • 可能会占用非常多的线程

不适用sleep()

采用非阻止方式编写等待任务的代码

通过阻止当前线程来等待 Task 完成的方法可能导致死锁和已阻止的上下文线程,且可能需要更复杂的错误处理方法。 下表提供了关于如何以非阻止方式处理等待任务的指南:

使用以下方式… 而不是… 若要执行此操作…
await Task.Wait 或 Task.Result 检索后台任务的结果
await Task.WhenAny Task.WaitAny 等待任何任务完成
await Task.WhenAll Task.WaitAll 等待所有任务完成
await Task.Delay Thread.Sleep 等待一段时间

CancellationToken结构体

属性

名称 介绍
CanBeCanceled 获取此标记是否能处于已取消状态
IsCancellationRequested 获取是否已请求取消此标记
None 返回一个空 CancellationToken 值
WaitHandle 获取在取消标记时收到信号的 WaitHandle

此部分来源详见CancellationToken 结构

Task类

Task类的重要方法

名称 介绍
WhenAll(IEnumerable<Task>) 创建一个任务,该任务将在可枚举集合中的所有 Task 对象都已完成时完成。
WhenAll(Task[]) 创建一个任务,该任务将在数组中的所有 Task 对象都已完成时完成。
WhenAll<TResult>(IEnumerable<Task<TResult>>) 创建一个任务,该任务将在可枚举集合中的所有Task<TResult>对象都已完成时完成。
WhenAll<TResult>(Task<TResult>[]) 创建一个任务,该任务将在数组中的所有 Task<TResult> 对象都已完成时完成。
WhenAny(IEnumerable<Task>) 任何提供的任务已完成时,创建将完成的任务。
WhenAny(Task, Task) 提供的任一任务完成时,创建将完成的任务。
WhenAny(Task[]) 任何提供的任务已完成时,创建将完成的任务。
WhenAny<TResult>(IEnumerable<Task<TResult>>) 任何提供的任务已完成时,创建将完成的任务。
WhenAny<TResult>(Task<TResult>, Task<TResult>) 提供的任一任务完成时,创建将完成的任务。
WhenAny<TResult>(Task<TResult>[]) 任何提供的任务已完成时,创建将完成的任务。
FromResult<TResult>(TResult) 创建指定结果的、成功完成的 Task<TResult>。

Linq

语言集成查询 (LINQ) 是一系列直接将查询功能集成到 C# 语言的技术统称。

委托

C# 中的委托(Delegate)类似于 C 或 C++ 中函数的指针。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。

委托(Delegate)特别用于实现事件和回调方法。所有的委托(Delegate)都派生自 System.Delegate 类。

  • 委托是可以指向方法的类型,调用委托变量是执行的就是所指向的方法。
  • .NET中定义了有返回值(Func)和无返回值(Action)的泛型委托。一般不用自定义类型。

声明委托

声明委托的语法如下:

1
delegate <return type> <delegate-name> <parameter list>

以单一String类型参数和int类型返回值举例:

1
public delegate int MyDelegate (string s);

委托的声明、实例化和使用举例:

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
using System;
delegate int NumberChanger(int i);
namespace TestDelegate;
class MyDelegate{
int num;
public static int Add(int i){
return i+i;
}
public static int Mutl(int i){
return i*i;
}
public static int getNum(){
return num;
}

static void Main(string[] args){
NumberCharger nc01 = new NumberChanger(Add);
nc01(10);
Console.WriteLine(getNum());

NumberCharger nc01 = new NumberChanger(Mutl);
nc01(10);
Console.WriteLine(getNum());
}
}

此部分来源详见C# 委托

Lambda

“Lambda表达式”是一个匿名函数,是一种高效的类似于函数式编程的表达式,Lambda简化了开发中需要编写的代码量。它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型,支持带有可绑定到委托或表达式树的输入参数的内联表达式。

所有Lambda表达式都使用Lambda运算符=>,该运算符读作”goes to”。Lambda运算符的左边是输入参数(如果有),右边是表达式或语句块。Lambda表达式x => x * x读作”x goes to x times x”。形如(参数)=>{操作}。

LINQ

下文介绍方法语法

Where:遍历集合中每一个元素,对于每一个元素使用表达式判断是否为True,为True将结果放回到返回集合中

LINQ中常用var推断类型

1
var items = list.Where(p=>p.Age>30);

Count:返回满足条件的元素个数

1
var n = list.Count(p=>p.Age>30);

Any:是否至少有一条数据

1
2
3
bool b = list.Any();//集合中是否有一条数据

bool b = list.Any(p=>p.Age>30);//集合中是否有一条数据Age大于30

Single:有且只有一条满足要求的数据。没有或者多于一条都会报错

1
Employee e = list.Single(p=>p.Name=="Jim");

SingleOrDefault:一个或默认值;最多一个符合要求,不满足则返回默认值

1
Employee e = list.SingleOrDefault(p=>p.Name=="Jim");

First:返回第一条,至少有一条

1
Employee e = list.First(p=>p.Name=="Jim");

FirstOrDefault:第一个或默认值


排序:

OrdeBy:正序排序
OrderByDescending:降序排序

1
2
3
var n = list.OrderBy(p=>p.Age);//根据年龄排序
var n = list.OrderByDescending(p=>p.Age);//根据年龄降序排序
var n = list.OrderByDescending(p=>p.Name[p.Name.Lenth-1]);//根据名字的最后一个字母降序排序

多规则排序:

ThenBy:再根据新的条件正序排序
ThenByDescending:再根据新的条件降序排序

1
var n = list.OrderBy(p=>p.Age).ThenBy(p=>p.Salary)//根据年龄排序,年龄相同的人按照工资排序

限制结果集:

Skip:跳过n条数据
Take:获取最多n条数据,不够有多少取多少

1
var n = list.Skip(2).Take(3);//从第二条数据开始获取三条数据

聚合函数:

Max:取最大值

1
var n = list.Max(p=>p.Age)//取年龄最大值,返回一个int,如35

Min:取最小值

Average:取平均值

1
var n = list.Where(p=>p.Age>+30).Average(p=>p.Salary)//取30岁及以上人的平均工资。工资若为int类型,则返回值为double,平均值可能求出小数

Sum:求和

Count:计数


分组:

GroupBy:参数为分组条件表达式

1
2
3
4
5
6
7
8
var n = list.GroupBy(p=>p.Age);//以年龄分组
foreach(var g in n){
Console.WriteLine(g.Key);//打印每组的key,这里key为年龄
foreach(var e in g){
Console.WriteLine(e);//打印该组中每个成员
}
Console.WriteLine(g.Max(m=>m.Salary))//打印每组中最高的工资
}

投影:

将集合中的每个元素逐个转换为另一种类型

Select:

1
2
3
4
IEnmuerable<int> i = list.Select(p=>p.Age);//只取所有人的年龄
IEnmuerable<String> i = list.Select(p=>"Name:"+p.Name+" Age:"+p.Age);//只取所有人的姓名和年龄
IEnmuerable<Dog> i = list.Select(p=>new Dog{Name=p.Name,Age=p.Age});//映射为另一种对象

匿名类:

1
2
3
4
var obj = new {Name="TestName",Age=3};
Console.WriteLine(obj.Name);//TestName

var items = list.Select(p=>new{Name = p.Name,Gender = p.Gender?"男":"女"})

转换:

ToArray/ToList

查询语法

一个等价的查询语法和方法语法的实例:

1
2
3
4
5
6
7
8
9
//查询语法
IEnumerable<int> numQuery1 =
from num in numbers
where num % 2 == 0
orderby num
select num;

//方法语法:
IEnumerable<int> numQuery2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);