目录
本文分两篇,基础篇点击:C#基础与进阶扩展合集-基础篇
一、进阶
1、Predicate
2、设置C#语言版本
3、ListCollectionView过滤集合
4、值类型与引用类型
5、程序设置当前项目工作目录
6、获取App.config配置文件中的值
7、Linq常用语句
8、并行LINQ
9、强引用与弱引用
10、using处理非托管资源
11、模块初始化器
12、序列化
13、并行编程
14、单元测试
二、进阶扩展
1、Adapt适配器
2、Mutex互斥及防止App多开
3、Monitor设置等待资源时间
4、扩展方法实现解构
5、Span实现切片
6、数组池减少GC工作
7、深度解析await关键字
8、Task
9、ValueTask
10、async异步编程
11、CancellationTokenSource
12、异步方法的异常处理
三、版本新增
1、范围运算符
2、字符串格式控制
3、数字分隔符
4、小数点前后保留格式
本文分两篇,基础篇点击:C#基础与进阶扩展合集-基础篇
一、进阶
1、Predicate
拥有一个或多个泛型参数并返回一个 bool 值,常用于对 collection 进行一组条件检索,类似于Func。
举例:Predicate pre=m=>m.Id==2;
2、设置C#语言版本
工程文件 x.csproj中修改
PropertyGroup节点内添加子节点:
<LangVersion>latest</LangVersion>
3、ListCollectionView过滤集合
使用ListCollectionView类构造函数注入列表
通过该类的 Filter属性过滤集合
List<Animal> animals = new List<Animal>() { new Animal(1,"ani1"),new Animal(2,"动物2") };List<Bear> bears = new List<Bear>();var tmp = animals.Adapt<List<Bear>>();tmp.ForEach(m => m.Description = "Animal adapt bear...");ListCollectionView view=new ListCollectionView(tmp);view.Filter = i => ((Bear)i).ID == 2;foreach (var animal in view)MessageBox.Show(((Bear)animal).Name);
4、值类型与引用类型
值类型:变量直接保存其数据,作为类的字段(成员变量)时,跟随其所属的实例存储,也就是存储在堆中;作为方法中的局部变量时,存储在栈上;
引用类型:变量保存其数据的引用(地址)分配在栈中,具体数据(实例)部署在托管堆中;
值类型:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型
引用类型:数组,用户定义的类、接口、委托,object,字符串
引用类型string:
string a = "A";string b = a;Console.WriteLine($"a:{a}\tb:{b}");a= "B";Console.WriteLine($"a:{a}\tb:{b}");
string为引用类型,上面示例看出string像值类型:
实际上,是由于运算符的重构所导致的结果。当a被重新赋值时,.NET为a在托管堆上重新分配了一块内存。这样做的目的是,使字符串类型与通俗意义上讲的字符串更接地气。
引用类型数组:
数组元素为值类型时,在托管堆中一次性分配全部值类型空间(堆中栈),并自动初始化;
元素为 引用类型时,先在托管堆分配一次空间,此时不会自动初始化任何元素(均为null)。等到有代码初始化某个元素的时,这个引用类型元素的存储空间才会被分配在托管堆上;
5、程序设置当前项目工作目录
Directory.SetCurrentDirectory(Path.GetDirectoryName(typeof(Test).Assembly.Location));
Environment.CurrentDirectory=Path.Combine(Directory.GetCurrentDirectory(),"..");
6、获取App.config配置文件中的值
1、获取appSettings节点值:
ConfigurationManager.AppSettings[key];
2、获取connectionStrings节点值:
var list= ConfigurationManager.ConnectionStrings;
string str="";
foreach (ConnectionStringSettings item in list)
{
if(item.Name=="ConTest")
str = item.ConnectionString;
}
7、Linq常用语句
定义LINQ扩展方法的一个类是System.Linq名称空间中的Enumerable;
Linq常用语句,详细讲解点击:C#-关于LINQ
select:以指定形式返回
Where查询特点条件(方式1:from in;方式2:Lambda表达式)
Order排序:1、descending 降序;2、ascending 升序
OfType查询特定类型
Join合并两集合通过指定键,返回指定结构类型集合
GroupJoin:俩集合通过指定键分组
Reverse反转集合元素顺序
GroupBy按指定键对自身分组
Any / All 判断是否(任意一个/全部)满足条件
Skip跳过指定数量元素
Take拿取指定数量元素
Count获取元素个数
Sum、Average、Max、Min获取集合总值、平均值、最大值、最小值
Concat连接集合
Distinct去重(去重类中某个字段需实现IEqualityComparer接口)
ElementAt获取指定索引元素(与[ ]类似)
First/Single、Last:获取集合中第一个、最后一个元素(如果集合中包含多个元素,使用Single会报错);
ToDictionary:将集合转换为字典;
ToList: 将集合转换为List;
SequenceEqual:判断两个集合是否相等;
8、并行LINQ
System.Linq名称空间中包含的类ParallelEnumerable可将查询的工作拆分到多个处理器上同时运行的多个线程中;
通常可使用AsParallel()方法让集合类以并行方式查询,该方法扩展了IEnumerable<TSource>接口,返回ParallelQuery<TSource>类;
示例如下,示例中并行LINQ所用时间约90ms,普通LINQ所用时间约为420ms,可以看出并行LINQ加快了代码运行速度
var list=Enumerable.Range(0, 5000_0000).Select(x => Random.Shared.Next(100)).ToList();
Stopwatch sw = Stopwatch.StartNew();
var avera= list.AsParallel().Where(m=>m<50).Select(m=>m).Average();
sw.Stop();
Stopwatch sw2 = Stopwatch.StartNew();
var avera2= list.Where(m => m < 50).Select(m => m).Average();
sw2.Stop();
Console.WriteLine(sw.Elapsed.TotalMilliseconds);//约90ms
Console.WriteLine(sw2.Elapsed.TotalMilliseconds);//约420ms
9、强引用与弱引用
1、在应用程序中实例化一个类或结构时,只要有代码引用它,就会形成强引用;
2、GC不能收集仍在引用的对象的内存,也就是强引用的内存,但它可以收集不在根表中直接或间接引用的托管内存;
3、弱引用允许创建和使用对象,但如果垃圾收集器碰巧运行,就会收集对象并释放内存,弱引用开销比小对象大,用于小对象没有意义;
4、弱引用使用WeakReference类创建的,使用构造函数传递强引用,其Target属性的值若不为null,则该对象仍可使用,若赋值给传递类型对象,则会再次创建该对象的强引用,不能被GC收集(注意:在访问Target时可能被GC收集,所以通常赋值后需对其进行null判断);
WeakReference weakReference = new WeakReference(new Pig());Pig pig = weakReference.Target as Pig;if (pig != null){//use pig}else{ //reference not available}
10、using处理非托管资源
方式一:声明一个析构函数(或终结器finalizer)作为类的成员;
C#中,析构函数在底层.NET体系结构中为终结器,编译器会隐式的把析构函数编译成等价于重写Finalize()方法的代码,如下:
protected override void Finalize(){try {//Finalizer implementation}finally {base.Finalize();}}
方式二:实现IDisposable或IAsyncDisposable接口;
C#中,推荐使用该方式替代析构函数,这些接口定义了一种模具(具有语言级的支持),该模式为释放非托管的资源提供了确定的机制,并避免产生析构函数固有的与GC相关的问题;
注意:若处理过程中出现异常则不会释放,通常在finally块中释放,如下:
People people = null;
try
{people = new();//other process
}
finally
{people.Dispose();
}
class People : IDisposable
{public void Dispose(){//implementation}
}
方式三:using语句和using声明(推荐),实现了对方式二的封装;
用于实现IDisposable接口的对象,当对象的引用超出作用域时,自动调用该对象的Dispose()或DisposeAsync()方法,如下示例,会生成与上面try块等价的IL代码;
using (People people = new())
{ //other process
}
11、模块初始化器
若需要在使用一个库的任何类型之前调用该库的初始化代码,可使用C#的一个新特性,模块初始化器[ModuleInitializer];
在使用该类的任何类型之前,会自动调用该特性标记的初始化方法,该方法必须是静态、无参,返回void,使用public或internal访问修饰符;
[ModuleInitializer]public static void Initializer(){Console.WriteLine("*******Module Initializer********");}
12、序列化
序列化相关详解:C#-序列化与反序列化(xml、json)
13、并行编程
关于并行编程详解:C#-关于并行编程
14、单元测试
运用NUnit单元测试框架:C#-单元测试NUnit框架的安装及使用
二、进阶扩展
1、Adapt适配器
安装NutGet包:Mapster
可理解成转换器,适配器适配的是不同类间相同的名称,不论字段或属性(必须为值类型或字符串类型),只要名字相同,都适配给目的对象;
注意:即使名称相同,属性或字段也不能适配成方法
Animal animal = new Animal(18);Bear bear = animal.Adapt<Bear>();Console.WriteLine(bear.Age.ToString());Console.WriteLine(bear.Description.ToString());Console.WriteLine("************************");Bear bear1=new Bear();Console.WriteLine(bear1.Age.ToString());Console.WriteLine(bear1.Description.ToString());Console.WriteLine("*************************");Banana banana = animal.Adapt(new Banana());Console.WriteLine(banana.Description);
2、Mutex互斥及防止App多开
1、继承自WaitHandle类:抽象基类,用于等待一个信号的设置(有静态方法WaitOne()、WaitAll()、WaitAny());
2、Mutex互斥锁可定义互斥名称,所以可用于跨进程的同步操作(因为操作系统可识别有名称的互斥,在不同进程间共享);
3、Mutex构造函数中,可指定互斥是否最初应由主调线程拥有、定义互斥名称、获取互斥是否已存在的信息;
用法1:跨进程互斥实现进程间同步(未命名互斥只能用于跨线程)
Mutex mutext = new Mutex(false,"MyConsole");
mutext.WaitOne();
Console.WriteLine($"{Process.GetCurrentProcess().ProcessName}:\tStart......");
Console.ReadLine();
mutext.ReleaseMutex();
Console.WriteLine($"{Process.GetCurrentProcess().ProcessName}:\tEnd.......");
用法2:防止App重复开启
Mutex mutext = new Mutex(false,"MyConsole",out bool createNew);
if (!createNew)return;
3、Monitor设置等待资源时间
lock关键字是由Monitor类实现(抛出异常也会解锁)如下:
Monitor.Enter(_obj);try{Count--;}finally { Monitor.Exit(_obj); }
Monitor相对于lock的优点在于,使用Monitor的TryEnter()方法,其中可传递一个超时值,用于指定等待被锁定的最长时间,若_obj被锁定,TryEnter()方法将布尔型的引用参数设置为true,并同步的访问_obj锁定状态,若另一个线程锁定_obj时间超过指定时间,TryEnter()将bool引用参数置为false,线程将不再等待,而是去执行其它操作,如下:
Monitor.TryEnter(_obj, 2000, ref _lockTaken);if (_lockTaken){try{Console.WriteLine(Thread.CurrentThread.Name + ":\t obj lock.....");Thread.Sleep(5000);Console.WriteLine(Thread.CurrentThread.Name + ":\t obj release.....");}finally{Monitor.Exit(_obj);}}elseConsole.WriteLine("Timeout,Run other.....");
4、扩展方法实现解构
了解扩展方法点击:扩展方法定义与使用
创建Deconstruct()方法(也称解构器),将分离部分放入out参数中,这里使用扩展方法实现解构,示例如下:
Stu stu = new Stu(98, "Auston");
stu.Deconstruct(out int score, out string name);
Console.WriteLine($"{name}:{score}");static class StuExtension
{public static void Deconstruct(this Stu stu, out int score, out string name){score = stu.Score;name = stu.Name;}
}
5、Span<T>实现切片
1、Span<T>,可快速访问托管与非托管的连续内存,如数组、长字符串;
2、可实现对数组部分进行访问或切片,不会复制数组元素,是从span中直接访问的,切片的两种方式①构造函数传递数组的开头与结尾;②Slice方法传递开头索引,提取到数组末尾;
3、可使用Span改变值,除了索引访问更改,还提供方法有:Clear()、填充Fill()、复制CopyTo()(不推荐,目标span不够大会抛异常)、复制推荐TryCopyTo()(span不够大不抛异常,而是返回false);
4、若只需对数组片段进行读访问,可使用ReadOnlySpan<T>;
int[] c = { 1, 3, 5, 8 };
Span<int> span = new Span<int>(c);
Span<int> span1= new Span<int>();
span[1] = 11;
span.Clear();
span.Fill(11);
Span<int> span2 = new Span<int>(c,0,3);
Span<int> span3 = span.Slice(0,3); //切片
ReadOnlySpan<int> span4 = new(c); //只读变量
if (!span.TryCopyTo(span3))Console.WriteLine("Argument");
6、数组池减少GC工作
通过ArrayPool类(名称空间System.Buffers)使用数组池,可减少垃圾收集器的工作,ArrayPool管理一个数组池,数组可以从这租借,并返回池中,内存在ArrayPool中管理。
创建ArrayPool<T>,调用静态Create()方法;
使用预定义共享池,通过访问Shared属性;
从池中租用内存,可调用Rent()方法,(池中数组元素数量最低16,且都是成倍增加);
内存(数组)返回到池中,调用Return()方法,可指定返回池之前是否清除该数组(false,下次租用数组的人可读取数据);
ArrayPool<int> arrayPool = ArrayPool<int>.Create(maxArrayLength: 100, maxArraysPerBucket: 10);
int[] arr = ArrayPool<int>.Shared.Rent(10);
arr[15] = 15;
Console.WriteLine($"Len={arr.Length}\tarr[15]={arr[15]}");//输出Len=16 arr[15]=15
ArrayPool<int>.Shared.Return(arr,true);
Console.WriteLine(arr[15]);//输出0
7、深度解析await关键字
await通常与async一同使用来实现异步编程,async没有await搭配使用将毫无意义;
使用Task任务的GetAwaiter()方法,返回一个TaskAwaiter<T>类型对象,该对象的OnCompleted()方法实现了INotifyCompletion接口,在任务完成时调用;
await实际就是编译器把await关键字后的所有代码放进了OnCompleted()方法的代码块中。
public static void Main(string[] args){TestAsync();Console.ReadLine();}public static async void TestAsync(){var awaiter = MyAsync().GetAwaiter();awaiter.OnCompleted(() =>{Console.WriteLine($"MyAsync ended....");});//await MyAsync();//Console.WriteLine("MyAsync ended.....");}public static async Task<string> MyAsync(){Thread.Sleep(100);Console.WriteLine(nameof(MyAsync));return nameof(MyAsync);}
8、Task
GetAwaiter()方法,用于await关键字的实现,详细如上;
ContinueWith()方法,用于延续任务;
RunSynchronously()方法,同步任务;
WaitAll()静态方法,阻塞调用任务,直到所有任务完成;
WhenAll()静态方法,返回一个任务,从而允许使用async关键字等待结果,因此不会阻塞等待的任务;
WhenAny()静态方法,用于等待任意一个任务结束;
注意:任务不一定使用线程池中的线程,也可以使用其他线程,调用RunSynchronously()任务以同步方式运行,以相同的线程作为主调线程,如示例中的 t4;
开始一个新任务方式有如下几种:
TaskMethod();Task t1 = Task.Run(TaskMethod);Thread.Sleep(100);Task t2 = Task.Factory.StartNew(TaskMethod);Thread.Sleep(100);Task t3 = new TaskFactory().StartNew(TaskMethod);Thread.Sleep(100);Task t4 = new(TaskMethod);//t4.Start();t4.RunSynchronously();static void TaskMethod(){Console.WriteLine($"Task ID:{Task.CurrentId?.ToString() ?? "no task"}");Console.WriteLine($"thread ID:{Thread.CurrentThread.ManagedThreadId}");Console.WriteLine($"Is background:{Thread.CurrentThread.IsBackground}");Console.WriteLine($"Is pool thread:{Thread.CurrentThread.IsThreadPoolThread}");Console.WriteLine("*****************Auston****************");}
使用泛型类Task<TResult>获取Task结果,示例如下
Task<(int result1, int result2)> t4 = new (TaskWithResult,(2,5));t4.Start();Console.WriteLine(t4.Result.Item1+$"\t{t4.Result.Item2}");static (int, int) TaskWithResult(object obj){(int a, int b) = ((int a, int b))obj;return (a * 10, b * 100);}
9、ValueTask
C#7新增可用作await的新类型,ValueTask是一个结构,其在堆上没有对象,相对于Task具有性能上的优势(当不能忽略任务开销的时候可使用);
10、async异步编程
async、await异步编程详解:C#-异步编程
11、CancellationTokenSource
CancellationTokenSource控制Task详解:C#-控制Task结束
12、异步方法的异常处理
若调用异步方法没有等待,try/catch不会捕获异常,因为在抛出异常前,就已经执行完毕了,若要捕获异常,需await等待异步方法。
static void PutError(){try{await ThrowExcp();}catch (Exception ex){Console.WriteLine(ex.ToString());}}static async Task ThrowExcp(){ await Task.Delay(1000);throw new Exception("Exception.....");}
三、版本新增
C#9新增顶级语句;
字符串的范围除SubString方法,C#8新增hat(^)、范围运算符([..]);
1、范围运算符
string rangstr ="hello,auston!" ;
Console.WriteLine(rangstr[..5]);//范围运算符
Console.WriteLine(rangstr[7^2]);//hat^运算符,从索引7往前数第2个字符
2、字符串格式控制
DateTime t = DateTime.Now;
Console.WriteLine($"{t:D}");//字符串格式控制
3、数字分隔符
int a = 2_2_2;//使用数字分隔符,提高代码可读性(编译器会忽略下划线)
Console.WriteLine($"{a:c}");
4、小数点前后保留格式
double d = 22.336_6;
Console.WriteLine($"{d:###.##}");//小数点后四舍五入保留2位
Console.WriteLine($"{d:000.00}");//小数点前保留3位,后保留2位