c#特性、反射、GC、AutoMapper
目录
水一期,这张内容没那么难
1.反射
反射(Reflection)有下列用途:
- 它允许在运行时查看特性(attribute)信息。
- 它允许审查集合中的各种类型,以及实例化这些类型。
- 它允许延迟绑定的方法和属性(property)。
- 它允许在运行时创建新类型,然后使用这些类型执行一些任务。
1.1 序列化
代码背景:(Serialize传入Object就能序列化,为此创建了接口,但是仍然不灵活,因为后续如果创建新对象时又要创建新接口)
var student = new Student(){
ID = 1,
Name = "阿信",
Age = 18,
};
Serialize(student).Dump();
string Serialize(IFoo obj){
return obj.Serialize();
}
interface IFoo{
string Serialize();
}
class Student : IFoo{
public int ID { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public Gender Gender { get; set; }
public int Class { get; set; }
public string Serialize()
{
return $"""
ID:{ID}
Name:{Name}
Age:{Age}
""";
}
}
enum Gender
{
Male,
Female
}
使用反射改进的序列化代码
var student = new Student(){
ID = 1,
Name = "阿信",
Age = 18,
Gender = Gender.Male,
Class = 2
};
Serialize(student).Dump();
string Serialize(object obj)
{
var res = obj
.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
// 获取:
// - Public:公共属性
// - Instance:实例属性(非静态)
// 结果:ID, Name, Age, Gender, Class 这5个属性
.Select(pi => new { key = pi.Name, value = pi.GetValue(obj) })
// 对每个属性pi:键值对
// key = 属性名(如"ID")
// value = 属性值(如1)
// 结果:[{key:"ID",value:1}, {key:"Name",value:"阿信"}, ...]
.Select(o => $"{o.key}:{o.value}");
return string.Join(Environment.NewLine, res);
}
class Student{
public int ID { get; set; }
public string Name { get; set; }
public int Age { get; set; }
public Gender Gender { get; set; }
public int Class { get; set; }
}
enum Gender
{
Male,
Female
}
从上述代码可以知晓反射的一些特点,例如
// 反射:在运行时查看/操作类型信息
// 就像照镜子:看自己有什么属性、方法
Type type = student.GetType(); // 照镜子看类型
PropertyInfo[] props = type.GetProperties(); // 看有什么属性
object value = prop.GetValue(student); // 读取属性值
1.2 和特性搭配
代码演示:
var student = new Student()
{
ID = 1,
Name = "阿信",
Age = 18,
Gender = Gender.Male,
Class = 2
};
Serialize(student).Dump();
string Serialize(object obj)
{
var res = obj
.GetType()
.GetProperties(BindingFlags.Public | BindingFlags.Instance)
.Where(o =>
{
var attr = o.GetCustomAttribute<BrowsableAttribute>();
if (attr is not null) return attr.Browsable;
return true;
})
// 获取:
// - Public:公共属性
// - Instance:实例属性(非静态)
// 结果:ID, Name, Age, Gender, Class 这5个属性
//.Select(pi => new { key = pi.Name, value = pi.GetValue(obj) })
.Select(pi =>
{
var attr = pi.GetCustomAttribute<BrowsableAttribute>();
if (attr is not null)
return new { Key = attr.SubName, Value = pi.GetValue(obj) };
else
return new { Key = pi.Name, Value = pi.GetValue(obj) };
})
// 对每个属性pi:键值对
// key = 属性名(如"ID")
// value = 属性值(如1)
.Select(o => $"{o.Key}:{o.Value}");
return string.Join(Environment.NewLine, res);
}
class Student
{
[Browsable(false)] //这里用到语法糖,自动把后面的Attribute删除了
public int ID { get; set; }
[Browsable("Alias")] //用Serialize方法修改后,通过反射可以将Name改名为Alias
public string Name { get; set; }
[Browsable(false, Tag = "123")] //特性独有写法,为未出现的属性赋值
public int Age { get; set; }
public Gender Gender { get; set; }
public int Class { get; set; }
}
[AttributeUsage(AttributeTargets.Property)] //限制只能在属性里使用
class BrowsableAttribute : Attribute
{
public bool Browsable { get; set; }
public string SubName { get; set; }
public string Tag { get; set; } = "123456";
// 构造函数1:只设置 Browsable
public BrowsableAttribute(bool b)
{
Browsable = b;
}
// 构造函数2:同时设置 SubName 和 Browsable
public BrowsableAttribute(string subName)
{
SubName = subName;
Browsable = true; //设置默认值,否则就是false
}
}
enum Gender
{
Male,
Female
}
1.3 跑分
- 自定义的性能基准测试框架,用于比较不同方法的执行效率
- 代码示例(传入泛型代码示例)
BenchmarkRunner<SimpleTester>();
void BenchmarkRunner<T>(int count = 100_000_000)where T:new(){ //泛型约束
var obj = new T(); //创建空实例用来调用实例方法
var methods = typeof(T)
.GetMethods()
.Where(mi => mi.GetCustomAttribute<BenchmarkAttribute>() is not null); //这里不限制就会有自带的方法
foreach (var method in methods)
{
var sw = Stopwatch.StartNew();
for (int i = 0; i < count; i++)
{
method.Invoke(obj, null); //invoke是实例方法,前面加obj=new T()就是这个考虑
}
sw.ElapsedMilliseconds.Dump(method.Name);
}
}
public class SimpleTester{
private IEnumerable<int> testList = Enumerable.Range(1,10).ToArray();
[Benchmark]
public int CalcMinByLinQ(){
return testList.Min();
}
[Benchmark]
public int CalcMinNaive(){
int min = int.MaxValue;
foreach (int i in testList)
{
if (i < min)
{
min = i;
}
}
return min;
}
}
[AttributeUsage(AttributeTargets.Method)]
class BenchmarkAttribute : Attribute{
}
-
如下代码则是不传入泛型的示例:
BenchmarkRunner(typeof(SimpleTester)); void BenchmarkRunner(Type type, int count = 100_000_000){ var obj = Activator.CreateInstance(type); // 创建指定类型的实例; var methods = type .GetMethods() .Where(mi => mi.GetCustomAttribute<BenchmarkAttribute>() is not null); //这里不限制就会有自带的方法 foreach (var method in methods) { var sw = Stopwatch.StartNew(); for (int i = 0; i < count; i++) { method.Invoke(obj, null); } sw.ElapsedMilliseconds.Dump(method.Name); } } public class SimpleTester{ private IEnumerable<int> testList = Enumerable.Range(1,10).ToArray(); [Benchmark] public int CalcMinByLinQ(){ return testList.Min(); } [Benchmark] public int CalcMinNaive(){ int min = int.MaxValue; foreach (int i in testList) { if (i < min) { min = i; } } return min; } } [AttributeUsage(AttributeTargets.Method)] class BenchmarkAttribute : Attribute{ }
2.特性
-
**特性(Attribute)**是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。您可以通过使用特性向程序添加声明性信息。一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。
-
特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。
-
反射与特性是密不可分的,特性单独拿出来没有太大意义
ropertyGrid 控件的使用:
void Main()
{
var faker = new Faker<Student>() //记得导入Bogus包
.RuleFor(s => s.ID, f => Guid.NewGuid()) // 或者直接用Guid.NewGuid
.RuleFor(s => s.FirstName, f => f.Person.FirstName)
.RuleFor(s => s.LastName, f => f.Person.LastName)
.RuleFor(s => s.Age, f => f.Random.Int(18, 30))
.RuleFor(s => s.DateOfBirth, f => f.Date.Past(20, DateTime.Now.AddYears(-18)))
.RuleFor(s => s.Skills, f => f.Random.ListItems(new List<string> { "C#", "Java", "Python", "JavaScript"}, 4));
var student = faker.Generate();
//student.Dump();
var pg = new PropertyGrid();
pg.SelectedObject = student;
pg.Dump();
}
class Student
{
[Browsable(false)]
public Guid ID { get; set; }
[Category("Name")] //集合在一个下拉框内
public string FirstName { get; set; }
[Category("Name")]
public string LastName { get; set; }
public int Age { get; set; }
public DateTime DateOfBirth { get; set; }
public List<String> Skills { get; set; }
}
3.垃圾回收(GC)
- 这一章概念居多,了解即可
3.1 什么是GC
- GC,Garbage Collection,自动找出并释放不再使用的内存空间机制,称之为垃圾回收机制。为数据申请内存空间的操作称之为分配,释放与申请内存空间的操作称之为释放。如果提前释放会导致程序崩溃,如果延后释放会导致内存居高不下。
3.2 标记清除算法
- .NET 根对象包括各个线程空间上的变量,静态变量/全局变量、GC 句柄和析构队列中的对象
- .NET 中将引用类型对象分为三类,分别是第 0 代(小对象)、第 1 代和第 2 代【小于 85000 字节属于小对象】。一般而言,第 0 代中的对象存活时间通常最短,第 1 代中的对象存活时间较长,第 2 代中的对象存活时间最长。如果一次 GC 回收了第 0、1、2 代,便称之为完整 GC。分代回收的目的是尽量减少每次执行垃圾回收处理时可回收的对象的数量,并减少处理所需的时间
- 反复执行分配与回收操作,可能导致堆上产生很多空余空间,这些空余空间又称为碎片空间。【地址不连续,空出的部分又无法分配大的对象】,那压缩机制可以通过移动已分配空间把碎片空间合并到一块,使得堆可以分配更大的对象。但是这种压缩空间会有很大难度的,会导致指针那些全部要重新分配,.NET 运行时提供的 GC 是支持压缩机制的,但只能在一定条件下启用。默认只在小对象堆启用,而大对象.NET 4.5.1 之前不压缩,之后选择性压缩。
- 下图为示例

3.3 析构队列
-
析构函数是对象被垃圾回收时自动调用的特殊方法,用于清理非托管资源(如文件句柄、网络连接、数据库连接等)
-
class FileHandler { // 构造函数 public FileHandler(string path) { _fileStream = File.OpenRead(path); } // 析构函数(也称为终结器) ~FileHandler() { // 清理非托管资源 if (_fileStream != null) { _fileStream.Close(); _fileStream.Dispose(); Console.WriteLine("文件已关闭"); } } private FileStream _fileStream; }
-
-
有时候我们写析构函数里面会有长耗时逻辑,这时候去垃圾回收的话就无法预估时间,.NET GC 是这样处理的,如果对象不再存活但定义了析构函数,那么对象将会被添加到一个专门的析构队列中并标记存活。析构函数执行完毕的对象,可以在下一轮 GC 中被回收,所以析构函数的对象至少需要执行两轮 GC。一般析构函数会用于非托管类型中定义。
3.4 GC两种模式
- 工作模式一旦确认无法中途更改。
- 工作站模式:适合内存占用量小的程序和桌面程序,它可以提供更短的响应时间。
- 服务器模式:适合内存占用量大的程序和服务程序,可以提供更高的吞吐量。
| 工作站模式 (Workstation GC) | 服务器模式 (Server GC) | |
|---|---|---|
| 设计目标 | 交互式应用程序(桌面、WinForms、WPF) | 高吞吐量服务端应用(ASP.NET、API服务) |
| GC线程 | 1个专用GC线程 | 多个GC线程(通常CPU核心数) |
| 暂停时间 | 短暂停,用户体验优先 | 可能较长暂停,吞吐量优先 |
| 内存使用 | 较保守,快速释放 | 较激进,允许更多内存占用 |
| GC频率 | 频繁 | 不频繁 |
| GC使用的线程 | 分配对象的线程 | 独立线程 |
- 普通GC:会导致更长的单次 STW 停顿时间(GC 处理的时候需要停顿其他线程),但消耗的资源比较小,并且支持压缩处理
- 后台GC:每次 STW 停顿的时间会更短一些,但停顿次数与消耗的资源会更多,并且不支持压缩处理
| 不同点 | 普通GC(前台GC) | 后台GC | 说明 |
|---|---|---|---|
| 目标代 | 第0、1、2代(全部) | 主要针对第2代,第0/1代仍在前台 | 后台GC不是只处理第2代,而是把第2代移到后台处理 |
| 执行时间 | 相对较短(但完全阻塞) | 第2代处理时间较长(但不阻塞主程序) | 实际执行时间后台GC可能更长,但用户无感 |
| STW停顿时间 | 整个GC过程都停顿 | 仅第0/1代GC时短暂停顿,第2代GC不阻塞 | 这是关键区别:后台GC大大减少了STW时间 |
| 执行线程 | 根据模式而定(工作站:1线程,服务器:多线程) | 专用后台线程处理第2代GC | 后台GC有专门的线程在后台运行 |
| 压缩处理 | 支持(特别是第2代) | 支持,但在后台进行 | 后台GC仍然进行压缩,只是在后台线程中 |
3.5 配置
一般我们只需要配置两个,一个是工作站服务器模式,一个是普通和后台 GC。在 xxx.csproj 文件中启用。
- true 服务器模式,false 工作站模式
<ServerGarbageCollection>true</ServerGarbageCollection>
- true 启用后台 GC,禁用后台GC,使用阻塞式GC
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
也可以直接修改生成好的目录下的 runtimeconfig.json 文件来进行
4.AutoMapper
- 当属性名对应时,映射代码如下
// See https://aka.ms/new-console-template for more information
using AutoMapper;
List<Student> students = new List<Student>() {
new Student{ ID=1, Address= "北京", Name = "黑无常", Phone = 13213 },
new Student{ ID=2, Address= "上海", Name = "白无常", Phone = 13213 },
};
var config = new MapperConfiguration(c => c.CreateMap<Student, StudentView>()); //14.0.0可以这么用,16.0.0就不能了
var mapper = config.CreateMapper();
var stuViewList = mapper.Map<List<StudentView>>(students); //断点测试可以看到这里的数据没有Phone
Console.ReadLine();
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public long Phone { get; set; }
}
public class StudentView
{
public int ID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
- 属性名不对应时,代码如下
// See https://aka.ms/new-console-template for more information
using AutoMapper;
List<Student> students = new List<Student>() {
new Student{ ID=1, Address= "北京", Name = "黑无常", Phone = 13213 },
new Student{ ID=2, Address= "上海", Name = "白无常", Phone = 13213 },
};
var config = new MapperConfiguration(c => c.CreateMap<Student, StudentView>()
.ForMember(model => model.Alias, express=> express.MapFrom(students => students.Name)));
var mapper = config.CreateMapper();
var stuViewList = mapper.Map<List<StudentView>>(students);
Console.ReadLine();
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public long Phone { get; set; }
}
public class StudentView
{
public int ID { get; set; }
public string Alias { get; set; }
public string Address { get; set; }
}
4.1 AgileObject
但是上面的代码看起来都有些复杂,有没有比AutoMapper更轻量的包呢?有的有的,叫AgileObject
// See https://aka.ms/new-console-template for more information
using AutoMapper;
using AgileObjects.AgileMapper;
List<Student> students = new List<Student>() {
new Student{ ID=1, Address= "北京", Name = "黑无常", Phone = 13213 },
new Student{ ID=2, Address= "上海", Name = "白无常", Phone = 13213 },
};
var config = new MapperConfiguration(c => c.CreateMap<Student, StudentView>()); //14.0.0可以这么用,16.0.0就不能了
var mapper = config.CreateMapper();
var stuViewList = mapper.Map<List<StudentView>>(students); //断点测试可以看到这里的数据没有Phone
//AgileObjects
var stuViewList2 = AgileObjects.AgileMapper.Mapper.Map(students).ToANew<List<StudentView>>();
Console.ReadLine();
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public long Phone { get; set; }
}
public class StudentView
{
public int ID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
r = config.CreateMapper();
var stuViewList = mapper.Map<List>(students); //断点测试可以看到这里的数据没有Phone
//AgileObjects
var stuViewList2 = AgileObjects.AgileMapper.Mapper.Map(students).ToANew<List>();
Console.ReadLine();
public class Student
{
public int ID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public long Phone { get; set; }
}
public class StudentView
{
public int ID { get; set; }
public string Name { get; set; }
public string Address { get; set; }
}
更多推荐
所有评论(0)