一、枚舉類型
枚舉類型也成為枚舉,它是一種創建數值類型的機制,這種值類型的可能取值是預定義的,而對于其中的每個可能取值,都有一個有意義的名稱。這看似簡單,但實際上枚舉類型功能強大。通過定義一組有效值,而程序員能夠理解表層含義。這樣,代碼的含義將不言自明,也不再模糊。
要定義枚舉,必須在標識符前面加上關鍵字enum,然后在枚舉體內定義一組有效值,并用逗號分隔它們。用作值名稱的標識符必須遵循的規則與變量標識符相同。
ps:枚舉值
最后一個枚舉值后面的逗號是可選的,但最好不要省略,這樣以后添加枚舉值將更容易。
ps:多名稱值
可以有多個名稱對應于同一個數值,這在有多個名稱表示同一個概念時很有用。為讓多個名稱對應于同一個數值,只需添加新名稱,并將其設置成與另一個名稱相等,如下所示。
枚舉是一組只能為數值的命名常量,因此最好讓每個名稱對應于不同的數值。定義枚舉時,編譯器默認將第一個枚舉值設置為整數零,其他值則依次加1。
ps:零值(zero value)
通常最好在枚舉中包含對應于0的名稱None。
枚舉支持大多數可用于整數值的運算符,但并非所有這些運算符對枚舉來說都有意義。對于枚舉,執行得最多的操作是相等和不等測試。由于枚舉屬于值類型,因此也可聲明可以為null的枚舉。
ps:枚舉的底層類型
枚舉包含的所有值都必須是同一種數據類型的,這種數據類型稱為底層類型(underlying type)。默認情況下,枚舉的底層類型為 int,但是也可以使用任何預定義的整數類型:byte、short、int、long、sbyte、ushort、uint和ulong。
1.1 位標志枚舉
通過使用位標志枚舉(flags enumeration),可組合其中的值。使用位標志枚舉時,可使用邏輯運算OR創建新的組合值。
為讓位標志枚舉的值能夠組合,所有值都必須是2的冪。這是因為組合多個值時,必須能夠確定結果為哪個離散值。因此,定義位標志枚舉時,必須指定名稱對應的值。
ps:Flags特性
常規枚舉和位標志枚舉之間的另一個差別是,后者需要使用Flags特性,它指定有關枚舉的額外元數據。
Flags特性還改變了組合得到的枚舉值的字符串表示(方法ToString返回的結果)。
雖然并非必須使用Flags特性,但是強烈建議這樣做,因為向編譯器和其他程序員清晰地表明了你的意圖。
在簡單枚舉中,可以讓名稱None或最常見的默認名稱對應于0,但是位標志枚舉與此不同,它要求0對應于名稱None,這個值意味著所有標志都未設置。
二、結構
在需要簡單的用戶定義的類型時,可將結構作為類的輕量級替代品。結構類似于類,可包含的成員類型與類相同,但是屬于值類型而不是引用類型。結構與類的不同之處如下:
- 接口不支持繼承。結構隱式地繼承System.ValueType,而后者繼承System.Object。就像類一樣,結構也可繼承接口
- 結構隱式地被密封,這意味著您不能繼承結構
- 結構不能有析構函數,不能聲明默認構造函數,也不能在結構體內初始化實例字段。如果結構提供了構造函數,就必須在其中給所有字段賦值
ps:基類庫中的結構
除 string 和 object 外,所有基本數據類型都被實現為結構。.NET Framework提供了200多個公有結構,下面是一些常用的結構:
System.DateTime
System.DateTimeOffset
System.Guid
System.TimeSpan
System.Drawing.Color
System.Drawing.Point
System.Drawing.Rectangle
System.Drawing.Size
在C#中,結構的聲明方法與類相同,只是需要使用關鍵字struct代替關鍵字class
2.1 方法
就像類可以定義方法一樣,結構也可以。這些方法要么是靜態方法,要么是實例方法,但是結構較常使用靜態共有方法和私有實例方法
運算符重載
由于結構是用戶定義的值類型,因此如果變量的類型為您定義的結構,就不能將大多數常見的運算符用于它。這是一種重大缺陷,所幸的是,C #通過運算符重載提供了一種解決這個問題的方式。
如果將運算符視為名稱特殊的方法,那么運算符重載就是一種特殊的方法重載。要聲明重載的運算符,可定義一個public static方法,其名稱為關鍵字operator和要重載的運算符的符號。另外,至少要有一個參數的類型與重載運算符所屬的類型相同。下表列出了可重載的運算符。
類別 | 運算符 |
---|---|
單目 | + - ! ~ ++ true false |
乘除 | * / % |
加減 | + - |
移位 | << >> |
關系 | < > <= >= |
邏輯 | & 丨 ^ |
相等性 | == != |
ps:語言互操作性
并非所有.NET 語言都支持運算符重載,因此如果創建的類要在其他語言中使用,它們應符合CLS,并提供與定義的重載運算符對應的替代品。
通常,應成組地重載運算符。例如,如果重載了相等運算符,也應重載不等運算符。對于這個指導原則,唯一的例外是求補運算符(~)和邏輯非運算符(!)。下表列出了應同時重載的成組運算符。
轉換運算符
在用戶定義的結構中,可重載運算符以便能夠對定義的數據執行常見的運算,同樣,也可創建重載的轉換運算符,以影響強制轉換和轉換過程。同樣,如果將轉換和強制轉換視為名稱特殊的函數,則轉換重載也是一種特殊的方法重載。
ps:顯示轉換和隱式轉換
隱式轉換是屬于擴大(widening)轉換,因為原始值不會因轉換而丟失數據。顯式轉換屬于縮小(Narrowing)轉換,因為原始值可能因轉換而丟失數據。
定義自己的轉換運算符時,應牢記這些行為。如果定義的轉換可能丟失數據,應將其定義為顯式轉換;如果定義的轉換是安全的,即不會丟失數據,就應將其定義為隱式轉換。
內置數據類型支持隱式轉換和顯式轉換,其中隱式轉換不需要特殊語法,但顯式轉換需要。對于自己定義的類型,可重載這些顯式轉換和隱式轉換,方法是聲明自己的轉換運算符,其規則與聲明運算符重載類似。
要聲明轉換運算符,可定義一個 public static方法,其名稱為關鍵字 operator,返回類型為要轉換到的類型。轉換運算符只接受一個參數,那就是要轉換的類型。
如果要聲明隱式轉換,就可在關鍵字operator前面加上implicit;否則,加上關鍵字explicit。有時結合使用轉換運算符和運算符重載,以減少要定義的運算符重載。
2.2 構造和初始化
就像必須給對象指定初始狀態一樣,結構也如此。對于對象,這是通過構造函數完成的,但結構是值類型,無需調用構造函數就可以創建結構變量。例如,可像下面這樣創建一個NumberStruct變量:
NumberStruct ns1;
上述代碼創建了一個新變量,但是字段處于未初始化狀態;如果此時試圖訪問字段,將發生編譯錯誤。通過調用構造函數,可確保字段被初始化了。
結構初始化的另一個方面是,不能將未完全初始化的結構變量賦給另一個結構變量,即不能將這樣的變量放在賦值運算符右邊。這意味著下面的代碼合法:
NumberStruct ns1 = new NumberStruct();
NumberStruct ns2 = ns1;
但下面的代碼非法:
NumberStruct ns1;
NumberStruct ns2 = ns1;
ps:自定義默認構造函數
不同于類,結構不能有自定義的默認構造函數,也不能在構造函數外面初始化結構。因此,創建結構時,所有字段都被初始化為零值。
可以提供重載的構造函數,并利用構造函數串接。然而,當提供重載的構造函數時,必須初始化所有字段,這可在該構造函數中顯式地進行,也可通過串接構造函數隱式地完成。
有趣的是,如果未顯式初始化的字段都可接受零值,就可串接默認構造函數。
struct NumberStruct
{
public int Value;
}
class NumberClass
{
public int value = 0;
}
class Test
{
static void Main()
{
NumberStruct ns1 = new NumberStruct ();
NumberStruct ns2 = ns1;
ns2.Value = 42;
NumberClass nc1 = new NumberClass ();
NumberClass nc2 = nc1;
nc2.value = 42;
Console.WriteLine ("Struct:{0),{1}", ns1.Value, ns2.Value);
Console.WriteLine ("Class:{0),{1}", nc1.Value, nc2.Value);
}
}
由于ns1和ns2都是值類型NumberStruct,它們有各自的存儲空間,因此給ns2.Number賦值不會影響ns1.Number的值。然而,由于nc1和nc2都是引用類型,并且指向同一個存儲位置,因此給nc2.Number賦值將影響nc1.Number的值。
ps:使用屬性還是公有字段
對于結構應使用屬性還是公有字段存在一些爭議。有些人認為總是應該使用屬性,即使是在結構這樣的簡單類型中;而有些人認為,在結構中使用公有字段是能夠接受的。
雖然使用公有字段更容易,但是這導致值類型是可以修改的,而通常不希望這樣。定義自己的結構時,別忘了它們是值類型,應像字符串一樣是不可修改的。為此,應提供可用于設置私有字段的構造函數,并提供只讀屬性用于獲取私有字段的值。
附:C#中一些易混淆概念——構造函數、this關鍵字、部分類、枚舉
1.構造函數
我們先創建一個類,如下面的代碼:
class Program
{
static void Main(string[] args)
{
}
}
//創建一個Person類
class Person
{
}
然后生成代碼。
我們使用.NET Reflector反編譯該程序集。會發現該類一被編譯,CLR會自動的為該類創建一個默認的構造函數。如下圖:
所以在創建該對象的時候,會默認的為該類生成一個無參數的空方法體的構造函數。如果我們不顯式的寫明構造函數,CLR會為我們調用默認的構造函數。
class Person
{
//聲明有實現的構造函數
public Person()
{
Console.WriteLine("我是超人!");
}
}
再次反編譯該程序集,會發現添加的構造函數覆蓋了C#編譯器默認為該類生成的構造函數,如下圖:
所以,當程序員手動添加了任意類型的構造函數,C#編譯器就不會為該類添加默認的構造函數。
構造函數的特點:
①訪問修飾符一般是Public②沒有返回值,方法名與類名稱一致;
2.This關鍵字的作用
①this關鍵字代表當前對象,當前運行在內存中的那一個對象。我們添加如下的代碼:
private int nAge;
public int NAge
{
get { return nAge; }
set { nAge = value; }
}
//聲明有實現的構造函數
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}
這時候我們反編譯該程序集,會看到如下結果:
可以看到this關鍵字代替的就是當前的Person對象。
②this關鍵字后面跟“:”符號,可以調用其它的構造函數
我們再添加如下的代碼:
#region 對象的構造函數
//聲明有實現的構造函數
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}
public Person(int nAge)
{
Console.WriteLine("超人的年齡{0}", nAge);
}
//使用this關鍵字調用了第二個一個參數的構造函數
public Person(int nAge, string strName)
: this(1)
{
Console.WriteLine("我是叫{0}的超人,年齡{1}", strName, nAge);
}
#endregion
我們創建該對象看看是否調用成功。在Main函數中添加如下代碼:
Person p = new Person(10,"強子");
我們運行代碼,看到的打印結果如下:
由結果我們可以分析出,當含有兩個默認參數的對象創建的時候應該先調用了一個參數的構造函數對對象進行初始化,然后有調用了含有兩個參數的構造函數對對象進行初始化。
那么到底是不是這個樣子呢?看下邊的調試過程:
通過上面的調試過程我們會發現,當構造函數使用this關鍵字調用其它的構造函數時,首先調用的是該調用的構造函數,在調用被調用的構造函數,先執行被調用的構造函數,在執行直接調用的構造函數。
為什么要這個順序執行?因為我們默認的傳值是10,我們需要打印的超人的年齡是“10”,如果先執行直接調用的構造函數,就會被被調用構造函數覆蓋。
3.部分類
在同一命名空間下可以使用partial關鍵字聲明相同名稱的類(同一命名空間下默認不允許出現相同的類名稱),叫做部分類或者伙伴類。
如下圖,當在同一命名空間下聲明相同名稱的類,編譯器報錯:
當我們使用Partial關鍵字時,可以順利編譯通過,如下圖:
分別添加如下的代碼:
partial class Person
{
private string strAddress;
public string StrAddress
{
get { return strAddress; }
set { strAddress = value; }
}
private string strNumber;
public string StrNumber
{
get { return strNumber; }
set { strNumber = value; }
}
public void Run()
{
}
}
partial class Person
{
#region 對象屬性
private int nAge;
public int NAge
{
get { return nAge; }
set { nAge = value; }
}
private string strName;
public string StrName
{
get { return strName; }
set { strName = value; }
}
#endregion
#region 對象的構造函數
//聲明有實現的構造函數
public Person()
{
this.NAge = 100;
Console.WriteLine("我是超人!");
}
public Person(int nAge)
{
Console.WriteLine("超人的年齡{0}", nAge);
}
public Person(int nAge, string strName)
: this(1)
{
Console.WriteLine("我是叫{0}的超人,年齡{1}", strName, nAge);
}
#endregion
public void Sing()
{
}
}
我們再次反編譯該程序集,會發現如下的結果:
我們會發現使用Partial關鍵字的兩個同名類,被編譯成了同一個類。
所以部分類的特點:
①必須在同一個命名空間下的使用Partial關鍵字的同名類
②部分類其實就是一個類,C#編譯器會把它們編譯成一個類
③在一個伙伴類中定義的變量可以在另一個伙伴類中訪問(因為他們就是一個類)。
4.Const關鍵字和Readonly關鍵字的區別
1)const關鍵字
在Main函數中添加如下的代碼:
const string strName = "強子";
Console.WriteLine("我的名字叫{0}",strName);
編譯過后,我反編譯該程序集發現如下結果:
發現定義的常量并沒有出現在反編譯的代碼中,而且使用Const常量的地方被常量代替了。
2)readonly關鍵字
添加如下代碼:
class cat
{
readonly string reOnlyName = "強子";
public cat()
{
Console.WriteLine(reOnlyName);
}
}
生成后反編譯該程序集發現,如下結果:
我們發現被readonly修飾的變量并沒有被賦值,這是什么回事呢?我們點擊cat類的構造函數時,看到如下結果:
我們發現被readonly修飾的變量是在被調用的時候賦值的。
那么被readonly修飾的變量的是就是不可變的么?當然不是,由反編譯的結果我們知道,readonly修飾的變量是在被調用的時候在構造函數中被賦值的,那么我們可以在構造函數中修改readonly的默認值
添加如下代碼:
class cat
{
readonly string reOnlyName = "強子";
public cat()
{
this.reOnlyName = "子強";
Console.WriteLine(reOnlyName);
}
}
在Main()函數中添加如下的代碼:
cat ct = new cat();
運行結果如下:
說明我們成功在構造函數中修改了readonly變量的值。
readonly和const的區別:
const常量在聲明的時候就必須賦初始值,這樣聲明變量可以提高程序的運行效率。而readonly變量聲明時可以不賦初始值,但一定要早構造函數中賦初始值。
也就是說,const變量在編譯的時候就要確定常量的值,而readonly是在運行的時候確定該變量的值的。
5.解析枚舉
枚舉的級別和類的級別一樣,可以自定義數據類型,可以在枚舉名稱后使用“:”來指明枚舉類型。看如下代碼:
//定義一個方向的枚舉類型,枚舉成員使用","分割
enum Direction:string
{
east,
west,
south,
north
}
編譯會報錯,錯誤信息如下:
由此我們可以知道枚舉的數據類型是值類型。
因為枚舉是數據類型,所以可以直接聲明訪問,如下代碼:
class Program
{
static void Main(string[] args)
{
//枚舉是數據類型可以直接聲明
Direction dr = Direction.east;
Console.WriteLine(dr);
Console.ReadKey();
}
}
//定義一個方向的枚舉類型,枚舉成員使用","分割
enum Direction
{
east,
west,
south,
north
}
也可以這樣訪問枚舉類型
class Program
{
static void Main(string[] args)
{
//枚舉是數據類型可以直接聲明
// Direction dr = Direction.east;
Person p=new Person();
//直接調用枚舉變量
p.dir = Direction.east;
Console.WriteLine(p.dir);
Console.ReadKey();
}
}
class Person
{
private string strName;
//直接聲明枚舉變量
public Direction dir;
}
每一個枚舉成員都對應了一個整型的數值,這個數值默認從0開始遞增,可以通過強制轉換獲取該枚舉所代表的值。可以通過如下的代碼訪問:
Direction dr = Direction.east;
int i = (int)dr;
我們還可以手動為每一個枚舉成員賦值,代表的是整型數值,賦值后該枚舉成員所代表的值就是所賦的值。如下代碼:
enum Direction
{
east=1,
west=0,
south=2,
north=3
}
將字符串轉換成枚舉
string strDir = "east";
//將字符串轉換成枚舉類型
Direction d1=(Direction)Enum.Parse(typeof(Direction),strDir);
//轉換的時候忽略大小寫
Direction d2 = (Direction)Enum.Parse(typeof(Direction), strDir,true);
最后我們再來探究一個空指針異常的問題
首先我們先聲明一個Dog類:
class Dog
{
private int nAge;
public int NAge
{
get { return nAge; }
set { nAge = value; }
}
private string strName;
public string StrName
{
get { return strName; }
set { strName = value; }
}
}
在Main()函數中我們這樣調用
Dog d = null;
d.StrName = "旺旺";
結果會報錯,如下圖
我們已經為屬性,封裝字段了,但是為什么沒有辦法給字段賦值呢?我們就來探究一下這個問題。
當我們實例化Dog對象,即
Dog d = new Dog();
.NET Framwork做了什么工作呢?如下圖:
那為什么會報錯呢,原因如下圖: