WPF 入门(四):样式(Style)

样式的应用

WPF 样式的主要应用方式有以下几种:

1. 基于类型的样式(Type-based Style)

只设置 TargetType,不设置 x:Key。自动应用于该类型的所有控件。

1
2
3
4
5
6
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Orange"/>
</Style>
</Window.Resources>
<!-- 所有 Button 都会自动应用该样式 -->

2. 基于键的样式(Keyed Style)

设置了 x:Key,只有通过 Style="{StaticResource ...}" 显式引用才会生效。

1
2
3
4
5
6
<Window.Resources>
<Style x:Key="RedButtonStyle" TargetType="Button">
<Setter Property="Background" Value="Red"/>
</Style>
</Window.Resources>
<Button Content="按钮" Style="{StaticResource RedButtonStyle}" />

3. 基于数据的样式(Data Style)

通过 DataType 属性为特定数据类型自动应用样式,常用于 DataTemplate。

1
2
3
4
5
6
7
8
9
<Window.Resources>
<DataTemplate DataType="{x:Type local:Person}">
<StackPanel>
<TextBlock Text="{Binding Name}"/>
<TextBlock Text="{Binding Age}"/>
</StackPanel>
</DataTemplate>
</Window.Resources>
<!-- 绑定 Person 类型数据时自动应用该模板 -->

4. 基于条件的样式(Style with Trigger)

通过 Trigger、DataTrigger、EventTrigger 等,根据控件状态或数据动态改变样式。

1
2
3
4
5
6
7
8
9
10
11
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Background" Value="Gray"/>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="Green"/>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<!-- 鼠标悬停时按钮背景变为绿色 -->

样式的继承(BasedOn)

WPF 支持样式继承,可以让一个样式基于另一个样式扩展或修改属性,避免重复代码。实现方式是使用 BasedOn 属性。

作用: - 复用已有样式的全部设置,只需补充或覆盖差异部分。 - 便于统一维护和风格扩展。

1. 显式样式的继承(继承有 x:Key 的样式)

通过 BasedOn="{StaticResource 样式键名}" 继承已有的显式样式。

1
2
3
4
5
6
7
8
9
10
<Window.Resources>
<Style x:Key="BaseButtonStyle" TargetType="Button">
<Setter Property="FontSize" Value="16"/>
<Setter Property="Foreground" Value="Blue"/>
</Style>
<Style x:Key="RedButtonStyle" TargetType="Button" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="Background" Value="Red"/>
</Style>
</Window.Resources>
<Button Content="按钮" Style="{StaticResource RedButtonStyle}" />

2. 隐式样式的继承(继承类型样式)

通过 BasedOn="{StaticResource {x:Type 控件类型}}" 继承类型的隐式样式。

1
2
3
4
5
6
7
8
9
10
11
12
<Window.Resources>
<!-- 隐式样式:所有 Button 默认应用 -->
<Style TargetType="Button">
<Setter Property="FontSize" Value="16"/>
<Setter Property="Foreground" Value="Blue"/>
</Style>
<!-- 继承类型样式 -->
<Style x:Key="MyButtonStyle" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="Red"/>
</Style>
</Window.Resources>
<Button Content="按钮" Style="{StaticResource MyButtonStyle}" />

MyButtonStyle 会继承所有 Button 的隐式样式设置,并额外设置背景色为红色。

WPF 入门(三):资源作用域(Resource Scope)

WPF 资源有多种作用域(层级),决定了资源的可见范围和优先级。常见资源作用域如下:

作用域 说明
控件级 仅对单个控件有效,直接在控件属性或 Resources 中定义
用户控件级 仅对 UserControl 及其子元素有效,定义在 <UserControl.Resources>
页面级 仅对 Page 及其子元素有效,定义在 <Page.Resources>
窗口级 仅对 Window 及其子元素有效,定义在 <Window.Resources>
应用级 对整个应用有效,定义在 <Application.Resources>
外部资源字典 独立的 ResourceDictionary 文件,可被其他作用域合并引用

控件级资源(Local Resource)

拥有最高的优先级。 如下,将按钮的背景色设置为红色:

1
<Button Content="Button 1" Width="100" Height="50" Background="Yellow" />

用户控件级资源(UserControl-level Resource)

定义在 <UserControl.Resources> 内,仅对该用户控件及其子元素有效。 如下,为用户控件内所有 Button 设置背景色为黄色:

1
2
3
4
5
6
7
<UserControl.Resources>
<Style TargetType="Button">
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Background" Value="Yellow"/>
</Style>
</UserControl.Resources>

页面级资源(Page-level Resource)

定义在 <Page.Resources> 内,仅对该页面及其子元素有效。

1
2
3
4
5
6
7
<Page.Resources>
<Style TargetType="Button">
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Background" Value="Yellow"/>
</Style>
</Page.Resources>

窗口级资源(Window-level Resource)

需要写在 <Window.Resources> 标签内,样式只能作用于当前窗口内的控件。

1
2
3
4
5
6
7
<Window.Resources>
<Style TargetType="Button">
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Background" Value="Yellow" />
</Style>
</Window.Resources>

应用级资源(Application-level Resource)

需要写在 <Application.Resources> 标签内,能作用于当前应用内所有窗口中控件的样式。

1
2
3
4
5
6
7
<Application.Resources>
<Style TargetType="Button">
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Background" Value="Yellow" />
</Style>
</Application.Resources>

外部资源字典(ResourceDictionary File)

可将资源单独存放在 .xaml 文件中,通过 MergedDictionaries 合并到任意作用域,实现资源共享。

1
2
3
4
5
6
7
<ResourceDictionary>
<Style TargetType="Button">
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Background" Value="Yellow" />
</Style>
</ResourceDictionary>

在 App.xaml 或 Window/Page 中引用:

1
2
3
4
5
6
7
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/MyResource.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>

资源查找顺序:WPF 会从控件级开始,逐级向上查找(控件 → 用户控件 → 页面 → 窗口 → 应用 → 外部资源字典),直到找到为止。

PS: 与HTML不同, WPF从控件向上找, 找到就会停止. HTML(CSS)的样式查找是反过来的, 是从上往下的。浏览器会从外层(如外部样式表、head 内 style、继承的样式)到内层,逐步应用样式。 如果有多个规则匹配同一元素,则根据“优先级(Specificity)”和“后出现覆盖前出现”的原则决定最终样式。

WPF 入门(二):布局控件

常见布局控件一览

控件名 说明
Grid 灵活分行分列
StackPanel 垂直/水平线性排列
DockPanel 停靠在上下左右或填充
WrapPanel 自动换行排列
Canvas 绝对定位
UniformGrid 等宽等高网格
Viewbox 可整体缩放内容

1. Grid(网格布局)

Grid 是最常用的布局控件之一,可以将界面划分为行和列,实现灵活的网格布局。

常用属性:

属性 说明
RowDefinitions 定义行集合
ColumnDefinitions 定义列集合
Grid.Row 指定子元素所在的行
Grid.Column 指定子元素所在的列
Grid.RowSpan 子元素跨越的行数
Grid.ColumnSpan 子元素跨越的列数

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Text="标题" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" />
<Button Content="按钮1" Grid.Row="1" Grid.Column="0" />
<Button Content="按钮2" Grid.Row="1" Grid.Column="1" />
</Grid>

Grid 适合用于需要复杂布局的场景。

Height 和 Width 属性说明

在 WPF 布局控件中,Height(高度)和 Width(宽度)属性用于设置控件的尺寸。

取值 说明
数值(如100) 以设备无关像素(DIP)为单位的长度,1 DIP=1/96 英寸
Auto 自动适应内容或父容器,由布局系统决定实际大小
* 用于 Grid 行高/列宽,按比例分配剩余空间,如2表示两份,表示一份

示例:

1
2
3
4
5
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- 高度自适应内容 -->
<RowDefinition Height="2*" /> <!-- 占用剩余空间的 2 份 -->
</Grid.RowDefinitions>
<Button Width="100" Height="30" Content="按钮" /> <!-- 固定宽高 -->

2. StackPanel(堆叠面板)

StackPanel 会将子元素按照指定方向(垂直或水平)依次排列。

常用属性:

属性 说明
Orientation 排列方向,Vertical(默认)或 Horizontal

细节说明:

  • 通常不需要显式设置 Height 和 Width,默认会根据内容自动调整大小。如果父容器有限制,也可以手动设置。
  • 内部元素不会重叠,而是依次排列。
  • 如果内容超出 StackPanel 的宽度(或高度),超出部分会被裁剪(不可见),不会自动换行。
  • 如需自动换行效果,可使用 WrapPanel。

示例:

1
2
3
4
5
<StackPanel Orientation="Vertical">
<Button Content="按钮1" />
<Button Content="按钮2" />
<Button Content="按钮3" />
</StackPanel>

StackPanel 适合用于简单的线性排列场景。

3. DockPanel(停靠面板)

占位

4. WrapPanel(换行面板)

占位

5. Canvas(画布)

占位

6. UniformGrid(等距网格)

占位

7. ViewBox(缩放容器)

占位

最近公司需要接入非接触读卡器到系统, 但是绝对没有必要再单独开发个桌面应用来进行串口通信, 因此准备使用Web的SerialPort来实现. 感觉挺好玩的, 就准备弄一个玩. 借了个读卡器设备试试.

通信参数

根据提供的开发文档可以得知通信参数

1
115200,8,n,1
含义为:

波特率:115200, 数据长度:8, 奇偶校验:无, 停止位:1

波特率

波特率表示每秒钟传送的码元(符号)的个数, 单位是波特(Baud,symbol/s).

与比特率的关系

通过不同的调制方式, 可以在一个码元符号上负载多个bit位信息

I为传信率, S为波特率, N为每个符号承载的信息量. 比特率=波特率*单个调制状态对应的二进制位数.

在串口通信中, 传输码元(符号)就是比特(bit), 即波特率=比特率=时钟周期的数值

串口模式下, 若波特率是115200, 1秒钟只能传送 数据, 而不是 数据.

这是因为在网络传输的时候为了区别一个字节的开始和结束需要在每个字节前加一位0, 后面加一位1. 其中有8个b是表示数据, 另外2b是控制信息. 在串口模式下传送数据, 传送1Byte有效数据需要10位.

波特率怎么计算

假设系统时钟50M,波特率115200. 则传输1bit需要 。需要计数 个。那么1Byte(串口传输格式为:1bit起始位+8bit数据位+1bit停止位)是不是循环计数10次434就可以传输完毕。

什么时候去采样串口线上的数据

观察上图,Buad_Flag信号(通道2)表示了传输1Bit传输的间隔,每遇到1个Buad_Flag=1的信号,数据线上切换1次数据,所以两个Buad_Flag=1之间的数据是稳定的数据,根据抽样定理是不是应该在两个Buad_Flag=1信号的中间去采样数据呢,其实就是在1bit数据持续期间的中间点采样,才能得到最稳定的数据。

WPF 入门(一):项目创建

1. 创建新项目

  1. 打开 Visual Studio 2022,选择 创建新项目
  2. 在搜索栏中输入 WPF,选择 WPF 应用程序
  3. 输入项目名称(如:WpfApp1),选择项目位置,选择框架版本(如:.NET 8.0),点击 创建

2. 主要文件说明

创建完成后,在解决方案资源管理器中可以看到如下主要文件:

文件名 作用说明
App.xaml 应用程序的入口,定义全局资源、应用程序级事件(如启动、退出),用于配置应用的整体行为。
AssemblyInfo.cs 包含程序集的元数据(如版本号、作者、描述等),用于描述和标识程序集。
MainWindow.xaml 主窗口的界面定义文件,负责应用启动后显示的主界面布局和控件声明。

3. 运行项目

运行项目,点击 F5 键或 调试 > 开始调试

会出现一个窗口,显示 MainWindow.xaml 的内容。

4. MainWindow.xaml中的内容

MainWindow.xaml 文件中的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>

</Grid>
</Window>
MainWindow.xaml 文件是 WPF 应用程序的主窗口的界面定义文件。

<Window> 标签定义了窗口的属性,如标题(Title)、高度(Height)、宽度(Width)等。

xmlns 在 XAML 中的作用类似于 C# 的 using,都是为了让你能直接使用某个命名空间下的类型。 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 告诉 XAML 解析器,x 是一个命名空间前缀,用于引用 XAML 文件中的 XAML 元素, :x 类似 C# 的别名。 xmlns:local="clr-namespace:WpfApp1" 表示当前窗口所在的命名空间。

State Design Pattern

需求

制作一个游戏中玩家操控的角色的动画控制器

将以上状态变化用代码实现

  1. 需要定义一系列的状态

    1
    2
    3
    4
    5
    public const int Idle = 0;
    public const int Run = 1;
    public const int Attack = 2;
    public const int Rise = 3;
    public const int Fall = 4;

  2. 定义一个存储当前状态的变量

    1
    public int State = Idle;

  3. 根据当前的状态, 执行该动作对应的效果

    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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    /// <summary>
    /// 动作:输入攻击指令
    /// </summary>
    public void InputAttackCommand()
    {
    if (State == Idle)
    {
    Console.WriteLine("播放Attack动画");
    State = Attack;
    }
    else if (State == Run)
    {
    Console.WriteLine("不能在奔跑时攻击");
    }
    else if (State == Attack)
    {
    Console.WriteLine("不能在攻击时攻击");
    }
    else if (State == Rise)
    {
    Console.WriteLine("不能在跳跃中攻击");
    }
    else if (State == Fall)
    {
    Console.WriteLine("不能在下落中攻击");
    }
    }

    /// <summary>
    /// 动作:攻击动作完成
    /// </summary>
    public void AttackActionComplete()
    {
    if (State == Idle)
    {
    Console.WriteLine("当前不处于攻击中");
    }
    else if (State == Run)
    {
    Console.WriteLine("当前不处于攻击中");
    }
    else if (State == Attack)
    {
    Console.WriteLine("完成Idle动画");
    State = Idle;
    }
    else if (State == Rise)
    {
    Console.WriteLine("当前不处于攻击中");
    }
    else if (State == Fall)
    {
    Console.WriteLine("当前不处于攻击中");
    }
    }

    ......

  4. 通过调用对应的动作就完成状态的转换了

    1
    stateMachine.InputAttackCommand();
    但是, 如果需要加入一个新的状态Walk, 则首先需要加入一个状态值
    1
    public const int Walk = 5;

然后需要对所有的动作都需要加入一个新的状态的判断

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/// <summary>
/// 动作:输入攻击指令
/// </summary>
public void InputAttackCommand()
{
if (State == Idle)
{
Console.WriteLine("播放Attack动画");
State = Attack;
}
else if (State == Run)
{
Console.WriteLine("不能在奔跑时攻击");
}
else if (State == Attack)
{
Console.WriteLine("不能在攻击时攻击");
}
else if (State == Rise)
{
Console.WriteLine("不能在跳跃中攻击");
}
else if (State == Fall)
{
Console.WriteLine("不能在下落中攻击");
}
else if (State == Walk)
{
//行走的处理
}
}

/// <summary>
/// 动作:攻击动作完成
/// </summary>
public void AttackActionComplete()
{
if (State == Idle)
{
Console.WriteLine("当前不处于攻击中");
}
else if (State == Run)
{
Console.WriteLine("当前不处于攻击中");
}
else if (State == Attack)
{
Console.WriteLine("完成Idle动画");
State = Idle;
}
else if (State == Rise)
{
Console.WriteLine("当前不处于攻击中");
}
else if (State == Fall)
{
Console.WriteLine("当前不处于攻击中");
}
else if (State == Walk)
{
//行走的处理
}
}

......

很明显这并没有遵守开放-关闭原则, 也不符合面向对象的设计.

改进

因此, 需要改进代码: 1. 定义一个IState接口, 在这个接口内间每个动作都有一个对应的方法. 2. 为每个状态实现一个状态类. 这些类将负责在对应状态下进行的行为. 3. 将状态对应的动作逻辑写入这些方法中.

类图

改进后的设计, 每个状态只需要专注自己的动作, 并且增加状态和动作不会影响到之前的代码, 这符合对扩展开放,对修改关闭.

修改StateMachine类

不再使用整数代表状态, 而是改为状态对象.

  • 原来的代码
    1
    2
    3
    4
    5
    6
    7
    public const int Idle = 0;
    public const int Run = 1;
    public const int Attack = 2;
    public const int Rise = 3;
    public const int Fall = 4;

    public int State = Idle;
  • 修改后的代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public IState IdleState { get; private set; }
    public IState RunState { get; private set; }
    public IState AttackState { get; private set; }
    public IState RiseState { get; private set; }
    public IState FallState { get; private set; }

    public IState State;

    public StateMachine()
    {
    this.IdleState = new IdleState(this);
    this.RunState = new RunState(this);
    this.AttackState = new AttackState(this);
    this.RiseState = new RiseState(this);
    this.FallState = new FallState(this);

    this.State = this.IdleState;
    }

定义

状态模式, 允许对象在内部状态改变时改变它的行为, 对象看起来好像修改了它的类.

构成

Context: 维护对当前状态的引用, 并在状态发生改变的时候切换到新的状态.

State: 定义一个IState接口或者State抽象类, 其中包含状态中的所有行为. 创建特定State类来实现IState接口或继承State抽象类, 在特定State类中实现这些行为(handle).

相关源码

Explicit Dependencies Principle

定义

方法和类应明确要求(通常通过方法参数或构造函数参数)它们所需的任何协作对象, 以便正常运行.

依赖项

如果您的类需要其他类来执行其操作, 那么这些其他类就是依赖项.

隐式依赖项

如果这些依赖项仅存在于类内部的代码中, 而不存在于其公共接口中, 则它们是隐式的.

显式依赖项

对于类的依赖项, 显示依赖项最常出现在对象的构造函数中; 对于更多本地依赖项, 显式依赖项通常出现在特定方法的参数列表中.

隐式依赖的缺点

隐式依赖关系的类比具有显式依赖关系的类的维护成本更高. 它们更难测试, 因为他们与协作者的耦合更紧密. 更难以分析副作用, 因为必须搜索整个类的代码库以查找对象实例化或对静态方法的调用. 与合作者的耦合更紧密, 导致设计更加僵化和脆弱.

显式依赖的优点

非常清楚的说明了执行特定职能所需的条件. 它们倾向于遵循最小意外原则, 减少修改代码影响的范围. 无论是在生产中还是在测试或debug过程中, 显式依赖项都可以轻松的替换为其他实现. 这使得它们更容易维护并且更愿意改变.

示例

示例1-隐式依赖:

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
31
32
33
34
35
36
37
38
39
public class User
{
public string Name;
}

public static class Context
{
public static User CurrentUser;

public static User GetCurrentUser()
{
return CurrentUser;
}
}

public class Client
{
public string Login()
{
return $"用户:{Context.GetCurrentUser().Name} 已登陆!";
}
}

internal class Program
{
public static void Main(string[] args)
{
var user = new User
{
Name = "A"
};

Context.CurrentUser = user;

var client = new Client();
Console.WriteLine(client.Login());
Console.ReadLine();
}
}

示例1中, Client对User对象的依赖是在Context静态类中, 并没有在公共接口中, 是隐式依赖项. 在Main中调用的时候, 并不能直观的了解到client和创建的user有什么联系, 需要查看内部具体的代码, 代码间的隔离性很差.

示例2-显式依赖:

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
public class User
{
public string Name;
}

public class Client
{
public string Login(User user)
{
return $"用户:{user.Name} 已登陆!";
}
}

internal class Program
{
public static void Main(string[] args)
{
var user = new User()
{
Name = "A"
};

var client = new Client();
Console.WriteLine(client2.Login(user));
Console.ReadLine();
}
}

在示例2中, 对于示例1进行了改进. 不再将user对象的引用存储在Context中去隐式传递依赖项, 而是直接将user作为client.Login方法的参数传递依赖项, 能更加直观的看出关联关系, 且对于Login方法内部的代码也更加具有隔离性.

相关源码

应用场景

使用VSCode阅读Markdown文件,希望能够直接内嵌PlantUML,并能在VSCode中阅读时能渲染PlantUML。

实现方法

  1. 在VSCode中安装PlantUML插件。
  2. 进入VSCode设置,选择“在settings.json中编辑”。
  3. 在打开的json文件中末尾加上
    1
    2
    "plantuml.render": "PlantUMLServer",
    "plantuml.server": "http://www.plantuml.com/plantuml"
    • server配置的是官方PlantUML的解析服务器地址。如需要自建PlantUML解析服务器:GitHub项目地址
  4. 在VSCode中按下快捷键Ctrl+Shift+P显示显示命令面板,输入Markdown:Change Preview Security Settings,选择Allow insecure content 允许不安全内容
  5. 在你的Markdown文件中,使用以下格式就能嵌入PlantUML
    1
    2
    3
    4
    5
    {% plantuml %}        Class01 <|-- Class02
    Class03 *-- Class04
    Class05 o-- Class06
    Class07 .. Class08
    Class09 -- Class10{% endplantuml %}

今天在看C#的IEnumerator 接口的官方文档,看到在实现IEnumerable接口的GetEnumerator()方法时额外写一个同名同参的方法。感到有些疑惑,为什么要这样写,这样写的好处在哪里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class People : IEnumerable
{
private Person[] _people;
public People(Person[] pArray)
{
_people = new Person[pArray.Length];

for (int i = 0; i < pArray.Length; i++)
{
_people[i] = pArray[i];
}
}

// Implementation for the GetEnumerator method.
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator) GetEnumerator();
}

public PeopleEnum GetEnumerator()
{
return new PeopleEnum(_people);
}
}
  • 注:PeopleEnum类实现了IEnumerator接口
  • IEnumerable.GetEnumerator()是显式实现接口成员,必须是默认访问修饰符。

在我实现接口的时候,我通常会直接隐式实现接口成员。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class People : IEnumerable
{
private Person[] _people;
public People(Person[] pArray)
{
_people = new Person[pArray.Length];

for (int i = 0; i < pArray.Length; i++)
{
_people[i] = pArray[i];
}
}

public IEnumerator GetEnumerator()
{
return new PeopleEnum(_people);
}
}
  • C# 要求隐式实现接口成员必须是public

但是这样做隐式实现接口成员在返回类型是可继承或接口,且外部调用时需要使用派生后的扩展的方法就需要进行类型转换。

1
2
3
People people = new People(null);
PeopleEnum enum1 = (PeopleEnum)people.GetEnumerator();
int enumLength = enum1.Length();

而使用官方文档那样写,调用扩展的方法就不需要进行类型转换。

1
2
3
People people = new People(null);
PeopleEnum enum1 = people.GetEnumerator();
int enumLength = enum1.Length();

安装

  1. 在VS2022中选择菜单项工具->NuGet包管理->管理解决方案的NuGet程序包
  2. 在打开的窗口中选择浏览->搜索BenchmarkDotNet->选择搜索结果中的BenchmarkDotNet->点击安装
  3. 所有弹出框都选择确定
  4. 等待完成安装

使用

  1. 创建一个的类,里面存放需要测试的函数Test1()Test2()
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class BenchmarkTest
    {
    public void Test1()
    {
    Console.WriteLine("Test1");
    }

    public void Test2()
    {
    Console.WriteLine("Test2");
    }
    }
  2. 对需要测试的函数增加[Benchmark]特性。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class BenchmarkTest
    {
    [Benchmark]
    public void Test1()
    {
    Console.WriteLine("Test1");
    }

    [Benchmark]
    public void Test2()
    {
    Console.WriteLine("Test2");
    }
    }
  3. main函数中使用BenchmarkRunner.Run<BenchmarkTest>(),将会调用BenchmarkTest类中的待测试的函数。
1
2
3
4
static void Main(string[] args)
{
BenchmarkRunner.Run<BenchmarkTest>();
}
  1. (可选)如果需要查看内存使用可以在class加上[MemoryDiagnoser]特性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[MemoryDiagnoser]
public class BenchmarkTest
{
[Benchmark]
public void Test1()
{
Console.WriteLine("Test1");
}

[Benchmark]
public void Test2()
{
Console.WriteLine("Test2");
}
}
  1. 将解决方案配置由Debug改为Release
  2. 点击界面按钮开始执行(不调试)或者快捷键Ctrl+F5,开始执行。
0%