原则16:避免创建不需要的对象

优质
小牛编辑
131浏览
2023-12-01

垃圾回收期在管理内存方面非常出色,它非常高效地移除不再使用的对象。但是无论你如何看待它,分配和销毁一个基于堆内存的对象花费处理器时间比分配和销毁不是基于堆内存的对象要多。在函数内创建大量的引用类型对象会引入严重的性能消耗问题。

所以不能让垃圾回收器超负荷工作。你可以借鉴一些简单的技巧最小化垃圾回收器的工作。所有的引用类型对象,即使是局部变量,都被分配存储在堆内存上。每个引用类型的局部变量在函数结束都会变为垃圾。一个最常见的坏实践是在 Windows 画图处理申请 GDI 对象:

// Sample one 
protected override void OnPaint(PaintEventArgs e) 
{
    // Bad. Created the same font every paint event. 
    using (Font MyFont = new Font("Arial", 10.0f)) 
    {
        e.Graphics.DrawString(DateTime.Now.ToString(), MyFont, Brushes.Black, new PointF(0, 0));
    } 
    base.OnPaint(e);
}

OnPainr() 会被频繁调用。每次被调用,你都会创建另一个包含相同设置的 Font 对象。垃圾回收器每次都需要为你清理。这间接导致低效。

相反,将 Font 对象从局部变量提升为成员变量。每次都重复使用相同的字体话窗口:

private readonly Font myFont = new Font("Arial", 10.0f);
protected override void OnPaint(PaintEventArgs e) 
{
    e.Graphics.DrawString(DateTime.Now.ToString(), myFont, Brushes.Black, new PointF(0, 0));base.OnPaint(e); 
}

你的程序不会每个绘画事件都产生垃圾。垃圾回收器的负担更少。你的程序也会跑的更快一点。当你把一个实现 IDisposable 的对象由局部变量提升为成员变量,如字体,那么你这个的类也要实现 IDisposable 。原则18会解释为什么那样做是对的。

如果一个引用类型的局部变量在函数中使用非常频繁,你需要将它提升为成员变量(值类型无关紧要)。上面绘画的字体就是最好的例子。只有常用的局部变量要频繁被访问才是好的候选。不是频繁调用就不用了。你应该避免重复创建相同的对象,也不要将每个局部变量转换为成员变量。

前面例子的静态属性 Brushes.Black 又是一个演示避免重复创建相同对象的技术。将常用的对象实例创建为静态成员变量。考虑前面例子中的黑色画刷。每次在要窗口使用黑色画东西,你都需要黑色画刷。如果你每次需要画东西都是申请一个新的,你会在运行中创建和销毁大量的黑色画刷。第一种做法是在你类中创建一个黑色画刷的成员变量,但这是远远不够的。程序可能创建大量窗口和控制,就会创建大量的黑色画刷。 .NET 框架设计者考虑到这点并且只创建一个黑色画刷让你需要的时候重复使用。 Brushes 类包含了大量的静态 Brush 对象,每个都是一种的常见颜色。在内部, Brushes 类使用懒惰算法,即只有当你需要的时候才创建。下面就是一个简单的实现:

private static Brush blackBrush; 
public static Brush Black 
{
    get 
    {
        if (blackBrush == null) 
            blackBrush = new SolidBrush(Color.Black);
        return blackBrush; 
    }
}

当你第一次使用黑色画刷, Brushes 类就会创建它。 Brushes 类保持黑色画刷的一个简单的引用,当你再需要的时就会返回这个引用。结果就是你只创建了一个黑色画刷并且可以一直重复使用。并且,如果你程序不需要穿件一个特殊的资源——比如,柠檬绿画刷——它就不会被创建。框架提供方法限制了对象的创建,在你完成目标的情况下使用最少的对象集。学会在你的程序中使用这样的技巧。

你已经学会两种技巧减少申请对象的数量,就像完成自己的业务一样。你可以将经常使用的局部变量提升为成员变量。你也可以使用单例模式让一个类提供常用的实例。最后这种技巧还包含了不可变类型的最终值。 System.String 类是不可变的:你构建好一个字符串之后,这个字符串的内容就不会被修改。任何时候你写的代码看起来像修改了字符串的内容,其实是创建了一个新的字符串对象,而旧的字符串就变为垃圾。这看似很无辜的实现:

string msg = "Hello, "; 
msg += thisUser.Name; 
msg += ". Today is "; 
msg += System.DateTime.Now.ToString();

和下面的写法一样是低效的:

string msg = "Hello, "; 
// Not legal, for illustration only: 
string tmp1 = new String(msg + thisUser.Name); 
msg = tmp1; // "Hello " is garbage. 
string tmp2 = new String(msg + ". Today is "); 
msg = tmp2; // "Hello <user>" is garbage. 
string tmp3 = new String(msg + DateTime.Now.ToString()); 
msg = tmp3; // "Hello <user>. Today is " is garbage.

字符串 tmp1, tmp2,和 tmp3 以及开始创建的消息(“Hello”),都是垃圾。string 类的 += 方法会创建新的字符串并返回。在字符串上连接字符不会修改原来的字符串。像前面的构造,可以使用 string.Format() 方法:

string msg = string.Format("Hello, {0}. Today is {1}",thisUser.Name, DateTime.Now.ToString());

对于更复杂的字符串操作,你可以使用 StringBuilder 类:

StringBuilder msg = new StringBuilder("Hello, "); 
msg.Append(thisUser.Name); 
msg.Append(". Today is "); 
msg.Append(DateTime.Now.ToString()); 
string finalMsg = msg.ToString();

StringBuilder 是一个可变字符串类用于构建不可变的 string 对象。在形成不可变 string 对象之前,它提供了配套的可变字符串方法去创建和修改文本内容。使用 StringBuilder 创建最终版本的字符串对象。更重要的是,学会这种设计思想。考虑当你设计不可变类型时(查看原则20),考虑创建一个对象去构建需要多次构造而成的最终对象。可以让使用者一步一步构建对象,并且维护这个不可变的类型。

垃圾回收器高效地管理你程序使用的内存。但是记住创建和销毁堆内存对象仍会花费时间。避免创建大量对象,不要创建你不需要的对象。同时避免创建在函数内部创建多个引用类型对象。相反,考虑提升局部变量为成员变量,或者为你的类型创建大多数常用的实例。最后,考虑创建一个可变类来构建不可变类。

小结:

这则原则相对简短,但是非常重要,尤其是对于菜鸟码农,总之,避免重复创建对象。

欢迎各种不爽,各种喷,写这个纯属个人爱好,秉持”分享“之德!