1. 需求背景
相信大家在实际开发中常常都会用到对象池的技术,及对对象的复用。通过对对象的复用可以大大简单对象的创建数量,同时也是减少对象频繁的创建和销毁流程,从而减少GC。但是在对对象复用时,常常会出现这么一种情况,就是在使用复用对象时,如果不进行全部赋值,那么就会使得对象数据有残留,如果这个对象正常使用时并不是全部需要赋值时,那为了避免数据残留,就需要自己手动为字段赋初值,这一点大大的降低了工作效率,同时如果字段过多时,也可能导致少赋值的情况,为了解决这一种情况,所有需要一个手段能够自动帮我们把复用对象中所有字段初始化。
2. 实现方式
在实现方式上,不考虑自己硬编码的情况下,很多人第一时间应该就会想到使用反射的方式去实现,因为这涉及到对一个未知对象的字段访问操作,但是反射的效率很低,在数据量大时会有十分严重的效率问题。
这里我提供了第二种实现方式,那便是表达式树去实现,对于表达式树,我相信很多人都不了解这个东西,我这里简单的说一下,所谓表达式树,其实就是把一个表达式拆成树的形式去表示,对于C#中,你可以将其理解为通过代码去写代码。具体的细节可以仔细搜索相关资料,我这里提供一个我个人认为讲的还不错的视频链接,有兴趣的可以看一下。6--C#语法--CSharp-表达式树--ExpressionTree_哔哩哔哩_bilibili
当然这里为了让大家直观的看出两者效率上的差距,两种实现方式我都会提供代码。
反射实现
internal static class ReflectInit<T> where T : class
{
private static Type _type;
private static FieldInfo[] _fieldInfos;
static ReflectInit()
{
_type = typeof(T);
_fieldInfos = _type.GetFields();
}
public static void Init(T obj)
{
foreach (FieldInfo field in _fieldInfos)
{
object defaultValue = field.FieldType.IsValueType
? Activator.CreateInstance(field.FieldType) : null;
field.SetValue(obj, defaultValue);
}
}
}
这里实现的比较粗糙,大家稍微理解参考一下即可。
表达式树实现
internal static class ExpressionInit<T> where T : class
{
private static Action<T> _initAction;
static ExpressionInit()
{
Type type = typeof(T);
var parameter = Expression.Parameter(type, "value");
var body = new List<Expression>();
foreach (FieldInfo fieldInfo in type.GetFields())
{
var field = Expression.Field(parameter, fieldInfo);
var defaultValue = Expression.Default(fieldInfo.FieldType);
body.Add(Expression.Assign(field, defaultValue));
}
var block = Expression.Block(body);
_initAction = Expression.Lambda<Action<T>>(block, parameter).Compile();
}
public static void Init(T obj)
{
_initAction(obj);
}
}
首先我们定义了一个泛型静态类对这个类型的对象作表达式缓存,在静态构造器初始化时,生成好对象的初始化表达式树,并且将结果的委托保存起来,在后续初始化时都不需要重新生成。
关于表达式树的生成,我这里简单说一下思路:
- 首先定义参数表达式,接收需要被初始化的对象
- 创建表达式列表,以用于后面生成表达式块
- 通过反射去获取所有成员变量,然后创建成员表达式,在创建默认值表达式,最后创建赋值表达式并将其加入到表达式列表中。这个一段代码翻译成C#代码如下:
不难看出,其实这一整段的代码其实就是遍历了所有成员遍历,并对他们赋默认值操作对象.成员变量 = default(对象.成员变量类型);
- 将表达式列表转换为表达式块,其翻译成C#为
{ ...多行代码段 }
- 将表达式树转换为委托,并通过静态变量缓存下来。
3. 效果展示
对象定义
var liSi = new Person()
{
name = "李四",
age = 15,
};
var zhangSan = new Person()
{
name = "张三",
age = 30,
friends = new List<Person>()
{
liSi
}
};
public class Person
{
public string name;
public int age;
public List<Person> friends;
public override string ToString()
{
return $"Person={{name={name}, age={age}, friends={friends}}}";
}
}
反射效果
Console.WriteLine(zhangSan);
ReflectInit<Person>.Init(zhangSan);
Console.WriteLine(zhangSan);
表达式树效果
Console.WriteLine(zhangSan);
ExpressionInit<Person>.Init(zhangSan);
Console.WriteLine(zhangSan);
可以看到两种方式都成功的将对象内的所有成员变量都赋值为了默认值。
效率测试
测试代码如下:
const int SIZE_1 = 10000;
const int SIZE_2 = 1000000;
const int SIZE_3 = 100000000;
var liSi = new Person()
{
name = "李四",
age = 15,
};
var zhangSan = new Person()
{
name = "张三",
age = 30,
friends = new List<Person>()
{
liSi
}
};
Console.WriteLine($"{SIZE_1}次下效率比较");
Console.WriteLine($"反射:{TestReflect(zhangSan, SIZE_1)}");
Console.WriteLine($"表达式树:{TestExpression(zhangSan, SIZE_1)}");
Console.WriteLine($"{SIZE_2}次下效率比较");
Console.WriteLine($"反射:{TestReflect(zhangSan, SIZE_2)}");
Console.WriteLine($"表达式树:{TestExpression(zhangSan, SIZE_2)}");
Console.WriteLine($"{SIZE_3}次下效率比较");
Console.WriteLine($"反射:{TestReflect(zhangSan, SIZE_3)}");
Console.WriteLine($"表达式树:{TestExpression(zhangSan, SIZE_3)}");
double TestReflect(Person person, int size)
{
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < size; i++)
ReflectInit<Person>.Init(person);
watch.Stop();
return watch.Elapsed.TotalSeconds;
}
double TestExpression(Person person, int size)
{
var watch = new Stopwatch();
watch.Start();
for (int i = 0; i < size; i++)
ExpressionInit<Person>.Init(person);
watch.Stop();
return watch.Elapsed.TotalSeconds;
}
public class Person
{
public string name;
public int age;
public List<Person> friends;
public override string ToString()
{
return $"Person={{name={name}, age={age}, friends={friends}}}";
}
}
测试结果如下:
可以看出在10000次比较下,反射是比表达式树要快的,这是因为表达式树在初始化时有作缓存,由此被拖慢了速度。
在缓存完毕后,在执行1000000次时,此时表达式树的效率就比反射要更加的快。
在执行100000000此时,此时数据量已经达到非常之大,此时两种的效率差距已经非常之大,反射居然需要整整29秒多才能完成,而表达式树仅仅只花了1秒多就完成了。
由此可见,表达式树只有在开始调用时效率会第一反射,在缓存完毕后,效率将会比反射要搞得多。