[C#] 值与引用

2364 查看

对于一个常用语言为C++的人来说,刚开始写C#时很容易因为不清楚C#中的值与引用而犯错误。下面就以一个简单的例子来说明这种小错误的来龙去脉。

1. list<T>比较的错误示例

namespace testValueAndRef
{
    class Program
    {
        static void Main(string[] args)
        {
            List<int> listA = new List<int>() { 1, 2, 3 };
            List<int> listB = new List<int>() { 1, 2, 3 };
            Console.WriteLine(listA.Equals(listB));
            Console.WriteLine(listA.GetHashCode().ToString());
            Console.WriteLine(listB.GetHashCode().ToString());
        }
    }
}

C++转到C#,我的第一印象是C#的基本类库好强大,似乎任何你能想到的基本操作都可以找到库函数。所以,当我想比较两个list里面的内容是否一致时,我就想当然地写了上面那句话listA.Equals(listB)。而结果,当然是出人意料的错误了,输出如下所示:

False
21083178
55530882

这里在false下面输出的两行分别为listAlistBHashCode,而默认情况下Equals()比较的就是两个ObjectHashCode,也就是对象实例引用的内存地址。
所以listAlistB之所以不相等,是因为默认情况下这里Equals()函数比较的是它们的地址而不是它们的内容。如果把第二句改成List<int> listB = listA;,则最后返回结果就是True

2. list<T>比较的解决方案

那么,如果解决这个问题呢?下面给出两种方案:
方案一:不采用Equals(),而是自己来实现两个列表的值的比较。例如:
var eq = listA.Except(listB).Count() == 0 && listB.Except(listA).Count() == 0;
需要指出的是,这里示例的比较方法中把两个list中元素看成无序的,及{1,2,3}{1,3,2}是相等了,如果你定义的两个list相等是指元素一对一地相等恐怕上面的方法不能胜任。
另外,有人指出可以用C#4中引入的Zip来实现两个list的比较,lista.Count()==listb.Count()&&lista.Zip(listb,Equals).All(a=>a);,参见stackoverflow

方案二:利用SequenceEqual,它属于System.Linq命名空间,可用于任何IEnumerable类。
这里只要将原始例子中的Equals那句换成listA.SequenceEqual(listB)即可。如果你的list里的元素是自定义类型,那么在使用SequenceEqual的时候还需要另外一个参数IEqualityComparer

下面把上面列子中list里的元素从int换成自定义的Point,那么在用SequencEqual时需要提供Point的比较方式。可以看到我们提供的TestComparer也就是重写了EqualsGetHashCode两个函数,而这两个函数是C#中类层次最高的父类Object中定义的,所以如果你在定义Point(他是继承Object的)的时候就可以重写这两个函数,这样的话可以直接用SequenceEqual

namespace testValueAndRef
{
    class Program
    {
        static void Main(string[] args)
        {
            var a = new Point(1, 2);
            var b = new Point(3, 4);
            var listA = new List<Point>() { a, b };
            var listB = new List<Point>() { a, b };
            Console.WriteLine(listA.SequenceEqual(listB, new TestComparer()));
        }
    }
    public class Point 
    {
        public int x;
        public int y;
        public Point(int s, int t)
        {
            x = s;
            y = t;
        }
    }
    public class TestComparer : IEqualityComparer<Point>
    {
        public bool Equals(Point p1, Point p2)
        {
            return p1.x.Equals(p2.x) && p1.y.Equals(p2.y);
        }
        public int GetHashCode(Point obj)
        {
            return obj.x.GetHashCode() + obj.y.GetHashCode();
        }
    }
}

3. 总结C#中的值与引用

上面的问题解决了,那么,为了更深刻地理解C#中的值和引用,下面我们来简单总结一下。

(1) 值类型

所谓值类型,就是指变量即代表值本身。C#中的基础数据类型(string除外)、枚举和结构体都属于值类型。
值类型都隐式派生自System.ValueType,而System.ValueType又继承自最高父类System.ObjectValueType的作用是确保其所有派生类型都分配在栈上而不是垃圾回收堆上。

创建和销毁分配在栈上的数据都很快,因为它的生命周期是由定义的作用域决定的。当结构变量离开定义域时,它就会立即从内存中移除。而分配在堆上的数据由.NET垃圾回收器监控。

每个值类型都有默认值(可以用default(type)查看),在定义值类型的变量时如果不赋值就使用(有些编译器在编译阶段就会检查并阻止这种情况)不会引起NullReferenceException异常,而是使用默认值。

值类型的赋值操作,把一个值类型赋值给另外一个时,就是对字段成员逐一进行复制。这样进行多次赋值后,该值会在栈中有多份拷贝。

(2) 引用类型

引用类型变量的值是内存地址,其真实内容存储在该内存地址处。C#中的stringArray、类、接口和委托都是引用类型,引用类型都隐式继承自System.Object。引用类型都分配在堆上,由GC去管理他们的创建和注销。

所有引用类型的默认值都是null,不赋值就直接使用会抛出NullReferenceException异常。

引用类型的赋值操作,就是在内存中重定向引用变量的指向。这和C++中很不一样,尤其要注意,引用类型赋值操作会使得多个变量共享同一数据块,任何一个变量对数据进行修改后,其他变量访问到的都是修改后的数据。

另外,既然值类型的赋值时拷贝一份数据,引用类型的赋值是直接赋数据的内存地址,那么对包含有引用类型的值类型如何处理呢?
假设我们定义了Rectangle的结构体中包含了一个Point类,下面是一个简单的例子。

struct Rectangle {
    public Point leftTop;
    public int rightBottomX, rightBottomY;
    public Rectangle(Point p, int x, int y)
    {
        leftTop = p;
        rightBottomX = x;
        rightBottomY = y;
    }
}
static void Main(string[] args)
{
    Rectangle r1 = new Rectangle(new Point(1, 2), 3, 4);
    Rectangle r2 = r1;
    r2.rightBottomX = 5;
    r2.leftTop.x = 6;
    r2.leftTop.y = 7;
   Console.WriteLine("[{0},{1},{2},{3}]", r1.leftTop.x, r1.leftTop.y, r1.rightBottomX, r1.rightBottomY);
   Console.WriteLine("[{0},{1},{2},{3}]", r2.leftTop.x, r2.leftTop.y, r2.rightBottomX, r2.rightBottomY);
}

输出结果是:

[6,7,3,4]
[6,7,5,4]

所以,当值类型包含其他引用类型时,赋值将生成一个引用的副本。这样就有两个独立的结构,每个都包含指向内存中同一个对象的引用。(浅拷贝)

注:本文大部分内容来自《精通C#(第6版)》(PS:怎么写一行小字啊(-__-)b)