對于一個常用語言為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++
轉(zhuǎn)到C#
逻谦,我的第一印象是C#
的基本類庫好強大应狱,似乎任何你能想到的基本操作都可以找到庫函數(shù)咬崔。所以,當我想比較兩個list
里面的內(nèi)容是否一致時,我就想當然地寫了上面那句話listA.Equals(listB)
伏嗜。而結(jié)果,當然是出人意料的錯誤了伐厌,輸出如下所示:
False
21083178
55530882
這里在false下面輸出的兩行分別為listA
和listB
的HashCode
承绸,而默認情況下Equals()
比較的就是兩個Object
的HashCode
,也就是對象實例引用的內(nèi)存地址挣轨。
所以listA
和listB
之所以不相等军熏,是因為默認情況下這里Equals()
函數(shù)比較的是它們的地址而不是它們的內(nèi)容。如果把第二句改成List<int> listB = listA;
卷扮,則最后返回結(jié)果就是True
荡澎。
2. list<T>比較的解決方案
那么,如果解決這個問題呢晤锹?下面給出兩種方案:
方案一:不采用Equals()
摩幔,而是自己來實現(xiàn)兩個列表的值的比較。例如:
var eq = listA.Except(listB).Count() == 0 && listB.Except(listA).Count() == 0;
需要指出的是鞭铆,這里示例的比較方法中把兩個list中元素看成無序的或衡,及{1,2,3}
和{1,3,2}
是相等了,如果你定義的兩個list
相等是指元素一對一地相等恐怕上面的方法不能勝任。
另外薇宠,有人指出可以用C#4
中引入的Zip
來實現(xiàn)兩個list
的比較偷办,lista.Count()==listb.Count()&&lista.Zip(listb,Equals).All(a=>a);
,參見stackoverflow
方案二:利用SequenceEqual
澄港,它屬于System.Linq
命名空間椒涯,可用于任何IEnumerable
類。
這里只要將原始例子中的Equals
那句換成listA.SequenceEqual(listB)
即可回梧。如果你的list
里的元素是自定義類型废岂,那么在使用SequenceEqual
的時候還需要另外一個參數(shù)IEqualityComparer
。
下面把上面列子中list
里的元素從int
換成自定義的Point
狱意,那么在用SequencEqual
時需要提供Point
的比較方式湖苞。可以看到我們提供的TestComparer
也就是重寫了Equals
和GetHashCode
兩個函數(shù)详囤,而這兩個函數(shù)是C#
中類層次最高的父類Object
中定義的财骨,所以如果你在定義Point
(他是繼承Object
的)的時候就可以重寫這兩個函數(shù),這樣的話可以直接用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. 總結(jié)C#中的值與引用
上面的問題解決了隆箩,那么,為了更深刻地理解C#
中的值和引用羔杨,下面我們來簡單總結(jié)一下捌臊。
(1) 值類型
所謂值類型,就是指變量即代表值本身兜材。C#
中的基礎(chǔ)數(shù)據(jù)類型(string
除外)理澎、枚舉和結(jié)構(gòu)體都屬于值類型。
值類型都隱式派生自System.ValueType
曙寡,而System.ValueType
又繼承自最高父類System.Object
糠爬,ValueType
的作用是確保其所有派生類型都分配在棧上而不是垃圾回收堆上。
創(chuàng)建和銷毀分配在棧上的數(shù)據(jù)都很快举庶,因為它的生命周期是由定義的作用域決定的秩铆。當結(jié)構(gòu)變量離開定義域時,它就會立即從內(nèi)存中移除灯变。而分配在堆上的數(shù)據(jù)由
.NET
垃圾回收器監(jiān)控。
每個值類型都有默認值(可以用default(type)
查看)捅膘,在定義值類型的變量時如果不賦值就使用(有些編譯器在編譯階段就會檢查并阻止這種情況)不會引起NullReferenceException
異常添祸,而是使用默認值。
值類型的賦值操作寻仗,把一個值類型賦值給另外一個時刃泌,就是對字段成員逐一進行復制。這樣進行多次賦值后,該值會在棧中有多份拷貝耙替。
(2) 引用類型
引用類型變量的值是內(nèi)存地址亚侠,其真實內(nèi)容存儲在該內(nèi)存地址處。C#中的string
俗扇、Array
硝烂、類、接口和委托都是引用類型铜幽,引用類型都隱式繼承自System.Object
滞谢。引用類型都分配在堆上,由GC
去管理他們的創(chuàng)建和注銷除抛。
所有引用類型的默認值都是null
狮杨,不賦值就直接使用會拋出NullReferenceException
異常。
引用類型的賦值操作到忽,就是在內(nèi)存中重定向引用變量的指向橄教。這和C++
中很不一樣,尤其要注意喘漏,引用類型賦值操作會使得多個變量共享同一數(shù)據(jù)塊护蝶,任何一個變量對數(shù)據(jù)進行修改后,其他變量訪問到的都是修改后的數(shù)據(jù)陷遮。
另外滓走,既然值類型的賦值時拷貝一份數(shù)據(jù),引用類型的賦值是直接賦數(shù)據(jù)的內(nèi)存地址帽馋,那么對包含有引用類型的值類型如何處理呢搅方?
假設(shè)我們定義了Rectangle
的結(jié)構(gòu)體中包含了一個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);
}
輸出結(jié)果是:
[6,7,3,4]
[6,7,5,4]
所以姨涡,當值類型包含其他引用類型時,賦值將生成一個引用的副本吧慢。這樣就有兩個獨立的結(jié)構(gòu)涛漂,每個都包含指向內(nèi)存中同一個對象的引用。(淺拷貝)
注:本文大部分內(nèi)容來自《精通C#(第6版)》(PS:怎么寫一行小字啊(-__-)b)