C#效率优化(1)-- 使用泛型时避免装箱

  本想接着上一篇详解泛型接着写一篇使用泛型时须要注意的一个性能问题,可是后来想着不如将以前的详解XX系列更正为如今的效率优化XX系列,记录在工做时遇到的一些性能优化的经验和技巧,若是有什么不足,还请你们多多指出;性能优化

  在使用集合时,一般为了防止装箱操做而选择List<T>、Dictionary<TKey, TValue>等泛型集合,可是在使用过程当中若是使用不当,依然会产生大量的装箱操做;ide

  首先,将值类型的实例当作引用类型来使用时,即会产生装箱,例如:性能

int num = 10;
object obj = num;
IEquatable<int> iEquatable = num;

  其次,对于自定义结构,在正常使用时,一般须要注意一些误装箱的操做:优化

public struct MyStruct
{
   public int MyNum;
}

  对该结构MyStruct的实例调用基类Object中的方法时,都会进行装箱操做,对于静态方法(Equals、ReferenceEquals)很好理解,对于实例方法,在CLR调用实例方法时,实际上会把调用这个方法的对象看成第一个参数传入实例方法,而基类Object中的实例方法都会将Object类型的对象做为第一个参数,所以也会发生装箱,这其中的实例方法包括GetType和虚方法Equals、GetHashCode、ToString;this

  其中,GetType方法自己就是经过堆内存中与实例数据一块儿存储的类型对象指针来获取实例类型信息的,对于值类型实例,自己就没有这个开销成员,此处应使用typeof()运算符代替避免装箱;spa

  三个虚方法能够经过在MyStruct中重写来防止装箱操做;可是对于Equals方法,有一些须要区别注意的地方:指针

  在调用值类型基类ValueType中的ValueType.Equals(object obj)方法进行比较操做时,会对当前实例和实参obj进行装箱,共两次装箱(抽象基类ValueType依然是类类型);在MyStruct中重写了该方法MyStruct.Equals(object obj),在调用myStruct1.Equals(myStruct2)时,依然会对myStruct2进行装箱,共一次装箱,此时咱们能够在MyStruct中声明一个Equals的重载方法,参数类型一样为MyStruct,同时对==和!=运算符进行重载:code

public struct MyStruct
{
    public int MyNum;
    public override bool Equals(object obj)  //调用时会对实参进行装箱
    {
        if (!(obj is MyStruct))
        {
            return false;
        }
        MyStruct other = (MyStruct)obj;  //拆箱
        return this.MyNum == other.MyNum;
    }
    public bool Equals(MyStruct other)  //重载Equals方法,避免装箱
    {
        return this.MyNum == other.MyNum;
    }
    public static bool operator ==(MyStruct left, MyStruct right)  //比较时一般采用==运算符
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyStruct left, MyStruct right)
    {
        return !(left == right);
    }
}

  此时,在调用myStruct1.Equals(myStruct2)、myStruct1 == myStruct二、myStruct1 != myStruct2时都再也不产生装箱操做;对象

  可是,在使用泛型方法时,例如对于如下的方法,重载方法并不会生效:blog

static bool MyFunc<T>(T obj1, T obj2)
{
    return obj1.Equals(obj2);
}

  查看其生成的IL代码能够清楚的知道不生效的缘由:

  其中默认对obj2进行了box指令调用,而对于obj1,在调用callvir指令时加入了前缀constrained指令,则会判断obj1的类型定义中是否存在Equals方法的重写,若是有则调用重写方法,若是没有,则装箱后调用基类ValueType中的虚方法;前面MyStruct的定义中重写了Equals方法,所以会调用该重写方法,此时只触发一次对obj2的装箱,但依然不是咱们想要的;

  为了不这个问题,咱们须要在MyStruct的定义中实现IEquatable<T>接口,并在这个泛型方法的声明中添加约束:

public struct MyStruct : IEquatable<MyStruct>
{
    public int MyNum;
    public override bool Equals(object obj)
    {
        if (!(obj is MyStruct))
        {
            return false;
        }
        MyStruct other = (MyStruct)obj;
        return this.MyNum == other.MyNum;
    }
    public bool Equals(MyStruct other)  //实现IEquatable<T>接口中的方法
    {
        return this.MyNum == other.MyNum;
    }
    public static bool operator ==(MyStruct left, MyStruct right)
    {
        return left.Equals(right);
    }
    public static bool operator !=(MyStruct left, MyStruct right)
    {
        return !(left == right);
    }
}
static bool MyFunc<T>(T obj1, T obj2) where T : IEquatable<T>
{
      return obj1.Equals(obj2);
}

  此时,查看其IL代码,能够发现没有了box指令,避免了装箱操做:

  对泛型集合List<Mystruct>使用一些内含比较的实例方法时,也会遇到上面的装箱问题,解决方法一样是实现IEquatable<T>接口;以经常使用的Contains方法举例:

  List<MyStruct>中的Contains方法中会调用泛型抽象类EqualityComparer<T>.Default的实例来进行比较,而在抽象类EqualityComparer<T>中,会根据类型参数T实例化对应的具体类实例,具体可查看EqualityComparer<T>.CreateComparer()中的实例生成逻辑,其中,会根据T是否实现了IEquatable<T>接口而实例化不一样的类的实例:

internal class GenericEqualityComparer<T>: EqualityComparer<T> where T: IEquatable<T>
internal class ObjectEqualityComparer<T>: EqualityComparer<T>

  这两个类的具体实现这里再也不赘述;

  基于上面的理解,对于值类型,实现基类的虚方法和IEquatable<T>接口对于避免装箱十分有必要;

 


若是您以为阅读本文对您有帮助,请点一下“推荐”按钮,您的承认是我写做的最大动力!

做者:Minotauros
出处:https://www.cnblogs.com/minotauros/

本文版权归做者和博客园共有,欢迎转载,但未经做者赞成必须保留此段声明,且在文章页面明显位置给出原文链接,不然保留追究法律责任的权利。

相关文章
相关标签/搜索