Effective C#读书笔记(2)
6.区别值类型和引用类型
在 c++里,所有的参数及返回数据都是以值类型进行传递的。以值类型进行传递是件很有效率的事。但是却遇到对象的浅拷贝(partial copying)问题。如果你对一个派生的对象COPY数据时,是以基类的形式进行COPY的,那么只有基类的部分数据进行了COPY。你就直接丢失了派生对象的所有信息。即使时使用基类的虚函数。
而java语言,所有的参数及返回数据都是以引用类型进行传递的。这一策略在性能上有缺陷。
当值类型包含其他引用类型时,赋值将生成一个引用的副本。这样就有两个独立的结构,每一个都包含指向内存中同一个对象的引用(也就是“浅复制”)。当想执行一个“深复制”,即将内部引用的状态完全复制到一个新对象中时,需要实现ICloneable接口。
如果按引用传递(ref)引用类型,被调用者可能改变对象的状态数据的值和所引用的对象。
如果是按值传递,只是复制了指向调用者对象的引用。所以改变对象的状态数据是可能的,但是无法把引用重新赋值给一个新的对象。(有一点象C++中的常量指针)
9.明白几个相等运算之间的关系
C#提供了4个不同的方法来断定两个对象是否是相等的:
public static bool ReferenceEquals
( object left, object right );
public static bool Equals
( object left, object right );
public virtual bool Equals( object right);
public static bool operator==( MyClass left, MyClass right );
两个引用类型的变量在引用同一个对象时,它们是相等的,两个值类型的变量的它们的类型和内容都是相同时,它们应该是相等的,这就是为什么相等测试要这么多方法了。
Object.ReferenceEquals() 在两个变量引用到同一个对象时返回true,也就是两个变量具有相同的对象ID。不管比较的类型是引用类型还是值类型的,这个方法总是检测对象ID,而不是对象内容。当你测试两个值类型是否相等时,ReferenceEquals()总会返回false,即使你是比较同一个值类型对象,它也会返回 false。因为参数要求两个引用对象,所以用两个值类型来调用该方法,会先使两个参数都装箱,这样一来,两个引用 对象自然就不相等了。
静态的Object.Equals()。这个方法在你不清楚两个参数的运行类型时什么时,检测它们是否相等。静态的Equals()是使用左边参数实例的Equals()方法来断定两个对象是否相等。
前两个方法可能从来不会修改。
Object.Equals() 方法使用对象的ID来断定两个变量是否相等。这个默认的object.Equals()函数的行为与Object.ReferenceEquals()确实是一样的。但是,对于值类型是不一样的。System.ValueType并没有重载 Object.Equals(),System.ValueType是所有你所创建的值类型(使用关键字struct创建)的基类。两个值类型的变量相等,如果它们的类型和内容都是一样的。ValueType.Equals()实现了这一行为。不幸的是,ValueType.Equals()并不是一个高效的实现。
如果你的类型须要遵从值类型的语义(比较内容)而不是引用类型的语义(比较对象ID)时,你应该自已重载实例的object.Equals()方法。这有一个标准的模式:
public class Foo
{
public override bool Equals( object right )
{
// check null:
// the this pointer is never null in C# methods.
if (right == null)
return false;
if (object.ReferenceEquals( this, right ))
return true;
// Discussed below.
if (this.GetType() != right.GetType())
return false;
// Compare this type's contents here:
return CompareFooMembers(
this, right as Foo );
}
}
原则是不管什么时候,在创建一个值类型时重载Equals()方法,并且你不想让引用类型遵从默认引用类型的语义时也重载Equals(),就像System.Object定义的那样。重载Equals()就意味着你应该重写GetHashCode(),
操作符==(),任何时候你创建一个值类型,重新定义操作符==()。原因和实例的Equals()是完全一样的。默认的版本使用的是引用的比较来比较两个值类型。效率远不及你自己任意实现的一个,所以,你自己写。当你比较两个值类型时,需要避免装箱。
C#给了你4种方法来检测相等性,但你只须要考虑为其中两个提供你自己的方法。你决不应该重载静态的Object.ReferenceEquals()和静态的Object.Equals(),因为它们提供了正确的检测,忽略运行时类型。你应该为了更好的性能而总是为值类型实例提供重载的Equals()方法和操作符==()。当你希望引用类型的相等与对象ID的相等不同时,你应该重载引用类型实例的Equals()。
10.明白GetHashCode()的缺陷
GetHashCode()仅在一种情况下使用:那就是对象被用于基于散列的集合的关键词,如经典的HashTable或者Dictionary容器。
基于散列(算法)的集合用散列值来优化查找。每一个对象产生一个整型的散列值,而该对象就存储在基于这个散列值的“桶”中。为了查找某个对象,你通过它的散列值来找到这个(存储了实际对象的)“桶”。在.Net里,每一对象都有一个散列值,它是由System.Object.GetHashCode()断定的。任何对GetHashCode()的重写都必须遵守下面的三个规则:
1、如果两个对象是相等的(由操作符==所定义),那么它们必须产生相同的散列值。否则,无法通过散列值在容器中找到对象。
2、对于任意对象A,A.GetHashCode()必须是实例不变的。不管在A上调用了什么方法,A.GetHashCode()必须总是返回同样的散列值。这就保证在某个“桶”中的对象始终是在这个“桶”中。
3、对于任意的输入,散列函数总是产生产生一个介于整型内的随机分布。这会让你在一个基于散列的容器取得好的效率。
如果你提供了自己的==版本,你就必须同时提供你自己版本的GetHashCode(),从而保证遵守了前面说的第一条规则。
唯一安置好规则2的方法就是,定义一个散列函数,它依懒于对象的一些不变的属性来返回散列值。例如,
public class Customer
{
private readonly string _name;
private decimal _revenue;
public Customer( string name ) :
this ( name, 0 )
{
}
public Customer( string name, decimal revenue )
{
_name = name;
_revenue = revenue;
}
public string Name
{
get { return _name; }
}
// Change the name, returning a new object:
public Customer ChangeName( string newName )
{
return new Customer( newName, _revenue );
}
public override int GetHashCode()
{
return _name.GetHashCode();
}
}
对customer类,使客户名成为一个恒定的。
使名字成为恒定的类型后,要怎样才能修改一个客户对象的名字呢
Customer c1 = new Customer( "Acme Products" );
myHashMap.Add( c1,orders );
// Oops, the name is wrong:
Customer c2 = c1.ChangeName( "Acme Software" );
Order o = myHashMap[ c1 ] as Order;
myHashMap.Remove( c1 );
myHashMap.Add( c2, o );