SkillTree OOP C# 第一天
本文內容資料引用來自上課內容。
1.物件導向本身來自於抽象的概念
抽象的概念,我自己解釋為思考如何將一堆具體化的東西,找出共同非常容易辨識的特徵當作欄位(Field )與方法(Property)。註1
以上課例子為例,汽車是一個代表性的名詞,但汽車的分類中只要座位數約七人以內,車頂高度約170公分,第一直覺就是休旅車,車頂高度約150左右座位人數約四人稱為房車,看到車子的外型流線或買不起就是超跑。
但以上都都稱為汽車。
註1:在class中宣告的變數稱之為欄位,class中寫Method稱之為方法。
2.培養抽象的概念
當同樣的需求,產生兩套的程式碼,在相同的效能之下以較簡單的Code勝出。
例子:1~100的總和的寫法,A寫法 1+2+3...+100,B寫法for迴圈,C寫法採用梯形公式 (1+100)*100/2=5050。
因此當然以C寫法勝出,因此演算法工程師應該工作內容就是優化程式碼的效能吧採取更聰明的做法達到相同的結果,如有對演算法工程師解釋錯誤請大大糾正謝謝。
3.OOP三大特性:封裝、繼承、多型
A.封裝
在產生class大多數的時候,都會在欄位與方法前方加上public或private。
在上圖之中,會發現Height的欄位當中開頭寫private(私有的),因此在new出來時是無法使用的,代表Height的欄位封印在A的class當中,也只能在A的class大括號範圍內使用。
若在欄位開頭寫public(公開的)就可以在new方法出來後做存取的使用。在class中定義的方法開頭寫public或private意思相同。
在應用上例子,今天如果產生一個DLL檔案給客戶使用,但不想讓使用者擅自修改欄位內的value或著看不見該欄位,就採取private的方式可以避免使用者進行修改。
建議對於欄位的value變更,透過方法來變更欄位內的value較為安全,因為加入條件讓使用者必須輸入限定範圍內的value,可以避免亂設定參數導致Exception發生。
對於封裝的分成 private、public、internal、protected、internal protected 有興趣朋友可上網自行研究差異性。
B.繼承
以下new出了Man只有單純的age、OL、Property三個欄位,但人的基本資料缺少了身高與體重。
在Woman的class中繼承People class,就可以在new Woman物件中使用People class中提供的欄位與方法。
繼承是可以將父類別所公開的欄位與方法取之使用。
這樣子只要寫一個People class就可以讓Man與Woman去繼承People得到身高、體重、BMI,可減少Man與Woman的class中寫入重複的欄位與方法。
以下為定義的class
class People
{
public double Height;
public double weight;
public bool gender;
public double BMI()
{
return weight / (Height * Height / 1000);
}
}
class Woman : People
{
public int Hips;//臀圍
public int Waistline;//腰圍
public int bust;//胸圍(cup問題在此略過)
public int age;//年齡
}
class Man
{
public double Property;//財產單位(萬)(不動產與動產合計之)
public int Educational_Level;//學歷:1~5(小學~博士)
public int OL;//職等1~10(二等專員、高等專員、副科長、科長、副理、經理、副總經理、總經理、副董、董事長)
public int age;//年齡
}
C.多型
多型意思是在同一個class當中有多個同樣的方法名稱用帶入參數數量或不同型態來做區隔,稱為多型。
class Program
{
static void Main(string[] args)
{
//帶入參數為int
var area1 = area.CalculateArea(10, 10);
//帶入參數為double
var area2 = area.CalculateArea(5.5, 6.6);
//帶入參數為int
var area3 = area.CalculateArea(5, 10, 10);
//帶入參數為double
var area4 = area.CalculateArea(5.5, 6.6, 10.5);
Console.Write(area1 + "\n");
Console.Write(area2 + "\n");
Console.Write(area3 + "\n");
Console.Write(area4 + "\n");
Console.ReadLine();
}
}
static class area
{
//方形
public static double CalculateArea(double Height, double width)
{
return Height * width;
}
//方形
public static double CalculateArea(int Height, int width)
{
return Height * width;
}
//梯形
public static double CalculateArea(double B1, double B2, double Height)
{
return (B1 + B2) * Height / 2;
}
//梯形
public static double CalculateArea(int B1, int B2, int Height)
{
return (B1 + B2) * Height / 2;
}
}
4.參考型別 VS 實質型別
先略過...
5.類別
類別包含可包巢狀型別、常數、欄位、屬性、方法、事件、建構式。
巢狀型別代表class,也就是說class中在宣告一個class。
A.修飾詞分為
- abstract
- sealed
- virtual
- override
- new
其中 abstract、sealed、virtual、override皆是使用在於方法,而不會使用在欄位上。
abstract:是代表抽象的一個方法,只宣告方法名稱與回傳值型態,藉由繼承方式子類別用再override實作出方法。(繼承class後的abstract方法都必須實作)
override:是繼承class之後,父類別中具有abstract或virtual修飾詞的方法,使用override來進行實作。
abstract:例如男女生都會出去花費,但男女生購買項目不大相同但共同都有消費的行為與紀錄消費清單。
static void Main(string[] args)
{
Man man = new Man();
man.consumption(200, (byte)1);
Woman woman = new Woman();
woman.consumption(150, (byte)1);
foreach (var item in man.GetconsumptionList())
{
Console.Write(item + "\n");
}
Console.Write("男生剩下"+man.getMoney() + "元\n");
foreach (var item in woman.GetconsumptionList())
{
Console.Write(item + "\n");
}
Console.Write("女生剩下" + woman.getMoney() + "元\n");
Console.ReadLine();
}
class
abstract class entertainment_abstract
{
//娛樂費
protected int RecreationFee = 1000;
//消費清單
protected List<string> consumptionList;
//男女生皆有消費的行為
abstract public void consumption(int money, byte flag);
//取得剩餘的錢
public int getMoney()
{
return RecreationFee;
}
//取得消費清單
public List<string> GetconsumptionList()
{
return consumptionList;
}
}
class Woman : entertainment_abstract
{
//實作abstract的消費方法
public override void consumption(int _money,byte _flag)
{
if (consumptionList == null)
consumptionList = new List<string>();
if (_flag == (byte)1)
consumptionList.Add("女生花了" + _money + "買衣服");
else if(_flag == (byte)2)
consumptionList.Add("女生花了" + _money + "買包包");
else
consumptionList.Add("女生花了" + _money + "買雜物");
RecreationFee -= _money;
}
}
class Man : entertainment_abstract
{
//實作abstract的消費方法
public override void consumption(int _money, byte _flag)
{
if (consumptionList == null)
consumptionList = new List<string>();
if (_flag == (byte)1)
consumptionList.Add("男生花了"+ _money + "買遊戲");
else if (_flag == (byte)2)
consumptionList.Add("男生花了" + _money + "買電腦");
else
consumptionList.Add("男生花了" + _money + "買雜物");
RecreationFee -= _money;
}
}
結果
virtual:與abstract相同具有被override的特性,與abstract差異性在有實作方法,而abstract沒有實作方法。
將class entertainment的consumption方法直接實作加上virtual即可讓繼承的子類別決定是否override該方法來執行。
class
class entertainment
{
//娛樂費
protected int RecreationFee = 1000;
//消費清單
protected List<string> consumptionList;
//男女生皆有消費的行為
public virtual void consumption(int money, byte flag)
{
if (consumptionList == null)
consumptionList = new List<string>();
if (flag == (byte)1)
consumptionList.Add("花了" + money + "吃早餐");
else if (flag == (byte)2)
consumptionList.Add("花了" + money + "吃午餐");
else if(flag == (byte)3)
consumptionList.Add("花了" + money + "吃晚餐");
else
consumptionList.Add("花了" + money + "吃零食");
RecreationFee -= money;
}
//取得剩餘的錢
public int getMoney()
{
return RecreationFee;
}
//取得消費清單
public List<string> GetconsumptionList()
{
return consumptionList;
}
}
class Woman : entertainment
{
//實作virtual的消費方法
public override void consumption(int _money,byte _flag)
{
if (consumptionList == null)
consumptionList = new List<string>();
if (_flag == (byte)1)
consumptionList.Add("花了" + _money + "買衣服");
else if(_flag == (byte)2)
consumptionList.Add("花了" + _money + "買包包");
else
consumptionList.Add("花了" + _money + "買雜物");
RecreationFee -= _money;
}
}
class Man : entertainment
{
//不實作任何動作...
}
執行結果
new:是一個具高危險性的修飾詞,稱為遮蔽,在方法或欄位中加上new可以覆蓋父類別中的欄位與方法,可能會產生不可預期結果。
※ new的使用情境是已確認當組件中的方法執行會產生錯誤,卻無從原始碼下手修改,才使用new的方式來解決。
6.常數const
關於const建議改用readonly field來替代,因為程式碼當中field加上const之後,在程式碼經過編譯過後會直接將值以head code方式
編譯進去代替宣告變數,導致無法單純變更宣告參數的組件,因此改用readonly變數來取代const然後用public方式來宣告,因為只
唯讀不能寫入,所以不會有被任意修改導致錯誤的問題發生。
7.屬性是方法的變形
屬性本身變形,變成本身具有方法的功能,以下class1就等於class2,變得更簡潔了。
public class Class1
{
private int _a = 0;
public int Get()
{
return _a;
}
public void Set(int value)
{
_a = value;
}
}
public class Class2
{
private int _a = 0;
public int A
{
//int X = class2.A; 取值時執行get大括號內的方法
get { return _a; }
// class.A = X; 寫入X值時執行set大括號內的方法
set { _a = value; }
}
}
8.方法的參數的特異功能
當參數的型態前方可以加上如下三種
- ref
- out
- params
ref 傳址:代表說宣告參數本身以傳送記憶體位置來帶入參數
static void Main(string[] args)
{
int a = 5;
Add(ref a);
Console.Write(a);
Console.ReadLine();
}
public static void Add(ref int x)
{
x = x + 1;
}
會發現Add方法本身沒有return任何參數,但藉由傳址的方式來x+1,因為ref本身是傳送記憶體位置,因此修改記憶體位置的值所以
Console.Write(a); 內的a是6。在一般的情況下沒有加入ref是傳送值,所以進入Add方法後的x變數是與原本帶入的a變數在記憶體中
都有各自的記憶體位置,因此修改x變數並不會影響到a變數。
ref:將變數的記憶體位置傳送至方法內。
以下程式碼在一般的情境下,並未使用ref的結果。
static void Main(string[] args)
{
TestClass A = new TestClass();
Console.WriteLine("初始值x=" + A.x);
Add(A);
Console.WriteLine("執行完Add方法後,x=" + A.x);
Console.ReadLine();
}
public static TestClass Add(TestClass B)
{
Console.WriteLine("進入Add方法後,x=" + B.x);
B.x = 10;
Console.WriteLine("B.x修改值為10,x=" + B.x);
B = new TestClass();
Console.WriteLine("B new新的class,x=" + B.x);
return B;
}
public class TestClass
{
public int x = 0;
}
在add方法帶入B參數時,實際上是傳送了class內的變數記憶體位址進入add方法,因此修改值也等於修改了A。
在add方法中再次new class後B也就在記憶體中產生新的class,因此修改值之後與原本A無關。
參數加上ref結果如下程式碼
static void Main(string[] args)
{
TestClass A = new TestClass();
Console.WriteLine("初始值x=" + A.x);
Add(ref A);
Console.WriteLine("執行完Add方法後,x=" + A.x);
Console.ReadLine();
}
public static TestClass Add(ref TestClass B)
{
Console.WriteLine("進入Add方法後,x=" + B.x);
B.x = 10;
Console.WriteLine("B.x修改值為10,x=" + B.x);
B = new TestClass();
Console.WriteLine("B new新的class,x=" + B.x);
return B;
}
public class TestClass
{
public int x = 0;
}
當Add方法加入了ref後,帶入add方法的參數是傳class的記憶體位址,因此對B變數做修改等於對A做修改。
out:只出不進的方式,當參數加上out代表會由方法內將此參數傳出。
static void Main(string[] args)
{
TestClass A;
Add(out A);
Console.WriteLine("執行完Add方法後,x=" + A.x);
Console.ReadLine();
}
public static void Add(out TestClass B)
{
B = new TestClass();
}
public class TestClass
{
public int x = 0;
}
9.委派
委派:可以將工作加入到某個等待被執行的委派當中,如下程式碼,在SomeDelegate method 代表method是一個委派可以加入相同參數的方法,
如程式碼中的Method1、Method2、Method3,都是丟給method來執行。
private delegate void SomeDelegate(string text);
static void Main(string[] args)
{
SomeDelegate method = Method1;
method += Method2;
method += Method3;
method.Invoke("HI");
Console.ReadLine();
}
public static void Method1(string text)
{
Console.WriteLine("Show Method1:" + text);
}
public static void Method2(string text)
{
Console.WriteLine("Show Method2:" + text);
}
public static void Method3(string text)
{
Console.WriteLine("Show Method3:" + text);
}
委派的應用,不管你在使用WPF或Windows Form對於控制項,想用跨執行續來寫入控制項的text或Content。
以下使用WPF作範例
View:
<Grid>
<DockPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="輸入內容:"/>
<TextBox Name="TB1" Width="60"/>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Orientation="Horizontal">
<Label Content="顯示輸入內容:" Height="35"/>
<Label Name="LabeL1" Content="(空白)" Height="35"/>
</StackPanel>
<Button Name="btn1" Click="btn1_Click" Content="Button" Height="20" />
</StackPanel>
</DockPanel>
</Grid>
產生執行緒原因是為了使用者按下Btn1,執行btn1_Click過程中不讓使用者畫面lock住等待btn1_Click內的程式碼跑完才讓使用者繼續使用。
以下為錯誤示範,一般直覺性的寫法如下:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void btn1_Click(object sender, RoutedEventArgs e)
{
Thread _Thread = new Thread(new ThreadStart(Error_SetLabel));
_Thread.IsBackground = true;
_Thread.Start();
}
void Error_SetLabel()
{
LabeL1.Content = TB1.Text;
}
}
我產生了一條執行緒,執行緒內的method會去執行修改LabeL1的內容,執行起來時滿頭霧水,為什麼會錯?阿不是已經Bild
但前面提到的跨執行緒的意思就是LabeL1本身就是一條Thread在運作執行中,當攻城獅自己new出一條Thread去執行修改LabeL1.Content,變成LabeL1.Content本身自己一條Thread但在btn1_Click Method中也 new Thread 去修改LabeL1,
同時有兩個Thread在指揮LabeL1.Content,請問他要聽誰的呢?
以下為正確的使用方法:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
public delegate void SomeDelegate();
private void btn1_Click(object sender, RoutedEventArgs e)
{
Thread _Thread = new Thread(new ThreadStart(SetLabel));
_Thread.IsBackground = true;
_Thread.Start();
}
void SetLabel()
{
//同步方式
LabeL1.Dispatcher.Invoke(SetLabel_delegate);
//非同步
LabeL1.Dispatcher.BeginInvoke(new SomeDelegate(SetLabel_delegate));
}
public void SetLabel_delegate()
{
LabeL1.Content = TB1.Text;
}
}
當自己的Thread需要去使某個控制項去修改屬性時,我們必須從控制項本身著手,把控制項本身的委派給它拖出來用,該怎麼把請出來呢?
在WPF當中是XXX.Dispatcher.Invoke,把要執行的方法丟進去()內即可,但這是屬於同步部分。
若要執行非同步可以使用BeginInvoke,但是必須只是宣告一個委派(delegate)把要執行的method丟進宣告的委派,再將委派丟進BeginInvoke
即可以大功告成了,非同步方法。
10.事件
事件:當觸發事件時可以連動到其他的類別或物件,自己本身感覺很像Trigger,當一觸發會引起一連串的函數。
事件的本身宣告完後要綁定上事件觸發後所要執行的事件,事件處方會以委派的方式執行,因此在宣告委派時使
方法使用事件的類別與事件的參考型別當作參數。
public partial class Form1 : Form
{
private Class1 obj;
public Form1()
{
InitializeComponent();
//一個類別
obj = new Class1();
//類別中的委派事件是XChanged加入obj_XChanged的方法
obj.XChanged += obj_XChanged;
}
void obj_XChanged(object sender, CustomEventArgs e)
{
MessageBox.Show("由" + e.OldValue.ToString() + "變成" + e.NewValue.ToString());
}
private void button1_Click(object sender, EventArgs e)
{
obj.X = (int)numericUpDown1.Value;
}
}
Class1
//宣告委派的事件類別與事件參數
public delegate void CustomEventHandler(Object sender, CustomEventArgs e);
public class Class1
{
//在類別中使用事件類別的委派,讓使用者將方法加入到該XChanged的委派
public event CustomEventHandler XChanged;
//事件的觸發方法
private void OnXChanged(int oldvalue, int newvalue)
{
if (XChanged != null)
{
//XChanged 執行 前端所綁定的方法事件,在此觸動事件obj_XChanged(在Form1的類別中)
XChanged(this, new CustomEventArgs() { OldValue = oldvalue, NewValue = newvalue });
}
}
private int _x;
public int X
{
get { return _x; }
set
{
if (_x != value)
{
int old = _x;
_x = value;
//當X的值進行指派時,帶入OnXChanged方法中
OnXChanged(old,value);
}
}
}
}
public class CustomEventArgs : EventArgs
{
public int OldValue
{ get; set; }
public int NewValue
{ get; set; }
}
我自己的解釋,像是在XChanged的方法上榜定了一個Trigger,當XChanged方法執行時將會觸動事件,是藉由委派CustomEventHandler來去執行obj_XChanged事件達成目的。
綁定是指obj.XChanged += obj_XChanged;這一行將觸動後所執行的方法與觸動事件的委派做結合。
一旦綁定後,後須就非常方便,在code過程中只要專注在OnXChanged方法上何時要使用OnXChanged,自然就會執行obj_XChanged事件。