C#簡介
.NET Framework是Microsoft為開發應用程式而建立的一個具有革命意義的平臺,它有執行在其他作業系統上的版本
.NET Framework的設計方式確保它可以用於各種語言,包括本書介紹的C#語言,以及C++、Visual Basic、JScript等
.NET Framework主要包含一個龐大的程式碼庫,可以在客戶語言中透過物件導向程式設計技術(OOP)來使用這些程式碼。這個庫分為多個不同的模組,這樣就可以根據希望得到的結果來選擇使用其中的各個部分。例如,一個模組包含Windows應用程式的構件,另一個模組包含網路程式設計的程式碼塊,還有一個模組包含Web開發的程式碼塊。一些模組還分為更具體的子模組,例如,在Web開發模組中,有用於建立Web服務的子模組
其目的是,不同作業系統可以根據各自的特性,支援其中的部分 或全部模組
.NET Framework還包含.NET公共語言執行庫 (Common Language Runtime,CLR),它負責管理用.NET庫開發的所有應用程式的執行
在.NET Framework下,編譯程式碼的過程有所不同,此過程包括兩個階段
-
把程式碼編譯為通用中間語言(Common Intermediate Language)CIL程式碼,這些程式碼並非專門用於任何一種作業系統
-
(Just-In-Time)JIT編譯器把CIL編譯為專用於OS和目標機器結構的本機程式碼,這樣OS才能執行應用程式。JIT的名稱反映了CIL程式碼僅在需要時才編譯的事實。這種編譯可以在應用程式的執行過程中動態發生,編譯過程會在後臺自動進行
目前有幾種JIT編譯器,每種編譯器都用於不同的結構,CIL會使用合適的JIT建立所需的本機程式碼
程式集
編譯應用程式時,所建立的CIL程式碼儲存在一個程式集中、程式集包含可執行應用程式檔案(.exe)和其他應用程式使用的庫(.dll)
除CIL外,程式集還包含後設資料(程式集中包含的資料的資訊)和可選的資源(CIL使用的其他資料,如檔案和圖片)。元資訊允許程式集是完全自描述的。不需要其他資訊就可以使用程式集
不必把執行應用程式需要的所有資訊都安裝到一個地方。可以編寫一些程式碼來執行多個應用程式所要求的任務。此時通常把這些可重用的程式碼放在所有應用程式都可以訪問的地方。在.NET Framework中 , 這個地方 是全域性程式集快取(Global Assembly Cache,GAC),把程式碼放在這個快取中是很簡單的,只需把包含程式碼的程式集放在包含該快取的目錄中即可
託管程式碼
在將程式碼編譯為CIL,再用JIT編譯器將它編譯為本機程式碼後, CLR的任務尚未全部完成,還需要管理正在執行的用.NET Framework編寫的程式碼(這個執行程式碼的階段稱為執行時)
CLR管理著應用程式,其方式是管理記憶體、處理安全性以及允許進行跨語言除錯等。相反,不受CLR控制執行的應用程式屬於非託管型別,某些語言(如C++)可以用於編寫此類應用程式,例如,訪問作業系統的底層功能。但是在C#中,只能編寫在託管環境下執行的程式碼。我們將使用CLR的託管功能,讓.NET處理與作業系統的任何互動
垃圾回收
託管程式碼最重要的一個功能是垃圾回收
這種.NET方法可以確保應用程式不再使用某些記憶體時,就會完全釋放這些記憶體。.NET垃圾回收會定期檢查計算機記憶體,從中刪除不再需要的內容。執行垃圾回收的時間並不固定,可能一秒鐘內會進行數千次的檢查,也可能幾秒鐘才檢查一次,不過一定會進行檢查
[!info]
因為在不可預知的時間執行這項工作,所以在設計應用程式時,必須留意這一點。需要許多記憶體才能執行的程式碼應自行完成清理工作,而不是坐等垃圾回收
建立.NET應用程式的所需步驟:
- 使用某種.NET相容語言(如C#)編寫應用程式程式碼
- 把程式碼編譯為CIL,儲存在程式集中
- 在執行程式碼時(如果這是一個可執行檔案就自動執行,或者在其他程式碼使用它時執行),首先必須使用JIT編譯器將程式集編譯為本機程式碼
- 在託管的CLR環境下執行本機程式碼,以及其他應用程式或程序
C#是型別安全的語言,在型別之間轉換時,必須遵守嚴格的規則。執行相同的任務時,用C#編寫的程式碼通常比用C++編寫的程式碼長。但C#程式碼更健壯,除錯起來也比較簡單,.NET始終可以隨時跟蹤資料的型別
.NET Framework沒有限制應用程式的型別。C#使用的是.NET Framework,所以也沒有限制應用程式的型別
變數和表示式
C#程式碼的外觀和操作方式與cpp和Java非常類似
- C#不考慮程式碼中的空白字元,C#程式碼由一系列語句組成,每條語句都用分號結束
- C#是塊結構語言,塊使用花括號界定,花括號不需要附帶分號。程式碼塊可以巢狀
- C#程式碼區分大小寫
可以使用#region
和#endregion
關鍵字(以#開頭實際上是預處理指令,並不是關鍵字)來定義要展開和摺疊的程式碼區域的開頭和結尾
#region /*註釋*/
//程式碼塊
#endregion
整數型別
//介於–128和127之間的整數
sbyte System.SByte
//介於0和255之間的整數
byte System.Byte
//介於–32 768和32 767之間的整數
short System.Int16
//介於0和65 535之間的整數
ushort System.UInt16
//介於–2 147 483 648和2 147 483 647之間的整數
int System.Int32
//介於0和4 294 967 295之間的整數
uint System.UInt32
//介於–9 223 372 036 854 775 808和9 223 372 036 854 775 807之間的整數
long System.Int64
//介於0和18 446 744 073 709 551 615 之間的整數
ulong System.UInt64
這些型別中的每一種都利用了.NET Framework中定義的標準型別,使用標準型別可以在語言之間互動操作,u是unsigned的縮寫
浮點型別
前兩種可以用+/–m×2^e的形式儲存浮點數,m和e的值因型別而異。decimal
使用另一種形式:+/– m×10^e
//m:0~2^24,e:-149~104
float System.Single
//m:0~2^53,e:-1075~970
double System.Double
//m:0~2^96,e:-28-0
decimal System.Decimal
文字和布林型別
//1個Unicode字元,儲存0~65 535之間的整數
char System.Char
//字串,字元數量沒有上限
string System.String
//布林值
bool System.Boolean
變數命名規則
- 首字元必須是字母、下劃線或@
- 其後的字元可以是字母、下劃線或數字
字面值跳脫字元
\0 //null 0x0000
\a //警告蜂鳴 0x0007
\b //退格 0x0008
\f //換頁 0x000C
\n //換行 0x000A
\r //回車 0x000D
\t //水平製表符 0x0009
\v //垂直製表符 0x000B
//可以使用\u後跟一個4位16進位制值來使用對應的Unicode跳脫字元
\u000D
也可以一字不變地指定字串,即兩個雙引號之間的所有字元都包含在字串中,包括行末字元和原本需要轉義的字元
Console.WriteLine("Verbatim string literal:
item 1");//error
//開頭使用@,一字不變地指定字串,無需使用跳脫字元
Console.WriteLine(@"Verbatim string literal:
item 1");
字串是引用型別,可賦予null
值,表示字串變數不引用字串
表示式
把運算元(變數和字面值)與運算子組合起來,就可以建立表示式,它是計算的基本構件
var1 = +var2//var1的值等於var2的值
var1 += var2//var1的值等於var1加var2,不會把負值變為正數
var1 = -var2//var1的值等於var2乘以-1
var1 =- var2//var1的值等於var1減var2
注意區分它們,前者是一元運算子,結合的是運算元
class Entrance //用數學運算子處理變數
{
static void Main(string[] args)
{
double firstNumber, secondNumber; string userName;
Console.WriteLine("Enter your name:");
userName = Console.ReadLine();
Console.WriteLine($"Welcome {userName}!");
Console.WriteLine("Now give me a number:");
//Readline得到的是字串,需要顯式轉換
firstNumber = Convert.ToDouble(Console.ReadLine());
Console.WriteLine("Now give me another number:");
secondNumber = Convert.ToDouble(Console.ReadLine());
Console.WriteLine($"The sum of {firstNumber} and {secondNumber} is " + $"{firstNumber + secondNumber}.");
Console.WriteLine($"The result of subtracting {secondNumber} from " + $"{firstNumber} is {firstNumber - secondNumber}.");
Console.WriteLine($"The product of {firstNumber} and {secondNumber} " + $"is {firstNumber * secondNumber}.");
Console.WriteLine($"The result of dividing {firstNumber} by " + $"{secondNumber} is {firstNumber / secondNumber}.");
Console.WriteLine($"The remainder after dividing {firstNumber} by " + $"{secondNumber} is {firstNumber % secondNumber}.");
Console.ReadKey();
}
}
[!tip]
和+運算子一樣,+=運算子也可以用於字串
運算子優先順序
由高到底:
- ++、--(字首),+、-(一元)
- *、/、%
- +、-
- <<、>>
- <、>、<=、>=
- ==、!=
- &
- ^
- |
- &&
- ||
- =、*=、/=、%=、+=、-=
- ++、--(字尾)
括號可用於重寫優先順序
名稱空間
名稱空間的主要目的是避免命名衝突,並提供一種組織程式碼的方式,使得程式碼更加清晰和易於管理
名稱空間可以巢狀在其他名稱空間中,形成一個層次結構
預設情況下,C#程式碼包含在全域性名稱空間中。這意味著對於包含在這段程式碼中的項,全域性名稱空間中的其他程式碼只需透過名稱進行引用就可以訪問它們
可以使用namespace
關鍵字為花括號中的程式碼塊顯式定義名稱空間,如果在該名稱空間程式碼的外部使用該名稱空間中的名稱,就必須寫出該名稱空間中的限定名稱
如果一個名稱空間中的程式碼需要使用在另一個名稱空間中定義的名稱,就必須包括對該名稱空間的引用。限定名稱在不同的名稱空間級別之間使用點字元(.)
using
語句本身不能訪問另一個名稱空間,除非名稱空間中的程式碼以某種方式連結到專案上,或者程式碼是在該專案的原始檔中定義的,或者是在連結到該專案的其他程式碼中定義的,否則就不能訪問其中包含的名稱
如果包含名稱空間的程式碼連結到專案上,那麼無論是否使用using
,都可以訪問其中包含的名稱。using
語句便於我們訪問這些名稱,減少程式碼量,以及提高可讀性
[!info]
C#6新增了using static
關鍵字,允許把靜態成員直接包含到C#程式的作用域中
把using static System.Console
新增到名稱空間列表中時,訪問WriteLine()
方法就不再需要在前面加上靜態類名
流程控制
19世紀中葉的英國數學家喬治●布林為布林邏輯奠定了基礎
布林賦值運算子可以把布林比較與賦值組合起來,與數學賦值運算子相同
var1 &= var2//var1的值是var1&var2的結果
var1 |= var2//var1的值是var1|var2的結果
var1 ^= var2//var1的值是var1 ^ var2的結果
[!quote]
多數流程控制語句在cpp中已學習,無需筆記
switch
語句的基本結構如下:
switch (expression)
{
case value1:
//當expression等於value1時執行的程式碼
break;
case value2:
//當expression等於value2時執行的程式碼
break;
//可以有多個case語句
default:
//如果expression的值與任何case都不匹配,則執行default部分的程式碼
break;
}
[!caution]
switch
語句在c++中執行完一個case
語句可以繼續執行其他case
語句,直到遇到break
但C#中不行,在執行完 一個case
塊後,再執行第二個case
語句是非法的
也可以使用return
語句,中斷當前函式的執行,不僅是中斷switch
結構的執行。也可以使用goto
語句,因為case
語句實際上是在C#程式碼中定義的標籤:goto case:...
這些條件也適用於default語句。default
語句不一定要放在比較操作列表的最後,還可以把它和case
語句放在一起。用break
或return
新增一個斷點,可確保在任何情況下,該結構都有一條有效的執行路徑
using static System.Console;
using System;
class Test
{
static void Main(string[] args)
{
const string myName = "god";
const string niceName = "pjl";
const string sillyName = "xwj";
string name; WriteLine("What is your name?");
name = ReadLine();
switch (name.ToLower())
{
case myName:
WriteLine("You have the same name as me!");
break;
case niceName:
WriteLine("My, what a nice name you have!");
break;
case sillyName:
WriteLine("That's a very silly name.");
break;
}
WriteLine($"Hello {name}!");
}
}
變數的更多內容
[!important] 隱式轉換規則
任何型別A,只要其取值範圍完全包含在型別B的取值範圍內,就可以隱式轉換為型別B如果型別A中的值在型別B的取值範圍內,也可以轉換該值,但必須使用顯式轉換
顯式轉換
//顯式型別轉換,彼此之間幾乎沒有什麼關係的型別或根本沒有關係的型別不能進行強制轉換
(destinationType)sourceVar
當使用checked
上下文時,如果整數運算的結果超出了該整數型別的表示範圍,則會引發一個OverflowException
異常。這通常用於確保算術運算不會導致資料丟失或錯誤的結果
設定溢位檢查上下文:
int a = 281;
byte b;//byte表示範圍:0~255
b = (byte)a;//系統無視轉換造成的資料丟失或錯誤
b = checked((byte)a);//會引發一個OverflowException異常
uncecked
則表示不檢查,不會引發異常
可以配置應用程式,讓這種型別的表示式都和包含checked
關鍵字一樣,在vistual studio2022中的Solution Exploer開啟Properties,選擇Build中的Advanced,勾選Check for arithmetic overflow
此後只要不顯示使用unchecked
都會預設檢查整數型別的算術運算結果是否溢位
使用Convert命令進行顯式轉換
使用ToDouble()
把Number
字串轉換為double
值,將引發異常
為成功執行此類轉換,所提供的字串必須是數值的有效表達方式,該數還必須是不會溢位的數
數值的有效表達方式是:首先是一個可選符號(+/-),然後是0位或多位數字,一個可選的句點(.)後跟1位或多位數字,還有一個可選的e/E,後跟一個可選符號和1位或多位數字,除了還可能有空格(在這個序列之前或之後),不能有其他字元
利用這些可選的額外資料,可將–1.2451e–24
這樣複雜的字串識別為數值
對於此類轉換,總是會進行溢位檢查,unchecked
關鍵字和專案屬性設定不起作用
//轉換示例
using System;
using static System.Console;
using static System.Convert;
class Test{
static void Main(string[] args)
{
short shortResult, shortVal = 4;
int integerVal = 67; long longResult;
float floatVal = 10.5F;
double doubleResult, doubleVal = 99.999;
string stringResult, stringVal = "17";
bool boolVal = true;
WriteLine("Variable Conversion Examples\n");
//float和short相乘,double可以容納它們,因此隱式轉換
doubleResult = floatVal * shortVal;
WriteLine($"Implicit, -> double: {floatVal} * {shortVal} -> {doubleResult}");
//float顯式轉換為short,會截斷小數部分
shortResult = (short)floatVal;
WriteLine($"Explicit, -> short: {floatVal} -> {shortResult}");
//Convert.string將bool和double型別顯式轉換為字串並拼接
stringResult = Convert.ToString(boolVal) + Convert.ToString(doubleVal);
WriteLine($"Explicit, -> string: \"{boolVal}\" + \" {doubleVal}\" -> " + $"{stringResult}");
//string顯式轉換為long,與int相加,自然long
longResult = integerVal + ToInt64(stringVal);
WriteLine($"Mixed, -> long: {integerVal} + {stringVal} - > {longResult}"); ReadKey();
}
}
複雜的變數型別
列舉enum
列舉是值型別,列舉使用一個基本型別來儲存,列舉型別可取的每個值都儲存為該基本型別的一個值,預設情況下為int
,可使用enum 列舉名 : 型別名
來指定該列舉的底層型別
enum Days{
Sunday,//0
Monday,//1
Tuesday,//2,以此類推
Wednesday,
Thursday,
Friday,
Saturday
}
class Test{
static void Main(){
//使用列舉
Days today = Days.Monday;
//輸出列舉的值(整數值)
Console.WriteLine((int)today); //輸出1
//輸出列舉的名稱
Console.WriteLine(today); //輸出Monday
//顯式地將整數轉換為列舉型別
Days day = (Days)2;
Console.WriteLine(day); //輸出Tuesday
// 列舉型別的比較
if (today == Days.Monday)
Console.WriteLine("Today is Monday.");
}
}
列舉的基本型別可以是byte、sbyte、short、ushort、int、uint、 long
和ulong
-
預設情況下,每個值都會根據定義的順序被自動賦予對應的基本型別值。可以使用賦值運算子來指定每個列舉的實際值
-
可以使用一個值作為另一個列舉的基礎值,為多個列舉指定相同的值
-
未賦值的任何值都會自動獲得一個初始值,該值比上一個明確宣告的值大1
結構struct
結構是值型別,可以組合多個資料成員到一個單一的型別中,通常用於表示小型的資料集合
struct Point
{
public int X; //公共欄位
public int Y; //公共欄位
//結構可以包含方法
public void Move(int deltaX, int deltaY)
{
X += deltaX;
Y += deltaY;
}
}
class Program
{
static void Main()
{
//建立結構的例項
Point point = new Point();
point.X = 10;
point.Y = 20;
//呼叫結構中的方法
point.Move(5, 5);
//輸出點的座標
Console.WriteLine($"Point coordinates: ({point.X}, {point.Y})");
//由於結構是值型別,所以將它傳遞給方法時,會傳遞它的一個副本
//可以使用ref或out關鍵字來傳遞它本身
MovePoint(point);
//輸出點的座標,它不會改變,因為MovePoint方法接收的是副本
Console.WriteLine($"Point coordinates after MovePoint: ({point.X}, {point.Y})");
}
static void MovePoint(Point p)
{
p.X += 10;
p.Y += 10;
}
}
[!warning]
cpp的結構體預設是public
,但C#不是
從C#7.2開始,結構體的成員預設是private
,結構體本身是型別,可見性取決於上下文
陣列
//字面值形式初始化陣列,不能宣告大小
int[] Array = {1,3,5,7,9};
//指定陣列大小的初始化,會給所有陣列元素賦予同一個預設值,數值型別是0,C#允許使用非常量的變數初始化陣列
int[] Array = new int[5];
//可以組合使用這兩種初始化方式
int[] Array = new int[5] {1,3,5,7,9};
//使用這種方式,陣列大小必須與元素個數相匹配,且必須使用常量定義大小
foreach迴圈
foreach
迴圈可以使用一種簡便的語法來定位陣列中的每個元素(和C++的範圍for很像)
foreach(變數型別 變數名 in 陣列名)
不過注意,foreach
迴圈對陣列內容只讀訪問,不能改變任何元素的值
for
迴圈才可以給陣列元素賦值
多維陣列
多維陣列只需要更多逗號
//零初始化
double[,]four = new double[3,4]
//字面值初始化
double[,] hillHeight = {
{ 1, 2, 3, 4 },
{ 2, 3, 4, 5 },
{ 3, 4, 5, 6 }
};
foreach
迴圈可以訪問多維陣列中的所有元素,其方式與訪問一維陣列相同
交錯陣列(陣列的陣列)
多維陣列可稱為矩形陣列,因為每一行的元素個數都相同,而交錯陣列每行的元素個數可能不同,其中的每一個元素都是另一個陣列,這些陣列都必須具有相同的基本型別
交錯陣列的初始化比多維陣列麻煩
//宣告建立主陣列
int[][] jaggedArray = new int[3][]
//然後依次初始化子陣列
jaggedArray[0] = new int[3];
jaggedArray[1] = new int[4];
jaggedArray[2] = new int[5];
//或者提供初始化表示式一次性初始化整個交錯陣列
jaggedArray = new int[][]{
new int[] {1,2,3},
new int[] {1,2,3,4},
new int[] {1,2,3,4,5}
};
遍歷交錯陣列也是複雜的多,若非必要無需使用
字串的處理
string
型別的變數可以看成char
變數的只讀陣列
string myString = "A string";
char myChar = myString[1];
//但不能採用這種方式為各個字元賦值,它是隻讀陣列
//使用陣列變數的ToCharArray()可以將一個字串轉換為一個字元陣列並返回,以此獲得一個可寫的char陣列
char[] myChars = myString.ToCharArray();
//在foreach迴圈中使用字串
foreach(var character in myString){
WriteLine(character);
}
與陣列一樣,還可以使用.Length
獲取元素個數,這將給出字串中的字元數
.ToLower()
和.ToUpper()
可以分別把字串轉換為小寫或大寫形式
.Trim()
刪除字串前後的空格,也可以刪除其他字元,只需在一個char
陣列中指定這些字元即可
char[] trimChars = {' ', 'e', 's'};
userResponse = userResponse.Trim(trimChars);//刪除trimChars陣列指定的字元
.TrimStart()
和.TrimEnd()
命令可以把字串前面或後面的空格刪掉,使用這些命令時也可以指定char
陣列
.PadLeft()
和.PadRight()
可以在字串的左邊或右邊新增空格,使字串達到指定的長度
.Replace("n1","n2")
用n2替換n1並返回
.Split()
用於將一個字串拆分成一個子字串陣列。這個方法根據指定的分隔符將字串分割成多個部分,並返回這些部分作為字串陣列
這些命令和之前的其他命令一樣,不會真正改變應用它的字串。把這個命令與字串結合使用,就會建立一個新的字串
函式
函式的定義包括函式名、返回型別以及一個引數列表,這個引數列表指定了該函式需要的引數數量和引數型別,函式的名稱和引數共同定義了函式的簽名
執行一行程式碼的函式可使用C#6引入的表示式體方法(expression-bodied method),使用=>(Lambda箭頭)來實現這一功能
static double Multiply(double myVal1, double myVal2)
{
return myVal1 * myVal2;
}
//使用表示式體方法
static double Multiply(double myVal1, double myVal2) => myVal1 * myVal2;
引數陣列
C#允許為函式指定一個(也只能指定一個)特殊引數,這個引數必須是函式定義中的最後一個引數,稱為引數陣列
引數陣列允許使用數量不定的引數呼叫函式,可使用params
關鍵字定義它們
引數陣列可以簡化程式碼,因為在呼叫程式碼中不必傳遞陣列,而是傳遞同型別的幾個引數,這些引數會放在可在函式中使用的一個陣列中
static 返回型別 函式名 (引數,params 型別名[] 陣列名){
//codes
}
static int SumValues(params int[] vals)
{
int sum = 0;
foreach (int val in vals) sum += val;
return sum;
}
引用引數和值引數
引用傳遞變數本身,值傳遞變數副本
//ref關鍵字指定引數既可引用傳遞
static void ShowDouble(ref int val) {
val *= 2;
WriteLine($"val doubled = {val}");
}
ShowDouble(ref Number);//函式呼叫時也必須顯式指定
ref
引數的變數不能是常量,且必須使用初始化過的變數,C#不允許ref
引數在使用它的函式中初始化
輸出引數
可以使用out
關鍵字指定所給的引數是一個輸出引數,out
關鍵字使用方式與ref
關鍵字相同(在函式定義和函式呼叫中用作引數的修飾符)
它的執行方式與引用引數幾乎完全一樣,因為在函式執行完畢後,該引數的值將返回給函式呼叫中使用的變數。但是二者存在一些重要區別:
- 把未賦值的變數用作
ref
引數是非法的,但可以把未賦值的變數用作out
引數,不過在方法內部必須對其進行賦值 - 在函式使用
out
引數時,必須把它看成尚未賦值,即呼叫程式碼可以把已賦值的變數用作out
引數,但儲存在該變數中的值會在函式執行時丟失
使用場景:
ref
引數:用於方法內部需要讀取和更新已知初始狀態的引數out
引數:用於將一個或多個新生成的值從方法中傳出
使用static
或const
關鍵字來定義全域性變數。如果要修改全域性變數的值,就需要使用static
,因為const
禁止修改變數的值
如果區域性變數和全域性變數同名,會遮蔽全域性變數
Main()
是C#應用程式的入口點,執行這個函式就是執行應用程式,Main
函式可以返回void
或int
,有一個可選引數string[] args
Main
函式可使用如下4種版本:
static void Main()
static void Main(string[] args)
static int Main()
static int Main(string[] args)
返回的int
值可以表示應用程式的終止方式,通常用作一種錯誤提示
可選引數args
是從應用程式外部接受資訊的方法,這些資訊在執行應用程式時以命令列引數的形式指定。在執行控制檯應用程式時,指定的任何命令列引數都放在這個args
陣列中
結構函式
結構除了資料還可以包含函式
struct CustomerName{
public string firstName,lastName;
public string Name() => firstName + " " + lastName;
}
把函式新增到結構中,就可以集中處理常見任務,從而簡化這個過程
static
關鍵字不是結構函式所必需的
函式過載
函式的返回型別不是其簽名的一部分,所以不能定義兩個僅返回型別不同的函式,它們實際上有相同的簽名
委託
委託是一種儲存函式引用的型別
委託的宣告類似於函式,但不帶函式體,且要使用delegate
關鍵字。委託的宣告指定了一個返回型別和引數列表
定義委託後,就可以宣告該委託型別的變數,把這個變數初始化為與委託具有相同返回型別和引數列表的函式引用,就可以使用該委託變數呼叫該函式
有了引用函式的變數,就可以執行無法用其他方式完成的操作。例如,可以把委託變數作為引數傳遞給一個函式,該函式就可以使用委託呼叫它引用的任何函式,而且在執行之前不必知道呼叫的是哪個函式
class Test
{
//定義委託,接受兩個double引數,返回double型別
//實際使用名稱任意,因此可以給委託型別和引數指定任意名稱
delegate double ProcessDelegate(double param1, double param2);
//定義兩個靜態方法
static double Multiply(double param1, double param2) => param1 * param2;
static double Divide(double param1, double param2) => param1 / param2;
static void Main(string[] args)
{
//宣告一個委託變數
ProcessDelegate process;
WriteLine("Enter 2 numbers separated with a comma:");
string input = ReadLine();
int commaPos = input.IndexOf(',');
double param1 = ToDouble(input.Substring(0, commaPos));
double param2 = ToDouble(input.Substring(commaPos + 1, input.Length - commaPos - 1));
WriteLine("Enter M to multiply or D to divide:");
input = ReadLine();
if (input == "M")
//要把一個函式引用賦給委託變數,需要使用略顯古怪的語法
/*類似於給陣列賦值,必須使用new關鍵字建立一個新委託
在new後指定委託型別,提供引用所需函式的引數
引數是使用的函式名但不帶括號
該引數與委託型別或目標函式的引數不匹配,這是委託賦值的特殊語法*/
process = new ProcessDelegate(Multiply);
else
process = new ProcessDelegate(Divide);
//使用委託呼叫所選的函式
WriteLine($"Result: {process(param1, param2)}");
}
}
也可以使用略微簡單的語法來將一個函式引用賦給委託變數:
if (input == "M")
process = Multiply;
else
process = Divide;
編譯器會發現process
變數的委託型別匹配兩個函式的簽名,於是自動初始化一個委託。可以自行確定使用哪種語法
已引用函式的委託變數就像函式一樣使用,但比起函式可以執行更多操作,例如可以透過引數將其傳遞給下一個函式
static void ExecuteFunction(ProcessDelegate process) => process(2.2, 3.3);
除錯和錯誤處理
輸出除錯資訊
Debug.WriteLine()
Trace.WriteLine()
這兩個命令函式用法幾乎完全相同,但一個命令僅在除錯模式下執行,而第二個命令還可用於釋出程式。Debug.WriteLine()
不能編譯到可釋出的程式在,在分佈版本中,該命令會消失,編譯好的程式碼檔案會比較小
這兩種方法包含在System.Diagnostics
名稱空間內
它們唯一的字串引數用於輸出訊息,而不使用{X}
語法插入變數值。這意味著必須使用+
串聯運算子等方式在字串中插入變數值
它們可以有第二個字串引數,用於顯示輸出文字的類別
using System.Diagnostics;
using static System.Console;
namespace DeBug
{
class Program
{
static void Main(string[] args)
{
int[] testArray = { 4, 7, 4, 2, 7, 3, 7, 8, 3, 9, 1, 9 };
//儲存最大值出現的所有索引
int[] maxValIndices;
//儲存返回的最大值
int maxVal = Maxima(testArray, out maxValIndices);
WriteLine($"Maximum value {maxVal} found at element indices:");
foreach (int index in maxValIndices)
WriteLine($"Maximum index:{index}");
}
static int Maxima(int[] integers, out int[] indices)
{
Debug.WriteLine("Maximum value search started.");
//初始化為長度為1的新陣列
indices = new int[1];
//初始化最大值為陣列第一個元素
int maxVal = integers[0];
//儲存最大值索引
indices[0] = 0;
//儲存最大值個數
int count = 1;
Debug.WriteLine(string.Format($"Maximum value initialized to {maxVal}, at element index 0."));
//迴圈忽略第一個值,因為已處理
for (int i = 1; i < integers.Length; i++)
{
Debug.WriteLine(string.Format($"Now looking at element at index {i}.")
);
if (integers[i] > maxVal)
{
maxVal = integers[i];
count = 1; indices = new int[1];
indices[0] = i;
Debug.WriteLine(string.Format($"New maximum found. New value is {maxVal}, at element index {i}."));
}
else
{
if (integers[i] == maxVal)
{
++count;
//建立對現有陣列indices的引用,它們指向同一塊記憶體區域
int[] oldIndices = indices;
indices = new int[count];
//從索引0開始把indices陣列的內容複製到oldIndices陣列
oldIndices.CopyTo(indices, 0);
indices[count - 1] = i;
Debug.WriteLine(string.Format($"Duplicate maximum found at element index {i}."));
}
}
}
Trace.WriteLine(string.Format($"Maximum value {maxVal} found, with {count} occurrences."));
Debug.WriteLine("Maximum value search completed.");
return maxVal;
}
}
}
各個文字部分都使用Debug.WriteLine()
和Trace.WriteLine()
函式進行輸出,這些函式使用string.Format()
函式把變數值巢狀在字串中,其方式與WriteLine()
相同。這比使用+
串聯運算子更高效
Debug.WriteLine(string.Format($"Duplicate maximum found at element index {i}."));
Debug.WriteLine(string.Format("Duplicate maximum found at element index {0}.",i));
//字串差值
Debug.WriteLine($"Duplicate maximum found at element index {i}.");
//傳統字串格式化
Debug.WriteLine("Duplicate maximum found at element index {0}.",i);
經本人測試,這四種方法都可以正常輸出,如果在舊版不支援字串插值C#或需要更復雜的格式化選項,如自定義數字、日期或其他型別格式時還是可選用string.Format
,一般情況字串插值更間接明瞭
[!note] 跟蹤點
vistual studio自帶的,可以便捷地新增額外資訊和刪除,和打斷點一樣,只是要在actions裡選擇在output裡輸出的資訊跟蹤點和Trace命令並不等價,在應用程式的已編譯版本中,跟蹤點是不存在的,只有應用程式執行在VS偵錯程式中時,跟蹤點才起作用
中斷模式
除了vs自帶的斷點,還可以生成一條判定語句時中斷
判定語句是可以用使用者定義的訊息中斷應用程式的指令。它們常用於應用程式的開發過程,作為測試程式能否平滑執行的一種方式
判定函式也有兩個版本:
Debug.Assert()
Trace.Assert()
兩個函式都是三引數:第1個引數是布林值,其值為false
時觸發判定語句,第2、3個引數是字串,分別將資訊寫到彈出對話方塊和output視窗
Trace.Assert(i > 10, "Variable out of bounds.", "Please contact vendor with the error code KCW001.");
錯誤處理
預料到錯誤的發生,編寫足夠健壯的程式碼以處理這些錯誤,而不必中斷程式的執行
C#包含結構化異常處理SEH(Structured Exception Handling)的語法。用3個關鍵字(try
、catch
、finally
)可以標記出能處理異常的程式碼和指令,如果發生異常,就使用這些指令處理異常
可以在catch
或finally
塊內使用async/await
關鍵字,用於支援先進的非同步程式設計技術,避免瓶頸,且可以提高應用程式的總體效能和響應能力
C#7.0引入了throw
表示式可以與catch
塊配合使用
可以只有try
塊和finally
塊,而沒有catch
塊,或者有一個try
塊和好幾個catch
塊。如果有一個或多個catch
塊,finally
塊就是可選的
-
try
包含丟擲異常的程式碼(在談到異常時,C#語言用“丟擲”這個術語表示“生成”或“導致”) -
catch
包含丟擲異常時要執行的程式碼,catch
塊可以使用<exceptionType>
,設定為只響應特定的異常型別(如System.IndexOutOfRangeException)以便提供多個catch
塊
還可以完全省略這個引數,讓通用的catch
塊響應所有異常
C#6引入了一個概念“異常過濾”,透過在異常型別表示式後新增when
關鍵字來實現。如果發生了該異常型別,且過濾表示式是true
, 就執行catch
塊中的程式碼 -
finally
包含始終會執行的程式碼,如果沒有產生異常,則在try
塊之後執行,如果處理了異常,就在catch
塊後執行,或者在未處理的異常“上移到呼叫堆疊”之前執行
“上移到呼叫堆疊”表示:SEH允許巢狀try…catch…finally
塊,可以直接巢狀,也可以在try
塊包含的函式呼叫中巢狀。例如如果在被呼叫的函式中沒有catch
塊能處理某個異常,就由呼叫程式碼中的catch
塊處理。如果始終沒有匹配的catch
塊,就終止應用程式finally
塊在此之前處理正是其存在的意義,否則也可以在try…catch…finally
結構的外部放置程式碼
[!info]
如果存在兩個處理相同異常型別的catch
塊,就只執行異常過濾器為true
的catch
塊中的程式碼。如果還存在一個處理相同異常型別的catch
塊,但沒有異常過濾器或異常過濾器是false
,就忽略它。只執行一個catch
塊的程式碼,catch
塊的順序不影響執行流
using static System.Console;
class Test
{
/*none不丟擲異常
simple生成一般異常
index生成IndexOutOfRangeException異常
nested index和filter生成異常和上者相同*/
//這些異常識別符號包含在全域性陣列中
static string[] eTypes = { "none", "simple", "index", "nested index", "filter" };
static void Main(string[] args)
{
foreach (string eType in eTypes)
{
try
{
WriteLine("Main() try block reached.");
WriteLine($"ThrowException(\"{eType}\") called.");
ThrowException(eType);
WriteLine("Main() try block continues.");
}
//僅當eType是filter時捕獲越界異常
catch (System.IndexOutOfRangeException e) when (eType == "filter")
{
WriteLine("Main() FILTERED System.IndexOutOfRangeException" + $"catch block reached. Message:\n\" {e.Message}\"");
}
//捕獲所有其他未被第一個catch塊捕獲的索引越界異常
catch (System.IndexOutOfRangeException e)
{
WriteLine("Main() System.IndexOutOfRangeException catch " + $"block reached. Message:\n\" {e.Message}\"");
}
//未指定異常型別,會捕獲所有未被捕獲的其他型別的異常
catch
{
WriteLine("Main() general catch block reached.");
}
//無論異常發生都會執行,表示異常塊結束
finally
{
WriteLine("Main() finally block reached.");
}
}
}
//根據傳遞的異常執行相應的操作
static void ThrowException(string exceptionType)
{
WriteLine($"ThrowException(\"{exceptionType}\") reached.");
switch (exceptionType)
{
case "none":
WriteLine("Not throwing an exception.");
break;
case "simple":
WriteLine("Throwing System.Exception.");
/*System.Exception是.NET框架中的基類異常型別
所有自定義異常或系統內建的異常都繼承自此型別
手動丟擲該異常*/
throw new System.Exception();
case "index":
//此處陣列越界,會跳轉到捕獲陣列越界的catch語句,因為不是filter,所以會交給第二個catch塊處理
WriteLine("Throwing System.IndexOutOfRangeException."); eTypes[5] = "error";
break;
case "nested index":
try
{
WriteLine("ThrowException(\"nested index\") " + "try block reached.");
WriteLine("ThrowException(\"index\") called.");
//跳轉,最後的執行和index相同
ThrowException("index");
}
catch
{
WriteLine("ThrowException(\"nested index\") general" + " catch block reached.");
}
**finally******
{
WriteLine("ThrowException(\"nested index\") finally" + " block reached.");
}
break;
case "filter":
try
{
WriteLine("ThrowException(\"filter\") " + "try block reached.");
WriteLine("ThrowException(\"index\") called.");
ThrowException("index");
}
catch
{
WriteLine("ThrowException(\"filter\") general" + " catch block reached.");
}
break;
}
}
}
[!info]
在case
塊中使用throw
時,不需要break
語句,使用throw
就可以結束該塊的執行
物件導向程式設計
C#中的物件從型別中建立,就像變數一樣,物件的型別在物件導向程式設計中叫類,可以使用類的定義例項化物件,類的例項==物件
物件的生命週期
每個物件都有一個明確定義的生命週期,除了“正在使用”的正常狀態之外,還有兩個重要的階段:
- 構造階段:第一次例項化一個物件時,需要初始化該物件。這個初始化過程稱為構造階段,由建構函式完成
- 析構階段:在刪除一個物件時,常常需要執行一些清理工作,例如釋放記憶體,這由解構函式完成
建構函式
物件的初始化過程是自動完成的,不需要自己尋找適用於儲存新物件的記憶體空間
但在初始化物件的過程中有時需要執行一些額外工作,例如需要初始化物件儲存的資料。建構函式就是用於初始化資料的函式
所有的類定義都至少包含一個建構函式。在這些建構函式中,可能有一個預設建構函式,該函式沒有引數,與類同名
類定義還可能包含幾個帶有引數的建構函式,稱為非預設的建構函式。程式碼可以使用它們以許多方式例項化物件,例如給儲存在物件中的資料提供初始值
在C#中使用new
關鍵字來呼叫建構函式
類名 物件名 = new 類名()
//可以使用非預設的建構函式來例項化物件
類名 物件名 = new 類名(引數列表)
建構函式與欄位、屬性和方法一樣,可以是公共或私有的。在類外部的程式碼不能使用私有建構函式例項化物件,而必須使用公共建構函式。透過把預設建構函式設定為私有的,就可以強制類的使用者使用非預設的建構函式
一些來沒有公共的建構函式,外部的程式碼不可能例項化它們,這些類稱為不可建立的類,不可建立的類不是完全沒有用的
解構函式
.NET Framework使用解構函式來清理物件。一般情況下不需要提供解構函式的程式碼,而由預設的解構函式自動執行操作。但如果在刪除物件例項前需要完成一些重要操作,就應提供具體的解構函式
例如如果變數超出範圍,程式碼就不能訪問它,但該變數仍存在於計算機記憶體的某個地方,只有.NET執行程式執行其垃圾回收,進行清理時,該例項才被徹底刪除
靜態成員和例項類成員
屬性、欄位和方法等成員是物件例項特有的
靜態成員,也稱共享成員(靜態方法、靜態屬性、靜態欄位)
- 靜態成員可以在類的例項之間共享,所以可以將它們看成類的全域性物件
- 靜態屬性和靜態欄位可以訪問獨立於任何物件例項的資料
- 靜態方法可以執行與物件型別相關但與物件例項無關的命令,在使用靜態成員時,甚至不需要例項化物件
靜態建構函式
使用類中的靜態成員時,需要預先初始化,宣告時可以給靜態成員提供一個初始值,但有時需要執行更復雜的初始化操作,或者在賦值、執行靜態方法之前執行某些操作
使用靜態建構函式可以執行此類初始化任務,一個類只能有一個靜態建構函式,該建構函式不能有訪問修飾符,也不能有任何引數
靜態建構函式不能直接呼叫,只能在下述情況下執行:
- 建立包含靜態建構函式的類例項時
- 訪問包含靜態建構函式的類的靜態成員時
在這兩種情況下,會首先呼叫靜態建構函式,之後例項化類或訪問靜態成員,無論建立多少個類例項,其靜態建構函式都只呼叫一次,所有非靜態建構函式也稱例項建構函式
靜態類
希望類只包含靜態成員,且不能用於例項化物件(如Console
)。為此一種簡單的方法是使用靜態類,而不是把類的建構函式設定為私有
靜態類只能只能包含靜態成員,不能包含例項建構函式。只可以有一個靜態建構函式
OOP技術
介面
介面是把公共例項(非靜態)方法和屬性組合起來,以封裝特定功能的一個集合。定義了介面後就可以在類中實現它,這樣類就可以支援介面所指定的所有屬性和成員
[!caution]
- 介面不能單獨存在,不能像例項化一個類那樣例項化介面
- 介面不能包含實現其成員的任何程式碼 ,只能定義成員本身
- 實現過程必須在實現介面的類中完成
一個類可以支援多個介面,多個類也可以支援相同的介面。所以介面的概念讓使用者和其他開發人員更容易理解其他人的程式碼
可刪除的物件
IDisposable
介面是.NET框架中一個非常重要的介面,它允許開發人員顯式釋放不再需要的物件所佔用的資源。支援IDisposable
介面的物件必須實現Dispose()
方法,即它們必須提供這個方法的程式碼
C#允許使用一種可以最佳化使用這個方法的結構,using
關鍵字可以在程式碼塊中初始化使用重要資源的物件,在該程式碼塊的末尾會自動呼叫Dispose()
方法:
<ClassName><VariableName> = new<ClassName>();
...
using (<VariableName>)
{
...
}
//也可以把初始化物件<VariableName>作為using語句的一部分
using (<ClassName><VariableName> = new<ClassName>())
{
...
}
在這兩種情況下,可在using
程式碼塊中使用變數<VariableName>
,並在程式碼塊的末尾自動刪除(在程式碼塊執行完畢後,呼叫Dispose()
方法)
繼承
繼承是OOP最重要的特性之一
任何類都可以從另一個類繼承,C#中的物件只能直接派生於一個基類,基類可以有自己的基類
基類可以定義為抽象類,抽象類不能直接例項化,要使用抽象類,必須繼承該類,抽象類可以有抽象成員,這些成員在基類中沒有實現程式碼,所以派生類必須實現它們
類可以是密封的,密封類不能用作基類,所以沒有派生類
在繼承一個基類時派生類不能訪問基類的私有成員,只能訪問其公共成員,但外部程式碼也可以訪問類的公共成員
因此C#提供了第三種可訪問性:protected
,只有派生類才能訪問protected
成員,外部程式碼不能訪問private
成員和protected
成員
除定義成員的保護級別外,還可以為成員定義其繼承行為。基類的成員可以是虛擬的,即成員可以在派生類中重寫
派生類可以提供成員的另一種實現程式碼,這種實現程式碼不會刪除原來的程式碼,仍可在類中訪問原來的程式碼,但外部程式碼不能訪問它們。如果沒有提供其他實現方式,透過派生類使用成員的外部程式碼就自動訪問基類中成員的實現程式碼
虛擬類不能是私有成員,因為不能既要求派生類重寫成員,又不讓派生類訪問該成員
C#中所有物件都有一個共同的基類object
(在.NET Framework中,它是System.Object
類的別名)
介面可以繼承自其他介面。與類不同的是,介面可以繼承多個基介面
多型性
表示在不同的上下文中,同一個介面、函式或者類可以有不同的實現和表現形式。具體來說,多型性允許不同型別的物件對同一訊息作出不同的響應
多型性的主要體現:
-
方法重寫:子類繼承父類時,可以重新定義父類中已經存在的非靜態(virtual/abstract)方法,這樣當透過父類引用指向子類物件並呼叫該方法時,實際執行的是子類重寫後的方法版本
-
介面實現:不同的類可以實現相同的介面,每個類按照自己的邏輯來實現介面中的方法,從而實現多型
-
向上轉型:父類引用指向子類物件,在執行時呼叫的實際方法取決於物件的實際型別,這就是所謂的動態繫結
-
抽象類與虛方法:在C#中,抽象類可以包含抽象方法(必須在派生類中實現),所有繼承自抽象類的子類都必須提供相應的方法實現
繼承的一個結果是派生於基類的類在方法和屬性上有一定的重疊,因此可以使用相同的語法處理從同一個基類例項化的物件
例如,如果基類Animal
有一個EatFood()
方法,則在其派生類Cow
和Chicken
中呼叫這個方法的語法是類似的:
//Cow和Chicken派生於Animal
Cow myCow = new Cow();
Chicken myChicken = new Chicken();
myCow.EatFood();
myChicken.EatFood();
多型性則更推進了一步,可以把某個派生型別的變數賦給基本型別的變數
Animal myAnimal = myCow;
不需要強制轉換,就可以透過該變數呼叫基類的方法
myAnimal.EatFood();//呼叫派生類中的EatFood()實現程式碼
//注意不能以相同的方式呼叫派生類上定義的方法
myAnimal.M();//error
//可以把基本型別變數轉換為派生類變數,以此呼叫派生類的方法
Cow myNewCow = (Cow)myAnimal;
myNewCow.M();
//如果原始變數的型別不是Cow或派生於Cow的型別,這個強制型別轉換就會引發一個異常
在派生於同一個類的不同物件上執行任務時,多型性是一種極有效的技巧,其使用的程式碼最少
不是隻有共享同一個基類的類才能利用多型性,只要派生類在繼承層次結構中有一個相同的類,它們就可以使用同樣的方法利用多型性
object
類是繼承層次結構中的根,可以把所有物件看成object
類的例項。這就是在建立字串時,WriteLine()
可以處理無數多種引數組合的原因,第一個引數後面的每個引數都可以看成一個object
例項,所以
可以把任何物件的輸出結果寫到螢幕上。為此,需要呼叫方法ToString()
介面的多型性
雖然不能像物件一樣例項化介面,但可以建立介面型別的變數,然後就可以在支援該介面的物件上使用該變數來訪問該介面提供的方法和屬性
例如,假定不使用基類Animal
提供的EatFood()
方法,而是把該方法放在IConsume
介面上。Cow
和Chicken
類也支援這個介面,唯一的區別是它們必須提供EatFood()
方法的實現程式碼(因為介面不包含實現程式碼),接著就可以使用下述程式碼訪問該方法
Cow myCow = new Cow();
Chicken myChicken = new Chicken();
IConsume consumeInterface;
//將Cow物件賦值給介面型別的變數
consumeInterface = myCow;
//透過consumeInterface呼叫Cow中實現的EatFood方法
consumeInterface.EatFood();
consumeInterface = myChicken;
consumeInterface.EatFood();
派生類會繼承其基類支援的介面。有共同基類的類不一定有共同介面,有共同介面的類也不一定有共同基類
物件之間的關係
繼承是物件之間的一種簡單關係,可以讓派生類完整地獲得基類的特性。物件之間還具有其他一些重要關係
包含關係
一個類包含另一個類,類似於繼承關係,但包含類可以控制對被包含類的成員的訪問,甚至在使用被包含類的成員前進行其他處理
用一個成員欄位包含物件例項,就可以實現包含關係。這個成員欄位可以是公共欄位,此時與繼承關係相同,容器物件的使用者就可以訪問它的方法和屬性,但不能像繼承關係那樣透過派生類訪問類的內部程式碼
可以讓被包含的成員物件變為私有成員,使用者就不能直接訪問任何成員,即使這些成員是公共的,但可以使用包含類的成員訪問這些私有成員
可以完全控制被包含的類對外提供什麼成員或不提供任何成員,還可以在訪問被包含類的成員前,在包含類的成員上執行其他處理
集合關係
一個類用作另一個類的多個例項的容器。這類似於物件陣列,但集合具有其他功能,包括索引、排序和重新設定大小等
集合基本就是一個增加了功能的陣列,集合以與其他物件相同的方式實現為類,通常以所儲存的物件名稱的複數形式來命名
陣列與集合的主要區別是,集合通常實現額外的功能,例如Add()
和Remove()
方法可新增和刪除集合中的項。且集合通常有一個Item
屬性,它根據物件的索引返回該物件。通常這個屬性還允許實現更復雜的訪問方式
運算子過載
可以把運算子用於從類例項化而來的物件,因為類可以包含如何處理運算子的指令
只能採用這種方式過載現有的C#運算子,不能建立新的運算子
事件
物件可以啟用和使用事件,作為它們處理的一部分。事件是非常重要的,可以在程式碼的其他部分起作用,類似於異常(但功能更強大)
例如可以在把Animal
物件新增到Animals
集合中時,執行特定的程式碼,而這部分程式碼不是Animals
類的一部分,也不是呼叫Add()
方法的程式碼的一部分。為此需要給程式碼新增事件處理程式,這是一種特殊型別的函式,在事件發生時呼叫。還需要配置這個處理程式,以監聽自己感興趣的事件
引用型別和值型別
- 值型別在記憶體的同一處(棧內)儲存它們自己和它們的內容
- 引用型別儲存指向記憶體中其他某個位置(堆內)的引用,實際內容儲存在這個位置
在使用C#時不必過多考慮這個問題
值型別和引用型別的一個主要區別是:值型別總是包含一個值,而引用型別可以是null
,表示它們不包含值。但可以使用可空型別建立值型別,使值型別在這個方面的行為類似於引用型別(即可以為null
)
string
和object
型別是簡單的引用型別,陣列也是隱式的引用型別,建立的每個類都是引用型別
定義類
C#使用class
關鍵字來定義類,定義了一個類後,就可以在專案中能訪問該定義的其他位置對該類進行例項化
預設情況下類宣告為內部的,即只有當前專案中的程式碼才能訪問它,可使用internal
訪問修飾符關鍵字來顯式地指定這一點,雖然沒有必要
public
關鍵字指定類是公共的,可由其他專案中的程式碼來訪問
[!hint]
internal
類強調的是封裝性和內部複用,適合於隱藏內部實現細節;而public
類則允許跨程式集共享和重用,適用於對外公開的介面和元件
可以指定類是抽象的(不能例項化,只能繼承,只有抽象類可以有抽象成員)或密封的(不能繼承,只能例項化,密封成員不能被重寫),使用兩個互斥的關鍵字abstract
或sealed
抽象類可以是公共的,也可以是內部的;密封類也可以是公共或內部的
在類定義中指定繼承,要在類名的後面加上一個冒號,後跟基類名
public class Test : Program
在C#的類定義中,只能有一個基類。如果繼承了一個抽象類,就必須實現所繼承的所有抽象成員(除非派生類也是抽象的)
編譯器不允許派生類的可訪問性高於基類,即內部類可以繼承於一個公共基類,但公共類不能繼承於一個內部基類
如果沒有使用基類,被定義的類就只繼承於基類System.Object
除了在冒號之後指定基類外,還可以指定支援的介面,基類只能有一個,但可以實現任意數量的介面
public class 類名 : 介面1,介面2
//當有基類時,需要先緊跟基類
public class 類名 : 基類,介面1,介面2
支援該介面的類必須實現所有介面成員,但如果不想使用給定的介面成員,可用提供一種“空”的實現方式(沒有函式程式碼)。還可以把介面成員實現為抽象類中的抽象成員
介面的定義
宣告介面使用interface
關鍵字
interface 介面
訪問修飾符關鍵字public
和internal
的使用方式是相同的,與類一樣,介面預設定義為內部介面,要使介面可以公開訪問,必須使用public
關鍵字
不能在介面中使用關鍵字abstract
和sealed
,因為這兩個修飾符在 介面定義中是沒有意義的(它們不包含實現程式碼,所以不能直接例項化,且必須是可以繼承的)
介面的繼承可以使用多個基介面
public interface 介面 : 介面1,介面2
介面不是類,所以沒有繼承System.Object
,但System.Object
的成員可以透過介面型別的變數來訪問。不能使用例項化類的方式來例項化介面
System.Object
因為所有類都繼承於System.Object
,所以這些類都可以訪問該類中受保護的成員和公共成員
下表是該類中的方法,未列出構造/解構函式,這些方法是.NET Framework中物件型別必須支援的基本方法
//返回bool,靜態方法
/*呼叫該方法的物件和另一物件進行比較,相等返回true,預設實現程式碼檢視物件是否引用同一個物件,可重寫該方法*/
object1.Equals(object2)
/*和上方法相同,但可以避免因object1為null而丟擲的異常,如果兩個物件都是空引用返回null*/
Object.Equals(object1,object2)
//返回bool,靜態方法
/*比較兩個物件引用是否指向記憶體中的同一個位置,是則返回true*/
ReferenceEquals(object1,object2)
//返回String,虛擬方法
/*將物件轉換為例項並返回,預設程式碼返回的字串通常包含型別名和雜湊程式碼(記憶體地址)*/
object1.ToString()
//返回object
/*建立一個新物件例項,將原物件的所有欄位值複製到新物件中,成員複製不會得到這些成員的新例項。新物件的任何引用型別成員都將引用與源類相同的物件,這個方法是受保護的,只能在類或派生的類中使用*/
MemberwiseClone()
//返回System.Type
/*可以獲得關於物件型別的各種資訊,如名稱、基型別、介面實現、成員(屬性、方法等)等*/
GetType()
//返回int,虛擬方法
/*它的目的是為物件生成一個雜湊碼,通常用於基於雜湊表的資料結構*/
GetHashCode()
在利用多型性時,GetType()
是一個有用的方法,允許根據物件的型別來執行不同的操作,而不是對所有物件都執行相同的操作
例如,如果函式接受一個object
型別的引數(表示可以給該函式傳輸任何資訊),就可以在遇到某些物件時執行額外任務。結合使用GetType()和typeof
(這是一個C#運算子,可以把類名轉換為System.Type
物件),就可以進行比較
if (myObj.GetType() == typeof(MyComplexClass))
{
// myObj is an instance of the class MyComplexClass.
}
建構函式和解構函式
建構函式名必須與包含它的類同名,沒有引數則是預設建構函式。建構函式可以公共或私有,私有即不能用這個建構函式來建立這個類的物件例項
解構函式由一個波浪號~
後跟類名組成,沒有引數和返回型別
解構函式不能被直接呼叫,它由垃圾回收器(GC)在確定物件不再被引用且需要回收記憶體時自動呼叫。呼叫這個解構函式後,還將隱式地呼叫基 類的解構函式,包括System.Object
根類中的Finalize()
呼叫
.NET框架中的大多數資源管理已經高度最佳化,使用using
語句和實現了IDisposable
的物件可以更有效地進行資源管理,對於非託管資源(檔案、資料庫連線),應優先考慮實現IDisposable
介面而非依賴解構函式
建構函式的執行序列
任何建構函式都可以配置為在執行自己的程式碼前呼叫其他建構函式
為了例項化派生的類,必須例項化它的基類。而要例項化這個基類,又必須例項化這個基類的基類,這樣一直到例項化System.Object
為止。結果是無論使用什麼建構函式例項化一個類, 總是首先呼叫System.Object.Object()
無論在派生類上使用預設/非預設建構函式,除非明確指定,否則就使用基類的預設建構函式
在C#中,建構函式初始化器允許在建構函式定義的冒號後面直接初始化類的成員變數。這樣可以提高程式碼的可讀性和減少冗餘程式碼,特別是在需要對多個成員進行相同操作時
public class DerivedClass : BaseClass
{
...
public DerivedClass(int i, int j) : base(i)
{
}
}
base
關鍵字指定.NET例項化過程使用基類中具有指定引數的建構函式(呼叫基類的建構函式)this
關鍵字指定在呼叫指定的建構函式前,.NET例項化過程對當前類使用非預設的建構函式(呼叫同一個類中的另一個建構函式)
這裡使用一個int引數,因此會呼叫BaseClass
的BaseClass(int i)
建構函式初始化基類的成員變數,也可以使用這個關鍵字指定基類建構函式的字面值
```cs
public class DerivedClass : BaseClass
{
public DerivedClass() : this(5, 6)
{
}
public DerivedClass(int i, int j) : base(i)
{
}
}
使用DerivedClass.DerivedClass()
建構函式,將得到如下執行順序:
- 執行
System.Object.Object()
建構函式 - 執行
BaseClass.BaseClass(int i)
建構函式 - 執行
DerivedClass.DerivedClass(int i, int j)
建構函式 - 執行
DerivedClass.DerivedClass()
建構函式
注意在定義建構函式時,不要建立無限迴圈
類庫專案
除了在專案中把類放在不同的檔案中之外,還可以把它們放在完全不同的專案中。如果一個專案只包含類以及其他相關的型別定義,但沒有入口點,該專案就稱為類庫
類庫專案編譯為.dll
程式集,在其他專案中新增對類庫專案的引用,就可以訪問它的內容。修改和更新類庫不會影響使用它們的其他專案
介面和抽象類
介面和抽象類都包含可以由派生類繼承的成員。介面和抽象類都不能直接例項化,但可以宣告這些型別的變數。若這樣做,就可以使用多型性把繼承這兩種型別的物件指定給它們的變數,接著透過這些變數來使用這些型別的成員,但不能直接訪問派生物件的其他成員
派生類只能繼承自一個基類,即只能直接繼承自一個抽象類,但可以用一個繼承鏈包含多個抽象類;而類可以使用任意多個介面
抽象類可以擁有抽象成員(沒有程式碼體,且必須在派生類中實現,否則派生類本身必須也是抽象的)和非抽象成員(擁有程式碼體,可以是虛擬的,這樣就可以在派生類中重寫)
介面成員必須都在使用介面的類上實現,它們沒有程式碼體。介面成員是公共的,但抽象類的成員可以是私有的(只要它們不是抽象的)、受保護的、內部的或受保護的內部成員(受保護的內部成員只能在應用程式的程式碼或派生類中訪問)
此外介面不能包含欄位、建構函式、解構函式、靜態成員或常量
- 抽象類主要用作物件系列的基類,這些物件共享某些主要特性,例如共同的目的和結構
- 介面則主要用於類,這些類存在根本性的區別,但仍可以完成某些相同的任務
假定有一個物件系列表示火車,基類Train
包含火車的核心定義,例如車輪的規格和引擎的型別。但這個類是抽象的,因為並沒有一般的火車
為建立一輛實際的火車,需要給該火車新增特性。為此派生一些類,Train
可以派生於一個相同的基類Vehicle
,客運列車可以運送乘客,貨運列車可以運送貨物,假設高鐵兩者都可以運送,為它們設計相應的介面
在進行更詳細的分解之前,把物件系統以這種方式進行分解,可以清晰地看到哪種情形適合使用抽象類,哪種情形適合使用介面
結構型別
物件是引用型別,把物件賦給變數時,實際上是把帶有一個指標的變數賦給了該指標所指向的物件
而結構是值型別,其變數包含結構本身,把結構賦給變數,是把一個結構的所有資訊複製到另一個結構中
淺度和深度複製
簡單地按照成員複製物件可以透過派生於System.Object
的MemberwiseClone()
方法來完成,這是一個受保護的方法,但很容易在物件上定義一個呼叫該方法的公共方法。該方法提供的複製功能稱為淺度賦值,因為它未考慮引用型別成員。因此新物件中的引用成員就會指向源物件中相同成員引用的物件
如果想要建立成員的新例項(複製值,不復制引用),此時需要使用深度複製
可以實現一個ICloneable
介面,以標準方式進行深度賦值,如果使用這個介面,就必須實現它包含的Clone()
方法。這個方法返回一個型別為System.Object
的值。可以採用各種處理方式,實現所選的任何一個方法體來得到這個物件
定義類成員
定義成員
public
:成員可以由任何程式碼訪問private
:成員只能由類中的程式碼訪問(如果沒有使用任何關鍵 字,就預設使用這個關鍵字)internal
:成員只能由定義它的程式集內部的程式碼訪問protected
:成員只能由類或派生類中的程式碼訪問
後兩個關鍵字可以結合使用,所以也有protected internal
成員,它們只能由程式集中派生類的程式碼來訪問
定義欄位
用標準的變數宣告格式(可以進行初始化)和前面介紹的修飾符來定義欄位
class Test
{
public int Int;
}
.NET Framework的公共欄位使用駝峰命名法,私有欄位一般全小寫
欄位可以使用關鍵字readonly
,表示該欄位只能在執行建構函式的過程或初始化語句賦值
class Test
{
public readonly int Int = 16;
}
[!important]
const
宣告編譯時常量,readonly
宣告執行時常量const
成員必須是靜態的,不需要例項即可訪問,在整個應用程式域中是一致的readonly
欄位可以是靜態也可以是例項
使用static
關鍵字將欄位宣告為靜態,靜態欄位必須透過定義它們的類來訪問,而不是透過這個類的物件例項來訪問
定義方法
方法使用標準函式格式、可訪問性和可選static
修飾符來宣告,與公共欄位一樣,公共方法也採用駝峰命名法
如果使用了static
關鍵字,這個方法就只能透過類來訪問,不能透過物件例項來訪問
可以在方法定義中使用下述關鍵字
virtual
:宣告一個虛方法,允許派生類重寫它override
:在派生類中重寫基類的虛方法abstract
:宣告一個抽象方法,必須在派生類中實現。sealed
(應用於override方法時):阻止方法被派生類重寫static
:宣告靜態方法,不依賴於類例項進行呼叫async
:用於非同步方法,表示方法包含非同步操作並可能返回Task
或Task<T>
extern
:宣告外部方法,通常用於P/Invoke呼叫非託管程式碼partial
:標識部分方法,用於拆分方法的定義到多個檔案中
定義屬性
屬性提供對類或結構體內部私有欄位的間接訪問。屬性允許控制對這些私有欄位的讀取和寫入操作,從而實現資料驗證、邏輯封裝等目的
屬性定義方式與欄位,但包含的內容比較多,屬性比欄位複雜,因為它們在修改狀態前還可以執行一些額外操作,也可能並不修改狀態
屬性擁有兩個類似於函式的塊,一個塊用於獲取屬性的值,一個塊用於設定屬性的值。這兩個塊也稱為訪問器,分別使用get
和set
關鍵字來定義
訪問器可以用於控制屬性的訪問級別。忽略其中一個塊來建立只讀或只寫屬性,這僅適用於外部程式碼,因為類中的其他程式碼可以訪問這些程式碼塊能訪問的資料。可以在訪問器上包含可訪問修飾符
屬性的基本結構包括標準的可訪問修飾符,後跟類名、屬性名和訪問器
get
塊必須有一個屬性型別的返回值,簡單屬性一般與私有欄位相關聯,以控制對這個欄位的訪問,此時get
塊可以直接返回該欄位的值
// Field used by property.
private int myInt;
// Property.
public int MyIntProp
{
get { return myInt; }
set { // Property set code. }
}
類外部的程式碼不能直接訪問myInt
欄位,因為其訪問級別是私有的。外部程式碼必須使用屬性來訪問該欄位。set
訪問器採用類似方式把一個值賦給欄位。可以使用關鍵字value
表示使用者提供的屬性值:
public int MyIntProp {
get { return myInt; }
set { myInt = value; }
}
value
等於型別與屬性相同的一個值,所以如果屬性和欄位使用相同的型別,就不必考慮資料型別轉換
這個簡單屬性只是用來阻止對myInt
欄位的直接訪問。在對操作進行更多控制時,屬性的真正作用才能發揮出來
set
{
if (value >= 0 && value<= 10)
myInt = value;
}
如果使用了無效值,通常繼續執行,但記錄下該事件,以備將來分析或直接丟擲異常是比較好的選擇,選擇哪個選項取決於如何使用類以及給類的使用者授予了多少控制權
set
{
if (value >= 0 && value<= 10)
myInt = value;
else
throw (new ArgumentOutOfRangeException("MyIntProp", value,
"MyIntProp must be assigned a value between 0 and 10."));
}
屬性可以使用virtual、override
和abstract
關鍵字,就像方法一 樣,但這幾個關鍵字不能用於欄位。訪問器可以有自己的可訪問性
只有類或派生類中的程式碼才能使用set
訪問器
訪問器可以使用的訪問修飾符取決於屬性的可訪問性,訪問器的可訪問性不能高於它所屬的屬性,即私有屬性對它的訪問器不能包含任何可訪問修飾符,而公共屬性可以對其訪問器使用所有的可訪問修飾符
C#6引入了一個名為“基於表示式的屬性”的功能,該功能可以把屬性的定義減少為一行程式碼
下面的屬性對一個值進行數學計算,使用Lambda箭頭後跟等式來定義:
//Field used by property
private int myDoubledInt = 5;
//Property
public int MyDoubledIntProp => (myDoubledInt * 2);
重構成員
“重構”表示使用工具修改程式碼,而不是手工修改。為此,只需要右擊類圖中的某個成員,或在程式碼檢視中右擊某個成員即可
public string myString;
右擊該欄位,選擇快速操作和重構,選擇需要的選項
private string myString;
public string MyString { get => myString; set => myString = value; }
myString
欄位的可訪問性變成private
,同時建立了一個公共屬性 MyString
,它自動連結到myString上
。顯然這會減少為欄位建立屬 性的時間
自動屬性
屬性是訪問物件狀態的首選方式,因為它們禁止外部程式碼訪問物件內部的資料儲存機制的實現,還對內部資料的訪問方式施加了更多控制
一般以非常標準的方式定義屬性,即透過一個公共屬性來直接訪問一個私有成員
對於自動屬性,可以使用簡化的語法宣告屬性,C#編譯器會自動新增未鍵入的內容,更確切的說,編譯器會宣告一個用於儲存屬性的私有欄位,並在屬性的get
和set
塊中使用該欄位
//會定義一個自動屬性
public int MyIntProp { get; set; }
按照通常的方式定義屬性的可訪問性、型別和名稱,但沒有給get
和set
訪問器提供實現程式碼。這些塊的實現程式碼和底層的欄位都由編譯器提供
[!tip]
輸入prop
後按Tab鍵兩次,就可以自動建立public int MyProperty {get; set;}
使用自由屬性時,只能透過屬性訪問資料,不能透過底層的私有欄位來訪問,因為不知道底層私有欄位的名稱,該名稱是在編譯期間定義的。但這並不是一個真正意義上的限制,因為可以直接使用屬性名
自動屬性的唯一限制是它們必須包含get
和set
訪問器,無法使用這種方式定義只讀或只寫屬性。但可以改變這些訪問器的可訪問性。例如,可採用如下方式建立一個外部只讀屬性
//只能在類定義的程式碼中訪問該屬性的值
public int MyIntProp { get; private set; }
C#6引入了只有get
訪問器的自動屬性和自動屬性的初始化器。不變資料型別的簡單定義是:一旦建立,就不會改變狀態。使用不變的資料型別有很多優點,比如簡化了併發程式設計和執行緒的同步
//只有get訪問器的自動屬性
public int MyIntProp { get; }
//自動屬性的初始化
public int MyIntProp { get; } = 9;
隱藏基類方法
當從基類繼承一個非抽象成員時,也就繼承了其實現程式碼,如果繼承的成員是虛擬的,就可以使用override
關鍵字重寫這段實現程式碼。無論繼承成員是否為虛擬,都可以隱藏這些實現程式碼。無論繼承的成員是否為虛擬,都可以隱藏這些實現程式碼
public class BaseClass
{
public void DoSomething()
{
//Base implementation.
}
}
public class DerivedClass : BaseClass
{
public void DoSomething()
{
//Derived class implementation, hides base implementation.
}
}
這段程式碼可以正常執行,但會生成一個警告,說明隱藏了一個基類成員,如果確實要隱藏該成員,可以使用new
關鍵字顯式地表明意圖
new public void DoSomething()
{
//Derived class implementation, hides base implementation.
}
其工作方式是完全相同的,但不會顯示警告
注意隱藏基類成員和重寫它們的區別
-
隱藏基類的實現程式碼,基類的實現依然可以被訪問,取決於如何訪問這個成員。若透過派生類的例項訪問,則呼叫的是派生類中隱藏的新實現;若基類中有其他方式可以訪問這個成員,則透過這種方式仍能訪問到基類的原始實現
-
重寫方法將替代基類中的實現程式碼,透過基類型別的引用呼叫該虛方法時,實際執行的是派生類中重寫的方法。但在派生類內部還是可以直接訪問基類中被重寫的方法
/*隱藏基類*/
class Test
{
public class BaseClass { public virtual void DoSomething() => WriteLine("Base imp"); }
public class DerivedClass : BaseClass { new public void DoSomething() => WriteLine("Derived imp"); }
static void Main(string[] args)
{
DerivedClass myObj = new DerivedClass();
BaseClass BaseObj;
BaseObj = myObj;
BaseObj.DoSomething();
//結果:Base imp
//基類方法不必是virtual,結果仍相同
}
/*重寫基類*/
class Test
{
public class BaseClass { public virtual void DoSomething() => WriteLine("Base imp"); }
public class DerivedClass : BaseClass { public override void DoSomething() => WriteLine("Derived imp"); }
static void Main(string[] args)
{
DerivedClass myObj = new DerivedClass();
BaseClass BaseObj;
BaseObj = myObj;
BaseObj.DoSomething();
//結果:Derived imp
//基類中成員被宣告為virtual或abstract即可在派生類中重寫
}
呼叫重寫或隱藏的基類方法
無論重寫/隱藏成員,都可以在派生類的內部訪問基類成員
這在許多情況下都是很有用的:
- 要對派生類的使用者隱藏繼承的公共成員,但仍能在類中訪問其功能
- 要給繼承的虛擬成員新增實現程式碼,而不是簡單地用重寫的新實現程式碼替換它
可使用base
關鍵字,它表示包含在派生類中的基類的實現程式碼
public class BaseDerivedClass
{
public virtual void DoSomething()
{
// Base implementation.
}
}
public class DerivedClass : BaseDerivedClass
{
public override void DoSomething()
{
// Derived class implementation, extends base class implementation.
base.DoSomething();
// More derived class implementation.
}
}
在DerivedClass
包含的DoSomething()
方法中,執行包含在BaseDerivedClass
中的DoSomething()
版本。base
使用的是物件例項,base
關鍵字不能用於訪問非虛方法、靜態方法或私有成員
也可以使用this
關鍵字,this
也可以用在類成員內部,也引用物件例項,,this
引用的是當前的物件例項,因此不能在靜態成員中使用this
關鍵字,因為靜態成員不是物件例項的一部分
this
關鍵字最常用的功能是把當前物件例項的引用傳遞給一個方法
public void doSomething()
{
TargetClass myObj = new TargetClass();
myObj.DoSomethingWith(this);
/*this的型別與包含上述方法的類相容。這個引數型別可以是類的型別、由這個類繼承的類型別,或者由這個類或System.Object實現的一個介面*/
}
this
關鍵字的另一個常見用法是限定區域性型別的成員
public class MyClass
{
private int someData;
public int SomeData
{
get
{
return this.someData;
}
}
}
許多開發人員都喜歡這個語法,它可以用於任意成員型別,因為可以一眼看出引用的是成員,而不是區域性變數
巢狀的型別定義
除了在名稱空間中定義型別,還可以在其他類中定義它們。這樣就可以在定義中使用各種訪問修飾符,也可以使用new
關鍵字來隱藏繼承於基類的型別定義
public class MyClass
{
public class MyNestedClass
{
public int NestedClassField;
}
}
//在MyClass的外部例項化myNestedClass,必須限定名稱
MyClass.MyNestedClass myObj = new MyClass.MyNestedClass();
using System;
using static System.Console;
namespace Test
{
public class ClassA
{
//私有屬性
private int State = -1;
//只讀屬性
public int OnlyReadState { get { return State; } }
public class ClassB
{
//巢狀類可以訪問包含它類的底層欄位,即使它是一個私有欄位
//因此仍然可以修改私有屬性的值
public void SetPrivateState(ClassA target, int newState) { target.State = newState; }
}
}
class Program
{
static void Main(string[] args)
{
ClassA myObject = new ClassA();
WriteLine($"myObject.State = {myObject.OnlyReadState}");
ClassA.ClassB myOtherObject = new ClassA.ClassB();
myOtherObject.SetPrivateState(myObject, 999);
WriteLine($"myObject.State = {myObject.OnlyReadState}");
}
}
}
介面的實現
介面成員的定義與類定義相似,但具有幾個重要區別:
- 不允許使用訪問修飾符,所有介面成員都是隱式公共的
- 介面成員不包含程式碼體,需要在實現該介面的類或結構中編寫
- 介面不能定義欄位成員
- 不能用關鍵字
static、virtual、abstract、sealed
來定義介面成員 - 禁止型別定義成員
要隱藏基介面中繼承的成員,和隱藏繼承的類成員一樣使用關鍵字new
定義它們
介面中定義的屬性可以定義訪問器get
和set
中的哪一個或都用於該屬性
介面沒有指定應如何儲存屬性資料,介面不能指定欄位,例如用於儲存屬性資料的欄位,介面和類一樣可以定義為類成員,但不能定義為其它介面的成員,因為介面不能包含型別定義
在類中實現介面
實現介面的類必須包含該介面所有成員的實現程式碼,且必須匹配指定的簽名,包括匹配指定的get
和set
,且必須是公共的
public interface IMyInterface
{
void DoSomething();
void DoSomethingElse();
}
public class MyClass : IMyInterface
{
public void DoSomething() { }
public void DoSomethingElse() { }
}
可使用關鍵字virtual
或abstract
來實現介面成員,但不能使用static
或const
。可以在基類上實現介面成員
public interface IMyInterface
{
void DoSomething();
void DoSomethingElse();
}
public class MyBaseClass
{
public void DoSomething() { }
}
public class MyDerivedClass : MyBaseClass, IMyInterface
{
//基類實現了介面的一個成員,因此會繼承過來,可以不用實現
public void DoSomethingElse() { }
}
繼承一個實現給定介面的基類,就意味著派生類隱式地支援這個介面
public interface IMyInterface
{
void DoSomething();
void DoSomethingElse();
}
public class MyBaseClass : IMyInterface
{
public virtual void DoSomething() { }
public virtual void DoSomethingElse() { }
}
public class MyDerivedClass : MyBaseClass
{
public override void DoSomething() { }
}
在基類中把實現程式碼定義為virtual
,派生類就可以可選的使用override
關鍵字來重寫實現程式碼,而不是隱藏它們
類顯式實現介面成員
如果由類顯式地實現介面成員,就只能透過介面來訪問該成員,不能桶過類來訪問,隱式成員可以透過類和介面來訪問
class Test{
interface IAnimal
{
void Speak();
}
public class Dog : IAnimal
{
//隱式實現IAnimal介面的Speak方法
public void Speak()
{
Console.WriteLine("Woof!");
}
}
public static void Main()
{
Dog dog = new Dog();
dog.Speak(); //輸出 "Woof!"
}
}
class Test{
interface IAnimal
{
void Speak();
}
public class Cat : IAnimal
{
//顯式實現IAnimal介面的Speak方法
void IAnimal.Speak()
{
Console.WriteLine("Meow!");
}
}
public static void Main()
{
Cat cat = new Cat();
((IAnimal)cat).Speak(); //輸出Meow!
//cat.Speak()會報錯
}
}
在顯式實現的情況下,Cat
類自身並沒有名為Speak
的公共成員,只有透過型別轉換為IAnimal
介面後才能呼叫到Speak
方法
其他屬性訪問器
如果在定義屬性的介面中只包含set
,就可給類中的屬性新增get
,反之亦然。但只有隱式實現介面時 才能這麼做。
大多數時候,都想讓所新增的訪問器的可訪問修飾符比介面中定義的訪問器的可訪問修飾符更嚴格。因為按照定義,介面定義的訪問器是公共的,也就是說,只能新增非公共的訪問器
如果將新新增的訪問器定義為公共的,那麼能夠訪問實現該介面的類的程式碼也可以訪問該訪問器。但是隻能訪問介面的程式碼就不能訪問該訪問器
部分類定義
如果所建立的類包含一種型別或其他型別的許多成員時,就很容易引起混淆,程式碼檔案也比較長。這時就可以使用#region
和#endregion
來給程式碼定義區域,就可以摺疊和展開各個程式碼區,使程式碼更具可讀性
可按這種方式巢狀各個區域,這樣一些區域就只能在包含它們的區域被展開後才能看到
另一種方法是使用部分類定義,把類的定義放在多個檔案中,例如可將欄位、屬性和建構函式放在一個檔案中,而把方法放在另一個檔案中。在包含部分類定義的每個檔案中對類使用partial
關鍵字即可
如果使用部分類定義,partial
關鍵字就必須出現在包含部分類定義的每個檔案的與此相同的位置
對於部分類,要注意的一點是:應用於部分類的介面也會應用於整個類
public partial class MyClass : IMyInterface1 { ... }
public partial class MyClass : IMyInterface2 { ... }
//和下面是等價的
public class MyClass : IMyInterface1, IMyInterface2 { ... }
基類可以在多個定義檔案中指定,但必須是同一個基類,因為C#中,類只能繼承一個基類
部分方法定義
部部分方法在一個部分類中定義,在另一個部分類中實現。在這兩個部分類中,都要使用partial
關鍵字
部分方法可以是靜態,但它們總是私有的,且不能有返回值,它們只可以使用ref
引數,部分方法也不能使用virtual、abstract、override、new、sealed、extern
修飾符
部分方法的重要性體現在編譯程式碼時,而不是使用程式碼時
using static System.Console;
class Test
{
public partial class MyClass
{
partial void DoSomethingElse();
public void DoSomething()
{
WriteLine("DoSomething() execution started.");
DoSomethingElse();
WriteLine("DoSomething() execution finished.");
}
}
public partial class MyClass
{
partial void DoSomethingElse() => WriteLine("DoSomethingElse() called.");
}
public static void Main()
{
MyClass Object= new();//簡化方式
Object.DoSomething();
}
}
/*output:
DoSomething() execution started.
DoSomethingElse() called.
DoSomething() execution finished.*/
刪除部分類的實現程式碼,輸出就如下所示:
DoSomething() execution started.
DoSomething() execution finished.
編譯程式碼時,如果程式碼包含一個沒有實現程式碼的部分方法,編譯器會完全刪除該方法,還會刪除對該方法的所有呼叫。執行程式碼時,不會檢查實現程式碼,因為沒有要檢查的方法呼叫。這會略微提高效能
與部分類一樣,在定製自動生成的程式碼或設計器建立的程式碼時,部分方法很有用。設計器會宣告部分方法,根據具體情形選擇是否實現它。如果不實現它,就不會影響效能,因為在編譯過的程式碼中並不存在該方法
示例應用程式
開發一個類模組,以便在後續章節中使用,該類模組包含兩個類:
Card
:表示一張標準的撲克牌,包含梅花、方塊、紅心和黑桃,其順序是從A到KDeck
:表示一副完整的52張撲克牌,在撲克牌中可以按照位置訪問各張牌,並可以洗牌
規劃應用程式
Card
類基本是由兩個只讀欄位suit
和rank
的容器,欄位指定為只讀的原因是“空白”的牌是沒有意義的,牌在建立好後也不能修改。把預設的建構函式指定為私有,並提供另一個建構函式,使用給定的suit
和rank
建立一副撲克牌
此外Card
類要重寫System.Object
的ToString()
方法,這樣才能獲得人們可以理解的字串來表示撲克牌。為使編碼簡單一些,為兩個欄位suit
和rank
提供列舉
Deck
類包含52個Card
物件,使用簡單的陣列型別,這個陣列不能直接訪問,對Card
物件的訪問要透過GetCaed()
方法來實現,該方法返回指定索引的Card
物件,這個類有一個Shuffle()
方法,用於重新排列陣列中的牌
編寫類庫
可以自己手動編寫,也可以藉助vs的類圖來快速設計,以下為使用類圖工具箱設計自動生成的程式碼:
//Suit.cs檔案
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CardLib
{
public enum Suit
{
Club,
Diamond,
Heart,
Spade
}
}
//Rank.cs檔案
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace CardLib
{
public enum Rank
{
Ace = 1,
Deuce,
Three,
Four,
Five,
Six,
Seven,
Eight,
Nine,
Ten,
Jack,
Queen,
King
}
}
新增Card類
//Card.cs檔案
namespace CardLib
{
public class Card
{
public readonly Suit suit;
public readonly Rank rank;
public Card(Suit newSuit, Rank newRank)
{
suit = newSuit;
rank = newRank;
}
private Card()
{
}
//重寫的ToString()方法將已儲存的列舉值的字串表示寫入到返回 的字串中,非預設的建構函式初始化suit和rank欄位的值
public override string ToString()
{
return "The" + rank + "of" + suit + "s";
}
}
}
新增Deck類
namespace CardLib
{
public class Deck
{
//私有成員變數陣列,儲存撲克牌物件
private Card[] cards;
//建構函式,在例項化Deck類時自動呼叫,初始化一副完整的撲克牌
public Deck()
{
cards = new Card[52];
//雙層迴圈遍歷4種花色和13種點數,生成所有牌並存入cards
for (var suitVal = 0; suitVal < 4; ++suitVal)
{
for (var rankVal = 0; rankVal < 13; ++rankVal)
{
//每種花色佔13個位置,因此需要將花色值*13再加上點數值來得到正確的陣列下標
//傳入引數分別轉換為列舉型別的花色值和點數值
cards[suitVal * 13 + rankVal] = new Card((Suit)suitVal, (Rank)rankVal);
}
}
}
//返回cards陣列中對應位置的card物件
public Card GetCard(int cardNum)
{
//檢查索引是否在有效範圍
if (cardNum >= 0 && cardNum <= 51)
return cards[cardNum];
else
throw (new System.ArgumentOutOfRangeException("cardNum", cardNum, "Value must be between 0 and 51."));
}
//用於對當前牌堆進行隨機洗牌
public void Shuffle()
{
//臨時儲存打亂順序後的撲克牌
Card[] newDeck = new Card[52];
//記錄新陣列中的每個位置是否已經分配牌
bool[] assigned = new bool[52];
//建立Random物件,用於生成隨機索引
Random sourceGen = new Random();
//遍歷原陣列的所有元素,將它們隨機放入newDeck中
for (int i = 0; i < 52; i++)
{
int destCard = 0;
bool foundCard = false;
//迴圈查詢未被分配的隨機位置,直到找到為止
while (!foundCard)
{
//生成一個0到51之間的隨機數作為目標索引
destCard = sourceGen.Next(52);
//檢查目標索引是否已佔用,若未佔用,則跳出迴圈
if (!assigned[destCard])
foundCard = true;
}
//將找到的位置標記為已分配,並從原陣列複製相應的Card物件至新陣列
assigned[destCard] = true;
newDeck[destCard] = cards[i];
}
//當所有牌都已隨機分配後,將新陣列的內容複製回原陣列,完成洗牌操作
newDeck.CopyTo(cards, 0);
}
}
}
這不是完成該任務的最高效方式,因為生成的許多隨機數都可能找不到空位置以複製撲克牌
然後新建一個控制檯應用程式,對它新增一個對類庫專案CardLib
的引用。因為新專案是建立的第二個專案,所以還需要指定該專案是解決方法的啟動專案
//新專案主檔案程式碼
using static System.Console;
using CardLib;
namespace CardClient
{
internal class Program
{
private static void Main(string[] args)
{
Deck myDeck = new Deck();
myDeck.Shuffle();
for (int i = 0; i < 52; i++)
{
Card tempCard = myDeck.GetCard(i);
Write(tempCard.ToString());
if (i != 51) Write("\n");
else WriteLine();
}
}
}
}
集合、比較和轉換
集合
使用陣列可以建立包含許多物件或值的變數型別,但陣列有一定的限制,最大的限制就是一旦建立好陣列,它們的大小就不可改變
C#中陣列實現為System.Array
類的例項,它們只是集合類中的一種型別。集合類一般用於處理物件列表,其功能比簡單陣列要多,功能大多是透過實現System.Collections
名稱 空間中的介面而獲得
集合的功能包括基本功能都可以透過介面來實現,所以不僅可以使用基本集合類,例如System.Array
,還可以建立自己的定製集合類。
這些集合可以專用於要列舉的物件(即要從中建立集合的物件)。這麼做的一個優點是定製的集合類可以是強型別化的。也就是說,從集合中提取項時,不需要把它們轉換為正確型別。另一個優點是提供專用的方法,例如,可以提供獲得項子集的快捷方法
System.Collections
名稱空間中的幾個介面提供了基本的集合功能:
-
IEnumerable
可以迭代集合中的項 -
ICollection
(繼承於IEnumerable
)可以獲取集合中項的個數,並能把項複製到一個簡單的陣列型別中 -
IList
(繼承於IEnumerable
和ICollection
)提供了集合的項列表,允許訪問這些項,並提供其他一些與項列表相關的基本功能 -
IDictionary
(繼承於IEnumerable
和ICollection
)類似於IList
,但提供了可透過鍵值而不是索引訪問的項列表
System.Array
類實現了IList、ICollection、IEnumerable
,但不支援IList
的一些更高階功能,它表示大小固定的項列表
使用集合
Systems.Collections
名稱空間中的類System.Collections.ArrayList
也實現了IList、ICollection、IEnumerable
介面,但實現方式比System.Array
更復雜。陣列的大小是固定不變的,而這個類可以用於表示大小可變的項列表
//Animal.cs檔案
using static System.Console;
namespace arrayANDadvancedSet
{
public abstract class Animal
{
//受保護name欄位用於儲存動物名稱
protected string name;
//公共屬性,提供對name欄位的訪問與修改
public string Name
{
get { return name; }
set { name = value; }
}
//預設建構函式,表示未指定名稱
public Animal()
{
name = "The animal with no name";
}
//帶引數建構函式,根據引數設定動物名稱
public Animal(string newName)
{
name = newName;
}
//輸出已餵食的動物名
public void Feed() => WriteLine($"{name} has been fed.");
}
}
//Animals.cs檔案,為了簡潔,把Cow和Chicken放到了一起,書並沒有這樣做
using static System.Console;
namespace arrayANDadvancedSet
{
public class Cow : Animal
{
//例項方法Milk,輸出奶牛擠奶的資訊
public void Milk() => WriteLine($"{name} has been milked.");
//Cow類建構函式,呼叫基類Animal的帶引數建構函式
public Cow(string newName) : base(newName)
{
}
}
public class Chicken : Animal
{
//例項方法LayEgg,輸出母雞下蛋的資訊
public void LayEgg() => WriteLine($"{name} has laid an egg.");
//Chicken類建構函式,呼叫基類Animal的帶引數建構函式
public Chicken(string newName) : base(newName)
{
}
}
}
//Program.cs檔案
using System.Collections;
using static System.Console;
namespace arrayANDadvancedSet
{
internal class Program
{
private static void Main()
{
//輸出建立Array型別集合的資訊並建立,大小為2
WriteLine("Create an Array type collection of Animal objects and use it:");
Animal[] animalArray = new Animal[2];
//建立並例項化一個Cow物件和一個Chicken物件,新增到陣列中
Cow myCow1 = new Cow("Lea");
animalArray[0] = myCow1;
animalArray[1] = new Chicken("Noa");
//遍歷Array輸出每種動物的詳細資訊
foreach (Animal myAnimal in animalArray)
{
WriteLine($"New {myAnimal.ToString()} object added to Array collection, Name = {myAnimal.Name}");
}
//輸出Array中動物數量
WriteLine($"Array collection contains {animalArray.Length} objects.");
//呼叫動物方法,第一個元素Cow被餵食,第二個元素Chinken下蛋
animalArray[0].Feed();
((Chicken)animalArray[1]).LayEgg();
WriteLine();
//輸出建立ArrayList型別集合的資訊並建立
WriteLine("Create an ArrayList type collection of Animal objects and use it:");
ArrayList animalArrayList = new ArrayList();
//向ArrayList中新增一個Cow物件和一個Chicken物件
Cow myCow2 = new Cow("Rual");
animalArrayList.Add(myCow2);
animalArrayList.Add(new Chicken("Andrea"));
//遍歷ArrayList輸出每種動物的詳細資訊
foreach (Animal myAnimal in animalArrayList)
{
WriteLine($"New {myAnimal.ToString()} object added to ArrayList collection, Name = {myAnimal.Name}");
}
//輸出ArrayList中動物數量
WriteLine($"ArrayList collection contains {animalArrayList.Count} objects.");
//呼叫動物方法,第一個元素Cow被餵食,第二個元素Chinken下蛋
((Animal)animalArrayList[0]).Feed();
((Chicken)animalArrayList[1]).LayEgg();
WriteLine();
//額外操作,移除第一個元素,餵食第二個元素
WriteLine("Additional manipulation of ArrayList:");
animalArrayList.RemoveAt(0);
((Animal)animalArrayList[0]).Feed();
//將animal的內容新增到animalArrayList,讓第三個元素下單
animalArrayList.AddRange(animalArray);
((Chicken)animalArrayList[2]).LayEgg();
//輸出原始Cow物件在animalArrayList中的索引
WriteLine($"The animal called {myCow1.Name} is at index {animalArrayList.IndexOf(myCow1)}.");
//修改原始Cow物件的名字的名字,然後輸出新名字
myCow1.Name = "Mary";
WriteLine($"The animal is now called {((Animal)animalArrayList[1]).Name}.");
}
}
}
這個示例建立了兩個物件集合,第一個集合使用System.Array
類,這是一個簡單陣列,第二個集合使用System.Collections.ArrayList
類。這兩個集合都是Animal
物件,在Animal.cs
中定義。Animal
類是抽象類,所以不能進行例項化。但透過多型性可使集合中的項成為派生於Animal
類的Cow
和Chicken
類例項
有幾個處理操作可以應用到Array
和ArrayList
集合上,但它們的語法略有區別。也有一些操作只能使用更高階的ArrayList
型別
//簡單陣列必須使用固定大小來初始化陣列才能使用
Animal[] animalArray = new Animal[2];
//而ArrayList集合不需要初始化其大小
ArrayList animalArrayList = new ArrayList();
陣列是引用型別,所以用一個長度初始化陣列並沒有初始化它所包含的項,要使用一個指定的項還需初始化
Cow myCow1 = new Cow("Lea");
animalArray[0] = myCow1;
animalArray[1] = new Chicken("Noa");
而ArrayList
集合沒有現成的項,也沒有null
引用的項。這樣就不能以相同的方式給索引賦予新例項,使用Add()
方法新增新項
Cow myCow2 = new Cow("Rual");
animalArrayList.Add(myCow2);
animalArrayList.Add(new Chicken("Andrea"));
以這種方式新增項之後,就可以使用與陣列相同的語法改寫該項
nimalArrayList[0] = new Cow("Alma");
使用foreach
結構迭代一個陣列是可以的,因為System.Array
類實現了IEnumerable
介面,這個介面的唯一方法GetEnumerator()
可以迭代集合中的各項
//它們使用foreach的語法是相同的
foreach (Animal myAnimal in animalArray)
foreach (Animal myAnimal in animalArrayList)
陣列使用Length
屬性輸出陣列中個數,而ArrayList
集合使用Count
屬性,該屬性是ICollection
介面的一部分
//Array
WriteLine($"Array collection contains {animalArray.Length} objects.");
//ArrayList
WriteLine($"ArrayList collection contains {animalArrayList.Count} objects.");
如果不能訪問集合(無論是簡單陣列還是較複雜的集合中的項),它們就沒有什麼用途。簡單陣列是強型別化的,可以直接訪問它們所包含的項型別,所以可以直接呼叫項的方法:
animalArray[0].Feed();
陣列型別是抽象型別Animal
,因此不能直接呼叫由派生類提供的方法,而必須使用資料型別轉換
((Chicken)animalArray[1]).LayEgg();
ArrayList
集合是System.Object
物件的集合(透過多型性賦給Animal
物件),所以必須對所有的項進行資料型別轉換
((Animal)animalArrayList[0]).Feed(); ((Chicken)animalArrayList[1]).LayEgg();
ArrayList
集合比Array
集合多出一些功能,可以使用Remove()
和RemoveAt()
方法刪除項,它們分別根據項的引用或索引從陣列中刪除項
animalArrayList.Remove(myCow2);
animalArrayList.RemoveAt(0);
ArrayList
集合可以用AddRange()
方法一次新增好幾項。這個方法接 受帶有ICollection
介面的任意物件,包括前面的程式碼所建立的 animalArray
陣列
animalArrayList.AddRange(animalArray);
AddRange()
方法不是ArrayList
提供的任何介面的一部分。這個方法專用於ArrayList
類,證實了可以在集合類中執行定製操作。
該類還提供了其他方法,如InsertRange()
,它可以把陣列物件插入到列表中的任何位置,還有用於排序和重新排序陣列的方法
定義集合
建立自己的強型別化的集合,一種方式是手動實現需要的方法,但這樣較耗時間,在某些情況下也非常複雜。可以從一個類中派生自己的集合,例如System.Collections.CollectionBase
類,這個抽象類提供了集合類的大量實現程式碼。這是推薦使用的方式
CollectionBase
類有介面IEnumerable、ICollection、IList
,但只提供了一些必要的實現程式碼,主要是IList的Clear()
和RemoveAt()
方法,以及ICollection
的Count
屬性。如果要使用提供的功能,就需要自己實現其他程式碼
CollectionBase
提供了兩個受保護的屬性,它們可以訪問儲存的物件本身。可以使用List、InnerList
,List
可以透過IList
介面訪問項,InnerList
則是用於儲存項的ArrayList
物件
例如,儲存Animal
物件的集合類定義可以如下:
public class Animals : CollectionBase
{
public void Add(Animal newAnimal)
{
List.Add(newAnimal);
}
public void Remove(Animal oldAnimal)
{
List.Remove(oldAnimal);
}
public Animals() {}
}
Add()
和Remove()
方法實現為強型別化的方法,使用IList
介面中用於訪問項的標準Add()
方法。這些方法現在只用於處理Animal
類或派生於Animal
的類,而前面的ArrayList
實現程式碼可處理任何物件
CollectionBase
類可以對派生的集合使用foreach
語法
WriteLine("Using custom collection class Animals:");
Animals animalCollection = new Animals();
animalCollection.Add(new Cow("Lea"));
foreach (Animal myAnimal in animalCollection)
{
WriteLine($"New { myAnimal.ToString()} object added to custom " + $"collection, Name = {myAnimal.Name}");
}
但不能使用下面的程式碼:
animalCollection[0].Feed();
要以這種方式透過索引來訪問項,就需要使用索引符
索引符
索引符indexer是一種特殊型別的屬性,可以把它新增到一個類中,以提供類似於陣列的訪問。可透過索引符提供更復雜的訪問,因為可以用方括號語法和使用複雜的引數型別,它最常見的一個用法是對項實現簡單的數字索引
在Animal
物件的Animals
集合中新增一個索引符
public class Animals : CollectionBase
{
... public Animal this[int animalIndex]
{
get { return (Animal)List[animalIndex]; }
set { List[animalIndex] = value; }
}
}
this
關鍵字需要和方括號中的引數一起使用,除此之外,索引符與其他屬性十分類似。在訪問索引符時,將使用物件名,後跟放在方括號中的索引引數
return (Animal)List[animalIndex];
對List
屬性使用一個索引符,即在IList
介面上,可以訪問CollectionBase
中的ArrayList
。ArrayList
儲存了項。這裡需要進行顯式資料型別轉換,因為IList.List
屬性返回一個System.Object
物件
為索引符定義了一個型別,使用該索引符定義了一個型別,使用該索引符訪問某項時,就可以得到這個型別,這種強型別化功能就可以編寫下述程式碼
animalCollection[0].Feed();
//而不是:
((Animal)animalCollection[0]).Feed();
鍵控集合和IDictionary
除IList
介面外,集合還可以實現類似的IDictionary
介面,允許項 透過鍵值(如字串名)進行索引,而不是透過索引。這也可以使用索引符來完成,但這次的索引符引數是與儲存的項相關聯的鍵,而不是int
索引,這樣集合就更便於使用者使用了
與索引的集合一樣,可以使用一個基類簡化IDictionary
介面的實現,這個基類就是DictionaryBase
,它也實現IEnumerable
和ICollection
,提供了對任何集合都相同的基本集合處理功能
DictionaryBase
與CollectionBase
一樣,實現透過其支援的介面獲得一些成員(不是全部成員)。DictionaryBase
也實現Clear
和Count
成員,但不實現RemoveAt()
。因為RemoveAt()
是IList
介面中的一 個方法,不是IDictionary
介面中的一個方法,但IDictionary
有一個Remove()
方法,這是一個應在基於DictionaryBase
的定製集合類上實現的方法
下面的程式碼是Animals
類的另一個版本,該類派生於DictionaryBase
。下面程式碼包括Add()、Remove()
和一個透過鍵訪問的索引符的實現程式碼
public class Animals : DictionaryBase
{
//引數是鍵值
//繼承於IDictionary介面,有自己的Add()方法,該方法帶有兩個object引數
public void Add(string newID, Animal newAnimal)
{
Dictionary.Add(newID, newAnimal);
}
//以一個鍵作為引數,與指定鍵值對應的項被刪除
public void Remove(string animalID)
{
Dictionary.Remove(animalID);
}
public Animals() { }
//索引符使用一個字串鍵值,而不是索引,用於透過Dictionary的繼承成員來訪問儲存的項,仍需進行資料型別轉換
public Animal this[string animalID]
{
get { return (Animal)Dictionary[animalID]; }
set { Dictionary[animalID] = value; }
}
}
基於DictionaryBase
的集合和基於CollectionBase
的集合之間的另一個區別是foreach
的工作方式稍有區別
foreach (Animal myAnimal in animalCollection)
{ WriteLine($"New {myAnimal.ToString()} object added to custom collection, Name = {myAnimal.Name}"); }
//等價基於CollectionBase的集合的程式碼:
foreach (DictionaryEntry myEntry in animalCollection) { WriteLine($"New {myEntry.Value.ToString()} object added to custom collection, Name = {((Animal)myEntry.Value).Name}"); }
有許多方式可以重寫這段程式碼,以便直接透過foreach
訪問Animal
物件,最簡單的方式是實現一個迭代器
迭代器
IEnumerable
介面允許使用foreach
迴圈。在foreach
迴圈中並不是只能使用集合類,在foreach
迴圈中使用定製類通常有很多優點
但是重寫使用foreach
迴圈的方式或者提供定製的實現方式並不簡單。一個較簡單的替代方法是使用迭代器,使用迭代器將有效地自動生成許多程式碼,正確地完成所有任務
迭代器的定義:它是一個程式碼塊,按順序提供要在foreach
塊中使用的所有值。一般情況下,該程式碼塊是一個方法,但也可以使用屬性訪問器和其他程式碼塊作為迭代器
無論程式碼塊是什麼,其返回型別都是有限制的,這個返回型別與所列舉的物件型別不同,例如在表示Animal
物件集合的類中,迭代器返回型別不可能是Animal
,兩種可能的返回型別是前面提到的介面型別IEnumerable
和IEnumerator
使用這兩種型別的場合:
- 如果要迭代一個類,可使用方法
GetEnumerator()
,其返回型別是IEnumerator
- 如果要迭代一個類成員,例如一個方法,則使用
IEnumerable
在迭代器塊中,使用yield
關鍵字選擇要在foreach
迴圈中使用的值
yield return<value>;
使用迭代器:
using static System.Console;
using System.Collections;
class Test
{
public static IEnumerable SimpleList()
{
yield return "string 1";
yield return "string 2";
yield return "string 3";
}
static void Main(string[] args)
{
foreach (string item in SimpleList())
WriteLine(item);
}
}
此處,靜態方法SimpleList
就是迭代器塊,因為是方法,所以使用IEnumberable
返回型別,使用yield
關鍵字為使用它的foreach
快提供了3個值,依次輸出到螢幕上
實際上並沒有返回string
型別的項,而是返回object
型別的值,因為object
是所有型別的基類,所以可以從yield
語句中返回任意型別
但編譯器的智慧程度很高,所以可以把返回值解釋為foreach
迴圈需要的任何型別。這裡程式碼需要string
型別的值,如果修改一行yield
程式碼使之返回整數,就會出現一個型別裝換異常
可以使用yield break
將資訊返回給foreach
迴圈的過程,遇到該語句時,迭代器的處理會立即中斷,使用該迭代器的foreach
迴圈也一樣
實現一個迭代器:
//primes.cs檔案
using System.Collections;
namespace Prime
{
//用於表示和生成指定範圍內的所有質數
public class Primes
{
//儲存範圍內質數最小值
private long min;
//儲存範圍內質數最大值
private long max;
//預設建構函式,初始化一個從2到100的質數生成器
public Primes() : this(2, 100) { }
//帶引數建構函式,自定義質數查詢範圍
public Primes(long minimum, long maximum)
{
//最小的質數是2
if (minimum < 2)
min = 2;
else
min = minimum;
max = maximum;
}
//實現IEnumberable介面,提供迭代器以遍歷質數序列
public IEnumerator GetEnumerator()
{
//遍歷所有數
for (long possiblePrime = min; possiblePrime <= max; possiblePrime++)
{
//假定當前數為質數
bool isPrime = true;
//檢查小於或等於其平方根的數作為因子
for (long possibleFactor = 2;
/*如果p可以分解為兩個因數a和b,且a>b,則必定有a<=Sqrt(p)
因為a>Sqrt(p),那麼b=p/a將小於Sqrt(p)
所以只需檢查所有<=平方根的因子即可*/
//能否被2到該數平方根之間的所有數整除,能即素數
possibleFactor <= (long)Math.Floor(Math.Sqrt(possiblePrime));
possibleFactor++)
{
//如果找到可整除因子則不是質數
long remainderAfterDivision = possiblePrime % possibleFactor;
if (remainderAfterDivision == 0)
{
isPrime = false;
break;
}
}
//若是質數則使用yield返回
if (isPrime)
{
yield return possiblePrime;
}
}
}
}
}
//測試檔案
using static System.Console;
using Prime;
class Test
{
static void Main(string[] args)
{
Primes primesFrom2To1000 = new Primes(2, 1000);
foreach (long i in primesFrom2To1000)
Write($"{i} ");
}
}
深度複製
第9章介紹了使用受保護方法System.Object.MemberwiseClone()
進行淺度複製
public class Cloner
{
public int Val;
public Cloner(int newVal)
{
Val = newVal;
}
//MemberwiseClone建立當前物件的一個淺複製
//對於引用型別成員複製引用而不是實際的物件內容
//對於值型別成員則直接複製其值
public object GetCopy() => MemberwiseClone();
}
深度複製:在建立物件的一個副本時,不僅複製了原始物件的所有基本資料型別的成員變數值,同時也複製了引用型別成員變數指向的物件,並且遞迴地對該物件所包含的引用型別成員也進行同樣的複製操作。換句話說,深度複製會生成一個與原物件完全獨立的新物件樹
深度複製:
//簡單類用於儲存整數值
public class Content
{
public int Val;
}
//實現ICloneable介面以支援克隆功能
public class Cloner : ICloneable
{
//定義一個Content型別的成員變數
public Content MyContent = new Content();
//建構函式
public Cloner(int newVal)
{
MyContent.Val = newVal;
}
//實現ICloneable介面的Clone方法,用於建立當前物件的淺度複製
//在該實現中僅對Clone類本身進行復制,沒有遞迴地複製引用型別成員
public object Clone()
{
//建立一個新的Cloner例項,將原Cloner例項中MyContent的Val屬性值傳遞給例項
Cloner clonedCloner = new Cloner(MyContent.Val);
//返回克隆後的Cloner物件,返回object型別
return clonedCloner;
}
}
使用包含在源Cloner
物件中的Content
物件(MyContent
)的Val
欄位,建立一個新Cloner
物件。這個欄位是一個值型別,所以不需要深度複製
如果Cloner
類的MyContent
欄位也需要深度複製,就需要使用下面的程式碼:
public class Cloner : ICloneable
{
public Content MyContent = new Content();
...
public object Clone()
{
//建立一個新的Cloner例項
Cloner clonedCloner = new Cloner();
//呼叫Clone方法進行深度複製,確保內容也被複制一份新的副本
clonedCloner.MyContent = MyContent.Clone();
return clonedCloner;
}
}
為使這段程式碼能正常工作,還需要在Content
類上實現ICloneable
介面
比較
物件之間比較有兩類:
- 型別比較
型別比較確定物件是什麼,或者物件繼承什麼 - 值比較
型別比較
所有的類都從System.Object
中繼承GetType()
方法,該方法和typeof()
運算子一起使用就可以確定物件的型別
if (myObj.GetType() == typeof(MyComplexClass))
//myObj is an instance of the class MyComplexClass.
封箱和拆箱
封箱boxing是把值型別轉換為System.Object
型別或轉換為由值型別實現的介面型別,拆箱unboxing是相反的轉換過程
struct MyStruct
{
public int Val;
}
//可以把這種型別的結構放在object型別的變數中對其封箱
//建立新變數後賦值
MyStruct valType1 = new MyStruct();
valType1.Val = 5;
//然後把它封箱在object型別的變數中
object refType = valType1;
當一個值型別變數被封箱時,實際上會建立一個新的物件例項,並將該值型別變數的值複製到這個新物件中。因此封箱後得到的物件包含的是原值型別的值的一個副本,而不是源值型別變數的引用
封箱後是建立了一個新的物件並儲存了源值的副本,它們的記憶體空間並不相同,修改不會影響對方
[!important]
但要注意,當把一個引用型別賦予物件時,實際上覆制的是對同一記憶體位置的引用,而不是複製整個物件的內容。這意味著新變數和原變數都指向同一個物件例項,修改會互相影響
class MyStruct//一旦改成類,在封箱後修改就會改變源值
{
public int Val;
}
valType1.Val = 6;
MyStruct valType2 = (MyStruct)refType;
WriteLine($"valType2.Val = {valType2.Val}");//6
//如果是struct,那麼拆箱後值還是初始值5
也可以把值型別封裝到介面型別中,只要它們實現這個介面即可。例如假定MyStruct
型別實現IMyInterface
介面
interface IMyInterface {}
struct MyStruct : IMyInterface
{
public int Val;
}
接著把結構封箱到一個IMyInterface
型別中,然後拆箱:
MyStruct valType1 = new MyStruct();
IMyInterface refType = valType1;
MyStruct ValType2 = (MyStruct)refType;
封箱是隱式執行的,但拆箱一個值需要進行顯式資料型別轉換
封箱非常有用,有兩個重要的原因:它允許在項的型別是object
的集合中使用值型別,其次,有一個內部機制允許在值型別上呼叫object
方法
在訪問值型別內容前必須進行拆箱
is運算子
is
運算子用於檢查物件是否為給定型別或是否可轉換為給定型別,如果是返回true
<expression>is<type>
- 如果是類型別,且表示式也是該型別或它繼承該型別或它可以封裝到該型別中,則結果為
true
- 如果是介面型別,且表示式也是該型別或它是實現該介面的型別,則結果為
true
- 如果是值型別,且表示式也是該型別或它可以拆箱到該型別中,則結果為
true
//checker.cs檔案
using System;
using static System.Console;
namespace checker
{
class Checker
{
//Check方法接受一個object型別的引數
public void Check(object param1)
{
if (param1 is ClassA)
WriteLine("Variable can be converted to ClassA.");
else
WriteLine("Variable can't be converted to ClassA.");
if (param1 is IMyInterface)
WriteLine("Variable can be converted to IMyInterface.");
else
WriteLine("Variable can't be converted to IMyInterface.");
if (param1 is MyStruct)
WriteLine("Variable can be converted to MyStruct.");
else
WriteLine("Variable can't be converted to MyStruct.");
}
}
interface IMyInterface { }
class ClassA : IMyInterface { }
class ClassB : IMyInterface { }
class ClassC { }
class ClassD : ClassA { }
struct MyStruct : IMyInterface { }
class Program
{
static void Main(string[] args)
{
//建立Checker類例項
Checker check = new Checker();
ClassA try1 = new ClassA();
ClassB try2 = new ClassB();
ClassC try3 = new ClassC();
ClassD try4 = new ClassD();
MyStruct try5 = new MyStruct();
//將try封箱為object型別
object try6 = try5;
WriteLine("Analyzing ClassA type variable:");
check.Check(try1);
WriteLine("\nAnalyzing ClassB type variable:");
check.Check(try2);
WriteLine("\nAnalyzing ClassC type variable:");
check.Check(try3);
WriteLine("\nAnalyzing ClassD type variable:");
check.Check(try4);
WriteLine("\nAnalyzing MyStruct type variable:");
check.Check(try5);
WriteLine("\nAnalyzing boxed MyStruct type variable:");
check.Check(try6);
}
}
}
如果一個型別沒有繼承一個類,該型別不會與該類相容
MyStruct
型別本身的變數和該變數的封箱變數與MyStruct
相容,因為不能把引用型別轉換為值型別
值比較
運算子過載
透過運算子過載可以對設計的類使用標準運算子,因為在使用特定的引數型別時,為這些運算子提供了自己的實現程式碼,其方式與過載方法相同,也是為同名方法透過不同的引數
可以在運算子過載的實現中執行任何需要的操作
要過載運算子,可給類新增運算子型別成員,它們必須是static
。一些運算子有多種用途,因此要指定要處理多少個運算元,以及這些運算元的型別。
一般情況下,運算元型別和定義運算子的類相同。但也可以定義處理混合型別的運算子
過載+
運算子,可使用下述程式碼:
AddClass1 operator +(AddClass1 op1, AddClass1 op2)
{
AddClass1 returnVal = new AddClass1();
returnVal.val = op1.val + op2.val;
return returnVal;
}
運算子過載和標準靜態方法宣告類似,但使用關鍵字operator
和運算子本身代替方法名,現在使用該類就是相加就是加Val
值
AddClass1 op3 = op1 + op2;
過載所有的二元運算子都是一樣的,一元運算子看起來也是類似的,但只有一個引數:
public static AddClass1 operator -(AddClass1 op1)
{
AddClass1 returnVal = new AddClass1();
//返回其相反數
returnVal.val = -op1.val;
return returnVal;
}
這兩個運算子處理的運算元型別與類相同,返回值也是該型別
class Test
{
public class AddClass1
{
public int val;
public static AddClass3 operator +(AddClass1 op1, AddClass2 op2)
{
AddClass3 returnVal = new AddClass3();
returnVal.val = op1.val + op2.val;
return returnVal;
}
}
public class AddClass2 { public int val; }
public class AddClass3 { public int val; }
public static void Main()
{
AddClass1 op1 = new AddClass1(); op1.val = 5;
AddClass2 op2 = new AddClass2(); op2.val = 5;
AddClass3 op3 = op1 + op2;
WriteLine(op3.val);
}
}
如果把相同的運算子新增到AddClass2
,程式碼就會出錯,因為它弄不清要使用哪個運算子。因此要注意不要把簽名相同的運算子新增到多個存在繼承或包含關係的類中
還要注意,如果混合了型別,運算元的順序必須與運算子過載的引數順序相同。如果使用了過載運算子和順序錯誤的運算元,操作就會失敗
AddClass3 op3 = op2 + op1;//error
當然,可以提供另一個過載運算子和倒序的引數:
public static AddClass3 operator +(AddClass2 op1, AddClass1 op2)
{
AddClass3 returnVal = new AddClass3();
returnVal.val = op1.val + op2.val;
return returnVal;
}
可以過載下列運算子:
- 一元運算子:+,–, !, ~,++,––, true, false
- 二元運算子:+,–,*,/,%, &,|, ^,<<,>>
- 比較運算子:==, !=,<,>,<=,>=
如果過載true和false運算子,就可以在布林表示式中使用類
不能過載賦值運算子,例如+=
,但這些運算子使用與它們對應的簡單運算子,所以不必擔心它們。過載+
意味著+=
如期執行
一些運算子必須成對過載,如果過載>
,就必須過載<
。許多情況下,可以在這些運算子中呼叫其他運算子,以減少需要的程式碼數量和可能發生的錯誤
public static bool operator >=(AddClass1 op1, AddClass1 op2) => (op1.val >= op2.val);
public static bool operator<(AddClass1 op1, AddClass1 op2) => !(op1 >= op2);//這裡使用取反,也可以直接比較
//Also need implementations for<= and > operators.
這同樣適用於==和!=,但對於這些運算子,通常需要重寫Object.Equals()
和Object.GetHashCode()
,因為這兩個函式也可以用於比較物件。重寫這些方法,可以確保無論類的使用者使用什麼技術,都能得到相同的結果。這不太重要,但應增加進來,以保證其完整性
//重寫Equals方法以比較兩個AddClass1例項的val屬性是否相等
public override bool Equals(object op1) => this.val == ((AddClass1)op1).val;
//重寫GetHashCode方法,基於val屬性生成雜湊碼
public override int GetHashCode() => val;
GetHashCode()
可根據其狀態,獲取物件例項的一個唯一int
值
注意Equals()
使用object
型別引數,我們需要使用這個簽名,否則就將過載這個方式,而不是重寫。類的使用者仍可以訪問預設的實現程式碼。這樣就必須使用資料型別轉換得到所需的結果,這常需要使用本章前面討論的is
運算子檢查物件型別
if (op1 is AddClass1)
{
return val == ((AddClass1)op1).val;
}
else
{
throw new ArgumentException($"Cannot compare AddClass1 objects with objects of type {op1.GetType().ToString()}");
}
如果傳給Equals
的運算元型別有誤或不能轉換為正確型別,就會丟擲一個異常,如果只允許對型別完全相同的兩個物件進行比較,就需要對if語句進行修改
if (op1.GetType() == typeof(AddClass1))
IComparable和IComparer介面
這兩個介面是.NET Framework中比較物件的標準方式。這兩個介面之間的差別如下:
IComparable
在要比較的物件的類中實現,可以比較該物件和另一個物件IComparer
在一個單獨的類中實現,可以比較任意兩個物件
一般使用IComparable
給出類的預設比較程式碼,使用其他類給出非預設的比較程式碼
IComparable
提供了一個方法CompareTo()
,該方法接受一個物件,當前物件小於比較物件則返回負數,大於比較物件則返回正數
例如,在實現該方法時,使其可以接受一個Person
物件,以便確定這個人比當前的人更年老還是更年輕。實際上,這個方法返回一個int
,所以也可以確定第二個人與當前的人的年齡差:
if (person1.CompareTo(person2) == 0)
{
WriteLine("Same age");
}
else if (person1.CompareTo(person2) > 0)
{
WriteLine("person 1 is Older");
}
else
{
WriteLine("person1 is Younger");
}
IComparer
也提供一個方法Compare()
。這個方法接受兩個物件, 返回一個整型結果,和ComparerTo()
相同
if (personComparer.Compare(person1, person2) == 0)
{
WriteLine("Same age");
}
else if (personComparer.Compare(person1, person2) > 0)
{
WriteLine("person 1 is Older");
}
else
{
WriteLine("person1 is Younger");
}
提供給這兩種方法的引數是System.Object
型別。這意味著可以比較一個物件與其他任意型別的另一個物件。所以在返回結果之前,通常需要進行某種型別比較,如果使用了錯誤型別會丟擲異常
.NET Framework在Comparer
類上提供了IComparer
介面的預設實現方式,Comparer
位於System.Collections
名稱空間中,可以對簡單型別以及支援IComparable
介面的任意型別進行特定文化的比較
可透過下面的程式碼使用它:
//這裡使用Comparer.Default靜態成員獲取Comparer類的一個例項,接著使用Compare()方法比較前兩個字串
string firstString = "First String";
string secondString = "Second String";
WriteLine($"Comparing '{firstString}' and '{secondString}', " + $"result: {Comparer.Default.Compare(firstString, secondString)}");
int firstNumber = 35;
int secondNumber = 23;
WriteLine($"Comparing '{firstNumber}' and '{ secondNumber }', " + $"result: {Comparer.Default.Compare(firstNumber, secondNumber)}");
Compare
類注意事項:
- 檢查傳給
Comparer.Compare()
的物件,看看它們是否支援IComparable
。如果支援,就使用該實現程式碼 - 允許使用
null
值,它表示“小於”其他任意物件 - 字串根據當前文化來處理。要根據不同的文化或語言處理字串,
Comparer
類必須使用其建構函式進行例項化,以便傳送用於指定所使用的文化的System.Globalization.CultureInfo
物件字串在處理時要區分大小寫。如果要以不區分大小寫的方式來處理它們,就需要使用CaseInsensitiveComparer
類,該類以相同的方式工作
對集合排序
許多集合類可以用物件的預設比較方式進行排序,或者用定製方法來排序
ArrayList
包含方法Sort()
,該方法使用時可不帶引數,此時使用預設的比較方式,也可給它傳IComparer
介面,以比較物件對
給ArrayList
填充了簡單型別時,例如整數或字串,就會進行預設比較。對於自己的類,必須在類定義中實現IComparable
或建立一個支援IComparer
的類來進行比較
System.Collections
名稱空間中的一些類(包括CollectionBase
)都沒有提供排序方法。如果要對派生於這個類的集合排序,就必須多做一些工作,自己給內部的List
集合排序
下面的例項說明如何使用預設和非預設的比較方式給列表排序:
//Person.cs檔案
namespace ListSort
{
//實現IComparable介面以支援排序功能
public class Person : IComparable
{
public string Name;
public int Age;
//建構函式
public Person(string name, int age)
{
Name = name; Age = age;
}
//實現IComparable介面的CompareTo方法,用於比較2個Person物件的年齡大小
//返回值為負數表示當前物件 < 傳入物件,為正數表示當前物件 > 傳入物件多少歲
public int CompareTo(object obj)
{
//檢查傳入物件是否為Person型別
if (obj is Person)
{
//將傳入物件轉換為Person型別以便訪問其Age屬性
Person otherPerson = obj as Person;
//返回差值
return this.Age - otherPerson.Age;
}
else
{ throw new ArgumentException("Object to compare to is not a Person object."); }
}
}
}
//PersonComparerName.cs檔案
using System.Collections;
namespace ListSort
{
//實現IComparer介面用於比較兩個物件
public class PersonComparerName : IComparer
{
//建立一個靜態預設例項方便全域性訪問
public static IComparer Default = new PersonComparerName();
//實現IComparer介面的Compare方法
public int Compare(object x, object y)
{
//檢查傳入的物件是否是Person型別
if (x is Person && y is Person)
{
//將物件轉換為Person型別,然後使用預設Comparer進行Name的比較
return Comparer.Default.Compare(((Person)x).Name, ((Person)y).Name);
}
else
{ throw new ArgumentException("One or both objects to compare are not Person objects."); }
}
}
}
//Program.cs檔案
namespace ListSort
{
//實現IComparable介面以支援排序功能
public class Person : IComparable
{
public string Name;
public int Age;
//建構函式
public Person(string name, int age)
{
Name = name; Age = age;
}
//實現IComparable介面的CompareTo方法,用於比較2個Person物件的年齡大小
//返回值為負數表示當前物件 < 傳入物件,為正數表示當前物件 > 傳入物件多少歲
public int CompareTo(object obj)
{
//檢查傳入物件是否為Person型別
if (obj is Person)
{
//將傳入物件轉換為Person型別以便訪問其Age屬性
Person otherPerson = obj as Person;
//返回差值
return this.Age - otherPerson.Age;
}
else
{ throw new ArgumentException("Object to compare to is not a Person object."); }
}
}
}
轉換
過載轉換運算子
可以定義型別之間的隱式和顯式轉換。如果在不相關的型別之間轉換,例如型別之間沒有繼承關係,也沒有共享介面,就必須這麼做
使用implicit
關鍵字來宣告一個使用者自定義型別的轉換運算子時,編譯器允許自動進行這種轉換
使用explicit
關鍵字宣告的轉換運算子要求必須顯式地使用型別轉換運算子來進行轉換
checked
關鍵字用於顯式啟用整數算術運算和轉換時的溢位檢查
as運算子
expression as type
只適用於下列情況
- expression的型別是type
- expression可以隱式轉換為type型別
- expression可以封箱到type型別中
如果不能從expression轉換到type,表示式結果就是null
泛型
一般情況下新的型別需要額外功能,所以常常需要用到新的集合類,因此建立集合類會花費大量時間,而泛型類是以例項化過程中提供的型別或類為基礎建立的,可以輕易地對物件進行強型別化
CollectionClass items = new CollectionClass();
items.Add(new ItemClass());
//使用以下程式碼:
CollectionClass<ItemClass> items = new CollectionClass<ItemClass>();
items.Add(new ItemClass());
尖括號是把型別引數傳給泛型型別的方式,定義了一個名為CollectionClass
的泛型類,它允許儲存任何與ItemClass
型別相容的物件
泛型不只涉及集合。建立一個泛型類,就可以生成一些方法,它們的簽名可以強型別化為需要的任何型別,該型別甚至可以是值型別或引用型別,處理各自的操作。 還可以把用於例項化泛型類的型別限制為支援某個給定的介面,或派生自某種型別,從而只允許使用型別的一個子集。泛型並不限於類,還可以建立泛型介面、泛型方法(可以在非泛型類上定義),甚至泛型委託。這將極大地提高程式碼的靈活性,正確使用泛型可以顯著縮短開發時間
[!note] C++模板和C#泛型類的一個區別
C++中,編譯器會檢測出在哪裡使用了模板的某個特定型別,然後編譯需要的程式碼來建立這個型別
C#中,所有操作都在執行期間進行
可空型別
泛型使用System.Nullable<T>
型別提供了使值型別為空的一種方式
//這兩個賦值是等價的
System.Nullable<int> nullableInt;
System.Nullable<int> snullableInt = new System.Nullable<int>();
宣告瞭一個變數nullableInt
,可以擁有int
變數能包含的任意值,還可以擁有值null
。和其他變數一樣,不能在初始化之前使用它
if(nullableInt == null)
//還可以使用HasValue型別,這不適用於引用型別
//true非空,false空
if(nullableInt.HasValue)
宣告可空型別變數一般使用下面的語法
int? nullableInt;
int?
是System.Nullable<int>
的縮寫
運算子和可空型別
int? op1 = 5;
//不能直接將一個可空型別與非可空型別進行算術運算
int result = op1 * 2;
//需要進行顯式轉換
int result = (int)op1 * 2;
//或透過Value屬性訪問其值
int result = op1.Value * 2;
??運算子
稱為空結合運算子,是一個二元運算子,允許給可能等於null
的表示式提供另一個值。如果第一個值不是null
,該運算子就等於第一個運算元,否則就等於第二個運算元
//這兩個表示式作用等價
op1 ?? op2
op1 == null ? op2 : op1
op1可以是任意可空表示式,包括引用型別和可空型別
int? op1 = null;
int result = op1 * 2 ?? 5;
//在結果中放入int型別的變數不需要顯式轉換,??運算子會自動處理這個轉換,還可以把??表示式的結果放在int?中
?.運算子
稱為條件成員訪問運算子或空條件運算子,有助於避免繁雜的空值檢查造成的程式碼歧義
class Person
{
public string Name { get; set; }
}
Person person = null;
string name = person?.Name;
//如果person為null,則name也會被賦值為null
如果沒有使用?.
運算子,嘗試訪問person.Name
將會導致NullReferenceException
異常。但使用了?.
後,當 erson
為null
時,name
會被賦予null
值,並且程式碼能夠安全執行下去
空條件運算子的另一個用途是觸發事件
//觸發事件常見方法:
var onChanged = OnChanged;
if (onChanged != null)
{
onChanged(this, args);
}
但這種模式不是執行緒安全的,因為有人會在null
檢查已經完成後退訂最後一個事件處理程式,此時會丟擲異常,使用空條件符可以避免這種情況
//如果OnChanged不為null,則會呼叫它的Invoke方法來觸發事件;若OnChanged為null,整個表示式會被評估為null
OnChanged?.Invoke(this, args);
使用可空型別
using static System.Math;
using static System.Console;
namespace Vector
{
//定義一個表示向量的類
class Vector
{
//向量的極座標屬性:極徑R和極角Theta
public double? R = null;
public double? Theta = null;
//計算並返回極角的弧度值
public double? ThetaRadians
{
get
{
return (Theta * Math.PI / 180.0);//角度轉換為弧度
}
}
//建構函式,根據給定的極徑和極角建立一個新的向量例項
public Vector(double? r, double? theta)
{
//確保極徑非負,並將極角限制在0~360度之間
if (r < 0)
{
r = -r;
theta += 180;
}
theta = theta % 360;
//設定向量的極徑和極角屬性
R = r;
Theta = theta;
}
public static Vector operator +(Vector op1, Vector op2)
{
try
{
//檢查兩個向量的有效性並進行加法運算
double newX = op1.R.Value * Sin(op1.ThetaRadians.Value) + op2.R.Value * Sin(op2.ThetaRadians.Value);
double newY = op1.R.Value * Cos(op1.ThetaRadians.Value) + op2.R.Value * Cos(op2.ThetaRadians.Value);
//計算新向量的極徑和極角
double newR = Sqrt(newX * newX + newY * newY);
double newTheta = Atan2(newX, newY) * 180.0 / PI;
//返回新的向量例項
return new Vector(newR, newTheta);
}
catch
{
//如果有無效資料,則返回一個包含null值的新向量
return new Vector(null, null);
}
}
//一元減法運算子過載,取相反向量
public static Vector operator -(Vector op1) => new Vector(-op1.R, op1.Theta);
//減法運算子過載
public static Vector operator -(Vector op1, Vector op2) => op1 + (-op2);
//重寫ToString方法,以字串形式重寫
public override string ToString()
{
string rString = R.HasValue ? R.ToString() : "null";
string thetaString = Theta.HasValue ? Theta.ToString() : "null";
//返回格式化的字串表示形式
return string.Format($"({rString}, {thetaString})");
}
}
class Program
{
static void Main(string[] args)
{
//獲取輸入向量
Vector v1 = GetVector("vector1");
Vector v2 = GetVector("vector2");
//輸出向量相加和相減的結果
WriteLine($"{v1} + {v2} = {v1 + v2}");
WriteLine($"{v1} - {v2} = {v1 - v2}");
}
//獲取向量極徑和極角的方法
static Vector GetVector(string name)
{
WriteLine($"Input {name} magnitude:");
double? r = GetNullableDouble();
WriteLine($"Input {name} angle (in degrees):");
double? theta = GetNullableDouble();
//建立並轉換為Vector型別返回
return new Vector(r, theta);
}
//獲取輸入的可空雙精度浮點數的方法
static double? GetNullableDouble()
{
double? result;
string userInput = ReadLine();
//嘗試將輸入轉換為double型別
try { result = double.Parse(userInput); }
catch { result = null; }
//如果轉換失敗,返回null,否則返回轉換結果
return result;
}
}
}
System.Collections.Generic名稱空間
該名稱空間包含用於處理集合的泛型型別
//T型別物件的集合
List<T>
//與K型別的鍵值相關的V型別的項的集合
Dictionary<K, V>
List<T>
泛型集合型別更快捷、更便於使用,會自動實現正常情況下需要實現的許多方法
//建立了一個T型別物件的集合
List<T> myCollection = new List<T>();
不需要定義類、實現方法或執行其他操作,可以把List<T>
傳給建構函式,在集合中設定項的起始列表。List<T>
還有一個Item
屬性,允許進行類似於陣列的訪問
T itemAtIndex2 = myCollectionOfT[2];
使用List<T>
:
static void Main(string[] args)
{
/*Animals animalCollection = new Animals();替換為下列程式碼*/
List<Animal> animalCollection = new List<Animal>();
animalCollection.Add(new Cow("Rual"));
animalCollection.Add(new Chicken("Donna"));
foreach (Animal myAnimal in animalCollection)
{
myAnimal.Feed();
}
}
對泛型列表進行排序和搜尋
和普通的介面有些區別,使用泛型介面IComparer<T>
和IComparable<T>
,它們提供了略有區別的、針對特定型別的方法
Comparison<T>
:這個委託型別用於排序方法,其返回型別和引數如下:int method(T objectA, T objectB)
Predicate<T>
:這個委託型別用於搜尋方法,其返回型別和引數如下:bool method(T targetObject)
可以定義任意多個這樣的方法,使用它們實現List<T>
的搜尋和排序方法
Dictionary<K, V>
這個型別可定義鍵/值對的集合,需要例項化兩個型別,分別用於鍵和值,以表示集合中的各項
使用強型別化的Add()
方法新增鍵/值對:
//初始化一個鍵為字串型別、值為整數型別的新字典
Dictionary<string, int> things = new Dictionary<string, int>();
things.Add("Green Things", 29);
things.Add("Blue Things", 94);
things.Add("Yellow Things", 34);
things.Add("Red Things", 52);
things.Add("Brown Things", 27);
可以使用Key
和Values
屬性迭代集合中的鍵和值:
foreach (string key in things.Keys)
{ WriteLine(key); }
foreach (int value in things.Values)
{ WriteLine(value); }
還可以迭代集合中的各個項,把每項作為一個KeyValuePair<K, V>
例項來獲取:
foreach (KeyValuePair<string, int> thing in things)
{
WriteLine($"{thing.Key} = {thing.Value}");
}
對於Dictionary<K, V>
要注意的一點是,每個項的鍵都必須是唯一的 。 如果要新增的項的鍵與已有項的鍵相同,就會丟擲ArgumentException
異常
所以,Dictionary<K, V>
允許把IComparer<K>
介面傳遞給其建構函式。如果要把自己的類用作鍵, 且它們不支援IComparable
或IComparable<K>
介面,或者要使用非預設的過程比較物件,就必須把IComparer<K>
介面傳遞給其建構函式
C#6引入了一個新特性:索引初始化器,它支援在物件初始化器內部初始化索引:
var zahlen = new Dictionary<int, string>()
{
[1] = "eins",
[2] = "zwei"
};
可以使用表示式體方法
public ZObject ToGerman() => new ZObject() { [1] = "eins", [2] = "zwei"};
定義泛型型別
定義泛型類
只需在類定義中包含尖括號語法:
class GenericClass<T>
T可以是任意識別符號,只需遵循通常的C#命名規則即可。泛型類可在其定義中包含任意多個型別引數,引數之間用逗號分隔:
class MyGenericClass<T1, T2, T3>
{
private T1 innerT1Object;
public MyGenericClass(T1 item)
{
//innerT1Object = new T1();
//不能假定為類提供了什麼型別,這樣無法編譯
innerT1Object = item;
}
public T1 InnerT1Object
{
get { return innerT1Object; }
}
}
型別T1的物件可以傳遞給建構函式,只能透過InnerT1Object
屬性對這個物件進行只讀訪問
//使用typeof運算子獲取型別引數的實際型別,並將其轉換為字串
public string GetAllTypesAsString()
{
return "T1 = " + typeof(T1).ToString() +
", T2 = " + typeof(T2).ToString() +
", T3 = " + typeof(T3).ToString();
}
可以做一些其他工作,尤其是對集合進行操作,因為處理物件組是非常簡單的,不需要對物件型別進行任何假設
[!caution]
在比較為泛型型別提供的型別值和null
時,只能使用運算子==和!=
default關鍵字
要確定用於建立泛型類例項的型別,需要知道它們是引用還是值型別
如果是值型別不能取null
值
public MyGenericClass()
{
innerT1Object = default(T1);
}
如果是引用型別賦予null
,值型別賦予預設值,default
關鍵字允許對必須使用的型別執行更多操作
約束型別
用於泛型類的型別稱為無繫結型別,因為沒有對它們進行任何約束。透過約束型別,可以限制可用於例項化泛型類的型別
//在類定義中,可以使用where關鍵字實現,可以提供多個約束,逗號隔開
class MyGenericClass<T> where T : constraint1, constraint2
可以使用多個where
語句,定義泛型類需要的任意型別或所有型別上的約束:
class MyGenericClass<T1, T2> where T1 : constraint1 where T2 : constraint2
約束必須出現在繼承說明符的後面:
class MyGenericClass<T1, T2> : MyBaseClass, IMyInterface where T1 : constraint1 where T2 : constraint2
泛型型別約束
struct //必須是值型別
class //必須是引用型別
base-class //必須是基類或繼承自基類,該結束可以是任意類名
interface //必須是介面或實現了介面
new() //必須有一個公共無引數建構函式
如果使用new()
作為約束,它必須是為型別指定的最後一個約束
可透過base-class
約束,把一個型別引數用作另一個型別引數的約束
class MyGenericClass<T1, T2> where T2 : T1
T2必須與T1的型別相同或繼承自T1,這稱為裸型別約束,表示一個泛型型別引數用作另一個型別引數的約束
class MyGenericClass<T1, T2> where T2 : T1 where T1 : T2
//型別引數不能迴圈,無法編譯
從泛型類中繼承
如果某個型別所繼承的基型別中受到了約束,該型別就不能解除約束。也就是說,型別T在所繼承的基型別中使用時,該型別必須受到至少與基型別相同的約束
//因為T在Farm<T>中被約束為Animal,把它約束為SuperCow就是把T約束為這些值的一個子集
class SuperFarm<T> : Farm<T> where T : SuperCow {}
//以下程式碼是錯誤的
class SuperFarm<T> : Farm<T> where T : struct{}
泛型運算子
在C#中,可以像其他方法一樣進行運算子的重寫,這也可以在泛型類中實現此類重寫
//定義一個靜態運算子過載方法,用於將一個Farm<T>物件與一個List<T>物件中的動物合併到一個新的Farm<T>中
public static Farm<T> operator +(Farm<T> farm1, List<T> farm2)
{
//建立一個新的Farm<T>例項,用於儲存合併後的動物集合
Farm<T> result = new Farm<T>();
//遍歷第一個Farm<T>型別中的所有動物並將其新增到新農場中
foreach (T animal in farm1.Animals)
{
result.Animals.Add(animal);
}
//遍歷第二個List<T>型別,僅將其中不存在於新農場的動物新增進去
foreach (T animal in farm2)
{
if (!result.Animals.Contains(animal))
{
result.Animals.Add(animal);
}
}
//返回合併後的新農場物件
return result;
}
//另一個過載版本,允許將List<T>物件放在前面進行合併操作。這裡採用右結合律,實際呼叫的是上面定義的方法
public static Farm<T> operator +(List<T> farm1, Farm<T> farm2) => farm2 + farm1;
泛型結構
可以用與泛型類相同的方式建立泛型結構
public struct MyStruct<T1, T2>
{
public T1 item1;
public T2 item2;
}
定義泛型方法
泛型方法中,返回型別或引數型別由泛型型別引數來確定
public T GetDefault<T>() => default(T)
可以透過非泛型類來實現泛型方法:
public class Defaulter
{
public T GetDefault<T>() => default(T);
}
但如果類是泛型的,就必須為泛型方法使用不同的識別符號
//該程式碼無法編譯,必須重新命名方法或類使用的型別T
public class Defaulter<T>
{
public T GetDefault<T>() => default(T);
}
泛型方法引數可以採用與類相同的方式使用約束,可以使用任意的類型別引數
public class Defaulter<T1>
{
public T2 GetDefault<T2>()
where T2 : T1
{
return default(T2);
}
}
為方法提供的型別T2必須與給類提供的T1相同或者繼承自T1。這是約束泛型方法的常用方式
定義泛型委託
定義委託
public delegate int MyDelegate(int op1, int op2);
定義泛型委託,只需要宣告和使用一個或多個泛型型別引數
public delegate T1 MyDelegate<T1, T2>(T2 op1, T2 op2) where T1: T2;
這裡也可以使用約束
變體
變體是協變和抗變的統稱
多型性允許把派生型別的物件放在基型別的變數中,但這不適用於介面
//以下程式碼無法工作
IMethaneProducer<Cow> cowMethaneProducer = myCow;
IMethaneProducer<Animal> animalMethaneProducer = cowMethaneProducer;
Cow
支援IMethaneProducer<Cow>
介面,第一行程式碼沒有問題,但第二行程式碼預先假定兩個介面型別有某種關係,但實際上這種關係並不存在,所以無法把一種型別轉換為另一種型別
因為泛型型別的所有型別引數都是不變的,但可以在泛型介面和泛型委託上定義變體型別引數
為使上面的程式碼工作,IMethaneProducer<T>
介面的型別引數T必須是協變的,有了協變的型別引數,就可以在MethaneProducer<Cow>
和IMethaneProducer<Animal>
之間建立繼承關係。這樣一種型別的變數就可以包含另一種型別的值,這與多型性類似,但更復雜些
抗變和協變是類似的,但方向相反。抗變不能像協變那樣把泛型介面值放在使用基型別的變數中,但可以把該介面放在使用派生型別的變數中
IGrassMuncher<Cow> cowGrassMuncher = myCow;
IGrassMuncher<SuperCow> superCowGrassMuncher = cowGrassMuncher;
協變
要把泛型型別引數定義為協變,可在型別定義中使用out
關鍵字
public interface IMethaneProducer<out T>
對於介面定義,協變型別引數只能用作方法的返回值或屬性get
訪問器
協變意味著子類型別的集合可以被看作是父類型別的集合。在泛型上下文中,如果一個型別引數用out
關鍵字標記為協變,則該型別引數可以在派生類上進行隱式轉換
抗變
要把泛型型別引數定義為抗變,可在型別定義中使用in
關鍵字
public interface IGrassMuncher<in T>
對於介面定義,抗變型別引數只能用作方法引數,不能用作返回型別
抗變允許父類型別的集合被視為子類型別的集合。在泛型上下文中,如果一個型別引數用in
關鍵字標記為抗變,則該型別引數可以在基類上進行隱式轉換
- 協變 關注的是“輸出”,即一個物件能夠產出的資料型別。它允許我們向上轉型泛型容器或委託,並且能夠正確地獲取其中包含的更基類型別的元素。
- 抗變 關注的是“輸入”,即一個函式或委託期望接收的資料型別。它允許我們將能處理更基類型別引數的方法或委託向下轉型,以便它們能處理更具體型別的引數
高階C#技術
::運算子和全域性名稱空間限定符
::
運算子提供了另一種訪問名稱空間中型別的方式。如果要使用一個名稱空間的別名,但該別名與實際名稱空間層次結構之間的界限不清晰,就必須使用::
運算子
using MyNamespaceAlias = MyRootNamespace.MyNestedNamespace;
namespace MyRootNamespace
{
namespace MyNamespaceAlias
{
public class MyClass { }
}
namespace MyNestedNamespace
{
public class MyClass { }
}
}
MyRootNamespace
中的程式碼使用以下程式碼引用一個類:
MyNamespaceAlias.MyClass
這行程式碼表示的類是MyRootNamespace.MyNamespaceAlias.MyClass
,而不是MyRootNamespace.MyNestedNamespace.MyClass
也就是說,MyRootNamespace.MyNamespaceAlias
名稱空間隱藏了由using
語句定義的別名,該別名指向MyRootNamespace.MyNestedNamespace
名稱空間。仍然可以訪問這個名稱空間以及其中包含的類,但需要使用不同的語法:
MyNestedNamespace.MyClass
//還可以使用::運算子
MyNamespaceAlias::MyClass
使用這個運算子會迫使編譯器使用由using
語句定義的別名,因此程式碼指向MyRootNamespace. MyNestedNamespace.MyClass
::
運算子還可以與global
關鍵字一起使用,它實際上是頂級根名稱空間的別名。這有助於更清晰地說明要指向哪個名稱空間
//明確指定使用全域性範圍內的System名稱空間
global::System.Collections.Generic.List<int>
定製異常
有時可以從包括異常的System.Exception
基類中派生自己的異常類,並使用它們,而不是使用標準的異常。這樣就可以把更具體的資訊傳送給捕獲該異常的程式碼,讓處理異常的捕獲程式碼更有針對性
例如,可以給異常類新增一個新屬性,以便訪問某些底層資訊,這樣異常的接收程式碼就可以做出必要的改變,或者僅給出異常起因的更多資訊
using System;
// 自定義異常類
public class CustomException : Exception
{
public CustomException() : base() { }
public CustomException(string message) : base(message) { }
public CustomException(string message, Exception inner) : base(message, inner) { }
}
class Program {
static void Main()
{
try
{
// 模擬一個可能引發異常的操作
TriggerCustomException();
} catch (CustomException ex) {
Console.WriteLine("Caught a custom exception: " + ex.Message);
} catch (Exception ex) {
Console.WriteLine("Caught an unexpected exception: " + ex.Message);
}
}
static void TriggerCustomException() {
// 丟擲自定義異常
throw new CustomException("This is a custom exception.");
}
}
事件
事件類似於異常,因為它們都由物件丟擲,並且都可以透過我們提供的程式碼來處理
但它們也有幾個重要區別,最重要的區別是沒有try...catch
類似的結構來處理事件,必須訂閱它們,訂閱一個事件的含義是提供程式碼,在事件發生時執行這些程式碼,它們稱為事件處理程式
單個事件可供多個處理程式訂閱,在該事件發生時,這些處理程式都會被呼叫,其中包含引發該事件的物件所在的類中的事件處理程式,事件處理程式也可能在其他類中
事件處理程式本身都是簡單方法。對事件處理方法的唯一限制是它必須匹配事件所要求的返回型別和引數。這個限制是事件定義的一部分,由一個委託指定
在事件中使用委託是非常有用的
基本處理過程如下所示:
- 應用程式建立一個可以引發事件的物件
例如,假定一個即時訊息傳送應用程式建立的物件表示一個遠端使用者的連線。當接收到遠端使用者透過該連線傳送來的訊息時,這個連線物件會引發一個事件 - 應用程式訂閱事件
為此,即時訊息傳送應用程式將定義一個方法,該方法可以與事件指定的委託型別一起使用,把這個方法的一個引用傳送給事件,而事件的處理方法可以是另一個物件的方法,例如,當接收到訊息時進行顯示的顯示裝置物件 - 引發事件後,就通知訂閱器
當接收到透過連線物件傳來的即時訊息時,就呼叫顯示裝置物件上的事件處理方法。因為使用的是一個標準方法,所以引發事件的物件可以透過引數傳送任何相關的資訊,這樣就大大增加了事件的通用性
處理事件
要處理事件,需要提供一個事件處理方法來訂閱事件,該方法的返回型別和引數應該匹配事件指定的委託
using System.Timers;
using static System.Console;
namespace Event
{
class Program
{
//記錄當前顯示到字串中的字元
static int counter = 0;
//要逐個顯示的字串
static string displayString = "This string will appear one letter at a time. ";
static void Main(string[] args)
{
//建立一個新例項,設定間隔時間為100毫秒
System.Timers.Timer myTimer = new System.Timers.Timer(100);
//當計時器觸發Elapsed事件時,呼叫WriteChar方法
myTimer.Elapsed += new ElapsedEventHandler(WriteChar);
//開始計時器
myTimer.Start();
//讓主執行緒等待足夠長的時間以確保所有字元都能被輸出
System.Threading.Thread.Sleep(displayString.Length * 100 + 100);
}
//事件處理程式,設定間隔時間後逐個顯示字串中的字元
static void WriteChar(object source, ElapsedEventArgs e)
{
//顯示字串中的下一個字元,並更新counter值
Write(displayString[counter++]);
//當counter大於等於字串長度時,表示所有字元已經輸出完畢,停止計時器
if (counter >= displayString.Length)
{
WriteLine($"字元數:{counter}");
((System.Timers.Timer)source).Stop();
}
}
}
}
用於引發事件的物件是System.Timers.Timer
類的一個例項。使用一個時間段來初始化該物件。當使用Start()
方法啟動Timer
物件時,就引發一系列事件,Main()
用100毫秒初始化Timer
物件,所以在啟動該物件後,1秒鐘內將引發10次事件
把處理程式與事件關聯起來,即訂閱它。為此可以使用+=運算子,給事件新增一個處理程式,其形式是使用事件處理方法初始化的一個新委託例項
myTimer.Elapsed += new ElapsedEventHandler(WriteChar);
這行程式碼在列表中新增一個處理程式,當引發Elapsed
事件時,就會呼叫該處理程式。可給列表新增多個處理程式,只要它們滿足指定的條件即可。當引發事件時會依次呼叫每個處理程式
可以使用方法組概念來簡化新增事件處理程式的語法:
myTimer.Elapsed += WriteChar;
最終結果是完全相同的,但不必顯式指定委託型別,編譯器會根據使用事件的上下文來指定它。但它降低了可讀性,不再能一眼看出使用了什麼委託型別
定義事件
using System.Timers;//用於使用定時器類
using static System.Console;
namespace DefineEvent
{
//委託型別,用於處理接收到訊息事件的方法
/*string引數把Connection物件收到的即時訊息傳送給Display物件
定義了委託或者找到合適的現有委託後,就可以把事件本身定義為Connection類的一個成員*/
public delegate void MessageHandler(string messageText);
//表示連線的類
public class Connection
{
//公共事件成員變數MessageArrived,MessageHandler是一個委託型別,用於指定觸發事件時需要呼叫的方法簽名
//當有新訊息時觸發此事件
public event MessageHandler MessageArrived;
//私有變數,用於定期檢查新訊息的System.Timers.Timer例項
private System.Timers.Timer pollTimer;
//建構函式,初始化定時器,並新增Elapsed事件處理程式
public Connection()
{
//設定定時器間隔為100毫秒
pollTimer = new System.Timers.Timer(100);
//當計時器結束時執行CheckForMessage方法
pollTimer.Elapsed += new ElapsedEventHandler(CheckForMessage);
}
//開始檢查新訊息,即啟動定時器
public void Connect() => pollTimer.Start();
//停止檢查新訊息,即停止定時器
public void Disconnect() => pollTimer.Stop();
//私有靜態隨機數生成器,用於模擬隨機接收訊息的情況
private static Random random = new Random();
//私有方法,作為定時器Elapsed事件的回撥函式
private void CheckForMessage(object source, ElapsedEventArgs e)
{
//檢查新訊息的通知資訊,並且僅在MessageArrived事件有訂閱者時才觸發事件
WriteLine("Checking for new messages.");
if ((random.Next(9) == 0) && (MessageArrived != null))
{
MessageArrived("Hello Mami!");
}
}
}
public class Display
{
//公共方法,用於輸出接收到的訊息到控制檯
public void DisplayMessage(string message) => WriteLine($"Message arrived: {message}");
}
class Program
{
static void Main(string[] args)
{
//建立一個Connection例項
Connection myConnection = new Connection();
//建立一個Display例項
Display myDisplay = new Display();
//訂閱Connection的MessageArrived事件,將myDisplay的DisplayMessage方法作為事件處理器
myConnection.MessageArrived += new MessageHandler(myDisplay.DisplayMessage);
//啟動檢查新訊息的過程
myConnection.Connect();
/*暫停主執行緒1500毫秒
由於主執行緒控制著整個程式的執行和輸出,所以在暫停期間,定時器仍然繼續工作並檢查是否有新訊息
這裡暫停主執行緒是為了確保在程式退出之前有足夠時間讓定時器有機會觸發事件並完成訊息輸出到控制檯的操作
如果不進行暫停操作,可能主執行緒會立即結束,導致無法看到任何訊息輸出*/
//System.Threading.Thread.Sleep(1500);
//阻塞,等待使用者按鍵,防止控制檯視窗立刻關閉
ReadKey();
}
}
}
宣告事件時,使用event
關鍵字,並指定要使用的委託型別,以這種方式宣告事件後,就可以引發它,做法是按名稱來呼叫它,就像它是一個其返回型別和引數是由委託指定的方法一樣
//宣告瞭一個事件,委託型別
public event MessageHandler MessageArrived;
//引發事件
MessageArrived("This is a message.");
匿名方法
匿名方法實際上並非傳統意義上的方法,它不是某個類上的方法,而純粹是為用作委託目的而建立的
要建立匿名方法,需要使用以下程式碼:
delegate(parameters)
{
// Anonymous method code.
};
parameters
是一個引數列表,這些引數匹配正在例項化的委託型別,由匿名方法的程式碼使用
使用匿名方法時要注意,對於包含它們的程式碼塊來說,它們是區域性的,可以訪問這個作用域內的區域性變數。如果使用這樣一個變數,它就成為外部變。外部變數在超出作用域時,是不會刪除的,這與其他區域性變數不同,在使用它們的匿名方法被銷燬時,才會刪除外部變數。這比我們希望的時間晚一些,所以要格外小心。如果外部變數佔用了大量記憶體,或者使用的資源在其他方面是比較昂貴的,就可能導致記憶體或效能問題
特性
特性可以為程式碼段標記一些資訊,而這樣的資訊又可以從外部讀取,並透過各種方式來影響所定義型別的使用方式。這種手段通常被稱為對程式碼進行裝飾
例如,要建立的某個類包含一個極簡單的方法,但即便簡單,除錯期間還是會對這一程式碼進行檢查。這種情況下就可以對該方法新增一個特性,告訴VS在除錯時不要進入該方法進行逐句除錯,而是跳過該方法,直接除錯下一條語句
[DebuggerStepThrough]
public void DullMethod()
[DebuggerStepThrough]
就是該特性,所有特性的新增都是將特性名稱用方括號括起來,並寫在應用的目的碼前即可,可以為一段目的碼新增多個特性
上述特性是透過DebuggerStepThroughAttribute
這個類來實現的,而這個類位於System.Diagnostics
名稱空間中,因此使用該特性必須使用using
語句來引用這一名稱空間,可以使用完整名稱,也可以去掉Attribute
字尾
透過上述方式新增特性後,編譯器就會建立該特性類的一個例項,然後將其與類方法關聯起來。某些特性可以透過建構函式的引數或屬性進行自定義,並在新增特性的時候進行指定
[DoesInterestingThings(1000, WhatDoesItDo = "voodoo")]
public class DecoratedClass {}
將值1000傳遞給了DoesInterestingThingsAttribute
的建構函式,並將WhatDoesItDo
屬性的值設定為字串"voodoo"
讀取特性
讀取特性值使用一種稱為反射的技術,反射可以在執行時動態檢查型別資訊,甚至是在建立物件的位置或不必知道具體物件的情況下直接呼叫某個方法
反射可以取得儲存在Type
物件中的使用資訊,以及透過System.Reflection
名稱空間中的各種型別來獲取不同的型別資訊
typeof
運算子從類中快速獲取型別資訊GetType()
方法從物件例項中獲取資訊- 反射技術從
Type
物件取得成員資訊,基於該方法,就可以從類或類的不同成員中取得特性資訊
最簡單的方法是透過Type.GetCustomAttributes()
方法來實現。這個方法最多使用兩個引數,然後返回一個包含一系列object
例項的陣列,每個例項都是一個特性例項。第一個引數是可選的,即傳遞我們感興趣的型別或若干特性的型別(其他所有特性均會被忽略)。如果不使用這一引數,將返回所有特性。第二個引數是必需的,即透過一個布林值來指示,只想瞭解類本身的資訊,還是除了該類之外還希望瞭解派生自該類的所有類
下面的程式碼列出DecoratedClass
類的特性
//獲取指定型別的Type物件
Type classType = typeof(DecoratedClass);
//獲取該型別上應用的所有自定義特性,包括從父類繼承的特性
object[] customAttributes = classType.GetCustomAttributes(true);
foreach (object customAttribute in customAttributes)
{
WriteLine($"Attribute of type {customAttribute} found.");
}
建立特性
透過System.Attribute
類進行派生,就可以自定義特性。一般來說,如果除了包含和不包含特定的特性外,我們的程式碼不需要獲得更多資訊就可以完成需要的工作,不必完成這些額外的工作。如果希望某些特性可以被自定義,則可以提供非預設的建構函式和可寫屬性
還需要為自定義特性做兩個選擇:要將其應用到什麼型別的目標(類、屬性或其他),以及是否可以對同一個目標進行多次應用
要指定上述資訊,需要對特性應用AttributeUsageAttribute
特性,該特性帶有一個型別為AttributeTargets
的建構函式引數值,透過|
運算子即可透過相應的列舉值組合出需要的值。該特性還有一個布林值型別的屬性AllowMultiple
,用於指定是否可以多次應用特性
下面的程式碼指定了一個特性可以應用到類或屬性中
//一個預定義特性,用於指定自定義特性的使用規則和範圍
[AttributeUsage(AttributeTargets.Class|AttributeTargets.Method, AllowMultiple = false)]
//自定義特性類
class DoesInterestingThingsAttribute : Attribute
{
//建構函式
public DoesInterestingThingsAttribute(int howManyTimes)
{
HowManyTimes = howManyTimes;
}
//公共屬性,用於儲存或獲取該特性所描述行為的具體內容
public string WhatDoesItDo { get; set; }
//只讀公共屬性,表示該特性執行有趣行為的次數
public int HowManyTimes { get; private set; }
}
初始化器
物件初始化器提供了一種簡化程式碼的方式,可以合併物件的例項化和初始化。集合初始化器提供了一種簡潔的語法,使用一個步驟就可以建立和填充集合
物件初始化器
public class Curry
{
public string MainIngredient { get; set; }
public string Style { get; set; }
public int Spiciness { get; set; }
}
該類有3個屬性,使用自動屬性語法定義,如果希望例項化和初始化該類的一個物件例項,就必須執行如下語句
Curry tastyCurry = new Curry();
tastyCurry.MainIngredient = "panir tikka";
tastyCurry.Style = "jalfrezi";
tastyCurry.Spiciness = 8;
如果類定義中未包含建構函式,這段程式碼就使用C#編譯器提供的預設無引數建構函式,為簡化該初始化過程,可提供一個合適的非預設建構函式
public Curry(string mainIngredient, string style, int spiciness) {
MainIngredient = mainIngredient;
Style = style;
Spiciness = spiciness;
}
這樣就可以把例項化和初始化合並起來
Curry tastyCurry = new Curry("panir tikka", "jalfrezi", 8);
程式碼可以工作,但它強制使用Carry
類的程式碼使用該建構函式,這將阻止使用無引數建構函式程式碼的執行,因此和C++一樣都需要提供無參建構函式
public Curry() {}
物件初始化器是不必在類中新增額外程式碼就可以例項化和初始化物件的方式。例項化物件時,要為每個需要初始化、可公開訪問的屬性或欄位使用名稱-值對,來提供其值
<ClassName><variableName> = new<ClassName>
{
<propertyOrField1> = <value1>,
<propertyOrField2> = <value2>,
...
<propertyOrFieldN> = <valueN>
};
重寫前面的程式碼,例項化和初始化一個Curry
型別的物件
Curry tastyCurry = new Curry
{
MainIngredient = "panir tikka",
Style = "jalfrezi",
Spiciness = 8
};
常常可以把這樣的程式碼放在一行上,而不會嚴重影響可讀性
使用物件初始化器時,不必顯式呼叫類的建構函式。如果像上述程式碼一樣省略建構函式的括號,就自動呼叫預設的無參建構函式。這是在初始化器設定引數值前呼叫的,以便在需要時為預設建構函式中的引數提供預設值
另外可以呼叫特定的建構函式。同樣,先呼叫這個建構函式,所以在建構函式中對公共屬性進行的初始化可能會被初始化器中提供的值覆蓋。只有能夠訪問所使用的建構函式(如果沒有顯式指出,就是預設的建構函式),物件初始化器才能正常工作
可以使用巢狀的物件初始化器
Curry tastyCurry = new Curry
{
MainIngredient = "panir tikka",
Style = "jalfrezi",
Spiciness = 8,
Origin = new Restaurant
{
Name = "King's Balti",
Location = "York Road",
Rating = 5
}
};
初始化了Restaurant
型別的Origin
屬性
物件初始化器沒有替代非預設的建構函式。在例項化物件時,可以使用物件初始化器來設定屬性和欄位值,但這並不意味著總是知道需要初始化什麼狀態。透過建構函式,可以準確地指定物件需要什麼值才能起作用,再執行程式碼,以便立即響應這些值
使用巢狀的初始化器時,首先建立頂級物件,然後建立巢狀物件。如果使用建構函式,物件的建立順序就反了過來
集合初始化器
使用值初始化陣列
int[] myIntArray = new int[5] { 5, 9, 10, 2, 99 };
這是一種合併例項化和初始化陣列的簡潔方式,集合初始化器只是把該語法擴充套件到集合上
List<int> myIntCollection = new List<int> { 5, 9, 10, 2, 99 };
透過合併物件和集合初始化器,就可以使用簡潔的程式碼(只能說可能增加了可讀性)來配置集合
List<Curry> curries = new List<Curry>();
curries.Add(new Curry("Chicken", "Pathia", 6));
curries.Add(new Curry("Vegetable", "Korma", 3));
curries.Add(new Curry("Prawn", "Vindaloo", 9));
可以使用如下程式碼替換
List<Curry> moreCurries = new List<Curry>
{
new Curry
{
MainIngredient = "Chicken",
Style = "Pathia",
Spiciness = 6
},
new Curry
{
MainIngredient = "Vegetable",
Style = "Korma",
Spiciness = 3
},
new Curry
{
MainIngredient = "Prawn",
Style = "Vindaloo",
Spiciness = 9
}
};
型別推理
強型別化語言表示每個變數都有固定型別,只能用於接收該型別的程式碼中
var
關鍵字會根據初始化表示式的型別推斷變數的實際型別,在用var
宣告變數時,必須同時初始化該變數,因為如果沒有初始值,編譯器就無法確定變數的型別
var
關鍵字還可以透過陣列初始化器來推斷陣列的型別
var myArray = new[] { 4, 5, 2 };
在採用這種方式隱式指定陣列型別時,初始化器中使用的陣列元素必須是以下情況中的一種:
- 相同的型別
- 相同的引用型別或空
- 所有元素的型別都可以隱式地轉換為一個型別
如果應用最後一條規則,元素可以轉換的型別就稱為陣列元素的最佳型別。如果這個最佳型別有任何含糊的地方,即所有元素的型別都可以隱式轉換為兩種或更多的型別,程式碼就不會編譯
要注意數字值不會解釋為可空型別
//無法編譯
var myArray = new[] { 4, null, 2 };
//但可以使用標準的陣列初始化器建立一個可空型別陣列
var myArray = new int?[] { 4, null, 2 };
var
關鍵字僅適用於區域性變數的隱式型別化宣告
匿名型別
常常有一系列類只提供屬性,什麼也不做,只是儲存結構化資料,在資料庫或電子表格中,可以把這個類看成表中的一行。可以儲存這個類的例項的集合類應表示表或電子表格中的多個行
匿名型別是簡化這個程式設計模型的一種方式,其理念是使用C#編譯器根據要儲存的資料自動建立型別,而不是定義簡單的資料儲存型別
//物件初始化器
Curry curry = new Curry
{
MainIngredient = "Lamb",
Style = "Dhansak",
Spiciness = 5
};
//使用匿名型別
var curry = new
{
MainIngredient = "Lamb",
Style = "Dhansak",
Spiciness = 5
};
匿名型別使用var
關鍵字,這是因為匿名型別沒有可以使用的識別符號,且在new
關鍵字的後面沒有指定型別名,這是編譯器確定我們要使用匿名型別的方式
建立匿名型別物件的陣列
using static System.Console;
class Test
{
static void Main()
{
var curries = new[]
{
new { MainIngredient = "Lamb", Style = "Dhansak", Spiciness = 5 },
new { MainIngredient = "Lamb", Style = "Dhansak", Spiciness = 5 },
new { MainIngredient = "Chicken", Style = "Dhansak", Spiciness = 5 }
};
//輸出為該型別定義的每個屬性的值
WriteLine(curries[0].ToString());
/*根據物件的狀態為物件返回一個唯一的整數
陣列中的前兩個物件有相同的屬性值,所以其狀態是相同的*/
WriteLine(curries[0].GetHashCode());
WriteLine(curries[1].GetHashCode());
WriteLine(curries[2].GetHashCode());
//由於匿名型別沒有重寫Equals,預設基於引用比較,這裡返回false
//即使屬性完全相同,因為它們是不同的物件例項
//==運算子也是基於引用比較,因此即使屬性值相同也會返回false
WriteLine(curries[0].Equals(curries[1]));
WriteLine(curries[0].Equals(curries[2]));
WriteLine(curries[0] == curries[1]);
WriteLine(curries[0] == curries[2]);
}
}
動態查詢
var
關鍵字本身不是型別,只是根據表示式推導型別,C#雖然是強型別化語言,但從C#4開始就引入了動態變數的概念,即型別可變的變數
引入的目的是為了在許多情況下,希望使用C#處理另一種語言建立的物件,這包括對舊技術的互動操作。另一個使用動態查詢的情況是處理未知型別的C#物件
在後臺,動態查詢功能由Dynamic Language Runtime(動態語言執行庫,DLR)支援。與CLR一樣,DLR是.NET4.5的一部分
使用dynamic
關鍵字定義動態型別,在宣告動態型別時不必初始化它的值
[!important]
動態型別僅在編譯期間存在,在執行期間會被System.Object
型別替代
高階方法引數
一些方法需要大量引數,但許多引數並不是每次呼叫都需要
可選引數
呼叫引數時,常常給某個引數傳輸相同的值,例如可能是一個布林值,以控制方法操作中不重要部分
public List<string> GetWords(string sentence, bool capitalizeWords = false)
為引數提供一個預設值,就使其成為可選引數,如果呼叫此方法時沒有為該引數提供值,就使用預設值
預設值必須是字面量、常量值或該值型別的預設初始值
使用可選引數時,它們必須位於方法引數列表的末尾,沒有預設值的引數不能放在預設值的引數後
//非法程式碼
public List<string> GetWords(bool capitalizeWords = false, string sentence)
命名引數
使用可選引數時,可能發現某個方法有幾個可選引數,但可能只想給第三個可選引數傳輸值
命名引數允許指定要使用哪個引數,這不需要在方法定義中進行任何特殊處理,它是一個在呼叫方法時使用的技術
method(引數名:值,引數名:值)
引數名是方法定義時使用的變數名,引數的順序是任意的,命名引數也可以是可選的
可以僅給方法呼叫中的某些引數使用命名引數。當方法簽名中有多個可選引數和一些必選引數時,這是非常有用的。可以首先指定必選引數,再指定命名的可選引數
如果混合使用命名引數和位置引數,就必須先包含所有的位置引數,其後是命名引數
Lambda表示式
複習匿名方法
給事件新增處理程式:
- 定義一個事件處理方法,其返回型別和引數匹配將訂閱的事件需要的委託的返回型別和引數
- 宣告一個委託型別的變數,用於事件
- 把委託變數初始化為委託型別的例項,該例項指向事件處理方法
- 把委託變數新增到事件的訂閱者列表中
實際過程會簡單一些,因為一般不使用變數來儲存委託,只在訂閱事件時使用委託的一個例項
Timer myTimer = new Timer(100);
myTimer.Elapsed += new ElapsedEventHandler(WriteChar);
訂閱了Timer
物件的Elapsed
事件。這個事件使用委託型別ElapsedEventHandler
,使用方法識別符號WriteChar
例項化該委託型別。結果是Timer
物件引發Elapsed
事件時,就呼叫方法WriteChar()
。傳給WriteChar()
的引數取決於由ElapsedEventHandler
委託定義的引數型別和Timer
中引發事件的程式碼傳送的值
可以透過方法組語法用更簡潔的程式碼獲得相同的效果
方法組語法是指不直接例項化委託物件,而是透過指定一個方法名來隱式轉換為委託型別。當某個方法的簽名與委託型別的簽名匹配時,可以直接將方法名用作該委託型別的例項
myTimer.Elapsed += WriteChar;
C#編譯器知道Elapsed
事件需要的委託型別,所以可以填充該型別。但大多數情況下,最好不要這麼做,因為這會使程式碼更難理解,也不清楚會發生什麼
使用匿名方法時,該過程會減少為一步:
- 使用內聯的匿名方法,該匿名方法的返回型別和引數匹配所訂閱事件需要的委託的返回型別和引數
//Elapsed事件新增一個匿名方法作為事件處理器
myTimer.Elapsed += delegate(object source, ElapsedEventArgs e)
{
WriteLine("Event handler called after {0} milliseconds.",
//獲取當前計時器週期間隔的毫秒數
(source as Timer).Interval);
};
這段程式碼像單獨使用事件處理程式一樣正常工作。主要區別是這裡使用的匿名方法對於其餘程式碼而言實際上是隱藏的。例如,不能在應用程式的其他地方重用這個事件處理程式。另外,為更好地加以描述,這裡使用的語法有點沉悶。delegate
關鍵字會帶來混淆,因為它具有雙重含義,匿名方法和定義委託型別都要使用它
Lambda表示式用於匿名方法
Lambda表示式是簡化匿名方法語法的一種方式,Lambda表示式還有其他用途
//使用Lambda表示式重寫上面的程式碼
myTimer.Elapsed += (source, e) => WriteLine("Event handler called after " + $"{(source as Timer).Interval} milliseconds.");
Lambda表示式會根據上下文和委託簽名自動推匯出引數型別,所以在Lambda表示式中不需要明確指定型別名
using static System.Console;
//委託型別,接受兩個int引數返回一個int
delegate int TwoIntegerOperationDelegate(int paramA, int paramB);
class Program
{
//靜態方法,接受一個委託作為引數
static void PerformOperations(TwoIntegerOperationDelegate del)
{
//兩層迴圈遍歷1到5之間的整數對
for (int paramAVal = 1; paramAVal <= 5; paramAVal++)
{
for (int paramBVal = 1; paramBVal <= 5; paramBVal++)
{
//呼叫傳入的委託並獲取運算結果
int delegateCallResult = del(paramAVal, paramBVal);
//輸出當前表示式的值
Write($"f({paramAVal}, " + $"{paramBVal})={delegateCallResult}");
//如果不是最後一列,則新增逗號和空格分隔各個運算結果
if (paramBVal != 5) { Write(", "); }
}
//每一次內層迴圈後換行
WriteLine();
}
}
static void Main(string[] args)
{
//使用Lambda表示式建立了三種運算的委託例項
WriteLine("f(a, b) = a + b:");
PerformOperations((paramA, paramB) => paramA + paramB);
WriteLine();
WriteLine("f(a, b) = a * b:");
PerformOperations((paramA, paramB) => paramA * paramB);
WriteLine();
WriteLine("f(a, b) = (a - b) % b:");
PerformOperations((paramA, paramB) => (paramA - paramB) % paramB);
}
}
上面的Lambda表示式分為3部分:
- 引數定義部分,這些引數都是未型別化的,因此編譯器會根據上下文推斷出它們的型別
- =>運算子把Lambda表示式的引數與表示式體分開
- 表示式體,指定了引數之間的操作,不需要指定這是返回值,編譯器知道(編譯器比我聰明多了,為什麼還要我寫程式碼啊啊啊!)
Lambda表示式的引數
Lambda表示式使用型別推理功能來確定所傳遞的引數型別,但也可以定義型別
(int paramA, int paramB) => paramA + paramB
優點是程式碼便於理解,缺點是不夠簡潔(我覺得還是可讀性更重要)
不能在同一個Lambda表示式同時使用隱式和顯式的引數型別
//錯誤的
(int paramA, paramB) => paramA + paramB
可以定義沒有引數的Lambda表示式,使用空括號表示
() => Math.PI
當委託不需要引數,但需要一個double值時,就可以使用該Lambda表示式
Lambda表示式的語句體
可將Lambda表示式看成匿名方法語法的擴充套件,所以還可以在Lambda表示式的語句體中包含多個語句。只需要把程式碼塊放在花括號中
如果使用Lambda表示式和返回型別不是void
的委託型別,就必須用return
關鍵字返回一個值,這與其他方法一樣
(param1, param2) =>
{
// Multiple statements ahoy!
return returnValue;
}
PerformOperations((paramA, paramB) => paramA + paramB);
//可以改寫為
PerformOperations(delegate(int paramA, int paramB)
{
return paramA + paramB;
});
[!hint]
在使用單一表示式時,Lambda表示式最有用也最簡潔
如果需要多個語句,則定義一個單獨的非匿名方法更好,也使程式碼更便於複用
Lambda表示式用作委託和表示式樹
可採用兩種方式來解釋Lambda表示式
第一,Lambda表示式是一個委託。即可以把Lambda表示式賦予一個委託型別的變數
第二,可以把Lambda表示式解釋為表示式樹。表示式樹是Lambda表示式的抽象表示,因此不能直接執行。可使用表示式樹以程式設計方式分析Lambda表示式,執行操作,以響應Lambda表示式