分类目录归档:未分类

SQL提高查询性能的几种方式

创建索引,提高性能

索引可以极大地提高查询性能,其背后的原理:

  1. 索引是的数据库引擎能够快速的找到表中的数据,它们类似于书籍的目录,使得你不需要逐页查找所需要的信息
  2. 索引能够帮助数据库引擎直接定位到所需的数据,从而大大减少磁盘I/O操作,如果没有索引,SQL SERSER可能需要执行全表的扫描来查询数据,这需要大量的磁盘I/O操作
  3. 在分布式查询中,如果远程服务器上的表有索引,那么只需要将所需要的数据行发送的请求服务器,而不是整个表,从而减少了网络的流量
  4. 查询优化器会使用索引统计信息来生成最有效的查询计划。

SQL Server 提供了多种类型的索引,以优化查询性能和满足不同的数据访问需求,以下是一些主要常用的索引类型:

  1. 聚集索引:每个表只能有一个聚集索引。这种索引决定了表中数据的物理存储顺序。聚集索引使用行的键值对数据进行排序和存储.
    CREATE CLUSTERED INDEX IDX_Table_Column ON Table (Column);
  2. 非聚集索引:非聚集索引与聚集索引不同,它不影响数据的物理存储顺序,而是创建一个不同的数据结构(B-tree),其中包含键值和对应行数据的指针。一个表可以有多个非聚集索引。
   CREATE NONCLUSTERED INDEX IDX_Table_Column 
   ON Table (Column);
  1. 唯一索引:唯一索引确保索引键中的每个值只出现一次。这意味着每个索引键对应一个唯一的数据行。唯一索引可以是聚集索引或非聚集索引。
   CREATE UNIQUE NONCLUSTERED INDEX IDX_Table_Column 
   ON Table (Column);
  1. 复合索引:复合索引是包含两个或更多列的索引。复合索引的顺序很重要,因为 SQL Server 将首先按照第一列排序,然后在每个第一列的值内按照第二列排序,依此类推。
   CREATE INDEX IDX_Table_Column1_Column2 
   ON Table (Column1, Column2);
  1. 过滤索引:过滤索引是非聚集索引的一种变体,它只包含满足特定过滤谓词的行。这可以减小索引的大小并提高查询性能。
    CREATE NONCLUSTERED INDEX IDX_Table_Column ON Table (Column) WHERE Column IS NOT NULL;
  2. 全文索引:全文索引用于在全文查询中快速查找文本数据中的词语。
   CREATE FULLTEXT INDEX ON Table (TextColumn) 
   KEY INDEX IDX_Table_Column;

避免在WHERE子句中使用NOT和<>运算符,提高性能

在SQL Server查询中,尽量避免在WHERE子句中使用NOT和<>运算符的主要原因是这两种运算符可能会降低查询性能。以下是具体的解释:

  1. 索引不利用: SQL Server通常会使用索引来加速查询。但是,当你使用NOT或<>运算符时,SQL Server可能无法有效地使用索引,因为这些运算符需要扫描所有的行而不只是索引的一部分。这可能导致查询速度变慢。
  2. 全表扫描: 当使用NOT或<>运算符时,SQL Server可能需要执行全表扫描,即需要检查表中的每一行以确定哪些行满足查询条件。全表扫描通常比使用索引扫描要慢得多。
  3. 结果预测困难: 对于优化器来说,预测使用NOT或<>运算符的查询结果的行数比较困难,这可能会导致生成的执行计划不是最优的。
    因此,尽管在某些情况下,使用NOT或<>运算符是必要的,但在可能的情况下,应尽量避免使用它们,以提高查询性能。

在某些情况下,我们可以通过其他查询语句来避免使用”NOT”和”<>”运算符达到同样的结果,这可能有助于SQL SERVER更有效地使用索引,从而提高查询性能

  1. 使用 = 和 IN 运算符: 如果你知道你想要查询的具体值,你可以使用 = 或 IN 运算符,而不是使用 <>。例如,如果你想要查询所有不是 ‘A’ 或 ‘B’ 的行,你可以将查询从 WHERE column <> ‘A’ AND column <> ‘B’ 改写为 WHERE column IN (‘C’, ‘D’, ‘E’, …)
  2. 使用 BETWEEN 运算符: 如果你想要查询的值在一个范围内,你可以使用 BETWEEN 运算符,而不是使用 <>。例如,如果你想要查询所有不在1到10之间的行,你可以将查询从 WHERE column NOT BETWEEN 1 AND 10 改写为 WHERE column < 1 OR column > 10。
  3. 使用 IS NULL 和 IS NOT NULL: 如果你想要查询的是空值或非空值,你可以使用 IS NULL 或 IS NOT NULL 运算符,而不是使用 <>。例如,如果你想要查询所有非空的行,你可以将查询从 WHERE column <> NULL 改写为 WHERE column IS NOT NULL。
  4. 使用EXISTS和NOT EXISTS:特别是在处理相关子查询时,EXISTS和NOT EXISTS在某些情况下可能比使用NOT和<>运算符更高效。

对于存储大数据集时,将表变量改为临时表,提高性能

表变量和临时表都是用于在SQL Server中存储一些临时数据的工具。它们之间存在一些关键的区别,包括在性能方面的差异。

表变量

表变量在SQL Server中被定义为一个变量,这意味着它的生命周期只在声明它的批处理或存储过程中。表变量通常用于存储返回不多的数据,例如几百行。
性能方面:

  1. 表变量不会导致重新编译,因此在某些情况下,它可以提高性能。
  2. 表变量不会在磁盘上创建,而是在内存中创建,通常可以提供更好的性能。
  3. 表变量不会参与事务,因此不会导致锁定和日志记录,这可能会提高性能。
    创建表变量,如下所示
DECLARE @TableVariable TABLE
(
    ID INT,
    Value NVARCHAR(50)
)

临时表

临时表在SQL Server中被定义为一个真正的表,存储在tempdb数据库中,并且可以在当前会话中使用。临时表通常用于存储大量数据,例如数千或数万行。
性能方面:

  1. 临时表可能会导致存储过程的重新编译,这可能会降低性能。
  2. 临时表在磁盘上创建,这可能会比在内存中创建表变量慢。
  3. 临时表参与事务,可能会导致锁定和日志记录,这可能会降低性能。
    创建临时表,如下所示
CREATE TABLE #TempTable
(
    ID INT,
    Value NVARCHAR(50)
)

总的来说,表变量和临时表各有优势,选择哪种类型取决于你的特定需求。如果你需要存储大量数据,或者需要使用索引、统计信息等功能,那么临时表可能是更好的选择。如果你只需要存储少量数据,并且希望避免重新编译和日志记录,那么表变量可能是更好的选择。

使用 OPTION(RECOMPILE),提高性能

在 SQL Server 中,OPTION (RECOMPILE) 是一种查询提示,它会使 SQL Server 在每次运行查询时都生成一个新的执行计划。这在某些情况下可以帮助提高查询性能。以下是其背后的原理:

  1. 参数灵敏性:当查询因参数值的变化而表现出不同的性能特性时,OPTION (RECOMPILE) 可以提高性能。这是因为每次查询执行时,SQL Server 都会根据当前参数值生成一个新的执行计划。
  2. 避免计划缓存问题:如果查询计划在缓存中占用大量空间,或者因为参数嗅探问题导致性能下降,那么 OPTION (RECOMPILE) 可以帮助解决这些问题。因为每次查询执行时,都会生成一个新的执行计划,而不是重用缓存中的旧计划。
  3. 数据修改操作:对于那些涉及大量数据修改的查询(如 INSERT、UPDATE、DELETE),使用 OPTION (RECOMPILE) 可以帮助 SQL Server 生成一个更优的执行计划,因为它会考虑到最新的数据分布。

以下是一个使用 OPTION (RECOMPILE) 的例子
假设我们有一个名为 Employees 的表,我们想要根据 salary 列的值来获取一些记录。我们可能会创建一个存储过程来执行这个查询,如下所示:

CREATE PROCEDURE GetEmployees @Salary INT
AS
BEGIN
    SELECT * FROM Employees WHERE Salary > @Salary
END

在这个存储过程中,SQL Server 会为第一次运行存储过程时的 @Salary 参数值生成一个执行计划。然后,对于后续的运行,它会重用这个执行计划,无论 @Salary 参数的值是多少。现在,假设 Employees 表中的 Salary 分布是不均匀的,有些薪水范围的员工数量远多于其他薪水范围。在这种情况下,为某个特定的 @Salary 值生成的执行计划可能对其他 @Salary 值并不是最优的。为了解决这个问题,我们可以在查询中使用 OPTION (RECOMPILE),如下所示:

CREATE PROCEDURE GetEmployees @Salary INT
AS
BEGIN
    SELECT * FROM Employees WHERE Salary > @Salary OPTION (RECOMPILE)
END

现在,每次运行存储过程时,SQL Server 都会为当前的 @Salary 参数值生成一个新的执行计划,这可以提高查询性能。

然而,需要注意的是,OPTION (RECOMPILE) 并不总是提高性能。因为每次查询执行时都生成新的执行计划会消耗CPU资源,所以如果查询非常频繁,可能会导致CPU资源的浪费。因此,建议在使用 OPTION (RECOMPILE) 时,应根据具体的查询和系统性能来进行权衡。

总结

以上是我工作时常使用提高性能的几种方法,性能优化是一个持续不断的过程,它需要我们在实践中不断地学习,尝试和改进。而且,每个数据库和每个查询都有其独特性,所以最有效的优化策略可能因情况而异。如果你们有更多的方法、技巧或者是实践经验,希望你们能在评论区分享哦。让我们一起在这个领域里进一步深化我们的知识,共同提高我们的技能。在这个过程中,我期待与你们的交流和学习,让我们一起在SQL查询性能优化的道路上不断前行。

作者:百宝门-后端组-李桂林

Blazor入门教程

Blazor简介

Blazor 是由Microsoft开发的一款基于.NET的开源交互式Web UI框架。Blazor使开发人员能够使用C#和HTML建立全堆栈的单页应用程序,并避免使用JavaScript。Blazor基于组件模型,该模型提供了以具有强类型的符合Razor标准的页面和组件的形式构建用户界面的能力。

Blazor的加入使得.NET开发人员有机会在客户端和服务器上使用同一种编程模型,同时享受到.NET的优势,比如其功能强大的运行时,标准库,语言互操作性和辅助开发者高效开发的工具等。

在Blazor中,有两个主要的托管模型:

  • ** Blazor Server: 在此模式下,应用程序在服务器端运行,并使用SignalR实时通讯框架与浏览器进行交互。这种模型要求永久的有效连接,但是客户端的计算和下载需求会大大减低,所有的逻辑运行都在服务器端。
  • ** Blazor WebAssembly: 在此模式下,应用程序直接在客户端的WebAssembly中运行,允许C#代码在浏览器中执行,不依赖服务器。

开发要求

可使用最新版本的 Visual Studio 2022、Visual Studio for Mac 或 Visual Studio Code 来生成 Blazor 应用。
本文使用 Visual Studio 2022.

无论使用哪种开发环境,都需要安装 .NET 6.0(或更高版本)SDK。 如果要使用 Visual Studio 2022,则需要包含“ASP.NET 和 Web 开发”工作负载。 安装后,即可开始生成 Blazor 应用。

创建应用

在这里,我们创建一个Blazor Server 模式的程序。

1. 启动 Visual Studio 2022 并选择“Create a new project”。

2. 在“Create a new project”窗口中,在搜索框中键入 Blazor,然后按 Enter。

3. 选择“Blazor Server 应用”模板并选择“下一步”。

4. 在“Configure new project”窗口中,输入 BlazorApp 作为项目名称,然后选择“下一步”。

5. 在“Additional information”窗口中,如果尚未选择,则在“框架”下拉列表中选择“.NET 7.0 (标准期限支持)”,然后单击“Create”按钮。

使用解决方案资源管理器查看项目内容。

Program.cs 是启动服务器以及在其中配置应用服务和中间件的应用的入口点。

App.razor 为应用的根组件。

Pages 目录包含应用的一些示例网页。

BlazorApp.csproj 定义应用项目及其依赖项,且可以通过双击解决方案资源管理器中的 BlazorApp 项目节点进行查看。

Properties 目录中的 launchSettings.json 文件为本地开发环境定义不同的配置文件设置。创建项目时会自动分配端口号并将其保存在此文件上。

6. 运行应用

单击 Visual Studio 调试工具栏中的“开始调试”按钮(绿色箭头)以运行应用。
Alt text

首次在 Visual Studio 中运行 Web 应用时,它将设置用于通过 HTTPS 托管应用的开发证书,然后提示你信任该证书。建议同意信任该证书。证书将仅用于本地开发,如果没有证书,大多数浏览器都会对网站的安全性进行投诉。

等待应用在浏览器中启动。转到以下页面后,你已成功运行第一个 Blazor 应用!
Alt text

显示的页面由位于 Pages 目录内的 Index.razor 文件定义。其内容如下所示:

  • Pages/Index.razor
@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

它已包含将其设置为主页并显示文本 Hello, world! 和 Welcome to your new app 的代码。它还包含一个 SurveyPrompt 组件,可呈现指向 Blazor 反馈调查的链接。

在 Blazor 中生成 UI 时,通常会在同一文件中将静态 HTML 和 CSS 标记与 C# 代码混用。 若要区分这些类型的代码,可使用 Razor 语法。 Razor 语法包括前缀为 @ 符号的指令,这些指令用于分隔 C# 代码、路由参数、绑定数据、导入的类以及其他特性。

  • @page 指令:该指令为 Blazor 提供路由模板。 在运行时,Blazor 通过将此模板与用户请求的 URL 相匹配来查找要呈现的页面。 在本例中,它可能与 http://yourdomain.com/index 形式的 URL 匹配。

试用计数器

在正在运行的应用中,通过单击左侧边栏中的“Counter”选项卡导航到“Counter”页。然后应显示以下页面:
Alt text

选择“Click me”按钮,在不刷新页面的情况下递增计数。若要在网页中递增计数器,通常需要编写 JavaScript,但对于 Blazor,你可以使用 C#。

可以在 Pages 目录内的 Counter.razor 文件处找到 Counter 组件的实现。

  • Pages/Counter.razor
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>


@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
  • @code 指令:该指令声明以下块中的文本是 C# 代码。 你可以根据需要将任意数量的代码块置于组件中。 你可以在这些代码块中定义组件类成员,并通过计算、数据查找操作或其他源设置其值。 在本例中,代码定义了一个名为 currentCount 的组件成员并设置了值。
  • 成员访问指令:如果要在呈现逻辑中包含成员的值,请使用 @ 符号,后跟 C# 表达式,例如成员的名称。 在本例中,@currentCount 指令用于在 标记中呈现 currentCount 成员的值。

在浏览器中对于 /counter 的请求(由位于顶部的 @page 指令指定)导致 Counter 组件呈现其内容。

每次点击“Click Me”按钮时:
触发 onclick 事件。
调用 IncrementCount 方法。
currentCount 为递增。
呈现该组件以显示更新的计数。

7. 添加组件

每个 razor 文件都会定义一个可重复使用的 UI 组件。

在 Visual Studio 中打开 Index.razor 文件。Index.razor 文件已存在,并且是在创建项目时创建的。它位于之前创建的 BlazorApp 目录中的 Pages 文件夹中。

通过在 Index.razor 文件末尾添加 元素,向应用主页添加 Counter。

  • Pages/Index.razor
@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<Counter />

单击“热重载”按钮,以将更改应用到正在运行的应用。然后 Counter 组件将出现在主页上。
Alt text

8. 修改组件

组件可以有参数,组件参数使用特性或子内容指定,这允许在子组件上设置属性。在 Counter 组件上定义参数,以指定每次点击按钮时的增量:

使用 [Parameter] 属性为 IncrementAmount 添加公共属性。
将 IncrementCount 方法更改为在递增 currentCount 值时使用 IncrementAmount。
下面的代码演示了怎样实现此目的。

  • Pages/Counter.razor
@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    [Parameter]
    public int IncrementAmount { get; set; } = 1;

    private void IncrementCount()
    {
        currentCount += IncrementAmount;
    }
} 

在 Index.razor 中,更新 元素以添加IncrementAmount 属性,该属性会将增量更改为 10,如以下代码中突出显示的行所示:

  • Pages/Index.razor
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<Counter IncrementAmount="10" /> 

通过单击“热重载”按钮将更改应用。Index 组件现在具有自己的计数器,每次点击“Click me”按钮时,该计数器会递增 10,如下图所示。(/counter 处的 Counter 组件(Counter.razor)将继续按 1 递增。)
Alt text

恭喜你已生成并运行首个 Blazor 应用!

9. 数据绑定和事件

在 Razor 组件中,可以将 HTML 元素数据绑定到 C# 字段、属性和 Razor 表达式值。 数据绑定支持在 HTML 和 Microsoft .NET 之间进行双向同步。

呈现组件时,数据从 HTML 推送到 .NET。 组件在事件处理程序代码执行后呈现自身,这就是为什么在触发事件处理程序后,属性更新会立即反映在 UI 中。

可使用 @bind 标记将 C# 变量绑定到 HTML 对象。 按名称将 C# 变量定义为 HTML 中的字符串。 在下面的练习中,可以看到数据绑定的示例。

创建 Todo 项

在项目的根目录(BlazorApp 文件夹)中创建一个名为 TodoItem.cs 的新文件,用于保存表示待办事项的 C# 类。

为 TodoItem 类使用以下 C# 代码。 通过使用 ? 将 Title 声明为可为空字符串。

public class TodoItem
{
    public string? Title { get; set; }
    public bool IsDone { get; set; } = false;
}

绑定 TodoItem 列表

现可在 Blazor 中将 TodoItem 对象集合绑定到 HTML。 若要绑定这些对象,请在 Pages/Index.razor 文件中进行以下更改:

在 @code 块中为待办项添加一个字段。 Todo 组件使用此字段来维护待办项列表的状态。
添加无序列表标记和 foreach 循环,以将每个待办项呈现为列表项 ()。

@page "/"

...

<ul>
    @foreach (var todo in todos)
    {
        <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
        </li>
    }
</ul> 

@code {

    ...

    private List<TodoItem> todos = new() { new TodoItem() {Title = "Item1", IsDone = true}, new TodoItem() {Title = "Item2", IsDone = false}};
}

动态创建元素

  • 在列表 li 下方添加一个文本输入 (input) 和一个按钮 (button)。
  • 添加 AddTodo 方法,并使用 @onclick 属性来为按钮注册方法。 点击按钮时,会调用 AddTodo C# 方法。
  • 在@code中增加AddTodo 方法,将具有指定标题的 TodoItem 添加到列表。 通过将 newTodo 设置为空字符串来清除文本输入的值。
@page "/"
...
<input placeholder="Something todo" @bind="newTodo" />
<button @onclick="AddTodo">Add todo</button>

ul>
    @foreach (var todo in todos)
    {
        <li>
            <input type="checkbox" @bind="todo.IsDone" />
            <input @bind="todo.Title" />
        </li>
    }
</ul> 

@code {    
    ...
    private List<TodoItem> todos = new() { new TodoItem() { Title = "Item1", IsDone = true }, new TodoItem() { Title = "Item2", IsDone = false } };

    private string? newTodo;

    private void AddTodo()
    {
        if (!string.IsNullOrWhiteSpace(newTodo))
        {
            todos.Add(new TodoItem { Title = newTodo });
            newTodo = string.Empty;
        }
    }
}

点击 Add todo 查看效果
Alt text

总结

Blazor是一款强大的Web开发框架,它为.NET开发者开辟了通往前端开发的新道路。通过使用Blazor,你可以运用你的C#和.NET技能进行全栈开发,这降低了学习入口和复杂性。

我们在本教程中接触到了Blazor的主要特性与概念,比如组件化、数据绑定和事件以及两种运行模式。你已经明白了如何用Blazor创建单页应用,以及Blazor与其他流行前端框架的差异。希望这些知识能够为你提供一个清晰的框架,帮助你理解Blazor的优势并决定是否在你的下个项目中使用它。

但是,记住我们只触及了表面;Blazor提供了更多的深度和复杂性等待你去探索。为了深入了解Blazor,你可以研究更复杂的例子,尝试使用Blazor去创建更实际的应用,或者深挖Blazor的文档以了解它的更多特性和优点。

总的来说,Blazor是.NET开发者的一种优秀选择,它扩展了.NET生态系统,使得它更全面,更具可达性。Blazor最大的魅力在于它拓宽了.NET开发者的视野,让开发者不再局限后端或桌面应用,前端的世界同样可以用熟悉的语言和工具去进行开发。

源代码地址: https://github.com/DXG88/BlazorApp

C# 12 中的新增功能

新的 C# 12 功能在预览版中已经引入. 您可以使用最新的 Visual Studio 预览版或最新的 .NET 8 预览版 SDK 来尝试这些功能。以下是一些新引入的功能:

  • 主构造函数
  • 集合表达式
  • 默认 Lambda 参数
  • 任何类型的别名
  • 内联数组
  • 拦截器
  • 使用nameof访问实例成员

主构造函数

现在可以在任何 class 和 struct 中创建主构造函数。 主构造函数不再局限于 record 类型。 主构造函数参数都在类的整个主体的范围内。 为了确保显式分配所有主构造函数参数,所有显式声明的构造函数都必须使用 this() 语法调用主构造函数。 将主构造函数添加到 class 可防止编译器声明隐式无参数构造函数。 在 struct 中,隐式无参数构造函数初始化所有字段,包括 0 位模式的主构造函数参数。

编译器仅在 record 类型(record class 或 record struct 类型)中为主构造函数参数生成公共属性。 对于主构造函数参数,非记录类和结构可能并不总是需要此行为。

主构造函数的参数位于声明类型的整个主体中。 它们可以初始化属性或字段。 它们可用作方法或局部函数中的变量。 它们可以传递给基本构造函数。

主构造函数指示这些参数对于类型的任何实例是必需的。 任何显式编写的构造函数都必须使用 this(…) 初始化表达式语法来调用主构造函数。 这可确保主构造函数参数绝对由所有构造函数分配。 对于任何 class 类型(包括 record class 类型),当主构造函数存在时,不会发出隐式无参数构造函数。 对于任何 struct 类型(包括 record struct 类型),始终发出隐式无参数构造函数,并始终将所有字段(包括主构造函数参数)初始化为 0 位模式。 如果编写显式无参数构造函数,则必须调用主构造函数。 在这种情况下,可以为主构造函数参数指定不同的值。

下面看下主构造函数的应用场景

初始化属性

以下代码初始化从主构造函数参数计算的两个只读属性:

public readonly struct Distance(double dx, double dy)
{
    public readonly double Magnitude = Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction = Math.Atan2(dy, dx);
}

前面的代码演示了用于初始化计算的只读属性的主构造函数。 Direction 和 Magnitude 的字段初始值设定项使用主构造函数参数。 主构造函数参数不会在结构中的其他任何位置使用。 前面的结构就像编写了以下代码一样:

public readonly struct Distance
{
    public readonly double Magnitude { get; }

    public readonly double Direction { get; }

    public Distance(double dx, double dy)
    {
        Magnitude = Math.Sqrt(dx * dx + dy * dy);
        Direction = Math.Atan2(dy, dx);
    }
}

当需要参数来初始化字段或属性时,利用新功能可以更轻松地使用字段初始值设定项。

创建可变状态

前面的示例使用主构造函数参数来初始化只读属性。 如果属性不是只读的,你还可以使用主构造函数。 考虑下列代码:

public struct Distance(double dx, double dy)
{
    public readonly double Magnitude => Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction => Math.Atan2(dy, dx);

    public void Translate(double deltaX, double deltaY)
    {
        dx += deltaX;
        dy += deltaY;
    }

    public Distance() : this(0,0) { }
}

在前面的示例中,Translate 方法了更改 dx 和 dy 组件。 这就需要在访问时计算 Magnitude 和 Direction 属性。 => 运算符指定一个以表达式为主体的 get 访问器,而 = 运算符指定一个初始值设定项。 此版本将无参数构造函数添加到结构。 无参数构造函数必须调用主构造函数,以便初始化所有主构造函数参数。

依赖关系注入

主构造函数的另一个常见用途是指定依赖项注入的参数。 下面的代码创建了一个简单的控制器,使用时需要有一个服务接口:

public interface IService
{
    Distance GetDistance();
}

public class ExampleController(IService service) : ControllerBase
{
    [HttpGet]
    public ActionResult<Distance> Get()
    {
        return service.GetDistance();
    }
}

主构造函数清楚地指明了类中所需的参数。 使用主构造函数参数就像使用类中的任何其他变量一样。

初始化基类

可以从派生类的主构造函数调用基类的主构造函数。 这是编写必须调用基类中主构造函数的派生类的最简单方法。 例如,假设有一个类的层次结构,将不同的帐户类型表示为一个银行。 基类类似于以下代码:

public class BankAccount(string accountID, string owner)
{
    public string AccountID { get; } = accountID;
    public string Owner { get; } = owner;

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
}

一个派生类将呈现一个支票帐户:

public class CheckAccount(string accountID, string owner, decimal overdraftLimit = 0) : BankAccount(accountID, owner)
{
    public decimal CurrentBalance { get; private set; } = 0;

    public void Deposit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
        }
        CurrentBalance += amount;
    }

    public void Withdrawal(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
        }
        if (CurrentBalance - amount < -overdraftLimit)
        {
            throw new InvalidOperationException("Insufficient funds for withdrawal");
        }
        CurrentBalance -= amount;
    }

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}, Balance: {CurrentBalance}";
}

总结

通过合理有效地利用主构造函数,我们可以创造出更灵活、更强大、更可控的代码构造。

集合表达式

集合表达式引入了新的语法来创建常见的集合值。 可以使用展开运算符 .. 将其他集合内联到这些值中。

以下示例演示了集合表达式的使用:

// Create an array:
int[] a = [1, 2, 3, 4, 5, 6, 7, 8];

// Create a span
Span<int> b  = ['a', 'b', 'c', 'd', 'e', 'f', 'h', 'i'];

// Create a 2 D array:
int[][] twoD = [[1, 2, 3], [4, 5, 6], [7, 8, 9]];

// create a 2 D array from variables:
int[] row0 = [1, 2, 3];
int[] row1 = [4, 5, 6];
int[] row2 = [7, 8, 9];
int[][] twoDFromVariables = [row0, row1, row2];

总结

集合表达式使得代码更简洁,操作更便捷。

默认 Lambda 参数

现在可以为 Lambda 表达式的参数定义默认值。 语法和规则与将参数的默认值添加到任何方法或本地函数相同。

Func<int, string, bool> isTooLong = (int x, string s = "") => s.Length > x;

总结

默认 Lambda 参数,弥补了Lambda不能设置默认参数的缺陷。

任何类型的别名

可以使用 using 别名指令创建任何类型的别名,而不仅仅是命名类型。 这意味着可以为元组类型、数组类型、指针类型或其他不安全类型创建语义别名。

using Point = (int x, int y);

总结

它提供了一个简短的,由开发者提供的名称,可以用来替代那些完整的结构形式。

内联数组(Inline Arrays)

运行时团队和其他库作者使用内联数组来提高应用的性能。 内联数组使开发人员能够创建固定大小的 struct 类型数组。 具有内联缓冲区的结构应提供类似于不安全的固定大小缓冲区的性能特征。 你可能不会声明自己的内联数组,但当它们从运行时 API 作为 System.Span 或 System.ReadOnlySpan 对象公开时,你将透明地使用这些数组。

内联数组的声明类似于以下 struct:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct Buffer
{
    private int _element0;
}

它们的用法与任何其他数组类似:

var buffer = new Buffer();
for (int i = 0; i < 10; i++)
{
    buffer[i] = i;
}

foreach (var i in buffer)
{
    Console.WriteLine(i);
}

区别在于编译器可以利用有关内联数组的已知信息。 你可能会像使用任何其他数组一样使用内联数组。

总结

内联数组对性能提高帮助很大。

拦截器(Interceptors)

警告:本次发布的预览版引入了一项叫做interceptors(拦截器)的新功能。这项新功能主要用于一些高级场景,尤其是将会带来更好的AOT编译能力。作为.NET 8的实验性功能,在未来的版本中有可能被修改甚至删除,因此,它不应该在生产环境中使用。

拦截器是一种方法,该方法可以在编译时以声明方式将对可拦截方法的调用替换为对其自身的调用。 通过让拦截器声明所拦截调用的源位置,可以进行这种替换。 此过程可以向编译中(例如在源生成器中)添加新代码,从而提供更改现有代码语义的有限能力。

在源生成器中使用拦截器修改现有编译的代码,而非向其中添加代码。 源生成器将对可拦截方法的调用替换为对拦截器方法的调用。

总结

拦截器很强大,进一步了解可以参考下面连接:
https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md

使用nameof访问实例成员

曾经为了访问实例成员,你频繁地编写nameof感到非常恼火吗?好消息是,C# 12 Preview 3为你带来解决方案。让我们一起看看这个神奇的功能是如何工作的:
记得以前,当尝试使用nameof关键字去访问一个实例字段时,你必须有一个对象的实例,对吧?
现在,告别这些限制吧!有了C# 12 Preview 3,我们只需要类就可以做到这一点。
给出一个实际的例子,让我们看看这个独特的特性在这段代码中是如何发挥作用的:

internal class NameOf
{
    public string S { get; } = "";
    public static int StaticField;
    public string NameOfLength { get; } = nameof(S.Length);
    public static void NameOfExamples()
    {
        Console.WriteLine(nameof(S.Length));       // 使用`nameof`访问实例成员
        Console.WriteLine(nameof(StaticField.MinValue));  // 使用`nameof`访问静态字段
    }
    [Description($"String {nameof(S.Length)}")]
    public int StringLength(string s)
    { return s.Length; }
}

你看到nameof如何处理S.Length 和 StaticField.MinValue了吗?这是C# 12 Preview 3的新特性!你不需要一个实例就可以获取S.Length的名称。你也可以用nameof获取StaticField.MinValue。
简单来说,想象你有一个叫做”NameOf”的玩具盒。以前,你必须爬进盒子里才能找到你最喜欢的玩具。
但现在呢?你只需要告诉你的魔术盒你想要什么(比如,你想要的玩具魔方的长度,或者芭蕾舞泰迪熊的最小数量),它就会给你,都不用进去!

总结

nameof的增强,让代码更少,逻辑更简单。

参考文档:
https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12

2023版:深度比较几种.NET Excel导出库的性能差异

引言

背景和目的

本文介绍了几个常用的电子表格处理库,包括EPPlus、NPOI、Aspose.Cells和DocumentFormat.OpenXml,我们将对这些库进行性能测评,以便为开发人员提供实际的性能指标和数据。

下表将功能/特点、开源/许可证这两列分开,以满足需求:

功能 / 特点EPPlusNPOIAspose.CellsDocumentFormat.OpenXml
开源
许可证MITApache商业MIT
支持的 Excel 版本Excel 2007 及更高版本Excel 97-2003Excel 2003 及更高版本Excel 2007 及更高版本

测评电脑配置

组件规格
CPU11th Gen Intel(R) Core(TM) i5-11320H @ 3.20GHz,2496 Mhz,4 个内核,8 个逻辑处理器
内存40 GB DDR4 3200MHz
操作系统Microsoft Windows 10 专业版
电源选项已设置为高性能
软件LINQPad 7.8.5 Beta
运行时.NET 6.0.21

准备工作

使用Bogus库生成6万条标准化的测试数据。

void Main()
{
    string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data.json");
    using var file = File.Create(path);
    using var writer = new Utf8JsonWriter(file, new JsonWriterOptions { Indented = true });
    var data = new Bogus.Faker<Data>()
        .RuleFor(x => x.Id, x => x.IndexFaker + 1)
        .RuleFor(x => x.Gender, x => x.Person.Gender)
        .RuleFor(x => x.FirstName, (x, u) => x.Name.FirstName(u.Gender))
        .RuleFor(x => x.LastName, (x, u) => x.Name.LastName(u.Gender))
        .RuleFor(x => x.Email, (x, u) => x.Internet.Email(u.FirstName, u.LastName))
        .RuleFor(x => x.BirthDate, x => x.Person.DateOfBirth)
        .RuleFor(x => x.Company, x => x.Person.Company.Name)
        .RuleFor(x => x.Phone, x => x.Person.Phone)
        .RuleFor(x => x.Website, x => x.Person.Website)
        .RuleFor(x => x.SSN, x => x.Person.Ssn())
        .GenerateForever().Take(6_0000)
        .Dump();
    JsonSerializer.Serialize(writer, data);
    Process.Start("explorer", @$"/select, ""{path}""".Dump());
}

Bogus输出结果

IdGenderFirstNameLastNameEmailBirthDateCompanyPhoneWebsiteSSN
1MaleAntonioPaucekAntonio.Paucek@gmail.com1987/10/31 5:46:50Moen, Willms and Maggio(898) 283-1583 x88626pamela.name850-06-4706
2MaleKurtGerholdKurt.Gerhold40@yahoo.com1985/11/1 18:41:01Wilkinson and Sons(698) 637-0181 x49124cordelia.net014-86-1757
3MaleHowardHegmannHoward2@hotmail.com1979/7/20 22:35:40Kassulke, Murphy and Volkman(544) 464-9818 x98381kari.com360-23-1669
4FemaleRosemariePowlowskiRosemarie.Powlowski48@hotmail.com1964/5/18 1:35:45Will Group1-740-705-6482laurence.net236-10-9925
5FemaleEuniceRogahnEunice84@gmail.com1979/11/25 11:53:14Rippin – Rowe(691) 491-2282 x3466yvette.net219-75-6886
……

创建公共类方便正式测评使用

void Main()
{
    string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json";
    LoadUsers(path).Dump();
}

List<User> LoadUsers(string jsonfile)
{
    string path = jsonfile;
    byte[] bytes = File.ReadAllBytes(path);
    return JsonSerializer.Deserialize<List<User>>(bytes);
}

IObservable<object> Measure(Action action, int times = 5)
{
    return Enumerable.Range(1, times).Select(i =>
    {
        var sw = Stopwatch.StartNew();

        long memory1 = GC.GetTotalMemory(true);
        long allocate1 = GC.GetTotalAllocatedBytes(true);
        {
            action();
        }
        long allocate2 = GC.GetTotalAllocatedBytes(true);
        long memory2 = GC.GetTotalMemory(true);

        sw.Stop();
        return new
        {
            次数 = i, 
            分配内存 = (allocate2 - allocate1).ToString("N0"),
            内存提高 = (memory2 - memory1).ToString("N0"), 
            耗时 = sw.ElapsedMilliseconds,
        };
    }).ToObservable();
}

class User
{
    public int Id { get; set; }
    public int Gender { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
    public DateTime BirthDate { get; set; }
    public string Company { get; set; }
    public string Phone { get; set; }
    public string Website { get; set; }
    public string SSN { get; set; }
}

代码解释

1、上面的代码单位是字节 (bytes)

2 、其中IObservable(System.IObservable)是用于处理事件流的接口,它实现了观察者模式。它表示一个可观察的序列,可以产生一系列的事件,并允许其他对象(观察者)来订阅和接收这些事件。IObservable 适用于动态的、实时的事件流处理,允许观察者以异步方式接收事件,可以用于响应式编程、事件驱动的编程模型等。

3、GC.GetTotalAllocatedBytes(true) 获取分配内存大小
GC.GetTotalMemory(true) 获取占用内存大小

性能测评

EPPlus

string path = Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json";
List<User> users = LoadUsers(path);

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.epplus.xlsx");
}).Dump("EPPlus");

void Export<T>(List<T> data, string path)
{
    using var stream = File.Create(path);
    using var excel = new ExcelPackage(stream);
    ExcelWorksheet sheet = excel.Workbook.Worksheets.Add("Sheet1");
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        sheet.Cells[1, i + 1].Value = props[i].Name;
    }
    for (var i = 0; i < data.Count; ++i)
    {
        for (var j = 0; j < props.Length; ++j)
        {
            sheet.Cells[i + 2, j + 1].Value = props[j].GetValue(data[i]);
        }
    }
    excel.Save();
}

输出结果

EPPlus (6.2.8) (2023/8/15)输出结果

次数ΞΞ分配内存ΞΞ内存提高ΞΞ耗时ΞΞ
1454,869,176970,1602447
2440,353,4881761776
3440,062,26401716
4440,283,58401750
5440,653,26401813

EPPlus (4.5.3.2)(2019/6/16)输出结果

次数ΞΞ分配内存ΞΞ内存提高ΞΞ耗时ΞΞ
1963,850,944192,0482765
2509,450,7926001897
3509,872,1604241920
4509,858,5764241989
5509,651,5124242076

由此看出 相比2019,到了2023年EPPlus的性能得到了略微的提升

NPOI

示例代码一:XSSFWorkbook

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.npoi.xlsx");
}).Dump("NPOI");

void Export<T>(List<T> data, string path)
{
    IWorkbook workbook = new XSSFWorkbook();
    ISheet sheet = workbook.CreateSheet("Sheet1");

    var headRow = sheet.CreateRow(0);
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        headRow.CreateCell(i).SetCellValue(props[i].Name);
    }
    for (var i = 0; i < data.Count; ++i)
    {
        var row = sheet.CreateRow(i + 1);
        for (var j = 0; j < props.Length; ++j)
        {
            row.CreateCell(j).SetCellValue(props[j].GetValue(data[i]).ToString());
        }
    }

    using var file = File.Create(path);
    workbook.Write(file);
    workbook.Close();
}

输出结果

NPOI (2.6.1)(2023/7/12)输出结果

次数ΞΞ分配内存内存提高耗时ΞΞ
11,589,285,792567,2725549
21,577,028,664967043
31,577,398,488488107
41,576,360,696-90,5129336
51,576,226,688-3,1208289

NPOI (2.4.1)(2018/12/18)输出结果

次数ΞΞ分配内存内存提高耗时ΞΞ
11,648,548,696526,8246947
21,633,685,1361207921
31,634,033,296248864
41,634,660,176-90,2008945
51,634,205,368-2,5848078

示例代码二:SXSSFWorkbook

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.npoi.xlsx");
}).Dump("NPOI");

void Export<T>(List<T> data, string path)
{
    IWorkbook workbook = new SXSSFWorkbook();
    ISheet sheet = workbook.CreateSheet("Sheet1");

    var headRow = sheet.CreateRow(0);
    PropertyInfo[] props = typeof(User).GetProperties();
    for (var i = 0; i < props.Length; ++i)
    {
        headRow.CreateCell(i).SetCellValue(props[i].Name);
    }
    for (var i = 0; i < data.Count; ++i)
    {
        var row = sheet.CreateRow(i + 1);
        for (var j = 0; j < props.Length; ++j)
        {
            row.CreateCell(j).SetCellValue(props[j].GetValue(data[i]).ToString());
        }
    }

    using var file = File.Create(path);
    workbook.Write(file);
    workbook.Close();
}

输出结果

NPOI (2.6.1)(2023/7/12)输出结果

次数分配内存内存提高耗时
1571,769,14411,495,4882542
2482,573,584965106
3481,139,296241463
4481,524,384481510
5481,466,616481493

NPOI (2.4.1)(2018/12/18)输出结果

次数分配内存内存提高耗时
1660,709,472537,5127808
2650,060,3768,1288649
3649,006,9524,1367064
4649,267,920-89,7766973
5649,955,024486538

经过测试 发现SXSSFWorkbook 确实比XSSFWorkbook 性能好,有显著提升
由此看出 相比2018,到了2023年NPOI的性能得到了略微的提升

Aspose.Cells

Util.NewProcess = true;
List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

SetLicense();

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.aspose2.xlsx");
}, 5).Dump("Aspose");

void Export<T>(List<T> data, string path)
{
    using var excel = new Workbook();
    excel.Settings.MemorySetting = MemorySetting.Normal;
    excel.Settings.CheckExcelRestriction = false;
    Worksheet sheet = excel.Worksheets["Sheet1"];
    sheet.Cells.ImportCustomObjects(data, 0, 0, new ImportTableOptions
    {
        IsFieldNameShown = true, 
        DateFormat = "MM/DD/YYYY hh:mm:ss AM/PM", 
        ConvertNumericData = false, 
    });
    excel.Save(path);
}

void SetLicense()
{
    Stream stream = new MemoryStream(Convert.FromBase64String(@"密钥"));
    stream.Seek(0, SeekOrigin.Begin);
    new Aspose.Cells.License().SetLicense(stream);
}

输出结果

Aspose.Cells (23.8.0)(2023/8/9)输出结果

次数分配内存内存提高耗时
1443,025,1123,471,9842889
2392,090,30430,2081863
3391,419,072-81716
4392,041,144241797
5392,078,992241689

Aspose.Cells (19.8.0)(2019/8/20)输出结果

次数分配内存内存提高耗时
1552,862,0562,987,0002913
2508,337,87249,7761750
3507,922,728241933
4507,949,584241781
5508,368,208241773

由此看出 相比2019,到了2023年Aspose.Cells的性能还是一样差不多,只是内存占用减少了

DocumentFormat.OpenXml

List<User> users = LoadUsers(Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\test-data.json");

Measure(() =>
{
    Export(users, Environment.GetFolderPath(Environment.SpecialFolder.Desktop) + @"\export.openXml.xlsx");
}).Dump("OpenXML");

void Export<T>(List<T> data, string path)
{
    using SpreadsheetDocument excel = SpreadsheetDocument.Create(path, SpreadsheetDocumentType.Workbook);

    WorkbookPart workbookPart = excel.AddWorkbookPart();
    workbookPart.Workbook = new Workbook();

    WorksheetPart worksheetPart = workbookPart.AddNewPart<WorksheetPart>();
    worksheetPart.Worksheet = new Worksheet(new SheetData());

    Sheets sheets = excel.WorkbookPart.Workbook.AppendChild<Sheets>(new Sheets());
    Sheet sheet = new Sheet
    {
        Id = excel.WorkbookPart.GetIdOfPart(worksheetPart),
        SheetId = 1,
        Name = "Sheet1"
    };
    sheets.Append(sheet);

    SheetData sheetData = worksheetPart.Worksheet.GetFirstChild<SheetData>();

    PropertyInfo[] props = typeof(User).GetProperties();
    {    // header
        var row = new Row() { RowIndex = 1 };
        sheetData.Append(row);
        row.Append(props.Select((prop, i) => new Cell
        {
            CellReference = ('A' + i - 1) + row.RowIndex.Value.ToString(),
            CellValue = new CellValue(props[i].Name),
            DataType = new EnumValue<CellValues>(CellValues.String),
        }));
    }
    sheetData.Append(data.Select((item, i) => 
    {
        var row = new Row { RowIndex = (uint)(i + 2) };
        row.Append(props.Select((prop, j) => new Cell
        {
            CellReference = ('A' + j - 1) + row.RowIndex.Value.ToString(),
            CellValue = new CellValue(props[j].GetValue(data[i]).ToString()),
            DataType = new EnumValue<CellValues>(CellValues.String),
        }));
        return row;
    }));
    excel.Save();
}

输出结果

DocumentFormat.OpenXml (2.20.0)(2023/4/7)输出结果

次数ΞΞ分配内存内存提高耗时ΞΞ
1614,013,080421,5523909
2613,007,112963487
3613,831,6721043465
4613,058,344243650
5613,161,096243521

DocumentFormat.OpenXml (2.9.1)(2019/3/14)输出结果

次数ΞΞ分配内存内存提高耗时ΞΞ
1542,724,752139,0803504
2542,478,208962897
3543,030,904242826
4542,247,544242957
5542,763,312242941

由此看出 相比2019,到了2023年DocumentFormat.OpenXml的性能反而越差啦

结论和总结

结论一:如果你想找开源,(旧版本免费),(最新版收费)EPPlus 依旧是最佳选择

次数ΞΞ分配内存ΞΞ内存提高ΞΞ耗时ΞΞ
1454,869,176970,1602447
2440,353,4881761776
3440,062,26401716
4440,283,58401750
5440,653,26401813

结论二:如果你想找速度快,很稳定,但收费的,Aspose.Cells 依旧是最佳选择

次数分配内存内存提高耗时
1443,025,1123,471,9842889
2392,090,30430,2081863
3391,419,072-81716
4392,041,144241797
5392,078,992241689

总结:
1、EPPlus表现不错,内存和耗时在开源组中表现最佳
2、收费的Aspose.Cells表现最佳,内存占用最低,用时也最短

作者 => 百宝门瞿佑明

此文章是对此前《.NET骚操作》2019年写的文章的更新和扩展
https://www.cnblogs.com/sdflysha/p/20190824-dotnet-excel-compare.html

深度比较常见库中序列化和反序列化性能的性能差异

背景和目的

本文介绍了几个常用的序列化和反序列化库,包括System.Text.Json、Newtonsoft.Json、 Protobuf-Net、MessagePack-Net,我们将对这些库进行性能测评

库名称介绍Github地址
System.Text.Json.NET Core 3.0及以上版本的内置JSON库,用于读写JSON文本。它提供了高性能和低分配的功能。System.Text.Json
Newtonsoft.Json也被称为Json.NET,是.NET中最常用的JSON序列化库之一。它提供了灵活的方式来转换.NET对象为JSON字符串,以及将JSON字符串转换为.NET对象。Newtonsoft.Json
Protobuf-Net.NET版本的Google’s Protocol Buffers序列化库。Protocol Buffers是一种语言中立、平台中立、可扩展的序列化结构数据的方法。Protobuf-Net
MessagePack-NetMessagePack是一个高效的二进制序列化格式,它允许你在JSON-like的格式中交换数据,但是更小、更快、更简单。MessagePack-Net

性能测试

测评电脑配置

组件规格
CPU11th Gen Intel(R) Core(TM) i5-11320H
内存40 GB DDR4 3200MHz
操作系统Microsoft Windows 10 专业版
电源选项已设置为高性能
软件LINQPad 7.8.5 Beta
运行时.NET 7.0.10

准备工作

0、导入Nuget包

1、Bogus(34.0.2)
2、MessagePack(2.5.124)
3、Newtonsoft.Json(13.0.3)
4、protobuf-net(3.2.26)
5、System.Reactive(6.0.0)

1、性能测试函数

IObservable<object> Measure(Action action, int times = 5)
{
    return Enumerable.Range(1, times).Select(i =>
    {
        var sw = Stopwatch.StartNew();

        long memory1 = GC.GetTotalMemory(true);
        long allocate1 = GC.GetTotalAllocatedBytes(true);
        {
            action();
        }
        long allocate2 = GC.GetTotalAllocatedBytes(true);
        long memory2 = GC.GetTotalMemory(true);

        sw.Stop();
        return new
        {
            次数 = i,
            分配内存 = (allocate2 - allocate1).ToString("N0"),
            内存提高 = (memory2 - memory1).ToString("N0"),
            耗时 = sw.ElapsedMilliseconds,
        };
    }).ToObservable();
}

这个测量函数的它的作用

多次执行指定的动作,并测量每次执行该动作时的内存分配和执行时间。

然后,对于每次操作,它创建并返回一个新的匿名对象,该对象包含以下属性:

次数:操作的次数。
分配内存:操作期间分配的内存量(操作结束后的已分配字节减去操作开始前的已分配字节)。
内存提高:操作期间内存的增加量(操作结束后的总内存减去操作开始前的总内存)。
耗时:操作的执行时间(以毫秒为单位)。

2、生成随机数据的函数

IEnumerable<User> WriteData()
{
    var data = new Bogus.Faker<User>()
        .RuleFor(x => x.Id, x => x.IndexFaker + 1)
        .RuleFor(x => x.Gender, x => x.Person.Gender)
        .RuleFor(x => x.FirstName, (x, u) => x.Name.FirstName(u.Gender))
        .RuleFor(x => x.LastName, (x, u) => x.Name.LastName(u.Gender))
        .RuleFor(x => x.Email, (x, u) => x.Internet.Email(u.FirstName, u.LastName))
        .RuleFor(x => x.BirthDate, x => x.Person.DateOfBirth)
        .RuleFor(x => x.Company, x => x.Person.Company.Name)
        .RuleFor(x => x.Phone, x => x.Person.Phone)
        .RuleFor(x => x.Website, x => x.Person.Website)
        .RuleFor(x => x.SSN, x => x.Person.Ssn())
        .GenerateForever().Take(6_0000);
    return data;
}

Bogus 是一个非常有用的 C# 库,它可以帮助你生成伪造的数据,或者说“假数据”。这在测试或开发阶段非常有用,你可以使用它来填充数据库,或者在没有实际用户数据的情况下测试应用程序。

如果想详细了解使用请参考 这篇文章https://www.cnblogs.com/sdflysha/p/20190821-generate-lorem-data.html

3、数据实体类

[MessagePackObject, ProtoContract]
public class User
{
    [Key(0), ProtoMember(1)]
    public int Id { get; set; }

    [Key(1), ProtoMember(2)]
    public int Gender { get; set; }

    [Key(2), ProtoMember(3)]
    public string FirstName { get; set; }

    [Key(3), ProtoMember(4)]
    public string LastName { get; set; }

    [Key(4), ProtoMember(5)]
    public string Email { get; set; }

    [Key(5), ProtoMember(6)]
    public DateTime BirthDate { get; set; }

    [Key(6), ProtoMember(7)]
    public string Company { get; set; }

    [Key(7), ProtoMember(8)]
    public string Phone { get; set; }

    [Key(8), ProtoMember(9)]
    public string Website { get; set; }

    [Key(9), ProtoMember(10)]
    public string SSN { get; set; }
}

开始性能测试

以下测试代码会加入写入文件,以模拟真实的使用场景,性能怎么样

1、System.Text.Json 性能测试

序列化测试代码

void TextJsonWrite()
{
    var data = WriteData();

    Measure(() =>
        {
            string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data1.json");
            using var file = File.Create(path);
            System.Text.Json.JsonSerializer.Serialize(file,data);
        })
    .Dump();
}

文件大小:14.3MB

测试结果

次数分配内存内存提高耗时
11,429,688,20067,3922494
21,429,960,3523202610
31,429,596,25682615
41,430,126,504-642753
51,429,549,184-4322918

反序列化测试代码

void TextJsonRead()
{
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data1.json");
        byte[] bytes = File.ReadAllBytes(path);
        System.Text.Json.JsonSerializer.Deserialize<List<User>>(bytes);
    }).Dump();
}

测试结果

次数分配内存内存提高耗时
142,958,53643,728212
243,093,44848185
342,884,40824120
442,883,31224129
543,100,89624117

2、Newtonsoft.Json 性能测试

序列化测试代码

void JsonNetWrite()
{
    var data = WriteData();

    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data2.json");
        var jsonData = Newtonsoft.Json.JsonConvert.SerializeObject(data);
        File.WriteAllText(path, jsonData);
    })
    .Dump();
}

文件大小:14.3MB

测试结果

次数分配内存内存提高耗时
11,494,035,69642,6082196
21,494,176,1443202289
31,494,684,672-242899
41,494,292,3762,1523393
51,495,260,472643499

反序列化测试代码

void JsonNetRead()
{
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data2.json");
        var jsonData = File.ReadAllText(path);
        var data = Newtonsoft.Json.JsonConvert.DeserializeObject<List<User>>(jsonData);
    })
    .Dump();
}

测试结果

次数分配内存内存提高耗时
192,556,92063,216275
292,659,78448314
392,407,73624245
492,616,91224276
592,416,12824305

3、ProtobufNet 性能测试

序列化测试代码

void ProtobufNetWrite()
{
    var data = WriteDataTwo();
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data3.bin");
        using var file = File.Create(path);
        Serializer.Serialize(file, data);
    }).Dump();
}

文件大小:7.71MB

测试结果

次数分配内存内存提高耗时
1712,168163,512170
26,760-192111
37,04028097
46,7602466
5244,200068

反序列化测试代码

void ProtobufNetRead()
{
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "test-data3.bin");
        using var file = File.OpenRead(path);
        Serializer.Deserialize<List<UserProtobuf>>(file);
    }
    ).Dump();
}

测试结果

次数分配内存内存提高耗时
129,485,8881,084,240113
228,242,8564896
328,340,6722485
428,333,0882480
528,242,8562476

4、MessagePack-Net 性能测试

序列化测试代码

void MessagePackNetWrite()
{
    var data = WriteDataThreee();
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "UserMessagePackData.bin");
        using var file = File.Create(path);
        MessagePackSerializer.Serialize(file, data);
    }).Dump();
}

文件大小:7.21MB

测试结果

次数分配内存内存提高耗时
180,5529,51252
27,4322446
37,4322445
4120,400-1,07246
57,4322448

反序列化测试代码

void MessagePackNetRead()
{
    Measure(() =>
    {
        string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "UserMessagePackData.bin");
        byte[] bytes = File.ReadAllBytes(path);
        MessagePackSerializer.Deserialize<List<UserMessagePack>>(bytes);
    }).Dump();
}

测试结果

次数分配内存内存提高耗时
135,804,7282482
235,804,7282465
335,804,7282456
435,804,7283266
535,806,24884880

结论

序列化性能测试结果

性能测试名称分配内存平均数 (bytes)耗时平均数 (ms)文件大小 (MB)分配内存百分比 (%)耗时百分比 (%)文件大小百分比 (%)
Newtonsoft.Json1,494,489,8722,85514.3100100100
System.Text.Json1,429,784,0992,67814.39593.8100
ProtobufNet195,3851027.710.0133.553.9
MessagePack-Net44,649477.210.00291.650.4

反序列化性能测试结果

性能测试名称分配内存平均数 (bytes)耗时平均数 (ms)分配内存百分比 (%)耗时百分比 (%)
Newtonsoft.Json92,531,496283100100
System.Text.Json42,807,42015246.254.7
ProtobufNet28,529,0729030.831.8
MessagePack-Net35,805,0326938.624.3

注:

1、 分配内存比例、耗时比例和文件大小比例都以 Newtonsoft.Json 的数值为基准,计算出的百分比表示在相比于 Newtonsoft.Json 的表现。
2、 分配内存平均数、耗时平均数是通过将给定的五次测试结果取平均值得出的。
3、 文件大小是由测试代码生成的文件大小,计算出的百分比表示在相比于 Newtonsoft.Json 的表现。

基于上述表格,我们可以得出以下结论:

  1. 内存分配:在内存分配方面,ProtobufNet 和 MessagePack-Net 显著优于 System.Text.Json 和 Newtonsoft.Json。它们的内存分配仅为 Newtonsoft.Json 的 0.01% 和 0.003%,这表明它们在处理大数据时的内存效率非常高。
  2. 耗时:在耗时方面,ProtobufNet 和 MessagePack-Net 也表现出超过其他两个库的性能。ProtobufNet 的耗时为 Newtonsoft.Json 的 3.6%,而 MessagePack-Net 的耗时仅为 2.1%。这意味着它们在处理大量数据时的速度非常快。
  3. 文件大小:在生成的文件大小方面,ProtobufNet 和 MessagePack-Net 的文件大小明显小于 System.Text.Json 和 Newtonsoft.Json。ProtobufNet 和 MessagePack-Net 的文件大小分别为 Newtonsoft.Json 文件大小的 53.9% 和 50.4%。这说明它们的序列化效率更高,能够生成更小的文件。
  4. System.Text.Json vs Newtonsoft.Json:在比较这两个库时,System.Text.Json 在内存分配和耗时方面都稍微优于 Newtonsoft.Json,但差距不大。在文件大小方面,它们的表现相同。

综上所述,如果考虑内存分配、处理速度和文件大小,ProtobufNet 和 MessagePack-Net 的性能明显优于 System.Text.Json 和 Newtonsoft.Json。

5、总结

基于上面的数据,个人一些看法,虽然我们平常用的是Newtonsoft.Json,但了解一些其他一些比较好的库的使用可以扩展视野,本次测试的库虽然加入了写入文件这方面的因素,但对性能影响不是很大,本以为ProtobufNet已经是性能最好的了,但上面的测试结果,显然 MessagePack-Net 性能最好,还有一个意外发现,针对NetCore 6.0,新出的库System.Text.Json性能比Newtonsoft.Json好5%

作者 => 百宝门瞿佑明

WPF动画入门教程

Windows Presentation Foundation (WPF)是一种用于创建Windows客户端应用程序的UI框架。它让我们能够创建丰富的图形界面,包括各种各样的动画效果。接下来,我们将介绍如何在WPF中创建简单的动画。文章最后将给出源码,源码包括文章中的动画和一个水印按钮,一个简单的时钟动画,一个复杂的时钟动画。

在WPF中,通常会使用以下的一些标签来创建和控制动画。

  1. Storyboard:

Storyboard 是 Window Presentation Foundation (WPF) 中一种强大的工具,可用于创建自定义动画效果。WPF 中的动画是通过变化特定属性的值来产生的,并且这些变化都是随时间而进行的。

Storyboard 主要特性和功能:

时间线控制: Storyboard 允许你控制动画时间线,包括开始时间,停止时间,持续时间等。

动画类型: Storyboard 支持各种类型的动画,如双精度动画,颜色动画,点动画等。

复杂动画: 通过组合多个动画效果,你可以创建复杂的动画。这可以通过在 Storyboard 中包含多个动画实现。

控制动画流程: Storyboard 提供了开始,暂停,恢复,停止等方法来控制动画的播放流程。

交互性: 在 XAML 中,可以通过Storyboard.TargetNameStoryboard.TargetProperty 属性来指定应用动画的对象与 property。

  1. Animation:

WPF中的动画通常通过更改属性的值来产生动画效果。例如,我们可以使用DoubleAnimation,它可以在指定的时间内将目标属性的值从一个浮点数改变为另一个浮点数。除了DoubleAnimation,WPF还提供了其他类型的Animation,如ColorAnimation、PointAnimation等。

  1. From, To, Duration:

From和To指定了动画的开始和结束值,而Duration决定了动画的持续时间。

  1. Storyboard.TargetName 和 Storyboard.TargetProperty:

这两个属性分别用于指定动画的目标对象和目标属性。

  1. Triggers:

Triggers类用于设定启动动画的条件。我们通常会在其中设定一些事件触发条件,比如按钮被点击。当事件被触发时,设定的动画效果就会开始执行。

以上就是WPF中常用的一些动画元素。要创建复杂的动画效果,你可能还需要了解更多的标签和属性,比如RepeatBehavior(用于设定动画的重复行为)、AutoReverse(用于设定动画播放结束后是否自动倒回)、KeyFrames(用于设定动画的关键帧)等等。

接下来,我们将介绍如何在WPF中创建简单的动画。

需要的工具:

Visual Studio

步骤 1:创建一个新的WPF项目

在Visual Studio中,通过点击文件 -> 新建 -> 项目来创建一个新的WPF应用程序。

步骤 2:向窗体中添加控件

在主窗口 MainWindow.xaml 文件中,我们将添加一个Button控件。我们将为此控件添加一个简单的动画效果。

<Window x:Class="WpfAnimationDemo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="WPF Animation Demo" Height="350" Width="525">
    <Grid>
        <Button Name="DemoButton" Content="Click me" Width="100" Height="50"/>
    </Grid>
</Window>

步骤 3:编写动画效果

我们创建一个当用户点击按钮时执行的动画效果。这个效果将使按钮的宽度在1秒钟内扩大到200。

通过代码实现

给button增加Click方法

<Button x:Name="DemoButton" Width="100" Height ="100" Content="Animate Me!" Click="DemoButton_Click"
        Grid.Row="0" Grid.Column="0"/>

“`csharp
private void DemoButton_Click(object sender, RoutedEventArgs e)
{
DoubleAnimation widthAnimation = new DoubleAnimation();
widthAnimation.From = 100; // 起始值
widthAnimation.To = 300; // 结束值
widthAnimation.Duration = new Duration(TimeSpan.FromSeconds(1)); // 动画长度

Storyboard storyboard = new Storyboard();
storyboard.Children.Add(widthAnimation);

Storyboard.SetTarget(widthAnimation, DemoButton);
Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(Button.WidthProperty));

storyboard.Begin();

}

这个方法是 DemoButton 的点击事件处理器。当点击这个按钮时,这个方法就会被调用。

点击时将会发生动画效果,按钮的宽度内部值从100逐渐变化到300,过程时间为1秒。这是通过WPF中的 Storyboard 和 DoubleAnimation 来完成的。

Storyboard 是动画的容器,而 DoubleAnimation 是这个动画的定义。设置起始值(From)、结束值(To)、动画的持续时间(Duration),并确认动画的目标(要改变的是哪个元素的哪个属性)。

最后,调用 Storyboard 的 Begin 方法以开始动画。

完整代码如下:

csharp
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}

private void DemoButton_Click(object sender, RoutedEventArgs e)
{
    DoubleAnimation widthAnimation = new DoubleAnimation();
    widthAnimation.From = 100; // 起始值
    widthAnimation.To = 300; // 结束值
    widthAnimation.Duration = new Duration(TimeSpan.FromSeconds(2)); // 动画长度

    Storyboard storyboard = new Storyboard();
    storyboard.Children.Add(widthAnimation);

    Storyboard.SetTarget(widthAnimation, DemoButton);
    Storyboard.SetTargetProperty(widthAnimation, new PropertyPath(Button.WidthProperty));

    storyboard.Begin();
}

}

### 通过xaml实现

下面我们用xaml来实现同样的效果。

xml
   

xml

   

这个<Window...>标签用于定义整个窗口的开始和结束。

xml

<Grid> 是WPF内的一种特布面板标签,它提供了一个灵活的格子系统,用于在多行和多列中进行UI元素布局。

xml
在这里,我们定义了一个按钮(Button)。Name属性是给按钮设定的名称,它在XAML和代码之间可以进行关联;Content属性设置按钮的文本为"Click me";Width和Height属性则设置了按钮的宽度和高度。 xml
Triggers标签指定触发器,它定义在一定的条件下触发某些行为。 xml
此处定义了一个EventTrigger事件触发器。该触发器在Button.Click事件——也就是按钮被点击的事件——发生时触发。 xml
BeginStoryboard会使得包含在其中的Storyboard开始播放。 xml
Storyboard是WPF中对动画的最高级别的封装。一个Storyboard可以包含多个动画,这些动画会在BeginStoryboard命令下同步启动。 xml
这段代码定义了一个DoubleAnimation双值动画。 这个动画的目标对象通过Storyboard.TargetName属性设置为myButton,也就是我们前面定义的按钮控件;目标动画属性通过Storyboard.TargetProperty设定为Width;From和To属性定义了动画开始和结束时Width的值;Duration定义了动画从开始到结束的持续时间。这里设定的动画效果是,在1秒的时间内,按钮的宽度从100变为200。 xml







“`

以上是各个元素的结束标签,用于指定相应元素的结束位置。

最终,这段XAML代码定义了一个窗口,窗口中有一个按钮。当该按钮被点击时,它的宽度将在1秒的时间内从100变为200,从而形成一个视觉上的动画效果。

步骤 4:运行你的动画

保存你的代码,运行程序,然后点击按钮观察动画效果。

运行效果

代码位置: https://github.com/DXG88/WPF.Animation

.NET中测量多线程基准性能

多线程基准性能是用来衡量计算机系统或应用程序在多线程环境下的执行能力和性能的度量指标。它通常用来评估系统在并行处理任务时的效率和性能。测量中通常创建多个线程并在这些线程上执行并发任务,以模拟实际应用程序的并行处理需求。

在此,我们用多个线程来完成一个计数任务,简单地测量系统的多线程基准性能,以下的5种测量代码(代码1,代码4,代码5,代码6,代码7)中,都设置了计数器,每一秒计数器的计数量体现了系统的性能。通过对比这些测量方法,可以直观地理解多线程、如何通过多线程充分利用系统性能,以及运行多线程可能存在的瓶颈。

测量方法

先用一个多线程的共享变量自增例子来做多线程基准性能测量:

//代码1:简单的多线程测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}
void DoWork()
{
    while (true)
    {
        totalCount++;
    }
}

//结果
48,493,031
48,572,321
47,788,843
48,128,734
50,461,679
……

因为在多线程环境中,线程之间的切换会导致一些开销,例如保存和恢复线程上下文的时间。如果上下文切换频繁发生,可能会对性能测试结果产生影响,因此上面的代码根据系统的CPU内核数设定启动测试线程的线程数量,这些线程对一个共享的变量进行自增操作。

有多线程编程经验的人不难看出,上面的代码没有正确地保护共享资源,会出现竞态条件。这可能导致数据不一致,操作顺序不确定,或者无法重现一致的性能结果。我们将用代码展示这种情况。

//代码2:展示出竞态条件的代码
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
void DoWork()
{
    while (true)
    {
        totalCount++;
        Console.Write($"{totalCount}"+",");
    }
}
//结果
1,9,10,3,12,13,4,14,15,16……270035,269913,270037,270038,270036,270040,269987,270042,270043……

代码2的运行结果可以看到,由于被不同线程操作,这些线程同时访问和修改totalCount的值,打印出来的totalCount不是顺序递增的。

可见,代码1没有线程同步机制,我们不能准确测量多线程基准性能。
C#中线程的同步方式,比如传统的锁机制(如lock语句、Monitor类、Mutex类、Semaphore类等)通常使用互斥机制来保护共享资源,以确保同一时间只有一个线程可以访问资源,避免竞争条件。这些锁机制会在代码块被锁定期间阻塞其他线程的访问,使得同一时间只有一个线程可以执行被锁定的代码。
这里使用lock锁作为线程同步机制,修正上面的代码,对共享的变量进行保护,避免共享变量同时被多个线程修改。

//代码3:使用lock锁
long totalCount = 0;
int threadCount = Environment.ProcessorCount;
object totalCountLock = new object();

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}

void DoWork()
{
    while (true)
    {
        lock (totalCountLock)
        {
            totalCount++;
            Console.Write($"{totalCount}"+",");
        }
    }
}

//结果
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……

这时的结果就是顺序输出。

我们用含lock的代码来测量多线程基准性能:

//代码4:运用含lock锁的代码测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;
object totalCountLock = new object();

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}
while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}
void DoWork()
{
    while (true)
    {
        lock (totalCountLock)
        {
            totalCount++;
        }
    }
}

//结果
16,593,517
16,694,824
16,514,421
16,517,431
16,652,867
……

保证多线程环境下线程安全性,还有一种方式是使用原子操作Interlocked。与传统的锁机制(如lock语句等)不同,Interlocked类提供了一些特殊的原子操作,如Increment、Decrement、Exchange、CompareExchange等,用于对共享变量进行原子操作。这些原子操作是直接在CPU指令级别上执行的,而不需要使用传统的阻塞和互斥机制。它通过硬件级别的操作,确保对共享变量的操作是原子性的,避免了竞争条件和数据不一致的问题。
它更适合用于特定的原子操作,而不是用作通用的线程同步机制。

//代码5:运用原子操作的代码测量多线程基准性能
long totalCount = 0;
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
for (int i = 0; i < threadCount; ++i)
{
    tasks[i] = Task.Run(DoWork);
}

while (true)
{
    long t = totalCount;
    Thread.Sleep(1000);
    Console.WriteLine($"{totalCount - t:N0}");
}

void DoWork()
{
    while (true)
    {
        Interlocked.Increment(ref totalCount);
    }
}
//结果
37,230,208
43,163,444
43,147,585
43,051,419
42,532,695
……

除了使用互斥锁、原子操作,我们也可以设法对多个线程进行数据隔离。ThreadLocal类提供了线程本地存储功能,用于在多线程环境下的数据隔离。每个线程都会有自己独立的数据副本,被储存在ThreadLocal实例中,每个ThreadLocal可以被对应线程访问到。

//代码6:运用含ThreadLocal的代码测量多线程基准性能
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
ThreadLocal<long> count = new ThreadLocal<long>(trackAllValues: true);

for (int i = 0; i < threadCount; ++i)
{
    int threadId = i;
    tasks[i] = Task.Run(() => DoWork(threadId));
}

while (true)
{
    long old = count.Values.Sum();
    Thread.Sleep(1000);
    Console.WriteLine($"{count.Values.Sum() - old:N0}");
}

void DoWork(int threadId)
{
    while (true)
    {
        count.Value++;
    }
}

//结果
177,851,600
280,076,173
296,359,986
296,140,821
295,956,535
……

上面的代码使用了ThreadLocal类,我们也可以自定义一个类,给每个线程创建一个对象作为上下文,代码如下:

//代码7:运用含自定义上下文的代码测量多线程基准性能
int threadCount = Environment.ProcessorCount;

Task[] tasks = new Task[threadCount];
Context[] ctxs = new Context[threadCount];

for (int i = 0; i < threadCount; ++i)
{
    int threadId = i;
    ctxs[i] = new Context();
    tasks[i] = Task.Run(() => DoWork(threadId));
}

while (true)
{
    long old = ctxs.Sum(v => v.TotalCount);
    Thread.Sleep(1000);
    Console.WriteLine($"{ctxs.Sum(v => v.TotalCount) - old:N0}");
}

void DoWork(int threadId)
{
    while (true)
    {
        ctxs[threadId].TotalCount++;
    }
}

class Context
{
    public long TotalCount = 0;
}

//结果:
1,067,502,570
1,100,966,648
1,145,726,019
1,110,163,963
1,069,322,606
……

系统配置

组件    规格                                
CPU      11th Gen Intel(R) Core(TM) i5-11300H
内存    16 GB DDR4                  
操作系统Microsoft Windows 10 家庭中文版          
电源选项已设置为高性能                      
软件    LINQPad 7.8.5 Beta                  
运行时  .NET 7.0.10                          

测量结果

测量方法1秒计数      性能百分比
未做线程同步    50,461,679118.6%
lock锁   16,652,86739.2%
原子操作(Interlocked)   42,532,695100%
ThreadLocal    295,956,535695.8%
自定义上下文(Context)    1,069,322,6062514.1%

结果分析

未作线程同步测量到的结果是不准确的,不能作为依据。

根据程序运行的结果可以看到,使用传统的lock锁机制,效率不高。使用原子操作Interlocked,效率比传统锁要高近2倍。
而实现了线程间隔离的2种方法,效率都比前面的方法要高。使用自定义上下文的程序效率是最高的。

线程间隔离的两种代码,它们主要区别在于线程安全性的实现方式。代码6使用ThreadLocal 类来实现,而代码7使用了自定义的上下文,用一个数组来为每个线程提供一个唯一的上下文。代码6使用的是线程本地存储(Thread Local Storage,TLS)来实现其功能。它是一种全局变量,可以被正在运行的所有线程访问,但每个线程所看到的值都是私有的。虽然这个特性使ThreadLocal在多线程编程中变得非常有用,但为了实现这个特性,它在内部实现了一套复杂的机制,比如它会创建一个弱引用的哈希表来存储每个线程的数据。这个内部实现细节增加了相应的计算和访问开销。

对于代码7,它创建了一个名为Context的类数组,每个线程都有其自己的Context对象,并在执行过程中修改这个对象。由于每个线程自身管理其Context对象,不存在任何线程间冲突,这就减少了许多额外的开销。

因此,虽然代码6代码7都实现了线程数据隔离,但代码7避开了ThreadLocal的额外开销,因此在性能上表现得更好。

结论

如果能实现线程间的隔离,可以大幅提高多线程代码效率,测量出系统的最大性能值。

作者:百宝门-后端组-周智

C#中的ConcurrentExclusiveSchedulerPair类

为什么使用ConcurrentExclusiveSchedulerPair?

现实生活中的例子是一个停车场的入口和出口,多辆车可以同时进入和离开停车场,但是只有一个车辆可以进入或离开一次。

这时候就需要保证同时只有一个车辆能够访问停车场的入口或出口,避免出现多辆车同时进出停车场的竞态条件和导致车辆堵塞的问题。

使用ConcurrentExclusiveSchedulerPair可以将需要独占访问的停车场入口和出口操作加入ExclusiveScheduler中,从而保证在任何时候都只有一个车辆能够进入或离开停车场,避免了竞态条件和数据不一致的情况。

ConcurrentExclusiveSchedulerPair类介绍

ConcurrentExclusiveSchedulerPair类是.NET Framework 4.0中引入的一种新的多线程编程工具,它提供了两个调度器,一个是并发调度器(ConcurrentScheduler),另一个是独占调度器(ExclusiveScheduler)。通过这两个调度器,可以实现多个任务的并行执行和互斥访问。

以下是创建ConcurrentExclusiveSchedulerPair对象的基本代码:

var pair = new ConcurrentExclusiveSchedulerPair();

在上述代码中,我们创建了一个ConcurrentExclusiveSchedulerPair对象。这个对象包含了两个调度器:并发调度器和独占调度器。

并发调度器

并发调度器是一种可以让多个任务并行执行的调度器。在并发调度器中,任务可以同时执行,而不需要等待其他任务完成。

以下是使用并发调度器来执行任务的示例:

var pair = new ConcurrentExclusiveSchedulerPair();
var concurrentScheduler = pair.ConcurrentScheduler;

Task.Factory.StartNew(() =>
{
    // 任务执行的代码
}, CancellationToken.None, TaskCreationOptions.None, concurrentScheduler);

在上述代码中,我们获取了ConcurrentExclusiveSchedulerPair对象的并发调度器,并使用Task.Factory.StartNew方法来创建一个任务,并使用并发调度器来调度任务的执行。

独占调度器

独占调度器是一种可以让任务独占执行的调度器。在独占调度器中,只有一个任务可以执行,其他任务必须等待前一个任务完成后才能执行。

以下是使用独占调度器来执行任务的示例:

var pair = new ConcurrentExclusiveSchedulerPair();
var exclusiveScheduler = pair.ExclusiveScheduler;

Task.Factory.StartNew(() =>
{
    // 任务执行的代码
}, CancellationToken.None, TaskCreationOptions.None, exclusiveScheduler);

在上述代码中,我们获取了ConcurrentExclusiveSchedulerPair对象的独占调度器,并使用Task.Factory.StartNew方法来创建一个任务,并使用独占调度器来调度任务的执行。

下面是完整案例

var pair = new ConcurrentExclusiveSchedulerPair();

var concurrentTaskFactory = new TaskFactory(pair.ConcurrentScheduler);
var exclusiveTaskFactory = new TaskFactory(pair.ExclusiveScheduler);
// 调度独占任务
exclusiveTaskFactory.StartNew(() =>
{
    Console.WriteLine("线程:{0}上正在执行独占任务1", Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1000);
});
exclusiveTaskFactory.StartNew(() =>
{
    Console.WriteLine("线程:{0}上正在执行独占任务2", Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1000);
});

// 等待所有任务完成
Task.WaitAll(
concurrentTaskFactory.StartNew(() =>
{
    Console.WriteLine("并发任务3在线程:{0}上执行", Thread.CurrentThread.ManagedThreadId);
}),
exclusiveTaskFactory.StartNew(() =>
{
    Console.WriteLine("独占任务3正在线程:{0}上执行", Thread.CurrentThread.ManagedThreadId);
    Thread.Sleep(1000);
}));

输出结果

线程:15上正在执行独占任务1

线程:15上正在执行独占任务2

独占任务3正在线程:15上执行

并发任务3在线程:15上执行

结论

总之,使用 ConcurrentExclusiveSchedulerPair 的目的是为了保证在高并发情况下,多个任务对共享资源进行读写操作时不会产生竞态条件和数据不一致的问题。这可以提高应用程序的稳定性和可靠性。

作者 => 百宝门瞿佑明

WPF MVVM模式简介

WPF是Windows Presentation Foundation的缩写,它是一种用于创建桌面应用程序的用户界面框架。WPF支持多种开发模式,其中一种叫做MVVM(Model-View-ViewModel)。

什么是MVVM?

MVVM是一种软件架构模式,它将应用程序分为三个层次:Model(模型),View(视图)和ViewModel(视图模型)。Model表示应用程序的数据和业务逻辑,View表示应用程序的用户界面,ViewModel表示View和Model之间的桥梁,它负责处理View的数据绑定和用户交互。

为什么要使用MVVM?

使用MVVM有以下几个好处:

  • 降低了View和Model之间的耦合度,使得它们可以独立地开发和测试。
  • 提高了代码的可重用性和可维护性,因为ViewModel可以在不同的View之间共享。
  • 简化了单元测试,因为ViewModel不依赖于具体的UI控件。
  • 支持双向数据绑定,使得View可以自动更新Model的变化,反之亦然。
  • 利用了WPF提供的强大特性,如命令、依赖属性、数据注解等。

下图我们可以直观的理解MVVM谁急模式:

View: 使用XAML呈现给用户的界面,负责与用户交互,接收用户输入,把数据展现给用户。

Model: 事物的抽象,开发过程中涉及到的事物都可以抽象为Model,例如姓名、年龄、性别、地址等属性.不包含方法,也不需要实现INotifyPropertyChanged接口.

ViewModel: 负责收集需要绑定的数据和命令,聚合Model对象,通过View类的DataContext属性绑定到View。同时也可以处理一些UI逻辑。

如何实现MVVM?

实现MVVM需要遵循以下几个步骤:

  1. 创建一个Model类,定义应用程序所需的数据和业务逻辑。
  2. 创建一个ViewModel类,继承自INotifyPropertyChanged接口,并实现属性变更通知。在ViewModel中定义与Model相关联的属性,并提供相应的命令来执行用户操作。
  3. 创建一个View类(通常是一个XAML文件),定义应用程序的用户界面。在View中使用数据绑定来连接ViewModel中的属性和命令,并设置相关的样式和行为。
  4. 在App.xaml或其他合适的地方创建一个ViewModel实例,并将其作为View中DataContext属性值。
  5. 示例代码:
// Model class
public class User
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// ViewModel class
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
using System.Windows;

namespace WpfApp1
{
    public class UserInfoViewModel : INotifyPropertyChanged
    {
        private User user;

        public UserInfoViewModel()
        {
            user = new User();
            SaveCommand = new RelayCommand(Save);
            CancelCommand = new RelayCommand(Cancel);
        }

        public string UserName
        {
            get { return user.Name; }
            set
            {
                user.Name = value;
                OnPropertyChanged("UserName");
            }
        }

        public int UserAge
        {
            get { return user.Age; }
            set
            {
                user.Age = value;
                OnPropertyChanged("UserAge");
            }
        }

        public string UserInfo
        {
            get { return $"Name:{UserName} Age:{UserAge}"; }
        }

        public ICommand SaveCommand { get; private set; }
        public ICommand CancelCommand { get; private set; }

        private void Save(object parameter)
        {
            // Save user data to database or service
            MessageBox.Show("User data saved!");

            OnPropertyChanged("UserInfo");
        }

        private void Cancel(object parameter)
        {
            // Close dialog window without saving data
            var window = parameter as Window;
            if (window != null)
                window.Close();

        }

        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

//Command class
public class RelayCommand : ICommand
{
    public RelayCommand(Action<object> action)
    {
        DoExecute = action;
    }

    public event EventHandler? CanExecuteChanged;
    public Func<object, bool>? CanExecution { set; get; }
    public Action<object>? DoExecute { set; get; }

    public bool CanExecute(object? parameter)
    {
        if (CanExecution != null)
        {
            CanExecute(parameter);
        }
        return true;
    }

    public void Execute(object? parameter)
    {
        DoExecute!.Invoke(parameter!);
    }
}
// View class (XAML file)

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="220" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>

        <!-- Labels and textboxes for user name and age -->

        <Label Content="Name:" Grid.Row="0" Grid.Column="0" Margin="10"/>
        <TextBox Text="{Binding UserName}" Grid.Row="0" Grid.Column="1" Margin="10"/>

        <Label Content="Age:" Grid.Row="1" Grid.Column="0" Margin="10"/>
        <TextBox Text="{Binding UserAge}" Grid.Row="1" Grid.Column="1" Margin="10"/>

        <Label Content="{Binding UserInfo}" Grid.Row="2" Grid.Column="1" Margin="10"/>
        <!-- Buttons for save and cancel commands -->

        <StackPanel Orientation= "Horizontal" HorizontalAlignment= "Right"
                    Grid.Row= "3" Grid.ColumnSpan= "2">
            <Button Content= "Save" Command="{Binding SaveCommand}"
                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,
                  AncestorType={x:Type Window}}}" Margin= "10"/>
            <Button Content= "Cancel" Command="{Binding CancelCommand}"
                    CommandParameter="{Binding RelativeSource={RelativeSource FindAncestor,
                  AncestorType={x:Type Window}}}" Margin= "10"/>
        </StackPanel>

    </Grid>
</Window>
// View code-behind file

using System.Windows;

namespace WpfApp1
{
   /// Interaction logic for UserInfoView.xaml

   public partial class MainWindow : Window
   {
      public MainWindow()
      {
         InitializeComponent();

         // Set the ViewModel as the DataContext of the View

         this.DataContext = new UserInfoViewModel();
      }
   }
}

运行结果如下:

代码位置:
https://github.com/DXG88/WpfApp1.git

有哪些MVVM框架?

虽然可以手动实现MVVM模式,但是也有许多第三方库提供了更方便和高效地使用MVVM模式。以下是一些常见且流行的MVVM框架:

  • Prism: 一个由微软支持的MVVM框架,提供了一系列服务和特性,如导航、模块化、事件聚合、命令、依赖注入等。
  • MVVM Light: 一个轻量级的MVVM框架,提供了一些基础类和组件,如ViewModelBase, RelayCommand, Messenger等。
  • Caliburn.Micro: 一个基于约定而非配置的MVVM框架,提供了一些高级特性,如屏幕激活/关闭生命周期管理、自动绑定、窗口管理器等。

—作者:百宝门-董校刚

P/Invoke之C#调用动态链接库DLL

本编所涉及到的工具以及框架:
1、Visual Studio 2022
2、.net 6.0

P/Invok是什么?

P/Invoke全称为Platform Invoke(平台调用),其实际上就是一种函数调用机制,通过P/Invoke就可以实现调用非托管Dll中的函数。

在开始之前,我们首先需要了解C#中有关托管与非托管的区别

托管(Collocation),即在程序运行时会自动释放内存;
非托管,即在程序运行时不会自动释放内存。

废话不多说,直接实操

第一步:

  1. 打开VS2022,新建一个C#控制台应用
  2. 右击解决方案,添加一个新建项,新建一个”动态链接库(DLL)”,新建完之后需要右击当前项目–> 属性 –> C/C++ –> 预编译头 –> 选择”不使用编译头”
  3. 在新建的DLL中我们新建一个头文件,用于编写我们的方法定义,然后再次新建一个C++文件,后缀以.c 结尾

第二步:

  1. 在我们DLL中的头文件(Native.h)中定义相关的Test方法,具体代码如下: #pragma once // 定义一些宏 #ifdef __cplusplus #define EXTERN extern "C" #else #define EXTERN #endif #define CallingConvention _cdecl // 判断用户是否有输入,从而定义区分使用dllimport还是dllexport #ifdef DLL_IMPORT #define HEAD EXTERN __declspec(dllimport) #else #define HEAD EXTERN __declspec(dllexport) #endif HEAD int CallingConvention Sum(int a, int b);
  2. 之后需要去实现头文件中的方法,在Native.c中实现,具体实现如下: #include "Native.h" // 导入头部文件 #include "stdio.h" HEAD int Add(int a, int b) { return a+b; }
  3. 在这些步骤做完后,可以尝试生成解决方案,检查是否报错,没有报错之后,将进入项目文件中,检查是否生成DLL (../x64/Debug)

第三步:

  1. 在这里之后,就可以在C#中去尝试调用刚刚所声明的方法,以便验证是否调用DLL成功,其具体实现如下:
   using System.Runtime.InteropServices;

   class Program
   {
       [DllImport(@"C:\My_project\C#_Call_C\CSharp_P_Invoke_Dll\x64\Debug\NativeDll.dll")]
       public static extern int Add(int a, int b);

       public static void Main(string[] args)
       {
           int sum = Add(23, 45);
           Console.WriteLine(sum);
           Console.ReadKey();
       }
   }

运行结果为:68,证明我们成功调用了DLL动态链库

C#中通过P/Invoke调用DLL动态链库的流程

  通过上述一个简单的例子,我们大致了解到了在C#中通过P/Invoke调用DLL动态链库的流程,接下我们将对C#中的代码块做一些改动,便于维护

  1. 在改动中我们将用到NativeLibrary类中的一个方法,用于设置回调,解析从程序集进行的本机库导入,并实现通过设置DLL的相对路径进行加载,其方法如下: public static void SetDllImportResolver (System.Reflection.Assembly assembly, System.Runtime.InteropServices.DllImportResolver resolver);
  2. 在使用这个方法前,先查看一下其参数 a、assembly: 主要是获取包含当前正在执行的代码的程序集(不过多讲解)

    b、resolber: 此参数是我们要注重实现的,我们可以通过查看他的元代码,发现其实现的是一个委托,因此我们对其进行实现。

    原始方法如下: public delegate IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath);
  3. 实现resolver方法: const string NativeLib = "NativeDll.dll"; static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); // 此处为Dll的路径 //Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } 该方法主要是用于区分在加载DLL时不一定只能是设置绝对路径,也可以使用相对路径对其加载,本区域代码是通过使用委托去实现加载相对路径对其DLL加载,这样做的好处是,便于以后需要更改DLL的路径时,只需要在这个方法中对其相对路径进行修改即可。
  4. 更新C#中的代码,其代码如下: using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll"; [DllImport(NativeLib)] public static extern int Add(int a, int b); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64","Release", "NativeDll.dll"); Console.WriteLine(dll); return libraryName switch { NativeLib => NativeLibrary.Load(dll, assembly, searchPath), _ => IntPtr.Zero }; } public static void Main(string[] args) { NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); int sum = Add(23, 45); Console.WriteLine(sum); Console.ReadKey(); } }
  5. 最后重新编译,检查其是否能顺利编译通过,最终我们的到的结果为:68

至此,我们就完成了一个简单的C#调用动态链接库的案例

  下面将通过一个具体实例,讲述为什么要这样做?(本实例通过从性能方面进行对比)

  1. 在DLL中的头文件中,加入如下代码: HEAD void CBubbleSort(int* array, int length);
  2. 在.c文件中加入如下代码: HEAD void CBubbleSort(int* array, int length) { int temp = 0; for (int i = 0; i < length; i++) { for (int j = i + 1; j < length; j++) { if (array[i] > array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } }
  3. C#中的代码修改:
    using System.Diagnostics; using System.Reflection; using System.Runtime.InteropServices; class Program { const string NativeLib = "NativeDll.dll";[DllImport(NativeLib)] public unsafe static extern void CBubbleSort(int* arr, int length); static IntPtr DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { string dll = Path.Combine(new DirectoryInfo(Environment.CurrentDirectory).Parent.Parent.Parent.Parent.ToString(), "x64", "Release", "NativeDll.dll"); //Console.WriteLine(dll); return libraryName switch { NativeLib =&gt; NativeLibrary.Load(dll, assembly, searchPath), _ =&gt; IntPtr.Zero }; } public unsafe static void Main(string[] args) { int num = 1000; int[] arr = new int[num]; int[] cSharpResult = new int[num]; //随机生成num数量个(0-10000)的数字 Random random = new Random(); for (int i = 0; i &lt; arr.Length; i++) { arr[i] = random.Next(10000); } //利用冒泡排序对其数组进行排序 Stopwatch sw = Stopwatch.StartNew(); Array.Copy(arr, cSharpResult, arr.Length); cSharpResult = BubbleSort(cSharpResult); Console.WriteLine($"\n C#实现排序所耗时:{sw.ElapsedMilliseconds}ms\n"); // 调用Dll中的冒泡排序算法 NativeLibrary.SetDllImportResolver(Assembly.GetExecutingAssembly(), DllImportResolver); fixed (int* ptr = &amp;arr[0]) { sw.Restart(); CBubbleSort(ptr, arr.Length); } Console.WriteLine($"\n C实现排序所耗时:{sw.ElapsedMilliseconds}ms"); Console.ReadKey(); } //冒泡排序算法 public static int[] BubbleSort(int[] array) { int temp = 0; for (int i = 0; i &lt; array.Length; i++) { for (int j = i + 1; j &lt; array.Length; j++) { if (array[i] &gt; array[j]) { temp = array[i]; array[i] = array[j]; array[j] = temp; } } } return array; }}
  4. 执行结果: C#实现排序所耗时: 130ms C实现排序所耗时:3ms 在实现本案例中,可能在编译后,大家所看到的结果不是很出乎意料,但这只是一种案例,希望通过此案例的分析,能给大家带来一些意想不到的收获叭。

最后

简单做一下总结叭,通过上述所描述的从第一步如何创建一个DLL到如何通过C#去调用的一个简单实例,也应该能给正在查阅相关资料的你有所收获,也希望能给在这方面有所研究的你有一些相关的启发,同时也希望能给目前对这方面毫无了解的你有一个更进一步的学习。

作者:百宝门-后端组-刘忠帅