本章對前壹章進行補充,介紹了構造C#方法的細節,探討了方法的各種關鍵字和方法重載的主題,之後介紹數組類型,也介紹了枚舉類型、結構類型,然後詳細介紹了值類型和引用類型之間區別,最後探討了可空數據類型以及?和?運算符。
4.1方法重載和參數修飾符
C#中有四種參數修飾符,分別為:
(無),此時為值傳遞。數據的副本就會被傳入函數,至於到底復制什麽,取決於參數是值類型還是引用類型,前者復制值類型的本身,後者復制的是引用(類似於c++指針)。
out,輸出參數,若參數用out聲明,則調用時也必須加上out。使用前不用賦值,函數退出時,要給參數賦值,否則編譯錯誤。
ref,引用參數,和out類似,區別主要有:
輸出參數(out)無需(不是必須,賦的值也會在後面重新賦值時覆蓋)傳遞前初始化,退出時必須給他賦值;
引用參數(ref)必須在傳遞前初始化,退出時可以(不是必須)改變他的值。
可以看出,以上兩種方式的優點就是,只使用壹次方法就可以獲得多個返回值!而常規方式,只能用return返回壹個值!
註意,即使將壹個值類型的參數用上述兩個修飾符進行聲明,也會改變參數的值!
params,參數數組。可以把可變數量的參數(相同類型)作為單個邏輯參數傳給方法。需要強調的是,這種方式聲明的參數數組,在調用時,可以有兩種方式,壹種是傳入強類型的數組(和用常規數組作為參數時壹致),另壹種是以逗號分割的項列表(這才是特有的方式),例如,下面的方法:
static double calculateaverage(int i,parmas double[] values)
{ //something}
則調用時,可以用如下兩種中任意壹種哦:
double[] b={1,2,3};
calculateaverage(100,b);//第壹種方式
calculateaverage(100,1,2,3);//第二種方式
第二種方式時,編譯器會自動將後三個參數打包成數組,作為double[]參數傳入。若不用params聲明,第二種方式將出錯!
成員重載:
是指壹組名稱相同,參數數量(或類型)不同的成員。
關鍵要確保方法的每壹個版本都有不同的參數組(只是返回類型不同是不夠唯壹的),能夠保證唯壹的條件是:
參數個數不同
參數修飾符不同(out,ref,“無”等)
參數類型不同(int,folat.......)
這種情況需要註意的是,雖然類型不同可以通過編譯,但是若僅僅是參數類型不同(參數個數和修飾符等都無法區別的情況下),如果重載方法的參數類型之間存在著隱式轉換關系(值類型和引用類型都存在此問題),那麽有可能出現調用時候的壹些問題:看如下情形:
01 static void Main(string[] args)
02 {
03 TestArgsType(10);
04 Console.Read();
05 }
06 static void TestArgsType(int args)
07 {
08 Console.WriteLine("int");
09 }
10 static void TestArgsType(uint args)
11 {
12 Console.WriteLine("uint");
13 }
則TestArgsType(10)執行的肯定是第壹重載方法(int版的)的,這也許不是妳想調用的方法,但是CLR就是這麽處理了,因為這兩個類型(uint和int)之間出現隱式轉換關系,或者說是包含關系,這就有可能出現意外的調用結果。另外,如下面的代碼,又將執行哪個方法呢(顯然是long版的)
01 static void Main(string[] args)
02 {
03 int i = 1;
04 TestArgsType(i);
05 Console.Read();
06 }
07 static void TestArgsType(long args)
08 {
09 Console.WriteLine("long");
10 }
11 static void TestArgsType(ulong args)
12 {
13 Console.WriteLine("ulong");
14 }
這裏要引伸出來關於隱式轉換的問題了,總結了壹下,對於上面出現的這種問題,CLR在編譯和運行時(若編譯通過)按如下規則進行處理(值類型和引用類型均適用):
(1)對於引用類型,作為實參時,肯定妳事先都指定為某種類型了,比如:
car mycar=new car();
testargstype(mycar);//testargstype為某個重載方法
對於這種引用類型,可以繼續看步驟(2)。
對於數值類型,有可能人為地指定了實參是某種類型,和上面的類似:
int c=10;
testargstype(c);
這種情況也可以繼續看步驟(2)了。
還存在壹種情況,就是沒有為實參顯式的指定為某種類型,如:
無論如何,最終實參也會被轉換為某種類型,那麽也可以進入步驟(2)了。
(2) 上壹步必然確定了實參的類型,那麽這壹步就是要去找是否有相應的重載方法,有三種情況:
壹種情況就是恰恰有和重載方法形參類型壹致(比如實參確定為int型,而形參也是int型),那麽顯然是可以通過編譯了,而且運行時調用的也是這個方法;
如果沒有找到形參類型完全壹致的那個重載方法,如果存在兼容類型的形參參數也可以啊(也就是上面確定的實參類型可以隱式轉換為重載方法的形參類型,例如int可以隱式轉換為long,也或者實參為子類,形參為父類),這時也可以通過編譯,並在運行時調用這個兼容重載方法;當然還有個問題需要解決的,那就是存在多個重載方法的形參都滿足這種可兼容模式(比如實參int,而重載方法存在long和double兩種兼容類型),那麽到底調用哪個方法呢?基本規則就是在可隱式轉換的那些重載函數中,根據形參按照由整型到浮點數,由取值範圍小到取值範圍大。給出壹個助記方法:
這是壹個由整型到浮點,由取值範圍小到大的順序構成的值類型鏈:
byte-sbyte-short-ushort-int-uint-long-ulong-float-double
對於任意壹個實參,首先在鏈中找到自己的位置, 比如對於實參是int型,先會找到int的位置,然後會按照形參是否為uint-long-ulong-float-double的順序查找,找到的第壹個就是要調用的方法,再比如對於uint型實參,會按照long-ulong-float-double順序查找(uint是無法隱式轉換為int的哦)。
若也不存在兼容類型的,那麽編譯將無法通過哦。
可以看出,若調用重載方法時不顯式的指定參數類型,且重載方法參數存在隱式轉換或繼承關系,那麽有可能導致意外的調用結果,因此,在調用時事先給出實參類型,設計重載方法時盡量不設計有可能導致上述問題的重載方法,應該是壹個好習慣。
4.2數組
數組當然是壹組相同類型的數據點。下界從0開始。
new(聲明數組)後,若不顯式填充,則每項都給予默認值。
總結壹下數組的初始化方法:
(1)逐個填充
int[] a=new int[3];//使用此方法時,必須指定數組大小;這樣才能默認填充這麽多個值,此時,所有數組中值均為0,下面可以對不想是默認的項逐個賦值。
a[0]=1;
a[1]=2;
(2)花括號初始化
以下三種方式均可:
int[] a=new int[]{1,2,3};
int[] a=new int[3]{1,2,3};
int[] a={1,2,3};
也就是說,不需要指定數組大小,因為可以通過花括號的項來推斷;new關鍵字是可選的。
若數組大小指定了,卻和花括號中項數不壹致,反而出錯,因此,推薦第三種,簡單!
在c#數組中,可以定義引用類型的數組,如datatable類型,string類型,甚至是object的數組,這時候,不僅需要對整個數組進行實例化,還需要對其中每壹個項進行實例化,記住,每壹項還需要進行實例化哦。
如:
object[] myobject=new object[2];
myobject[0]=10; //此項為值類型,直接賦值
myobject[1]=new datatime(1988,1,3);//為引用類型,需要實例化
多維數組:
包括了矩形數組和交錯數組。
矩形數組:每壹行的長度都相同的多維數組。
int[,] a =new int[1,2];
交錯數組:
包含壹些內部數組,每壹個都有各自的上界。
int[][] a=new int [5][3];
這些數組的聲明方法和初始化,有興趣的可以查看相關資料,這裏不再贅述。
另外,數組可以作為參數和返回值。
4.3枚舉
枚舉用於創建壹組符號名和已知數字值的類型。
定義方法:
enum emptype
{
manager,
grnut=10,
contractor,
}
註意,最後壹個逗號可以有也可以沒有!默認情況下,第壹個元素若沒有賦值,則被默認是0,若其中任意壹個沒有賦值,則默認是上壹個成員的數值加1,則可以推斷,contractor為11。這些值不壹定連續,也不壹定唯壹,沒有限制。
默認時,枚舉值的存儲類型為int類型,若想更改,則可以使用冒號來設置,C#支持byte,sbyte,short,ushort,int,uint,long,ulong。
使用方法:
enum emptype:byte
{
manager,
grnut=10,
contractor,
}; //可以有結尾分號
枚舉成員的訪問權限隱含為public 。
對於enum的使用,壹般就是直接用點運算符來訪問裏面的符號名,也可以根據符號名,獲取對應的整數值(需要強制轉換),例如:
string thisstr=emptype.manager.Tostring();
byte thisbyte=(byte)emptype.manager;
對於enum,我不經常使用,對它的用法,覺得也比較別扭。
例如可以用new實例化這個枚舉,但似乎也沒什麽用途。
emptype a = new emptype();//這句沒錯
再比如下個用法:
emptype a=emptype.manager;
若要獲取枚舉中的符號名名稱,用a.tostring()即可,若要獲取a對應的數值,必須根據底層存數類型,進行強制轉換
(byte)a,這是由於,雖然emptype中的各個符號對應的是數值,但是並非真的是數值類型,因此必須轉換成為數值。
另外,下面兩個t1和t2竟然是壹個類型~,確實是用的不大習慣。
emptype t1 = emptype .test1;
emptype t2 = new emptype ();
if (t1 == t2)
{
//do something
}
看到這裏,如果對於面向對象有壹定基礎的話,也許會問這個枚舉和靜態類的靜態變量和常量有什麽區別嗎?
public static class Day
{
public const int Sun = 1;//常量哦
public static int Mon = 2;//靜態變量
}
從不變性來看,雖然const數據成員的確是存在的,但其含義卻不是我們所期望的(靜態變量也是如此)。const數據成員只在某個對象生存期內是常量,而對於整個類而言卻是可變的,因為類可以創建多個對象,不同的對象其const數據成員的值可以不同(const數據成員的初始化能在類構造函數的初始化中進行)。怎樣才能建立在整個類中都恒定的常量呢?別指望const數據成員了,應該用類中的枚舉常量來實現。枚舉常量不會占用對象的存儲空間,它們在編譯時被全部求值。枚舉常量的缺點是:它的隱含數據類型是整數,其最大值有限,且不能表示浮點數(如PI=3.14159)。
從直觀性來看,C# 枚舉和常量應用區別之枚舉的定義中加入描述(Description)。
詳細請看:《C# 枚舉和常量應用區別淺析》/art/200908/144474.htm
實際上,枚舉的應用也是比較廣泛的,雖然其他的類型也能實現他的功能,但是在性能等方面,枚舉具有不可替代的作用,有時間也應該深入研究壹下。
4.4結構
結構是可以包含許多數據字段和操作這些字段的成員的用戶自定義類型。結構有許多類似於類的特性,可以看成是輕量級的類類型。結構可以定義構造函數,實現接口,還包含許多屬性、方法、事件以及重載運算符。但是結構無法繼承。
初始化壹個結構體,有兩種方式,壹種是創建變量後,為每壹個公***字段數據賦值。
例如定義壹個結構體:
struct point
{
public int a;
public int b;
public void display()
{
Console.WriteLine("a:{0},b:{1}",a, b);
}
}
則使用時:
point p;
p.a=1;
p.b=2;
p.display();
另外壹種方式是使用new關鍵字來創建變量,他會調用結構的默認構造函數(因此,結構中不允許再定義沒有參數的構造函數,也就是這個系統提供的默認構造函數無法覆蓋),並自動為公***字段賦予默認值。如:
point p=new point();
p.display();
還壹種方式,就是利用自定義的構造函數,這需要在定義結構時顯式進行定義才可使用。
結構體的應用也是十分廣泛的,由於在語法和操作上與類類型有相似之處,這裏在說明兩者區別後,不再詳述結構體,在後面介紹類類型時,這裏應該有更好的理解了。
4.5值類型與引用類型
值類型都隱式派生自system.valuetype,其唯壹目的是重寫了由system.object定義的虛方法來使用基於值而不是基於引用的語法,重寫會改變定義在基類中的虛(也可能是抽象的)方法的實現,事實上,valuetype定義的實例方法和由object定義的完全相同。由於值類型使用基於值的語法、結構,生命期可以預測。
當用賦值運算符(=)將壹種類型賦值給另外壹個時,有兩種情況:
將壹個值類型賦值給另外壹個時,對字段成員逐壹進行復制(對於int這樣簡單類型,唯壹需要復制的成員就是數值,對於結構,如上point,則a,b的值都會被復制到新的結構變量中。)棧上會有這個類型的兩個副本,每個都被獨立操作,壹個的變化,不影響另外壹個。
將壹個引用類型賦值給另外壹個時,在內存中重定向引用變量的指向。兩個引用指向托管堆中的同壹個對象,因此,其中壹個的改變,同時會影響另外壹個。
包含引用類型的值類型:
若在壹個值類型中包含引用類型,比如結構中有壹個引用類型,則將這個結構賦值給另外壹個結構時,將會把結構中值類型進行賦值,而將結構中的引用進行復制,因此,更改結構中的值類型不影響另外壹個結構中對應的值類型,而兩個結構中的實例由於保存的是都是這個對象的引用,改變其中壹個實例的內容,必然影響另外壹個結構中實例的內容。 (非常感謝redjackwong 更正了敘述問題)
這就是所謂的“淺復制”,還有壹種稱為“深復制”,即將內部引用的狀態完全復制到壹個新的對象中去,需要實現ICLoneable結構,之後的篇章中會講到。
按值傳遞和按引用傳遞引用類型:
本章壹開始就說了參數有四種類型,其中就有按值傳遞(無修飾符)和按引用傳遞(ref和out(這兩個的區別前面以後說了,以下不再分開說)),
對於壹個值類型,按值傳遞實參時,方法中對於形參的操作,不會影響實參值,若用引用傳遞,則形參的改變,也會導致實參的變化:
01 static void Main(string[] args)
02 {
03 int a = 1;
04 testint(a);
05 Console.WriteLine(a);//a還是1
06 testint(ref a);
07 Console.WriteLine(a);//a是2了
08 }
09 static void testint( int a)
10 {
11 a = 2;
12 }
13 static void testint(ref int a)
14 {
15 a = 2;
16 }
引用類型,采用值傳遞和引用傳遞,效果就又是壹個樣子,他們都可以改變實參的內容,唯壹不同的就是是否能改變實參的指向(因為實參就是壹個引用)
按值傳遞壹個引用類型(如person)時,如:
static void send(person p)
{
p.age=99;// 這句是有效的
p=new person("lee",99);// 這句是無效的,不會影響到實參的指向
}
按引用傳遞壹個引用類型(如person)時,如:
static void send(ref person p)
{
p.age=99;// 這句是有效的
p=new person("lee",99);// 這句是有效的,實參的指向改變了
}
前者類似於p是壹個常量指針,可以訪問和修改它指向的對象內容,但是不能更改它的指向(new)。
後者類似於可變的指針,可以訪問和修改它指向的對象,也可以更改它的指向,使之重新指向另外壹個對象。
這個區別是在很多書籍中都沒有介紹過的!
再簡單總結壹下壹些遺漏的值類型與引用類型的區別:
前者不能被繼承,後者可以;前者默認構造函數是系統提供的,用戶不能自定義壹個沒有參數的構造函數,後者當然可以。
4.6可空類型
.NET2.0發布後,支持壹種可空類型。它就是可以表示所有基礎類型的值加上null,為了定義壹個可空變量類型,應在底層數據類型中添加問號(?)作為後綴進行聲明。
註意,這種類型只對值類型合法,定義壹個可空引用類型是不合法的(引用類型本身就可以為null!)
定義方法:
int? a=10;
int?[] b=new int?[5];
Nullable<int> c=10;
public int? getsomething()
{
}
註:?後綴記法是創建壹個泛型system.nullable<T>的縮寫。
可以通過hasvalue或!=運算符判斷是否為空。
對於這個可空類型,可以通過value屬性或直接的獲取這個值(可以是空)。
最後,可以使用?運算符,在當獲取的值是null時,將壹個值賦值給這個類型,這相當於壹個判定語句,當為空時賦壹個值,不為空時,不理睬。如:
int? a=dr.getsomtthing()?100
如果dr.getsomtthing()返回壹個空值null,則將100賦值給a,否則將dr.getsomtthing()的返回值賦值給a。
類似如下語句:
1 int? a;
2 if (dr.getsomtthing() is Nullable)//之前少了個括號(,再次多謝 redjackwong 了!
3 {
4 a=dr.getsomtthing();
5 }
6 else
7 {
8 a=100;
9 }
從性能上看,在C#中,枚舉的真正強大之處是它們在後臺會實例化為派生於基類System.Enum的結構。這表示可以對它們調用方法,執行有用的任務。註意因為.NET Framework的執行方式,在語法上把枚舉當做結構是不會有性能損失的。實際上,壹旦代碼編譯好,枚舉就成為基本類型,與int和float類似。
testargstype(10);
這種情況,就存在將這個給定的數值依照某種轉換規則進行轉換的過程,規則如下:
如果整數沒有後綴,則其類型為以下類型中可表示其值的第壹個類型:int、uint、long 、ulong 。還可根據以下規則使用後綴指定文字類型:
如果使用 L 或 l,那麽根據整數的大小,可以判斷出其類型為 long 還是 ulong 。(
註意也可用小寫字母“l”作後綴。但是,因為字母“l”容易與數字“1”混淆,會生成編譯器警告。為清楚起見,請使用“L”。)
如果使用 U 或 u ,那麽根據整數的大小,可以判斷出其類型為 uint 還是 ulong 。
如果使用 UL、ul、Ul、uL、LU、lu、Lu 或 lU,則整數的類型為 ulong 。