16 避免创建不必要的对象
GC(垃圾回收)为我们管理内存,以一种比较有效的方式移除不使用的对象。但是不管怎样分配和销毁基于堆管理的对象
都会占用不少处理器时间,所以请不要加重GC的负担,如下是一种比较糟糕的方式来分配GDI对象:
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);
}
OnPaint事件会被频繁地调用,而每调用一次该事件都会创建一个Font对象,GC需要每次都清理这些对象,这是非常没有效率的。
如果我们此时能把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);
}
对于string对象,我们也应注意string的内容是不变的,一旦你创建一个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以及原始的msg对象都成为垃圾被GC回收了。string上的+=操作符实际是创建了一个新的string对象并返回。
此时你应该使用string.Format()方法来避免创建新对象:
string msg = string.Format("Hello, {0}. Today is {1}",thisUser.Name, DateTime.Now.ToString());
对于更复杂的string操作,你可以使用StringBuilder类型:
StringBuilder msg = new StringBuilder("Hello, ");
msg.Append(thisUser.Name);
msg.Append(". Today is ");
msg.Append(DateTime.Now.ToString());
string finalMsg = msg.ToString();
GC虽然有效地管理内存,但是创建多余的对象还是会损耗处理器时间,所以应避免创建过多的对象,不要创建你不需要的对象。
17 实现标准的Dispose模式
一个标准的模式是通过.NET框架来处理非托管资源。使用你的类的用户也期望你能按照这个标准模式来处理。标准模式就是使用IDisposable接口来释放你的非托管资源,当客户端忘记这样做的时候会使用finalizer被动的释放。它和GC一起来保证你的对象在必要的时候被终结。这是处理非托管资源的正确方式,所以你应该彻底地理解它。
类层次的根基类应该实现IDisposable接口来释放资源。该类还应该增加一个finalizer来作为被动防御机制。惯例方法就是在虚方法内执行释放资源的委托方法,这样派生类能重写该方法以释放它们自己的资源,而且派生类必须记得调用基类版本的虚方法来释放基类的相关资源。
如果有非托管资源,你的类则必须有一个finalizer方法。你不应总是去信任客户端来调用Dispose()方法来处理资源释放。如果忘记Dispose()而你又没有finalizer方法,则可能会导致内存泄漏。
当GC运行时,它从内存移除任何没有终结器(有finalizer方法)的垃圾对象。那些有终结器的对象仍旧在内存里。这些对象被添加到一个终结队列,然后GC会在这些对象上生成一个新线程来运行终结器。在终结器线程完成它的工作后,GC才能从内存移除这些对象,所以那些需要终结的对象会比不需要终结的对象在内存中呆的时间更长。但是你不用为这些性能问题担心。
实现IDisposable是一个标准的方式,它通知用户运行时系统必须及时地释放持有资源的对象。IDisposable接口只包含一个方法:
public interface IDisposable
{
void Dispose();
}
实现IDisposable.Dispose()方法是为了4个任务:
1.释放所有非托管资源
2.释放所有托管资源(包含未挂钩的事件)
3.设置一个状态标志来指示对象是否被释放。你需要检查这个状态,如果释放后再次释放该对象,则抛出ObjectDisposed异常。
4.禁止终结器。调用GC.SuppressFinalize(this)来停止使用终结。
实现IDisposable,你也完成了两个任务,一是提供了客户端来及时释放所有托管资源的机制,二是避免了客户端去执行终结器。
简单示例:
public class MyResourceHog : IDisposable
{
// Flag for already disposed
private bool alreadyDisposed = false;
// Implementation of IDisposable.
// Call the virtual Dispose method.
// Suppress Finalization.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
// Virtual Dispose method
protected virtual void Dispose(bool isDisposing)
{
// Don‘t dispose more than once.
if (alreadyDisposed)
return;
if (isDisposing)
{
// elided: free managed resources here.
}
// elided: free unmanaged resources here.
// Set disposed flag:
alreadyDisposed = true;
}
public void ExampleMethod()
{
if (alreadyDisposed)
throw new ObjectDisposedException(
"MyResourceHog",
"Called Example Method on Disposed object");
// remainder elided.
}
}
如果派生类需要执行额外的清理,它将实现基类的虚拟方法,如下:
public class DerivedResourceHog : MyResourceHog
{
// Have its own disposed flag.
private bool disposed = false;
protected override void Dispose(bool isDisposing)
{
// Don‘t dispose more than once.
if (disposed)
return;
if (isDisposing)
{
// TODO: free managed resources here.
}
// TODO: free unmanaged resources here.
// Let the base class free its resources.
// Base class is responsible for calling
// GC.SuppressFinalize( )
base.Dispose(isDisposing);
// Set derived class disposed flag:
disposed = true;
}
}
注意基类和派生类都包含了disposed状态标志,并且上面的类都没有实现终结方法(因为没有包含非托管资源)。
下面是一个终结器(析构函数)使用代码:
public class BadClass
{
// Store a reference to a global object:
private static readonly List<BadClass> finalizedList =
new List<BadClass>();
private string msg;
public BadClass(string msg)
{
// cache the reference:
msg = (string)msg.Clone();
}
~BadClass()
{
// Add this object to the list.
// This object is reachable, no
// longer garbage. It‘s Back!
finalizedList.Add(this);
}
}
此时当类执行终结方法时,它会将它自己的引用放到一个全局列表中,它只是为了使自己可及,于是它又存活了。如果你实际需要终结一个恢复的对象,它将不会被终结。GC将不会从内存中移除任何只有通过终结器队列才能到达的对象,但是可能对象已经被终结,如果这样,这些对象几乎可以肯定是不再使用了。虽然BadClass的成员仍然在内存中,但是它们像被disposed或finalized。没有办法控制终结器的顺序,这种工作将是不可靠的。
在托管环境,你不需要创建终结器,你只需要在包含了非托管资源的类中或包含了实现IDisposable的成员类中创建终结器。
18 值类型和引用类型的区别
值类型或引用类型,结构或类,你应该使用哪个?这不是C++,C++里所有类型都做为值类型来定义。这也不是JAVA,JAVA里所有类型都是引用类型。当你创建类型时,在选择struct或class关键字是简单的,但是如果你之后更改了类,你将会做更多的工作来更新那些使用了你的类的客户端应用。
这里不是简单的喜欢用哪个或另一个。正确与否取决于你如何去使用你新创建的类型。值类型不是多态的,它们更适合存储应用操作的数据。引用类型能多态并且应用来定义应用的行为。
在C#中你使用struct或class关键字来声明类型是否是值类型或引用类型。值类型应是轻量级、小类型。引用类型形成你的类层次结构。
如下代码:
private MyData myData;
public MyData Foo()
{
return myData;
}
// call it:
MyData v = Foo();
TotalSum += v.Value;
如果MyData是值类型,那么返回值会复制一份然后赋值给V.
如果MyData是引用类型,你引用了一个内部变量。这违法了封装性。更改代码如下:
public MyData Foo2()
{
return myData.CreateCopy();
}
// call it:
MyData v = Foo();
TotalSum += v.Value;
现在v是myData的一份副本。作为引用类型,两个对象都创建在堆上。你不用担心暴露内部数据,相反,你已经创建了一个额外的对象在堆上。
类型通过公共方法和属性来导出值类型数据。
考虑下面代码段:
private MyType myType;
public IMyInterface Foo3()
{
return myType as IMyInterface;
}
// call it:
IMyInterface iMe = Foo3();
iMe.DoWork();
myType变量仍然通过方法Foo3来返回,但是现在不是直接访问内部数据,而是通过调用已定义的接口的方法。你现在不是为了MyType对象的数据内容,而是获得它的行为(接口主要定义类的行为方法)。通过这些代码,我们可以知道值类型存储值,而引用类型存储行为。
现在我们来深入了解一下这些类型是如何在内存中存储以及与性能相关的存储模型。考虑如下类:
public class C
{
private MyType a = new MyType();
private MyType b = new MyType();
// Remaining implementation removed.
}
C cThing = new C();
上面代码一共创建了多少对象?每个有多大?这都视情况而定。如果MyType是值类型,你做了一次分配,分配了两倍MyType的大小;如果是引用类型,做了三次分配,一次是为对象C(8个字节),另外2次是c里的2个MyType对象。产生这不同的结果是因为值类型内联存储在一个对象里,而引用类型不是。每个引用类型变量都包含一个引用,需要额外的空间来存储。
为了能透彻地理解,考虑如下分配:
MyType[] arrayOfTypes = new MyType[100];
如果MyType是值类型,一次性就分配100个MyType对象。但如果MyType对象是引用类型,只分配了一次,数组每个元素都是null。当你初始化数组的每个元素时,你将执行101次分配(101次分配将会比一次性分配要花费更多时间).分配大量的引用类型会导致堆碎片,并且使你的应用变慢。如果你创建这些大量的类型是为了存储数据,你应当选择值类型。
决定使用值类型还是引用类型是比较重要的。把值类型变为类类型是一个深远的改变。考虑如下类:
public struct Employee
{
// Properties elided
public string Position
{
get;
set;
}
public decimal CurrentPayAmount
{
get;
set;
}
public void Pay(BankAccount b)
{
b.Balance += CurrentPayAmount;
}
}
这个相当简单的类包含了一个支付方法。时过境迁,系统运行越来越好,然后你决定有不同类型的员工:销售人员可以获得提成,管理人员可以获得奖金。如是你决定将Employee结构改为类:
public class Employee2
{
// Properties elided
public string Position
{
get;
set;
}
public decimal CurrentPayAmount
{
get;
set;
}
public virtual void Pay(BankAccount b)
{
b.Balance += CurrentPayAmount;
}
}
这将会打破现在使用了你的Employee结构的代码,返回值变成了返回引用。参数从传值变成了传引用。如下代码将发生很大改变:
Employee e1 = Employees.Find(e => e.Position == "CEO");
BankAccount CEOBankAccount = new BankAccount();
decimal Bonus = 10000;
e1.CurrentPayAmount += Bonus; // Add one time bonus.
e1.Pay(CEOBankAccount);
现在一次性增加奖金现在变成了永久的增加了。改变类型的同时,影响了行为的改变。
类类型可以定义公共职责的多种不同的实现,而结构只应用来存储数据。值类型在内存管理上会更有效率,它有较少的堆碎片,较少的垃圾,较少的迂回。更重要的是当从方法或属性返回时,值类型返回的是副本。但是考虑扩展性,值类型对面向对象技术的支持很有限。你应该考虑所有的值类型都是sealed。
如果你对下面几个问题都回答YES,那么你应创建一个值类型。
1.这个类型的主要责任释放是存储数据?
2.它的访问数据成员的公共接口完全由属性来定义?
3.你确信这个类型永远不会有子类?
4.你确信这个类型永远不会有多种形态?
建立底层的数据存储使用值类型,建立应用的行为使用引用类型。
19 确保0是一个有效的值类型状态
缺省的.NET系统会将所有对象都设置为0.因为你不能阻止其他语言使用0来初始化值类型,所以用0来作为你的类型缺省值吧。
一个比较特殊的例子是枚举。不要创建一个不把0作为有效状态的枚举类型。所有的枚举类型都是从System.ValueType继承来的。默认的枚举值是从0开始,但是你可以修改默认行为:
public enum Planet
{
// Default starts at 0 otherwise.
Mercury = 1,
Venus = 2,
Earth = 3,
Mars = 4,
Jupiter = 5,
Saturn = 6,
Neptune = 7,
Uranus = 8
// First edition included Pluto.
}
Planet sphere = new Planet();
sphere是0,但它不是一个有效的值。当你创建你自己的枚举类型时,请确保0是其中的一个有效的值。
20 偏爱不变的原子的值类型
内容不变的类型是简单的,一旦创建,它们就是常量。不变的类型也是线程安全的。多个线程可以访问同样的内容,绝对不会看到不一致的内容。不变类型的内容是不能改变的,它会很好地工作在基于hash的集合里。对于内容不变的类型Object.GetHashCode()返回的值是相同的。但是要使每个类型都内容不变,这是很难的。你需要不断的克隆新对象来修改其内容状态。分解你的类型为自然形成的单一实体的结构,即原子的类型。一个客户类不是一个原子类,因为它可能包含很多信息(如:地址、电话等),任何一个独立的信息也都可能会改变。原子类是一个单一的实体,如果改变它的组成字段,将会引发异常。
如果你需要将地址类作为一个struct,请让它不变。任何更改实例字段都设置为只读的,如下:
public struct Address2
{
// remaining details elided
public string Line1
{
get;
private set;
}
public string Line2
{
get;
private set;
}
public string City
{
get;
private set;
}
public string State
{
get;
private set;
}
public int ZipCode
{
get;
private set;
}
}
现在你有一个基于公共接口的不变的类型了。为了使它更有用,你需要增加必要的构造函数来初始化完整的地址结构信息。但请记住struct已有一个缺省的构造函数,缺省所有string类型成员都为Null,ZIPCode为0:
public Address2(string line1,
string line2,
string city,
string state,
int zipCode) :
this()
{
Line1 = line1;
Line2 = line2;
City = city;
ValidateState(state);
State = state;
ValidateZip(zipCode);
ZipCode = zipCode;
}
使用不可变类型需要一点稍微不同的调用顺序来修改它的状态.你的修改是通过创建一个新对象而不是修改已存在的实例来完成.
// Create an address:
Address2 a2 = new Address2("111 S. Main","", "Anytown", "IL", 61111);
// To change, re-initialize:
a2 = new Address2(a1.Line1,a1.Line2, "Ann Arbor", "MI", 48103);
一旦一个新的地址对象被构造了,它的值就是永远固定了.这是相当安全的,如果a2在构造中出现了异常,它不会影响到原来的值。
上面的Address2结构并不是严格的不可变类型。私有的setter仍然可以改变内部数据状态。改为真正不可变类型如下:
public struct Address3
{
// remaining details elided
public string Line1
{
get { return Line1; }
}
private readonly string line1;
public string Line2
{
get { return line2; }
}
private readonly string line2;
public string City
{
get { return city; }
}
private readonly string city;
public string State
{
get { return state; }
}
private readonly string state;
public int ZipCode
{
get { return zip; }
}
private readonly int zip;
public Address3(string line1,
string line2,
string city,
string state,
int zipCode) :
this()
{
this.line1 = line1;
this.line2 = line2;
this.city = city;
ValidateState(state);
this.state = state;
ValidateZip(zipCode);
this.zip = zipCode;
}
}
值类型不支持派生,所以你不能期望用派生类来修改其字段。但应注意不可变类型中的引用类型,它可能导致能修改内部数据,从而失去不可变的特性。
如:
public struct PhoneList
{
private readonly Phone[] phones;
public PhoneList(Phone[] ph)
{
phones = ph;
}
public IEnumerable<Phone> Phones
{
get
{
return phones;
}
}
}
Phone[] phones = new Phone[10];
// initialize phones
PhoneList pl = new PhoneList(phones);
// Modify the phone list:
// also modifies the internals of the (supposedly)
// immutable object.
phones[5] = Phone.GeneratePhoneNumber();
因为数组是一个引用类型,所以修改元素的值直接破坏了其内部数据,为了解决该问题,你需要复制一份该数组的副本.
// Immutable: A copy is made at construction.
public struct PhoneList2
{
private readonly Phone[] phones;
public PhoneList2(Phone[] ph)
{
phones = new Phone[ph.Length];
// Copies values because Phone is a value type.
ph.CopyTo(phones, 0);
}
public IEnumerable<Phone> Phones
{
get
{
return phones;
}
}
}
Phone[] phones2 = new Phone[10];
// initialize phones
PhoneList p2 = new PhoneList(phones);
// Modify the phone list:
// Does not modify the copy in pl.
phones2[5] = Phone.GeneratePhoneNumber();
不可变类型编写简单且容易维护。不要盲目地为每个属性创建get和set访问器。你存储数据的第一选择应是不可变的原子的值类型。改善C#编程的50个建议(16-20),布布扣,bubuko.com
改善C#编程的50个建议(16-20)
原文:http://blog.csdn.net/edcvf3/article/details/22696887