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表達式
浙公網安備 33010602011771號