文章目錄
- c#語法
- 基本元素
- 關鍵字
- 操作符(operator)
- 類型轉換
- 標識符(Identifier)
- 語句
- try語句
- 迭代語句(循環語句)
- 索引器
- 文本(字面值)
- 五大數據類型
- 引用類型:
- 值類型:
- 變量、對象與內存
- 裝箱和拆箱
- 類
- 類的實例化
- 類的三大成員(屬性、方法、事件)
- 屬性(property)
- 方法(函數)
- 方法參數
- 值參數
- 引用參數
- 輸出參數
- 數組參數
- 具名參數
- 可選參數
- 擴展方法(this參數)
- 方法的重載
- 構造器(constructor)
- 析構器
- 委托
- Action委托
- Function類型委托
- 委托的聲明(自定義委托)
- 委托的一般使用
- 事件
- 事件的應用
- 自定義事件
- 事件的命名
- 事件與委托的關系
- 靜態成員與實例成員
- 類的繼承
- 重寫與多態
- Hide
- Polymorphism 多態
- 接口,抽象類,SOLID,單元測試
- SOLID
- 抽象類與開閉原則
- 接口
- 依賴與耦合
- 接口解耦示例
- 依賴反轉原則
- 接口隔離,反射,特性,依賴注入
- 接口隔離
- 胖接口及其產生原因
- 顯式接口實現
- 反射 Reflection
- 依賴注入
- 依賴注入框架
- 泛型,partial類,枚舉,結構體
- 泛型(generic)
- 基本原理
- 泛型類實例
- 泛型接口實例
- 泛型集合
- 泛型委托
- action委托
- function委托
- partial 類
- 減少派生類
- 枚舉類型
- 枚舉示例
- 枚舉的比特位用法
- 結構體
- 結構體可以實現接口
- 總結
c#語法
基本元素
小扳手是屬性,小方塊是方法,小閃電是事件
關鍵字
操作符(operator)
typeof | 查看類型的內部結構 |
---|---|
defult | 返回內存塊全刷成0的值 |
new(關鍵字) | 創建一個類型的實例 |
var(關鍵字) | 聲明隱式類型的變量 |
checked | 檢查這個值有沒有溢出 |
sizeof | 獲取基本數據類型所占字節數 |
explicit | 顯示類型轉換 |
implicit | 隱式類型轉換 |
int x=defult(int)
uint y=checked(x+1)
??用于判斷一個變量在為null時,返回雙問號后面的一個指定的值。
類型轉換
隱式轉換:不丟失精度的轉換、子類向父類的轉換(int轉long等)、裝箱
顯式轉換:可能丟失精度的轉換、拆箱、使用convert類、ToString方法與個數據類型的Parse的轉換
class program
{static void Main(string[] args){Stone stone=new Stone();stone.Age=5000;Monkey wukongSun =(Monkey)stone;Console.WriteLine(wukongSun.Age);}
}
class Stone
{public int Age;public static explicit operator Monkey(Stone stone){Monkey m=new Monkey(); m.Age=stone.Age/500; //將石頭轉換為猴子,石頭500歲為猴子1歲return m;} //顯示類型轉換就是一個目標類型的實例構造器,這個構造器寫在被轉換的這個數據類型里
}
class Monkey
{public int Age;
}
標識符(Identifier)
大小寫規范(變量用駝峰法、類名和名稱空間用帕斯卡命名法(單詞首字母大寫))
語句
方法體里面才有語句。
表達式語句、聲明語句(int x)、嵌入式語句(在 if 判斷后面嵌入的一條語句)、選擇語句(if、switch) 、標簽語句(在一條語句前面加上標識符)、塊語句(用花括號括起來的)
try語句
try語句提供一種機制,用于捕捉在塊的執行期間發生的各種異常,catch語句會執行錯誤時的代碼。
class Calculator
{public int Add(string a1, string a2){int a = 0, b = 0;try{a = int.Parse(a1); //把string轉化intb = int.Parse(a2); }catch{Console.WriteLine("錯誤");}int result = a + b;return result;}
}catch(ArgumentException ane)
{Console.WriteLine(ane.Message);
}//catch里可以加入ArgumentException參數,如果有ArgumentException異常會指出出現錯誤的類型,此外還有FormatException異常等
迭代語句(循環語句)
while、do-while、for、foreach
foreach語句用于枚舉一個集合的元素,并對該集合中的每個元素執行一次相關的嵌入式語句(類似python中的for)。
int[] intArray = new int[] { 2, 2, 3, 4, 5, 6, 7, 8 };
foreach (int i in intArray)
{//i是迭代變量,代表數組的元素Console.WriteLine(i);
}
實現了IEnumerable這個接口的類就是可以遍歷的集合,能夠被遍歷的集合都可以獲得自己的迭代器。數組屬于arry這個類。
int[] intArray = new int[] { 2, 2, 3, 4, 5, 6, 7, 8 };
IEnumerator<int> enumerator = intArray.AsEnumerable().GetEnumerator();
//獲取數組的迭代器
while (enumerator.MoveNext())
{Console.WriteLine(enumerator.Current);
} //會打印出數組元素
索引器
它使對象能夠用與數組相同的方式(即使用下標)進行索引。
文本(字面值)
整數、實數、字符、字符串、布爾、空
float x=3.0F
double y=4.0D
long z=3Ldynamic myVar=100 //動態類型
五大數據類型
由object派生出兩大類型,所有類型都繼承object。
引用類型:
引用變量存儲的是實例(數據)在堆內存上的內存地址。
類(class):windows、Form、Console、String
字段(field ):為一個對象或者類存儲數據。也叫成員變量。只讀字段只能進行初始化,不能再被賦值。
class Student
{public int Age; //字段public int sg;public chus() //構造器{Student ss=new Student();ss.Age = 1;}
}
接口(Interfaces)
委托(Delegates)
委托就是將方法作為參數(模仿函數指針)
值類型:
值類型的數據會直接保存在變量的內存空間中
結構體(Structures):int32、int64、Single、Double、long,用struct聲明
枚舉(Enumerations):用enum聲明
值類型存儲在內存的棧里,引用類型存儲在堆里。值類型沒有實例,值類型的實例和變量是合二為一的。
變量、對象與內存
變量就是用來存儲數據,以變量名所對應的內存地址為起點,以其數據類型所要求的存儲空間為長度的一塊內存區域。變量名表示變量的值在內存中的位置,每個變量都有一個類型以決定什么樣的值能夠存入變量。
變量一共有7種:靜態變量、實例變量(成員變量、字段)、數組元素、值參數、引用參數、輸出形參、局部變量。簡單的講,局部變量就是方法體里聲明的變量,在棧上分配內存。
參數只有三種:值參數、引用參數、輸出參數
成員變量聲明后不賦值,默認值為把內存塊都刷成0
裝箱和拆箱
裝箱:object類型變量obj所要引用的一個值,它不是堆上的實例,而是棧上的值類型的值,obj會先把這個值類型的值copy一下,然后在堆上找一塊可以存儲的空間,然后把這塊空間變成對象,將這塊地址存入到obj這個變量中。(把值類型數據從棧搬到堆)(二級指針?)
拆箱:獲得object類型變量引用的值類型的值,存儲到棧上。
可以強轉獲得:
int x=100;
object obj;
obj=x; //裝箱
int y=(int)obj; //拆箱
裝箱和拆箱從堆里往棧里搬東西,所以會損失性能
類
類本身是抽象數據和抽象行為的載體。
類的修飾符:new、public、protected、internal、private、abstract、sealed、static
繼承關系分組:abstract(必須繼承)、sealed(禁止繼承)
訪問級別:public、internal
未嵌套在其他類型中的類的默認可訪問性為internal,同一名稱空間下的類可以訪問;public公有的,其他項目也可訪問這個類;
public所有地方都可以訪問到這個成員;internal此命名空間類都可訪問,別的項目無法訪問;類成員的默認可訪問性為private,僅供此類訪問。
類成員的訪問級別不能超過類,以類的訪問級別為上限。
類的實例化
new 類名();
引用變量
引用變量可以引用類的實例
類名 引用變量名= new 類名()
Form myform = new Form();
this代表當前類的實例
靜態類中不能聲明實例成員
類的三大成員(屬性、方法、事件)
屬性(property)
存儲數據,組合起來表示類或對象當前的狀態(成員變量),是一種用于訪問對象或類型的特征的成員,特征反映了狀態。例如豆腐的溫度,高溫這個特征反映了豆腐現在的狀態,所以通過屬性反映類型的狀態。
屬性是字段的自然擴展,由字段發展而來,字段更偏向于實例對象在內存中的布局,property更偏向于反映現實世界對象的特征。語法糖,把背后復雜的機制隱藏了起來。
internal class Program
{static void Main(string[] args){Calculator c = new Calculator();Student stu1=new Student();Student stu2 =new Student();Student stu3 =new Student();stu1.SetAge(20);stu2.SetAge(20);stu3.SetAge(20);int avgAge=(stu1.GetAge()+stu2.GetAge()+stu3.GetAge())/3;Console.WriteLine(avgAge);}
}
class Student
{ private int Age; //設置為私有的,public int GetAge() //獲取這個字段的值{return this.Age;}public void SetAge(int value) //對我們設置的值進行約束{if (value >= 0 && value <= 120){this.Age= value; }else{throw new Exception("Age value has error"); //出現錯誤程序終止運行}}
}
類里面的屬性應該這樣寫:
private int age; //字段
public int Age //屬性
{get //獲取這個屬性的值{return this.age;}set //約束屬性值{if (value >= 0 && value <= 120){this.age = value;}else{throw new Exception("age error");}}
}
//如果一個屬性只有get沒有set說明是只讀屬性
建議用屬性去獲取字段的值,字段最好是private或protected,打出propfull按兩下tap鍵,會生成屬性的完整聲明。
方法(函數)
表示類或對象能做什么
當一個函數以類的成員的身份出現時, 就叫做方法。方法永遠都是類或結構體的成員。
方法參數
值參數
值參數:聲明是不帶修飾符的形參。值形參會創建新的存儲位置。
引用類型的變量存儲的值就是我們引用類型實例在堆內存當中的地址。引用類型的傳值參數傳遞的是實例的地址(類似指針),但是在方法里去改變這個參數的值,會改變方法外部引用實例的值,但如果在方法里重新實例化(創建一個新的對象)就不會改變(因為重新實例化后,指向的不是同一塊地址)。
引用參數
引用形參是用ref(像&和*符號)修飾符聲明的形參,與值形參不同,引用形參不回創建新的存儲位置。
值類型引用參數直接指向實際參數的內存地址。變量在作為引用形參前,必須先明確賦值。(一級指針形參)
引用類型的引用參數,傳參傳進來的是一個引用類型的變量存儲的地址(這個地址存儲的是對象在堆內存中的地址),而我們的引用參數存儲的是引用類型變量所指向的的地址(實例的地址),他們指向的內存地址就是同一個地址。如果在方法內部創建新對象,外部變量也會改變。(類似二級指針,但不完全一樣)
輸出參數
用out修飾符聲明的形參是輸出形參。類似于引用形參,輸出形參不創建新的存儲位置。輸出形參表示的存儲位置恰是在該方法調用中作為實參給出的那個變量所表示的存儲位置,它和傳進來的實參指向的是同一個內存位置。不要求輸出參數在傳入方法前要明確的賦值(因為你是通過這個參數向外輸出,原來賦的值就會被丟棄)
值類型輸出參數方法體內必須有對輸出變量的賦值操作,輸出參數并不創建變量的副本。
nternal class Program{static void Main(string[] args){double x;bool b = Student.TryParse("789", out x);if (b == true){Console.WriteLine(x + 1);}}}class Student{public string Name;public static bool TryParse(string input,out double result) //返回值是bool類型輸出參數為double類型{try{result = double.Parse(input);return true; //沒有異常返回true,并賦值}catch {result = 0;return false;}}}
引用類型輸出參數,如果在方法體內去新創建一個實例,那么我們的輸出參數和我們的引用變量指向的是同一個地址,這個地址所存儲的就是我們新創建的對象在堆內的地址。
數組參數
數組參數在聲明時用params關鍵字修飾(只能有一個參數由params修飾,并且是形參列表中的最后一個)
具名參數
具名參數嚴格意義上并不是參數的種類,而是參數的使用方法。
static void Main(string[] args)
{opop(name: "tim", age: 34);
}
static void opop(string name,int age)
{Console.WriteLine("name:{0},agg:{1}",name,age);
}
優點:提高代碼可讀性,不受參數列表順序約束
可選參數
參數因為具有默認值而變得“可選”,不推薦使用可選參數
static void Main(string[] args)
{opop();
}
static void opop(string name="tim",int age=23)
{ //給參數設置默認值Console.WriteLine("name:{0},agg:{1}",name,age);
}
擴展方法(this參數)
方法必須是公有的、靜態的被public static所修飾,this參數必須是參數列表中的第一個,由this修飾。而且必須放在一個靜態類中。
當我們無法對一個類型的源碼進行修改的時候,可以使用擴展方法為目標數據類型來追加方法。
nternal class Program{static void Main(string[] args){double x = 3.14159;double y = x.Round(4);//Round本來是double里的函數,擴展之后可以直接調用,x作為第一個參數Console.WriteLine(y);}}static class Student //創建一個靜態類,構建方法{public static double Round(this double input,int digits) //第一個參數前面加上this{double result=Math.Round(input,digits);return result; } }
方法的重載
方法的名字可以相同,但是方法的簽名(代表唯一性)不能夠一樣。
方法簽名:由方法的名稱、類型形參的個數和它每一個形參的類型和種類(值、引用或輸出)組成。方法簽名不包含返回類型。
public int Add(ref int a,int b)
//加上ref變成傳引用的參數 public int Add(out int a,int b)
//加上out變成傳輸出的參數
可以改變參數類型或者種類、或者增加參數數量構成重載
構造器(constructor)
構造器是類型(值類型、引用類型)的成員之一,構造自己的內存塊(對實例進行初始化),沒有返回值,不需要寫方法返回類型,構造器=構造函數、成員函數=方法。靜態構造器只能構造靜態成員,無法構造實例成員。
Student stu = new Student(); //調用構造器
//如果沒有寫構造器,編譯器會自動生成一個默認的構造器,會對類里面的字段進行初始化
public Student(int initld,string initName)
{ //帶參數的構造器this.ID = initld; //this指當前類this.Name = initName;
}
Student stu = new Student(2,"opop");
//類的實例化也要帶上參數
打出ctor,按兩下tap鍵會自動生成一個構造器
析構器
當我們對象在內存當中沒有任何變量在引用它的時候,這個對象就不能夠再被訪問了,這塊內存為垃圾內存,就需要回收(例如實例化了一個對象,程序運行結束后,會回收實例的內存,此時就會執行析構函數)。在回收過程中如果我們想讓它做些事情,就放在析構器里。
~類名(){}
委托
委托是函數指針的“升級版”,可以把函數作為方法的參數。直接調用:通過函數名調用,間接調用:通過函數指針調用。
Action委托
internal class Program
{static void Main(string[] args){Student stu=new Student();Action action = new Action(stu.Report);//獲得方法名stu.Report(); //直接調用action.Invoke(); //間接調用action();}
}
class Student
{public void Report(){Console.WriteLine("111");}public int Add(int a,int b){int result = a + b;return result;}public int Sub(int a,int b){int result = a - b;return result; }
}
Function類型委托
static void Main(string[] args)
{Student stu=new Student();Action action = new Action(stu.Report);Func<int, int, int> func1 = new Func<int, int, int>(stu.Add);Func<int, int, int> func2 = new Func<int, int, int>(stu.Sub);//前面兩個是參數類型,后面是返回值類型,括號里面是方法int x = 100;int y = 200;int z = 0;z = func1.Invoke(x, y); //間接調用函數,也可以省略Invoke(仿照函數指針格式)Console.WriteLine(z);z= func2.Invoke(x, y);Console.WriteLine(z);
}
委托的聲明(自定義委托)
委托是一種類,類是數據類型所以委托也是一種數據類型。聲明方式與一般類不同,委托與所封裝的方法必須“類型兼容”(參數類型和返回值類型)。
public delegate double Calc(double x,double y);
//delegate聲明委托關鍵字,后面是目標方法的返回值類型,委托名,目標方法參數
Student stu = new Student();Calc calc1=new Calc(stu.Add); //傳遞給委托一個相同類型參數的方法Calc calc2 = new Calc(stu.Sub);Calc calc3 = new Calc(stu.Mul);Calc calc4 = new Calc(stu.Div);
double a=100,b=100;
c = calc1(a,b); //間接調用,傳遞參數
委托的一般使用
(1)把方法當作參數傳給另一個方法:模板方法、回調(callback)方法調用指定的外部方法。
當以回調方法來使用委托時,把委托類型的參數傳進主調方法里面去,被傳進主調方法里面的委托類型參數內部,會封裝一個被回調的方法。常位于代碼末尾,一般沒有返回值
缺點:(1)這是一種方法級別的緊耦合(2)使可讀性下降,debug難度增加(3)委托回調、異步調用和多線程糾纏在一起,會使代碼難以閱讀和維護(4)使用不當可能造成內存泄漏和程序性能下降
事件
事件的應用
事件:使類或對象具備通知能力的成員。類或對象通知其他類或對象的機制,為c#特有,用于對象或類間的動作協調與信息傳遞。例如響鈴這個事件,讓手機具備了通知關注者的能力。
消息(事件參數):由事件發送出來的與事件本身相關的數據。
響應事件(處理事件):根據通知和時間參數來采取行動的行為。
事件處理器:處理時間時具體所做的的事情。
事件的功能=通知+可選的事件參數
1.事件的擁有者:對象
2.事件成員:事件只能出現在+=或-=左邊(要么為事件添加事件處理器,要么移除處理器)
3.事件的響應者:當一個事件發生時,被通知的對象或類。它們會使用自己所擁有的事件處理器,來根據業務邏輯處理這個發生了的事件。
4.事件的處理器:本質上是一個回調方法。
5.事件訂閱:當一個事件發生時,事件的擁有者都會通知誰(被通知的對象一定是訂閱了這個事件)。把事件處理器與事件關聯在一起,拿什么樣的方法(事件處理器)才能夠處理這個事件。例如,孩子迷路了,孩子能夠告訴你一個地理信息(當前所在地方的路名等信息),那么這個時候你需要準備的事件處理器,就必須是一個能夠處理地理信息的方法。所以用于訂閱事件的事件處理器必須和事件遵守同一個約定(既約束了事件能夠把什么樣的消息發送給事件處理器,也約束了事件處理器能夠處理什么樣的消息)。如果事件是使用某個約定定義的,而且事件處理器也遵循同樣的約定,我們就說事件處理器與事件是匹配的,就可以拿這個處理器去訂閱這個事件。(約定可以理解為委托的參數,不一樣類型的參數或者不同數量的參數可以構成不同的方法以供回調)
internal class Program
{static void Main(string[] args){Timer timer = new Timer(); //事件擁有者timer.Interval = 1000;Boy boy = new Boy(); //事件響應者timer.Elapsed += boy.Action; //事件訂閱,事件訂閱操作符是+=,左邊是事件,右邊是處理器timer.Start();Console.ReadLine();}
}
class Boy
{internal void Action(object sender, ElapsedEventArgs e) //事件處理器{Console.WriteLine("jump!");}
}
internal class Program
{static void Main(string[] args){Form form = new Form(); //事件的擁有者(Form類)Con con = new Con(form); //事件響應者(Con類)//事件的處理結果是作用在事件擁有者身上的(響應函數傳的是擁有者參數)form.ShowDialog();}
}
class Con
{private Form form;public Con(Form form) //構造函數,傳引用類型{if(form != null){this.form = form; //讓實例字段指向之前new出來的Form類型實例//事件是form.Clickthis.form.Click += this.FormClicked;//事件訂閱,選擇這個時間的響應處理器}}private void FormClicked(object sender, EventArgs e) //事件處理器{this.form.Text = DateTime.Now.ToString();}
}
internal class Program
{static void Main(string[] args){myForm form = new myForm(); //事件擁有者同時也是事件的響應者form.Click+=form.Formclick; //事件:click,事件的訂閱form.ShowDialog();}
}
class myForm : Form //繼承
{internal void Formclick(object sender, EventArgs e) //事件處理器{this.Text = DateTime.Now.ToString();}
}
internal class Program
{static void Main(string[] args){myForm form = new myForm();form.ShowDialog();}
}
class myForm : Form //繼承
{private TextBox textBox;private Button button; //事件的擁有者public myForm(){this.textBox = new TextBox();this.button = new Button();this.Controls.Add(this.textBox);this.Controls.Add(this.button);this.button.Click += this.ButtonClicked; //事件訂閱//事件:click,//事件的響應者是我們這個myForm對象,因為ButtonClick這個處理器是myForm類型//的實例方法}private void ButtonClicked(object sender, EventArgs e) //事件處理器{this.textBox.Text = "Hello Wrold!!!!!!!!!!!";}
}
自定義事件
public event 委托名 事件名 //簡要聲明
//public能夠被外界訪問到,evnt聲明事件關鍵字,約束這個事件的委托類型,聲明事件的名字
//最好打斷點看看過程
using System;
using System.Threading;
namespace EventExample
{class Program{static void Main(string[] args){// 1.事件擁有者//觸發這個事件一定是事件擁有著內部邏輯觸發的這個事件var customer = new Customer();// 2.事件響應者var waiter = new Waiter();// 3.Order 事件成員 5. +=事件訂閱customer.Order += waiter.Action;customer.Action();customer.PayTheBill();}}// 該類用于傳遞點的是什么菜,作為事件參數,需要以 EventArgs 結尾,且繼承自 EventArgspublic class OrderEventArgs:EventArgs{public string DishName { get; set; }public string Size { get; set; }}// 聲明一個委托類型,因為該委托用于事件處理,所以以 EventHandler 結尾// 注意委托類型的聲明和類聲明是平級的public delegate void OrderEventHandler(Customer customer, OrderEventArgs e);public class Customer{// 委托類型字段private OrderEventHandler orderEventHandler;// 完整的事件聲明public event OrderEventHandler Order{add { this.orderEventHandler += value; }remove { this.orderEventHandler -= value; }}public double Bill { get; set; }public void PayTheBill(){Console.WriteLine("I will pay ${0}.",this.Bill);}public void WalkIn(){Console.WriteLine("Walk into the restaurant");}public void SitDown(){Console.WriteLine("Sit down.");}public void Think(){for (int i = 0; i < 5; i++){Console.WriteLine("Let me think ...");Thread.Sleep(1000);}if (this.orderEventHandler != null) //觸發這個事件,判斷這個委托是否為空{var e = new OrderEventArgs();e.DishName = "Kongpao Chicken";e.Size = "large";this.orderEventHandler.Invoke(this,e); //使用委托字段,間接調用}}public void Action(){Console.ReadLine();this.WalkIn();this.SitDown();this.Think();}}public class Waiter{// 4.事件處理器public void Action(Customer customer, OrderEventArgs e){Console.WriteLine("I will serve you the dish - {0}.",e.DishName);double price = 10;switch (e.Size){case "small":price *= 0.5;break;case "large":price *= 1.5;break;default:break;}customer.Bill += price;}}
}
事件的本質就是委托字段的一個包裝器,這個包裝器對委托字段的訪問起限制作用,謹防“借刀殺人”。加了event關鍵字相當于允許存在兩個相同名稱的變量,一個是對外public的order事件,一個是對外private的order委托。我們可以把委托的參數列表看作是事件發生后發送給事件響應者的“事件消息”。事件基于委托,處理器需要啥,事件就傳遞啥(決定了參數列表),同時處理器是一個方法,能存儲方法及其參數類型的只有委托。
父類(基類)轉子類
public void Action(object sender, EventArgs e) //任何類型都是object的子類,所以都可以被傳參
{//as表示轉化為Customer customer = sender as Customer; //向下轉換(父類轉子類),因為objct基類訪問不了子類屬性OrderEventArgs order = e as OrderEventArgs;Console.WriteLine("I will serve you the dish - {0}.", order.DishName); //轉化類型后,可以訪問DishNamedouble price = 10;customer.Bill += price;
}
事件的命名
事件與委托的關系
-
事件真的是“以特殊方式聲明的委托字段/實例”嗎?
-
- 不是!只是聲明的時候“看起來像”(對比委托字段與事件的簡化聲明,field-like)
- 事件聲明的時候使用了委托類型,簡化聲明造成事件看上去像一個委托的字段(實例),而 event 關鍵字則更像是一個修飾符 —— 這就是錯覺的來源之一
- 訂閱事件的時候 += 操作符后面可以是一個委托實例,這與委托實例的賦值方法語句相同,這也讓事件看起來像是一個委托字段 —— 這是錯覺的又一來源
- 重申:事件的本質是加裝在委托字段上的一個“蒙版”(mask),是個起掩蔽作用的包裝器。這個用于阻擋非法操作的“蒙版”絕不是委托字段本身
-
為什么要使用委托類型來聲明事件?
-
- 站在 source 的角度來看,是為了表明 source 能對外傳遞哪些消息
- 站在 subscriber 的角度來看,它是一種約定,是為了約束能夠使用什么樣簽名的方法來處理(響應)事件
- 委托類型的實例將用于存儲(引用)事件處理器
-
對比事件與屬性
-
- 屬性不是字段 —— 很多時候屬性是字段的包裝器,這個包裝器用來保護字段不被濫用
- 事件不是委托字段 —— 它是委托字段的包裝器,這個包裝器用來保護委托字段不被濫用
- 包裝器永遠都不可能是被包裝的東西
在上圖中是被迫使用事件去做 !=
和 .Invoke()
,學過事件完整聲明格式,就知道事件做不了這些。在這里能這樣是因為簡略格式下事件背后的委托字段是編譯器自動生成的,這里訪問不了。
總結:事件不是委托類型字段(無論看起來多像),它是委托類型字段的包裝器,限制器,從外界只能訪問 += -= 操作。
靜態成員與實例成員
靜態(static)成員在語義上表示“類的成員”,類與生俱來的。
實例(非靜態)成員在語義上表示“對象成員”,屬于對象的而不是某個類。例如對于人這個類,它的靜態屬性為平均身高、平均體重;而對于某個人(實例)的屬性應該為身高、體重。
綁定(Binding)指的是編譯器如何把一個成員與類或對象關聯起來
如果類的成員函數是靜態的可以通過類名直接調用該函數,如果不是靜態則需要類的實例化對象才能用。
類的繼承
繼承就是完整接收父類成員的前提下(字段、屬性、方法、事件),對父類進行橫向(類成員變多)與縱向(不增加類成員個數,對某些類成員的版本進行擴充或者是對類成員的重寫)的擴展。
基類–派生類、父類–子類
一個派生類的實例,也是基類的實例;基類的實例不一定是派生類的實例。那么我們可以用一個父類類型的變量去引用子類類型的實例。派生類的訪問級別不能超過基類。
Vehicle vehicle =new Car();
子類會全部繼承父類的成員。一旦一個類成員被加入繼承鏈中,就不能把它去掉了。
public公有的,其他項目也可訪問這個類;未嵌套在其他類型中的類的默認可訪問性為internal,同一名稱空間下的類可以訪問;
public其他地方都可以訪問到這個成員;internal此命名空間類都可訪問,別的項目無法訪問;類成員的默認可訪問性為private,僅供此類訪問。
base可以訪問基類對象
在子類中修改和父類共有的屬性,會修改父類屬性的值(重寫)。
class Vehicle
{public Vehicle(string owner){this.Owner=owner;}public string Owner { get; set; }
}
class Car:Vehicle
{//當我們Car的構造器被調用時,那么它會默認調用父類的無參構造器//如果父類構造器有參數,那么我們子類構造器要給出參數public Car(string owner) : base(owner){//因為在父類構造器里已經把Owner的值設置為owner了,沒必要在設置一遍了。}
}
當父類的成員被protected修飾時,它的子類都可以訪問這個成員,而不在這個繼承鏈上的其他類型無法訪問這個成員。
重寫與多態
不同的子類可能有不同的行為,所以要進行重寫。
子類對父類成員的重寫。因為類成員個數還是那么多,只是更新版本,所以又稱為縱向擴展。注:重寫時,Car 里面只有一個版本的 Run。
重寫需要父類成員標記為 virtual,子類成員標記 override。注:被標記為 override 的成員,隱含也是 virtual 的,可以繼續被重寫。
virtual:可被重寫的、名義上的、名存實亡的
class Program
{static void Main(string[] args){var car = new Car();car.Run();// Car is running!var v = new Vehicle();v.Run();// I'm running!}
}
class Vehicle
{public virtual void Run(){Console.WriteLine("I'm running!");}
}
class Car : Vehicle
{public override void Run(){Console.WriteLine("Car is running!");}
}
Hide
如果子類和父類中函數成員簽名相同,但又沒標記 virtual 和 override,稱為 hide 隱藏。
這會導致 Car 類里面有兩個 Run 方法,一個是從 Vehicle 繼承的 base.Run(),一個是自己聲明的 this.Run()。
Vehicle v=new Car;
可以理解為 v 作為 Vehicle 類型,它本來應該順著繼承鏈往下(一直到 Car)找 Run 的具體實現,但由于 Car 沒有 Override,所以它找不下去(找不到最新的版本),只能調用 Vehicle 里面的 Run(打印出來的是 Im running!)。
注:
- 新手不必過于糾結 Override 和 Hide 的區分、關聯。因為原則上是不推薦用 Hide 的。很多時候甚至會視 Hide 為一種錯誤
- Java 里面是天然重寫,不必加 virtual 和 override,也沒有 Hide 這種情況
- Java 里面的 @Override(annotation)只起到輔助檢查重寫是否有效的功能
Polymorphism 多態
多態:C# 支持用父類類型的變量引用子類類型的實例。函數成員的具體行為(版本)由對象決定。
回顧:因為 C# 語言的變量和對象都是有類型的,就導致存在變量類型與對象類型不一致的情況,所以會有“代差”。
class Program
{static void Main(string[] args){Vehicle v = new RaceCar();v.Run();// Race car is running!Car c = new RaceCar();c.Run();// Race car is running!Console.ReadKey();}
}
class Vehicle
{public virtual void Run(){Console.WriteLine("I'm running!");}
}
class Car : Vehicle
{public override void Run(){Console.WriteLine("Car is running!");}
}
class RaceCar : Car
{public override void Run(){Console.WriteLine("Race car is running!");}
}
class Program
{static void Main(string[] args){Vehicle v = new Car();v.Run(); //Car is running!}
}
class Vehicle
{public virtual void Run() //如果不重寫就會調用這個方法{Console.WriteLine("I'm running!");}
}
class Car : Vehicle
{public override void Run(){Console.WriteLine("Car is running!");}
}
接口,抽象類,SOLID,單元測試
SOLID
-
SRP:Single Responsibility Principle
-
OCP:Open Closed Principle
-
LSP:Liskov Substitution Principle
-
ISP:InterfaceSegregation Principle
-
DIP:Dependency Inversion Principle
SOLID(單一功能、開閉原則、里氏替換、接口隔離以及依賴反轉)是由羅伯特·C·馬丁在21世紀早期引入的記憶術首字母縮略字,指代了面向對象編程和面向對象設計的五個基本原則。
首字母 | 指代 | 概念 |
---|---|---|
S | 單一功能原則 | 對象應該僅具有一種單一功能。 |
O | 開閉原則 | 軟件體應該是對于擴展開放的,但是對于修改封閉的。 |
L | 里氏替換原則 | 程序中的對象應該是可以在不改變程序正確性的前提下被它的子類所替換。參考 契約式設計。 |
I | 接口隔離原則 | 多個特定客戶端接口要好于一個寬泛用途的接口。 |
D | 依賴反轉原則 | 一個方法應該遵從“依賴于抽象而不是一個實例”。依賴注入是該原則的一種實現方式。 |
抽象類與開閉原則
抽象類指的是函數成員至少有一個沒有被完全實現的類,一但類里有抽象方法,這個類就變成了抽象類。
abstract class Student //抽象類
{abstract public void Study(); //抽象方法,方法沒有被完全實現,沒有方法體,用abstract修飾//抽象方法不能是private,可以是public,internal,因為最后肯定是這個抽象類的子類去實現這個方法//接口要求成員方法必須是public
}
因為抽象類有未被實現的方法,沒有具體的行為,所以不允許實例化一個抽象類(如果去調用這個方法的話,計算機不知道怎么執行)。
一個類不允許實例化,它就只剩兩個用處了:
作為基類,在派生類里面實現基類中的 abstract 成員
聲明基類(抽象類)類型變量去引用子類(已實現基類中的 abstract 成員)類型的實例,這又稱為多態
所以我們應該封裝那些不變的、穩定的、固定的和確定的成員,而把那些不確定的、有可能改變的的成員聲明為抽象成員,并且留給子類實現。
class Program
{static void Main(string[] args){Vehicle v = new Car(); //多態v.Run();}
}
abstract class Vehicle
{public abstract void Run();
}
class Car : Vehicle
{public override void Run() //對這個抽象類進行重寫,如果你不重寫,那你也要是抽象類{Console.WriteLine("Car is running!"); //所以抽象類專為基類而生的}
}
接口
純抽象類(純虛類)==接口(所有成員都是抽象的,沒有實現)。
接口和抽象類都是“軟件工程產物”,如果不是為了修復 bug 和添加新功能,別總去修改類的代碼,特別是類當中函數成員的代碼。防止違反開閉原則。接口為解耦而生:“高內聚,低耦合”,方便單元測試。
class Program
{static void Main(string[] args){Vehicle v = new Car();v.Run();}
}
abstract class VehicleBase //純抽象類(接口)
{abstract public void Run();abstract public void Fill();abstract public void Stop();
}
abstract class Vehicle:VehicleBase
{public override void Fill(){Console.WriteLine("Fill");}public override void Stop(){Console.WriteLine("Stop");}
}
class Car : Vehicle
{public override void Run(){Console.WriteLine("Car is running!");}
}
interface IVehicle //接口可以這樣寫
{void Run(); void Fill(); //接口要求成員必須是public,并且接口所有成員都是abstract,所以省去避免重復void Stop();
}
abstract class Vehicle:IVehicle //不完全實現的抽象類
{public void Fill() //去掉override,因為上面去掉了abstract{Console.WriteLine("Fill");}public void Stop(){Console.WriteLine("Stop");}abstract public void Run();
}
class Car : Vehicle //根據不完全實現的基類去創建具體類
{public override void Run(){Console.WriteLine("Car is running!");}
}
依賴與耦合
現實世界中有分工、合作,面向對象是對現實世界的抽象,它也有分工、合作。
類與類、對象與對象間的分工、合作。
在面向對象中,合作有個專業術語“依賴”,依賴的同時就出現了耦合。依賴越直接,耦合就越緊。
Car 與 Engine 緊耦合的示例:
class Program
{static void Main(string[] args){var engine = new Engine();var car = new Car(engine);car.Run(3);Console.WriteLine(car.Speed);}
}class Engine
{public int RPM { get; private set; }public void Work(int gas){this.RPM = 1000 * gas;}
}class Car
{// Car 里面有個 Engine 類型的字段,它兩就是緊耦合了// Car 依賴于 Engineprivate Engine _engine;public int Speed { get; private set; }public Car(Engine engine){_engine = engine;}public void Run(int gas){_engine.Work(gas);this.Speed = _engine.RPM / 100;}
}
緊耦合的問題:
基礎類一旦出問題,上層類寫得再好也沒轍
程序調試時很難定位問題源頭
基礎類修改時,會影響寫上層類的其他程序員的工作
所以程序開發中要盡量避免緊耦合,解決方法就是接口。
接口:
約束調用者只能調用接口中包含的方法
讓調用者放心去調,不必關心方法怎么實現的、誰提供的
接口解耦示例
以老式手機舉例,對用戶來說他只關心手機可以接(打)電話和收(發)短信。
對于手機廠商,接口約束了他只要造的是手機,就必須可靠實現上面的四個功能。
用戶如果丟了個手機,他只要再買個手機,不必關心是那個牌子的,肯定也包含這四個功能,上手就可以用。用術語來說就是“人和手機是解耦的”。
class Program
{static void Main(string[] args){//var user = new PhoneUser(new NokiaPhone());var user = new PhoneUser(new EricssonPhone()); //構造器傳參user.UsePhone(); //如果我們的EricssPhone手機壞了,我們只需要修改一下傳入的參數即可//而類里面的方法都不需要修改,用接口可以降低耦合Console.ReadKey();}
}class PhoneUser
{private IPhone _phone;public PhoneUser(IPhone phone){_phone = phone;}public void UsePhone(){_phone.Dail();_phone.PickUp();_phone.Receive();_phone.Send();}
}interface IPhone
{void Dail();void PickUp();void Send();void Receive();
}class NokiaPhone : IPhone
{public void Dail(){Console.WriteLine("Nokia calling ...");}public void PickUp(){Console.WriteLine("Hello! This is Tim!");}public void Send(){Console.WriteLine("Nokia message ring ...");}public void Receive(){Console.WriteLine("Hello!");}
}class EricssonPhone : IPhone
{public void Dail(){Console.WriteLine("Ericsson calling ...");}public void PickUp(){Console.WriteLine("Hello! This is Tim!");}public void Send(){Console.WriteLine("Ericsson ring ...");}public void Receive(){Console.WriteLine("Good evening!");}
}
沒有用接口時,如果一個類壞了,你需要 Open 它再去修改,修改時可能產生難以預料的副作用。引入接口后,耦合度大幅降低,換手機只需要換個類名,就可以了。等學了反射后,連這里的一行代碼都不需要改,只要在配置文件中修改一個名字即可。
在代碼中只要有可以替換的地方,就一定有接口的存在;接口就是為了解耦(松耦合)而生。
松耦合最大的好處就是讓功能的提供方變得可替換,從而降低緊耦合時“功能的提供方不可替換”帶來的高風險和高成本。
高風險:功能提供方一旦出問題,依賴于它的功能都掛
高成本:如果功能提供方的程序員崩了,會導致功能使用方的整個團隊工作受阻
依賴反轉原則
解耦在代碼中的表現就是依賴反轉。單元測試就是依賴反轉在開發中的直接應用和直接受益者。
人類解決問題的典型思維:自頂向下,逐步求精。
在面向對象里像這樣來解決問題時,這些問題就變成了不同的類,且類和類之間緊耦合,它們也形成了這樣的金字塔。
依賴反轉給了我們一種新思路,用來平衡自頂向下的思維方式。
平衡:不要一味推崇依賴反轉,很多時候自頂向下就很好用,就該用。
接口隔離,反射,特性,依賴注入
接口隔離
接口即契約:甲方“我不會多要”;乙方“我不會少給”。
●乙方不會少給:硬性規定,即一個類只要實現了接口,就必需實現接口里面的所有方法,一旦未全部實現,類就還只是個抽象類,仍然不能實例化
●甲方不會多要:軟性規定,是個設計問題
胖接口及其產生原因
觀察傳給調用者的接口里面有沒有一直沒有調用的函數成員,如果有,就說明傳進來的接口類型太大了。換句話說,這個“胖”接口是由兩個或兩個以上的小接口合并起來的。
顯式接口實現
接口隔離的第三個例子,專門用來展示 C# 特有的能力 —— 顯式接口實現。
C# 能把隔離出來的接口隱藏起來,直到你顯式的使用這種接口類型的變量去引用實現了該接口的實例,該接口內的方法才能被你看見被你使用。
這個例子的背景是《這個殺手不太冷》,WarmKiller 有其兩面性,即實現 IGentleman 又實現 IKiller。
之前見過的多接口的默認實現方式,該方式從業務邏輯來說是有問題的,一個殺手不應該總將 Kill 方法暴露出來。
換言之,如果接口里面有的方法,我們不希望它輕易被調用,就不能讓它輕易被別人看到。這就要用到接口的顯式實現了。
class Program
{static void Main(string[] args){var wk = new WarmKiller();wk.Love(); //類里面的方法都能被調用wk.Kill(); }
}
interface IGentleman
{void Love();
}
interface IKiller
{void Kill();
}
class WarmKiller : IGentleman, IKiller
{public void Love(){Console.WriteLine("I will love you forever ...");}public void Kill(){Console.WriteLine("Let me kill the enemy ...");}
}
顯式接口
static void Main(string[] args)
{IKiller killer = new WarmKiller();killer.Kill(); //Ikiller類型變量可以調用var wk = (WarmKiller) killer;wk.Love(); //看不到kill方法
}
class WarmKiller : IGentleman, IKiller
{public void Love(){Console.WriteLine("I will love you forever ...");}// 顯式實現,只有當該類型(WarmKiller)實例被 IKiller 類型變量引用時該方法才能被調用void IKiller.Kill(){Console.WriteLine("Let me kill the enemy ...");}
}
反射 Reflection
反射:你給我一個對象,我能在不用 new 操作符也不知道該對象的靜態類型的情況下,我能給你創建出一個同類型的對象,還能訪問該對象的各個成員。
這相當于進一步的解耦,因為有 new 操作符后面總得跟類型,一跟類型就有了緊耦合的依賴。依靠反射創建類型不再需要 new 操作符也無需靜態類型,這樣使得很多時候耦合可以松到忽略不計。
反射不是 C# 語言的功能,而是 .NET 框架的功能,所以 .NET 框架支持的語言都能使用反射功能。
C# 和 Java 這種托管類型的語言和 C、C++ 這些原生類型的語言區別之一就是反射。
單元測試、依賴注入、泛型編程都是基于反射機制的。
當程序處于動態期(dynamic)用戶已經用上了,不再是開發時的靜態期(static)。動態期用戶和程序間的操作是難以預測的,如果你要在開始時將所有情況都預料到,那程序的復雜度難免太高,指不定就是成百上千的 if else,即使你真的這樣做了,寫出來的程序也非常難維護,可讀性很低。很多時候更有可能是我們在編寫程序時無法詳盡枚舉出用戶可能進行的操作,這時我們的程序就需要一種以不變應萬變的能力。
注:
-
.NET Framework 和 .NET Core 都有反射機制,但它們的類庫不太一樣,使用反射時用到的一些 API 也略有差別,示例是基于 .NET Core 的
-
反射畢竟是動態地去內存中獲取對象的描述、對象類型的描述,再用這些描述去創建新的對象,整個過程會影響程序性能,所以不要盲目過多地使用反射
依賴注入
例一:反射的原理以及和反射密切相關的一個重要技能 —— 依賴注入。
例一沿用的是一開始那個 Tank 的例子。
static void Main(string[] args)
{// ITank、HeavyTank 這些都算靜態類型ITank tank = new HeavyTank();// ======華麗的分割線======// 分割線以下不再使用靜態類型var t = tank.GetType();object o = Activator.CreateInstance(t);MethodInfo fireMi = t.GetMethod("Fire");MethodInfo runMi = t.GetMethod("Run");fireMi.Invoke(o, null);runMi.Invoke(o, null);
}
...
依賴注入框架
依賴注入需要借助依賴注入框架,.NET Core 的依賴注入框架就是:Microsoft.Extensions.DependencyInjection。
依賴注入最重要的元素就是 Container(容器),我們把各種各樣的類型和它們對應的接口都放在(注冊)容器里面。回頭我們創建實例時就找容器要。
容器又叫做 Service Provider
在注冊類型時我們可以設置容器創建對象的規則:每次給我們一個新對象或是使用單例模式(每次要的時候容器都給我們同一個實例)。
具體怎么使用容器此處按下不表,主要還是看依賴注入怎么用。
static void Main(string[] args)
{// ServiceCollection 就是容器var sc = new ServiceCollection();// 添加 ITank,并設置其對應的實現類是 HeavyTanksc.AddScoped(typeof(ITank), typeof(HeavyTank));var sp = sc.BuildServiceProvider();// ===========分割線===========// 分割線上面是整個程序的一次性注冊,下面是具體使用ITank tank = sp.GetService<ITank>();tank.Fire();tank.Run();
}
Driver 創建時本來需要一個 IVehicle 對象,sp 會去容器里面找,又由于我們注冊了 IVehicle 的實現類是 Car,所以會自動創建一個 Car 實例傳入 Driver 的構造器 。
泛型,partial類,枚舉,結構體
泛型(generic)
●為什么需要泛型:避免成員膨脹或類型膨脹
●正交性:泛型類型(類、接口、委托……) 泛型成員(屬性、方法、字段……)
●類型方法的參數推斷
●泛型與委托,Lambda 表達式
泛型在面向對象中的地位與接口相當。其內容很多,今天只介紹最常用最重要的部分。
基本原理
正交性:泛型和其它的編程實體都有正交點,導致泛型對編程的影響廣泛而深刻。
泛化 <-> 具體化
泛型類實例
示例背景:開了個小商店,一開始只賣蘋果,賣的蘋果用小盒子裝上給顧客。顧客買到后可以打開盒子看蘋果顏色。
class Program
{static void Main(string[] args){var apple = new Apple { Color = "Red" };var box = new Box { Cargo = apple };Console.WriteLine(box.Cargo.Color);}
}
class Apple
{public string Color { get; set; }
}
class Box
{public Apple Cargo { get; set; }
}
后來小商店要增加商品(賣書),有下面幾種處理方法:
一:我們專門為 Book 類添加一個 BookBox 類的盒子。
static void Main(string[] args)
{var apple = new Apple { Color = "Red" };var box = new AppleBox { Cargo = apple };Console.WriteLine(box.Cargo.Color);var book = new Book { Name = "New Book" };var bookBox = new BookBox { Cargo = book };Console.WriteLine(bookBox.Cargo.Name);
}
現在代碼就出現了“類型膨脹”的問題。未來隨著商品種類的增多,盒子種類也須越來越多,類型膨脹,不好維護。
二:用同一個 Box 類,每增加一個商品時就給 Box 類添加一個屬性。
class Program
{static void Main(string[] args){var apple = new Apple { Color = "Red" };var book = new Book { Name = "New Book" };var box1 = new Box { Apple = apple };var box2 = new Box { Book = book };}
}...class Book
{public string Name { get; set; }
}class Box
{public Apple Apple { get; set; }public Book Book { get; set; }
}
這會導致每個 box 變量只有一個屬性被使用,也就是“成員膨脹”(類中的很多成員都是用不到的)。
三:Box 類里面的 Cargo 改為 Object 類型。
class Program
{static void Main(string[] args){var apple = new Apple { Color = "Red" };var book = new Book { Name = "New Book" };var box1 = new Box { Cargo = apple };var box2 = new Box { Cargo = book };Console.WriteLine((box1.Cargo as Apple)?.Color);}
}...class Box
{public Object Cargo{ get; set; }
}
使用時必須進行強制類型轉換或 as,即向盒子里面裝東西省事了,但取東西時很麻煩。
泛型登場
泛型類定義時相當于原材料,使用時再加工成具體的形狀
class Program
{static void Main(string[] args){var apple = new Apple { Color = "Red" };var book = new Book { Name = "New Book" };var box1 = new Box<Apple> { Cargo = apple }; //使用泛型實體之前要進行泛化,這里泛化為一個裝蘋果的類型//那么Apple類型就替換了Tcargo這個類型var box2 = new Box<Book> { Cargo = book };Console.WriteLine(box1.Cargo.Color);Console.WriteLine(box2.Cargo.Name);}
}
class Apple
{public string Color { get; set; }
}
class Book
{public string Name { get; set; }
}
class Box<TCargo> //泛化需要在類名后加上<>,里面放所要泛化的類型(自定義名)
{public TCargo Cargo { get; set; }
}
泛型接口實例
class Program
{static void Main(string[] args){//var stu = new Student<int>();//stu.Id = 101;//stu.Name = "Timothy";var stu = new Student<ulong>(); //特化為ulongstu.Id = 1000000000000001;stu.Name = "Timothy";var stu2 = new Student();stu2.Id = 100000000001;stu2.Name = "Elizabeth";}
}interface IUnique<T> //如果有學生類,老師類等都可以繼承這個泛型接口
{T Id { get; set; }
}// 泛型類實現泛型接口
class Student<T> : IUnique<T> //因為要繼承上面的泛型接口,所以這里也要是泛型
{public T Id { get; set; }public string Name { get; set; }
}// 具體類實現特化化后的泛型接口
class Student : IUnique<ulong>
{public ulong Id { get; set; }public string Name { get; set; }
}
也可以這樣寫:
class Program
{static void Main(string[] args){var stu = new Student(); //這里就不用特化了stu.Id = 1000000000000001;stu.Name = "Timothy";}
}
class Student: IUnique<ulong> //也可以在類實現這個泛型接口時,進行特化
{public T Id { get; set; }public string Name { get; set; }
}
泛型集合
static void Main(string[] args)
{IList<int> list = new List<int>(); //動態數組listfor (var i = 0; i < 100; i++) //Ilist泛型類{list.Add(i);}foreach (var item in list){Console.WriteLine(item);}
}
List定義:
public class List<T> : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList
{...
}
- IEnumerable:可迭代
- ICollection:集合,可以添加和移除元素
泛型委托
action委托
action委托只能引用沒有返回值的方法
static void Main(string[] args)
{Action<string> a1 = Say;a1("Timothy");Action<int> a2 = Mul;a2(1);
}static void Say(string str)
{Console.WriteLine($"Hello, {str}!");
}static void Mul(int x)
{Console.WriteLine(x * 100);
}
function委托
static void Main(string[] args)
{Func<int, int, int> f1 = Add;Console.WriteLine(f1(1, 2));Func<double, double, double> f2 = Add;Console.WriteLine(f2(1.1, 2.2));
}static int Add(int a, int b)
{return a + b;
}static double Add(double a, double b)
{return a + b;
}
配合 Lambda 表達式:
//Func<int, int, int> f1 = (int a, int b) => { return a + b; };
Func<int, int, int> f1 = (a, b) => { return a + b; };
Console.WriteLine(f1(1, 2));
partial 類
減少派生類
學習類的繼承時就提到過一個概念,“把不變的內容寫在基類里,在子類里寫經常改變的內容”。這就導致一個類中只要有經常改變的內容,我們就要為它聲明一個派生類,如果改變的部分比較多,還得聲明多個或多層派生類,導致派生結構非常復雜。
有 partial 類后,我們按照邏輯將類切分成幾塊,每塊作為一個邏輯單元單獨更新迭代,這些分塊合并起來還是一個類。
枚舉類型
- 人為限定取值范圍的整數
- 整數值的對應
- 比特位式用法
枚舉示例
如何設計員工類的級別屬性。
- 使用數字? 大小不明確
- 使用字符串? 無法約束程序員的輸入
使用枚舉,即限定輸入,又清晰明了:
class Program
{static void Main(string[] args){var employee = new Person{Level = Level.Employee};var boss = new Person{Level = Level.Boss};Console.WriteLine(boss.Level > employee.Level);// TrueConsole.WriteLine((int)Level.Employee);// 0Console.WriteLine((int)Level.Manager); // 100Console.WriteLine((int)Level.Boss); // 200Console.WriteLine((int)Level.BigBoss); // 201}
}enum Level
{Employee,Manager = 100,Boss = 200,BigBoss,
}class Person
{public int Id { get; set; }public string Name { get; set; }public Level Level { get; set; }
}
枚舉的比特位用法
class Program
{static void Main(string[] args){var employee = new Person{Name = "Timothy",Skill = Skill.Drive | Skill.Cook | Skill.Program | Skill.Teach//將enum按位或可以讓這個對象擁有多個技能};Console.WriteLine(employee.Skill); // 15// 過時用法不推薦//Console.WriteLine((employee.Skill & Skill.Cook) == Skill.Cook); // True// .NET Framework 4.0 后推薦的用法Console.WriteLine((employee.Skill.HasFlag(Skill.Cook))); // True}
}[Flags]
enum Skill
{Drive = 1, //二進制0001Cook = 2, //0010Program = 4, //0100Teach = 8, //1000
}class Person
{public int Id { get; set; }public string Name { get; set; }public Skill Skill { get; set; }
}
結構體
- 值類型,可裝/拆箱
- 可實現接口,不能派生自類/結構體
- 不能有顯式無參構造器
class Program
{static void Main(string[] args){Student stu = new Student { Id = 101, Name = "Timothy" };// 裝箱:復制一份棧上的 stu ,放到堆上去,然后用 obj 引用堆上的 student 實例object obj = stu;// 拆箱Student stu2 = (Student)obj; //強制Console.WriteLine($"#{stu2.Id} Name:{stu2.Name}");}
}struct Student
{public int Id { get; set; }public string Name { get; set; }
}
因為是值類型,所以拷貝時是值復制:
static void Main(string[] args)
{var stu1 = new Student { Id = 101, Name = "Timothy" };// 結構體賦值是值復制var stu2 = stu1;stu2.Id = 1001;stu2.Name = "Michael";Console.WriteLine($"#{stu1.Id} Name:{stu1.Name}"); //如果Student為類,輸出1001 Michael,因為類為引用類型//輸出為 #101 Name:Timothy
}
結構體可以實現接口
class Program
{static void Main(string[] args){var stu1 = new Student { Id = 101, Name = "Timothy" };stu1.Speak();}
}interface ISpeak //接口
{void Speak();
}struct Student : ISpeak //
{public int Id { get; set; }public string Name { get; set; }public void Speak() //實現抽象方法{Console.WriteLine($"I'm #{Id} student {Name}");}
}
注:
- 將結構體轉換為接口時要裝箱
- 結構體不能有基類或基結構體,只可以實現接口
- 結構體不能有顯示無參構造器
總結
因為之前模模糊糊的大致過了一遍,可以看我上一次發布的關于c#的文章,真的是毫無邏輯。這次花了4天學完了,能夠聽一個好老師講學著會比較清晰,
劉鐵猛《C#語言入門詳解》全集,在b站上一直聽這個老師的課,講的很好,真的是深入淺出。因為我也是初學者某些地方也不太懂,只能把我學習過程的筆記整理一下供大家參考。