[C#] 值与引用

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容