[C#] 值与引用

作者: 丑小丫大笨蛋 | 来源:发表于2016-03-21 00:39 被阅读167次

    对于一个常用语言为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)

    相关文章

      网友评论

        本文标题:[C#] 值与引用

        本文链接:https://www.haomeiwen.com/subject/dcrklttx.html