《c#10 in a nutshell》--- 读书随记(8)
Chaptor 9. LINQ Operators
内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的
Overview
分成三类:
- Sequence in, sequence out
- Sequence in, single element or scalar value out
- Nothing in, sequence out
我们首先介绍三个类别中的每一个以及它们包含的查询操作符
Sequence→Sequence

Filtering
IEnumerable
Where, Take, TakeLast, TakeWhile, Skip, SkipLast, SkipWhile, Distinct, DistinctBy
Projecting
IEnumerable
用 lambda 函数转换每个元素。Select 和 SelectMany 在EF Core中会执行inner joins、left outer joins、cross joins和non-equi joins。
Select, SelectMany
Joining
IEnumerable
将一个序列的元素与另一个序列的元素网格化。Join 和 GroupJoin 操作员的设计目的是提高本地查询的效率,并支持 inner joins 和 left outer joins。Zip 操作符逐步枚举两个序列,在每个元素对上应用一个函数。Zip 操作符将类型参数命名为 TFirst 和 TSecond,而不是将类型参数命名为 TOut 和 TInner
Join, GroupJoin, Zip
Ordering
IEnumerable
OrderBy, OrderByDescending, ThenBy, ThenByDescending, Reverse
Grouping
IEnumerable
IEnumerable
GroupBy, Chunk
Set operators
IEnumerable
Concat, Union, UnionBy, Intersect, IntersectBy, Except, ExceptBy
Conversion methods: Import
IEnumerable→IEnumerable
OfType, Cast
Conversion methods: Export
IEnumerable
ToArray, ToList, ToDictionary, ToLookup, AsEnumerable, AsQueryable
Sequence→Element or Value
Element operators
IEnumerable
First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault, ElementAt, ElementAtOrDefault, MinBy, MaxBy, DefaultIfEmpty
Aggregation methods
IEnumerable
Aggregate, Average, Count, LongCount, Sum, Max, Min
Quantifiers
IEnumerable
All, Any, Contains, SequenceEqual
Void→Sequence
Generation methods
void→IEnumerable
制造一个简单的序列
Empty, Range, Repeat
Chaptor 10. LINQ to XML
暂时跳过
Chaptor 11. Other XML and JSON Technologies
跳过XML
Working with JSON
Utf8JsonReader
System.Text.Json.Utf8JsonReader是针对 UTF-8编码的 JSON 文本的优化的只进阅读器
byte[] data = File.ReadAllBytes ("people.json");
Utf8JsonReader reader = new Utf8JsonReader (data);
while (reader.Read())
{
switch (reader.TokenType)
{
case JsonTokenType.StartObject:
Console.WriteLine ($"Start of object");
break;
case JsonTokenType.EndObject:
Console.WriteLine ($"End of object");
break;
case JsonTokenType.StartArray:
Console.WriteLine();
Console.WriteLine ($"Start of array");
break;
case JsonTokenType.EndArray:
Console.WriteLine ($"End of array");
break;
case JsonTokenType.PropertyName:
Console.Write ($"Property: {reader.GetString()}");
break;
case JsonTokenType.String:
Console.WriteLine ($" Value: {reader.GetString()}");
break;
case JsonTokenType.Number:
Console.WriteLine ($" Value: {reader.GetInt32()}");
break;
default:
Console.WriteLine ($"No support for {reader.TokenType}");
break;
}
}
JsonReaderOptions
可以通过传递一个 JsonReaderOptions 实例给 Utf8JsonReader
-
C-Style comments
默认情况下,JSON 中的注释会引发 JsonException。将 CommentProcessing 属性设置为 JsonCommentProcessing.Skip 会导致忽略注释,而 JsonCommentHandling.Allow 导致读者识别注释并发出 JsonTokenType。当遇到注释标记时。注释不能出现在其他标记的中间。 -
Trailing commas
根据标准,对象的最后一个属性和数组的最后一个元素不能有后面的逗号。将 AllowTrailingCommas 属性设置为 e 将放宽此限制。 -
Control over the maximum nesting depth
默认情况下,对象和数组可以嵌套到64级。将 MaxDepth 设置为不同的数字将覆盖此设置。
Utf8JsonWriter
System.Text.Json.Utf8JsonWriter是一个只能向前走的writer,支持以下几种格式:
- String
- DateTime
- 数字
- bool
- JSON null
- Arrarys
var options = new JsonWriterOptions { Indented = true };
using (var stream = File.Create ("MyFile.json"))
using (var writer = new Utf8JsonWriter (stream, options))
{
writer.WriteStartObject();
// Property name and value specified in one call
writer.WriteString ("FirstName", "Dylan");
writer.WriteString ("LastName", "Lockwood");
// Property name and value specified in separate calls
writer.WritePropertyName ("Age");
writer.WriteNumberValue (46);
writer.WriteCommentValue ("This is a (non-standard) comment");
writer.WriteEndObject();
}
JsonDocument

Chapter 12. Disposal and Garbage Collection
有些对象需要显式的拆分代码来释放资源,例如打开的文件、锁、操作系统句柄和非托管对象。在.NET中,这叫做 disposal ,通过 IDisposable 接口来支持。未使用对象占用的托管内存也必须在某个时候回收; 这个功能称为 garbage collection 垃圾收集,由 CLR 执行。
销毁与垃圾收集的不同之处在于,销毁通常是显式启动的,垃圾收集是完全自动的。换句话说,程序员负责释放文件句柄、锁和操作系统资源,而 CLR 负责释放内存。
IDisposable, Dispose, and Close
public interface IDisposable
{
void Dispose();
}
C # 的 using 语句提供了一个语法快捷方式,可以使用 try/finally 块对实现 IDisposable 的对象调用 Dispose:
using (FileStream fs = new FileStream ("myFile.txt", FileMode.Open))
{
// ... Write to the file ...
}
编译器会将其转换为:
FileStream fs = new FileStream ("myFile.txt", FileMode.Open);
try
{
// ... Write to the file ...
}
finally
{
if (fs != null) ((IDisposable)fs).Dispose();
}
Standard Disposal Semantics
.NET 在其销毁逻辑中遵循一套事实上的规则。这些规则不是.NET 或 C # 语言与生俱来的; 它们的目的是为消费者定义一致的协议
- 一件物品被处置后,就无可救药了。无法重新激活它,并且调用它的方法或属性(Dispose 除外)会引发 ObjectDisposedException。
- 重复调用对象的 Dispose 方法不会导致错误。
- 如果一次性对象 x“拥有”一次性对象 y,x 的 Dispose 方法会自动调用 y 的 Dispose 方法ー除非另有指示。
根据规则3,容器对象自动释放其子对象。Windows 窗体容器控件(如 Form 或 Panel)就是一个很好的例子。容器可以容纳许多子控件,但是您不能显式地释放它们中的每一个; 关闭或释放父控件或窗体会解决所有问题。另一个例子是在 DeflateStream 中包装 FileStream。处理 DeflateStream 也处理 FileStream ーー除非您在构造函数中另有说明。
When to Dispose
一个可以遵循的安全规则(几乎在所有情况下)是“如果有疑问,销毁”包装非托管资源句柄的对象几乎总是需要销毁来释放句柄。示例包括文件或网络流、网络套接字、 Windows 窗体控件、 GDI+ pens、brushes和bitmaps。相反,如果类型是可抛弃的,它通常(但不总是)直接或间接引用非托管句柄。这是因为非托管句柄提供了通往操作系统资源、网络连接和数据库锁等“外部世界”的网关,这些外部世界是对象在被不恰当地放弃时可能在自身之外制造麻烦的主要手段。
Clearing Fields in Disposal
一般来说,你不需要清除一个对象的销毁方法中的字段。但是,从对象在其生命周期内在内部订阅的事件中取消订阅是一种很好的做法(例如,请参阅“托管内存泄漏”)。从此类事件中取消订阅可以防止接收不必要的事件通知,并防止无意中使对象在垃圾收集器(GC)眼中保持活动状态。
Automatic Garbage Collection
不管一个对象是否需要一个自定义拆卸逻辑的销毁方法,在某个时候它在堆上占用的内存必须被释放。CLR 通过自动 GC 完全自动地处理它的这一侧。您自己从不释放托管内存
public void Test()
{
byte[] myArray = new byte[1000];
...
}
执行 Test 时,将在内存堆上分配一个容纳1,000字节的数组。数组由存储在本地变量堆栈上的变量 myArray 引用。当方法退出时,这个局部变量 myArray 将弹出作用域,这意味着没有任何东西可以引用内存堆上的数组。孤立的数组就有资格在垃圾收集中被回收。
对象变为孤立对象后,垃圾收集不会立即发生。与街道上的垃圾收集不同,它是定期发生的,尽管(与街道上的垃圾收集不同)没有固定的时间表。CLR 根据一系列因素(如可用内存、内存分配量以及自上次收集以来的时间)来决定何时收集内存(GC 自我调优以优化应用程序的特定内存访问模式)。这意味着在孤立对象和从内存释放对象之间存在不确定的延迟。这种延迟可以从纳秒到天不等。
GC 不会在每次收集时收集所有垃圾。相反,内存管理器将对象分为几代,GC 收集新几代(最近分配的对象)的频率高于旧几代(长寿命对象)。
Roots
Root 是让物体活着的东西。如果一个对象没有被 root 直接或间接引用,它将有资格被垃圾收集。
Root 可以是下面几种情况之一:
- 执行方法(或其调用堆栈中的任何方法)中的局部变量或参数
- 静态变量
- 队列中存储准备finalization的对象的对象

代码不可能在被删除的对象中执行,所以如果有任何可能(实例)方法执行,它的对象必须以某种方式被引用。
注意,一组循环引用的对象在没有 root 裁判的情况下被认为是死的。换句话说,不能按照 root 对象的箭头(引用)访问的对象是不可访问的ーー因此需要进行收集
Finalizers
在从内存释放对象之前,将运行其 finalizer
class Test
{
˜Test()
{
// Finalizer logic...
}
}
运行 Finalizers 是可能的,因为垃圾收集工作在不同的阶段。首先,GC 标识可以删除的未使用对象。没有 Finalizers 的会被立即删除。那些挂起的(未运行的) Finalizers 保持活动状态(目前) ,并被放到一个特殊的队列中。
此时,垃圾收集完成,程序继续执行。然后,Finalizer 线程启动并开始与程序并行运行,从特殊队列中挑选对象并运行它们的终结方法。在每个对象的 Finalizer 运行之前,它仍然是非常活跃的ーー这个队列充当了一个 root 对象。在它被排出队列并执行 Finalizer 之后,对象变成孤立的,并将在下一个集合中被删除(为了生成该对象)。
Finalizers 可能是有用的,但它们附带了一些条件:
- Finalizers 减慢了内存的分配和收集(GC 需要跟踪哪些 Finalizers 已经运行)。
- Finalizers 延长对象和任何被引用对象的寿命(它们都必须等待下一辆垃圾车进行实际删除)。
- 我们不可能预测一组对象的 Finalizers 调用的顺序。
- 对于什么时候调用对象的 Finalizer,您的控制是有限的。
- 如果代码在一个 Finalizer 块中,其他对象就无法回收
总之,Finalizers 有点像律师ーー尽管在某些情况下,你确实需要他们,但一般而言,除非绝对必要,你不想使用他们。如果你真的使用它们,你需要100% 确定你明白它们为你做了什么。
以下是一些实施 Finalizers 的指导方针:
- 确保 Finalizer 快速执行
- 永远不要阻塞你的 Finalizer
- 不要引用其他可终结的对象
- 不要抛出异常
即使在构造过程中抛出异常,CLR 也可以调用对象的 Finalizer。出于这个原因,在写 Finalizer 的时候不要假设字段已经被正确地初始化。
How the GC Works
标准 CLR 使用分代标记-压缩 GC,该 GC 对存储在托管堆上的对象执行自动内存管理。GC 被认为是一个跟踪 GC,因为它不会干扰对对象的每一次访问,而是间歇性地唤醒并跟踪存储在托管堆上的对象图,以确定哪些对象可以被认为是垃圾,从而进行收集。
GC 在执行内存分配(通过 new 关键字)时发起垃圾收集,这可能是在分配了某个内存阈值之后,也可能是在其他时候,为了减少应用程序的内存占用。这个过程也可以通过调用 System.GC.Collect 手动启动。在垃圾收集过程中,所有线程都可以被冻结
GC 从它的 root 对象引用开始,遍历对象图,将它接触到的所有对象标记为可访问的。当此过程完成时,所有未标记的对象将被视为未使用,并将受到垃圾回收的影响。
没有 Finalizers 的未使用对象会立即丢弃,带有 Finalizers 的未使用对象会在 GC 完成后排队等待 Finalizer 线程处理。然后,这些对象就有资格在下一个 GC 中收集以生成对象(除非重新生成)。
其余的“活动”对象随后被移动到堆的开始(压缩) ,从而为更多的对象释放空间。这种压缩有两个目的: 一是防止内存碎片,二是允许 GC 在分配新对象时采用一种非常简单的策略,即始终在堆的末尾分配内存。这可以防止维护一个空闲内存段列表这一可能非常耗时的任务。
如果在垃圾收集之后没有足够的空间为新对象分配内存,并且操作系统无法授予更多内存,则会引发 OutOfMemoryException。
Optimization Techniques
GC 采用了各种优化技术来减少垃圾收集时间。
Generational collection
最重要的优化是 GC 是分代的。这利用了这样一个事实: 尽管很多对象被迅速地分配和丢弃,但是某些对象是长寿命的,因此不需要在每次收集中进行跟踪。
基本上,GC 将托管堆分为三代。刚刚分配的对象在 Gen0中,存活一个收集周期的对象在 Gen1中; 所有其他对象在 Gen2中。Gen0和 Gen1被称为短命的一代。
CLR 使 Gen0部分相对较小(通常大小为几百 KB 到几 MB)。当 Gen0部分填满时,GC 会启动一个 Gen0收集ーー这种情况相对比较常见。GC 对 Gen1应用类似的内存阈值(作为 Gen2的缓冲区) ,因此 Gen1收集也相对快速和频繁。然而,包含 Gen2的完整收集需要更长的时间,因此很少发生。

为了给出一些非常粗略的数字,Gen0收集可能需要不到一毫秒的时间,这在典型的应用程序中是不足以被注意到的。然而,在具有大型对象图的程序上,完整的集合可能需要长达100ms 的时间。这些数字取决于许多因素,因此可能有很大的差异,特别是在 Gen2的情况下,它的大小是无限的(与 Gen0和 Gen1不同)。
结果是短寿命对象在使用 GC 方面非常有效。
The Large Object Heap
对于大于某个阈值(目前为85,000字节)的对象,GC 使用一个单独的堆,称为大型对象堆(Large Object Heap,LOH)。这样可以防止压缩大型对象的成本,并防止过多的 Gen0集合ーー如果没有 LOH,分配一系列16MB 的对象可能会在每次分配后触发 Gen0集合。
缺省情况下,LOH 不受压缩的影响,因为在垃圾收集期间移动大块内存的代价高得令人望而却步。这有两个后果:
- 分配可能会比较慢,因为 GC 不能总是简单地在堆的末尾分配对象ーー它还必须在中间寻找差距,这需要维护一个可用内存块的链表。
- LOH 容易碎片化。这意味着一个对象的释放可以在 LOH 中创建一个洞,以后可能难以填补。例如,86,000字节对象留下的空洞只能由85,000字节到86,000字节之间的对象填充(除非有另一个空洞相邻)。
Workstation versus server collection
.NET 提供两种垃圾收集模式: 工作站和服务器。工作站是默认的; 您可以通过将以下内容添加到应用程序中来切换到服务器 .Csproj 文件:
<PropertyGroup>
<ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup>
在构建项目时,此设置将写入应用程序的. runtimeconfig.json 文件,CLR 在该文件中读取它
"runtimeOptions": {
"configProperties": {
"System.GC.Server": true
}
}
启用服务器收集时,CLR 为每个核分配一个单独的堆和 GC。这加速了收集,但是消耗了额外的内存和 CPU 资源(因为每个核心需要自己的线程)。如果机器在运行许多其他进程时启用了服务器收集,这可能导致 CPU 超订,这对工作站尤其有害,因为它使操作系统整体上感觉没有响应。
服务器收集只在多核系统上可用: 在单核设备(或单核 virtual)上,设置被忽略。
Background collection
在工作站和服务器模式下,CLR 默认启用后台收集。可以通过将以下内容添加到应用程序的.csproj 文件:
<PropertyGroup>
<ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
</PropertyGroup>
在构建项目时,此设置将写入应用程序的. runtimeconfig.json 文件,CLR 在该文件中读取它
"runtimeOptions": {
"configProperties": {
"System.GC.Concurrent": false,
}
}
GC 必须在收集期间冻结(阻塞)您的执行线程。后台收集使这些延迟时间最小化,使应用程序响应更快。这是以稍微多消耗一点 CPU 和内存为代价的。因此,通过禁用后台收集,您可以完成以下任务:
- 略微减少 CPU 和内存的使用
- 增加垃圾收集发生时的暂停(或延迟)
后台集合的工作原理是允许应用程序代码与 Gen2收集并行运行。(Gen0和 Gen1收集被认为足够快,因此它们不能从这种并行性中获益。)
后台收集是以前称为并发收集的一个改进版本: 它消除了一个限制,即并发收集在运行 Gen2收集时,如果 Gen0部分填满,那么并发收集将不再是并发的。这使得持续分配内存的应用程序响应能力更强。
GC notifications
如果禁用后台收集,则可以要求 GC 在发生full gc(阻塞)之前通知您。这是针对服务器场配置的: 其思想是在收集之前将请求转移到另一个服务器。然后立即启动该垃圾收集,并等待它完成后再将请求重新路由回该服务器。
要启动通知,请调用 GC.RegisterForFullGCNotification。然后,启动另一个线程,该线程首先调用 GC.WaitForFullGCMethod。当此方法返回指示集合接近的 GCNotificationStatus 时,可以将请求重新路由到其他服务器并强制执行手动集合。然后调用 GC.WaitForFullGCComplete: 当此方法返回时,集合完成,并且可以再次接受请求。然后重复整个循环。
Forcing Garbage Collection
您可以随时通过调用 GC.Collect 手动强制执行垃圾回收。在没有参数的情况下调用 GC.Collect 会激发一个full GC。如果传入一个整数值,则只收集该值的代数,因此 GC.Collect (0)只执行快速 Gen0收集。
一般来说,通过允许 GC 决定何时进行收集,可以获得最佳性能: 强制收集会不必要地将 Gen0对象提升到 Gen1(将 Gen1对象提升到 Gen2) ,从而影响性能。它还可能破坏 GC 的自调优能力,即 GC 在应用程序执行时动态调整每一代的阈值以最大限度地提高性能。
但也有例外。干预最常见的情况是应用程序睡眠一段时间: 一个很好的例子是执行日常活动的 Windows 服务(可能是检查更新)。这样的应用程序可能使用 System.Timers.Timer 每24小时启动一次活动。在完成活动之后,24小时内没有进一步的代码执行,这意味着在这段时间内,没有内存分配,因此 GC 没有机会激活。无论服务在执行其活动时消耗了多少内存,它都将在随后的24小时内继续消耗内存ーー即使对象图是空的!解决方法是在每日活动结束后立即调用 GC.Collect
为了确保收集被 Finalizers 延迟的对象,可以调用 WaitforPendingFinalizer 并重新收集:
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
通常这是一个循环: 运行 Finalizers 的行为可以释放出更多的对象,而这些对象本身就有 Finalizers。
调用 gc.Collect 的另一种情况是,当您测试一个具有 Finalizer 的类时。
Tuning Garbage Collection at Runtime
静态 GCSettings.LatencyMode 属性确定 GC 如何平衡延迟和总体效率。将其默认值 Interactive 更改为 LowLatency 或 SustainedLowLatency 指示 CLR 更快地(但更频繁地)收集。如果您的应用程序需要非常快速地响应实时事件,那么这是非常有用的。将模式更改为 Batch 可以以牺牲可能较差的响应性为代价实现吞吐量的最大化,这对于批处理非常有用。
如果在. runtimeconfig.json 文件中禁用后台收集,将不支持 SustainedLowLatency。
您还可以通过调用 GC.TryStartNoGCArea 告诉 CLR 暂时挂起垃圾回收,并使用 GC.EndNoGCArea 恢复它。
Memory Pressure
运行时根据许多因素(包括计算机上的总内存负载)决定何时启动收集。如果程序分配非托管内存,运行时对其内存使用情况的感知将是不切实际的,因为 CLR 只知道托管内存。您可以通过指示 CLR 假定已经分配了指定数量的非托管内存来减轻这种情况; 您可以通过调用 GC.AddMemoryPressure 来实现这一点。若要撤消此操作(释放非托管内存时) ,请调用 GC.RemoveMemoryPressure
Array Pooling
如果应用程序经常实例化数组,则可以通过数组池 array pooling 避免大部分垃圾收集开销。.NETCore3中引入了数组池并通过“租用”一个数组来工作,这个数组稍后将返回到池中进行重用。
在System.Buffers中,调用ArrayPool的Rent方法
int[] pooledArray = ArrayPool<int>.Shared.Rent (100); // 100 bytes
这将从全局共享数组池中分配一个(至少)100字节的数组。池管理器可能会给您一个比您要求的数组大的数组(通常,它以2的幂分配)。完成数组处理后,调用 Return: 这会将数组释放到池中,允许再次租用同一个数组:
ArrayPool<int>.Shared.Return (pooledArray);
需要注意的是:数组池的一个限制是,没有什么可以阻止您在数组被返回后继续(非法)使用它,所以您需要仔细编码以避免这种情况。请记住,您不仅可以破坏自己的代码,还可以破坏其他使用数组池的 API,例如 ASP.NET Core。
与使用共享数组池不同,您可以创建一个自定义池并从中租用。这防止了破坏其他 API 的风险,但增加了总体内存使用(因为它减少了重用的机会) :
var myPool = ArrayPool<int>.Create();
int[] array = myPool.Rent (100);

浙公网安备 33010602011771号