# C Sharp-方法 Method C# 為物件導向程式語言,故所有的 function 都將隸屬於一個Class下,稱作方法 Method。 ## 方法宣告 C# 中每個方法都應該宣告在某一個 class 底下,並且在宣告時聲明其回傳值,格式如下: ```C# class 類名 { (修飾字) 回傳值 方法名稱(參數){ 程式碼區塊 } } ``` 宣告一個方法,主要由以下 5 部分構成: 1. 修飾字:非必要,用來修飾方法的特性,例如:存取層級、靜態、是否覆寫父類...等。 2. 回傳值:如同宣告變數一般,需要宣告方法回傳值的型別,確保型別安全。 3. 方法名稱:用以呼叫方法,公開方法以大寫開頭命名,私有方法則為小寫。 4. 參數:可以宣告 0 個或多個參數,用以將外部資料輸入方法內處理。 5. 程式碼區塊:描述方法執行過程,由多個[[陳述句(Statement)]]構成。 ### 修飾字 - 存取修飾詞:`public`、`private`、`protected` ...等,須注意 C# 方法在未指明存取權的情況下,預設是 `private` ,意即 class 外部不可存取此方法;若要讓該方法可以被外部存取,則應設為 `public` 。 - `static`:靜態,意即方法不須在 class 實例化的情況下使用。表示此方法只需要單純的接受參數並輸出結果,不涉及實例。 - `virtual`:虛擬方法,意即方法可以被子類覆寫。在抽象類別中亦可宣告虛擬方法來對方法做部分定義。 - `override`:意即覆寫父類方法,可以覆寫帶有 `virtual` 及 `abstract` 修飾字的方法,或者再對父類的 `override` 進行覆寫。 - `new`:意即撰寫一個與父類同名的方法,但方法不是來自對父類方法的覆蓋,而是一個全新的子類方法。 - `abstract`:抽象方法,意即該方法不提供實作,而是提供定義讓子類 `override` 實作。 ### 參數 參數或稱引數,用來將資料傳入方法內部進行處理。參數可以添加修飾字,讓參數具有特性。 #### 參數修飾字 - `out`:當方法需要返回多個值時,可以指定一個或多個參數用來返回值。當參數被標上 out 時,意味著該參數會在方法執行後輸出。 例如: ```C# static bool Hello(int a , out string b, out int c) { b = a.ToString() + "World"; c = a + 1; return true; } static void Main(string[] args) { bool a = Hello( 1 , out string b, out int c); Console.WriteLine(a); // 輸出 True Console.WriteLine(b); // 輸出 1World Console.WriteLine(c); // 輸出 2 Console.ReadLine(); } ``` - `ref`:與 `out` 類似,不同處在於 ref 要求該參數必須先初始化,並在方法內修改後輸出。 例如: ```C# static void ModifyValue(ref int value) { value = 42; } static void Main(string[] args) { int number = 10; // 需要對 ref 參數初始化 ModifyValue(ref number); // number 被方法修改為 42 Console.WriteLine(number); // 輸出 42 Console.ReadLine(); } ``` - `params`:不定長度引數,在定義函式時,有時無法事先得知要傳遞的參數個數,透過陣列收集是方式之一。 注意:若有多個參數時,不定長度引數必須是方法的最後一個參數。 例如: ```C# static int Sum(params int[] numbers) { int sum = 0; foreach (int num in numbers) { sum += num; } return sum; } static void Main(string[] args) { int result = Sum(1, 2, 3, 4); Console.WriteLine(result); // 輸出 10 Console.ReadLine(); } ``` - `in`:只讀複本。in 引數不能被呼叫的方法修改。使用 in 參數的好處是可以避免複製一些很大的結構體,從而提高效能。參閱:[Stack Overflow](https://stackoverflow.com/questions/52820372/why-would-one-ever-use-the-in-parameter-modifier-in-c)、[Microsoft devblogs](https://devblogs.microsoft.com/premier-developer/the-in-modifier-and-the-readonly-structs-in-c/) 例如: ```C# struct Product { public int ProductId { get; set; } public string ProductName { get; set; } // ... other properties } public static void Modify(in Product product) { // product = new Product(); // 錯誤,無法修改 product // product.ProductName = "test"; // 錯誤,無法修改成員變數 Console.WriteLine($"Id: {product.ProductId}, Name: {product.ProductName}"); } static void Main(string[] args) { Product p = new Product(); Modify(p); Console.ReadLine(); } ``` ## 建構式 Constructor 建構式為與 class 同名的方法。當物件被 `new` 實例化時,就是執行建構式內的陳述句。 ```C# class MyClass { private int count; public MyClass (int count) { // 注意,建構式不需要宣告回傳值與靜態,它天然就是 static void this.count = count; // 其他用來初始化物件的陳述句 } } internal class Program { static void Main(string[] args) { MyClass c = new MyClass(1); // 此時執行建構式內的陳述句 } } ``` ### 建構式繼承 建構式的父類別若具有建構式,則子類別也必須呼叫該建構式: ```C# internal class Program { static void Main(string[] args) { Human human = new Sabor(1); // 實例化時 } } abstract class Human { public Human(int num1) { Console.WriteLine("人類:" + num1); } // 會先呼叫父類別建構式 印出 2 } class Sabor : Human { public Sabor(int num2) : base(num2 + 1) { // 建構式後面用 : base() 來呼叫父類建構式 Console.WriteLine("劍士:" + num2); // 父類建構式執行完畢之後才呼叫這個建構式 } } ``` ## 屬性 Property 將私有變數設置對外公開方法,傳統上將頻繁設置 `Get...()` 跟 `Set...()` 方法,因此衍生出 存取子 ( Accessor ) 之概念,即 Getter & Setter 。 在 C# 中將此一概念整合為 **屬性**,將私有的欄位,轉變為可公開存取的屬性。作法是宣告該私有欄位大寫的同名屬性 ( C#中,小寫表私有變數或區域變數 ),並在內宣告 get 及 set ,接著用 `{ }` 包裹相應陳述句即可。 快速鍵:輸入 `prop` + `[tab]` + `[tab]` ```C# class MyClass { private int count; public int Count { get { return count; } set { if( value > 10 ) // 可以在輸入時設置條件限制,避免錯誤值被輸入。 count = value; // C# 中每個 setter 自動會有一個 value 作為輸入。 product = count * multiplier; } } } ``` ## 靜態方法 Static Method 靜態方法有別於一般方法需要進行實例化後才能呼叫,靜態方法**可以直接被呼叫**。 由於靜態方法於程式運作時便存在在記憶體中,因此不須實例。由於此特性,靜態方法不允許呼叫非靜態的欄位、屬性或方法。 使用靜態修飾字 `static` 來宣告靜態方法,通常是方法不需要實例進行計算的方法,只需要接受參數並輸出特定的計算結果時使用。 ```C# class MyTool { public static int Sum (int a, int b) { Console.WriteLine("執行相加計算"); // 其實系統中的 Console.WriteLine 就是 Static Method return a+b; } } internal class Program { static void Main(string[] args) { MyTool.Sum(1, 1); // 不需要 new instance 即可執行該方法。 } } ``` ## 虛擬方法 Virtual Method 虛擬方法意味著該方法等待被覆寫。 ```C# abstract class Animal { public virtual void Eat() { Console.WriteLine("吃"); } } class Bird : Animal { public override void Eat() { Console.WriteLine("啄食"); } } ``` ## 抽象方法 Abstract Method 抽象方法與虛擬方法的不同處在於,抽象方法僅定義方法**名稱**與**回傳值**,無法宣告方法主體。 ```C# abstract class Animal { public abstract void Eat(); } class Bird : Animal { public override void Eat() { Console.WriteLine("啄食"); // 覆寫抽象方法也需使用 override 修飾詞 } } ``` 要注意的是,抽象方法只允許宣告在抽象類之中,並且必須是 `public`。 抽象方法類似於介面,它揭示了這個類別所應呈現的公開方法,意味著類別具備某種「特性」。 ## 覆寫 override & new 當使用子類繼承父類方法時,可以選擇使用 `override` 關鍵字覆寫方法,或 `new` 關鍵字宣告一個同名方法。 在使用 `override` 時,即使在實例 cast 父類的狀況下 (意思是使用父類進行變數宣告來操作子類實例),仍然執行覆寫後的方法;而使用 `new` 時,在實例 cast 父類的狀況下,則回去執行父類的方法。區分 `override` 跟 `new` 的用途,在於區分使用[[多型 Polymorphism|多型]]時,子類方法和父類方法的選擇問題。 以下範例說明 `override` 和 `new` 的差別: ```C# class Program { static void Main(string[] args) { BaseClass ct1 = new Child_override(); BaseClass ct2 = new Child_new(); ct1.prinf(); // 執行覆寫後的方法 -> 印出"這是覆寫方法" ct2.prinf(); // 執行父類的方法 -> 印出"父類虛擬方法" Console.ReadKey(); } } abstract public class BaseClass { public virtual void prinf() // virtual { Console.WriteLine("父類虛擬方法"); } } public class Child_override : BaseClass { public override void prinf() // override { Console.WriteLine("這是覆寫方法"); } } public class Child_new : BaseClass { public new void prinf() // new 修飾字表示這個方法只是同名方法,它遮蔽了父類的方法參考 { Console.WriteLine("這是新方法"); } } ``` ## 方法內呼叫父類方法 撰寫子類別方法時,若要呼叫父類別的方法,則使用關鍵字 `base` 來代表父類別。 ```C# class Program { static void Main(string[] args) { Sabor s = new Sabor(); s.Eat(); // 印出 : 人類吃 & 劍士跑。 } } public class Human { public virtual void Eat() { Console.WriteLine("人類吃"); } } public class Sabor : Human { public override void Run() { // 子類的任意方法中,都可以調用父類方法。 base.Eat(); // 這裡呼叫了父類方法。 Console.WriteLine("劍士跑"); } } ``` ## 方法多載 Overload 當撰寫方法時,希望相同概念的方法根據輸入的參數值不同而有所變化。 此時可以撰寫**相同名稱**但**不同參數**的方法來多載方法。 例如,撰寫一個計數器,當無參數時,直接 +1,有參數時,增加指定數,輸入為字串時,先轉換成數字再增加指定數。 ```C# class Counter { private static int current = 0; public static void Count() { current++; } public static void Count(int value) { current += value; } public static void Count(string value) { bool success = int.TryParse(value, out int number); if (success) { current += number; }; } } ``` --- ## 方法範例-樂透(抽牌法)-抽象化的優點 沒有整理前的程式碼,即使寫了註解,也是看起來很長,要抓Bug的時候不好找,得一行一行看。 ```C# // 整理前的程式碼長這樣: static void Main(string[] args) { int[] myNumbers = new int[6]; for (int i = 0; i < myNumbers.Length; ++i) { reinput: Console.WriteLine($"請輸入您的樂透號碼(0~49):第{i + 1}個"); if (int.TryParse(Console.ReadLine(), out int number)) { if (number < 0 || number > 49) { Console.WriteLine($"您輸入的號碼必須是1~49這個範圍"); goto reinput; } } else { Console.WriteLine($"請輸入數字"); goto reinput; } // 檢查有沒有重複輸入 for (int j = 0; j < i; ++j) { if (number == myNumbers[j]) { Console.WriteLine($"你輸入了重複的數字"); goto reinput; } } myNumbers[i] = number; } Console.Write($"\n您輸入的號碼是:"); foreach (int i in myNumbers) { Console.Write($"{i}\t"); } Random ran = new Random(); int[] result = new int[6]; // 使用抽牌法取亂數 int[] cards = new int[49]; for (int i = 0; i < cards.Length; ++i) { cards[i] = i + 1; } for (int i = 0; i < result.Length; ++i) { int index = ran.Next(0, cards.Length); result[i] = cards[index]; // 每抽出一個要少一張牌 cards[index] = cards[cards.Length - 1]; Array.Resize(ref cards, cards.Length - 1); } Console.Write($"\n中獎號碼是:"); foreach (int i in result) { Console.Write($"{i}\t"); } // 檢查中獎 int hit = 0; for (int i = 0; i < myNumbers.Length; ++i) { for (int j = 0; j < result.Length; ++j) { if (myNumbers[i] == result[j]) { ++hit; } } } Console.Write($"\n你中了:{hit}個號碼"); Console.ReadKey(); } ``` --- 將程式碼重大區塊整理成方法後,程式設計師重新檢查程式碼時,只需要檢查大塊抽象概念的邏輯互相呼叫關係是否正確即可。 ```C# Lottos lottoMachine = new Lottos(); int[] myNumbers = lottoMachine.UserInputNumbers(6); // 輸入6個號碼 lottoMachine.Show6NumberInConsole($"\n您輸入的號碼是:", myNumbers); int[] prizeNumbers = lottoMachine.DrawCards(6, 49); // 取6個號碼,最大數字49 lottoMachine.Show6NumberInConsole($"\n中獎號碼是:", prizeNumbers); int hit = lottoMachine.CheckPrize(myNumbers, prizeNumbers); // 比對中獎號碼 Console.Write($"\n你中了:{hit}個號碼"); Console.ReadKey(); ``` 拆分成大塊的程式碼邏輯後,如果出現 Bug ,只需要去檢查錯誤對應的程式碼區塊即可。 並且將抽象概念切分出來後,就能設計重用性:像是下面的抽牌法就可以設計最大值多少,取幾張。 ```C# class Lottos { public void Show6NumberInConsole(string msg, int[] numbers) { Console.Write(msg); foreach (int i in numbers) { Console.Write($"{i}\t"); } } // 抽牌法 count:抽幾張 max:最大數字 public int[] DrawCards(int count, int max) { Random ran = new Random(); int[] result = new int[count]; int[] cards = new int[max]; for (int i = 0; i < cards.Length; ++i) { cards[i] = i + 1; // 牌組初始化 } for (int i = 0; i < result.Length; ++i) { int index = ran.Next(0, cards.Length); // 用亂數從牌堆取一個牌的索引 result[i] = cards[index]; // 記錄抽牌結果 cards[index] = cards[cards.Length - 1]; // 每抽出一個要少一張牌,做法是將最後一張牌跟中獎牌交換 Array.Resize(ref cards, cards.Length - 1); // 接著長度減1,中獎牌剛好被移出陣列 } return result; } // 使用者輸入數字 public int[] UserInputNumbers(int count) { int[] result = new int[count]; for (int i = 0; i < result.Length; ++i) { reinput: Console.WriteLine($"請輸入您的樂透號碼(0~49):第{i + 1}個"); if (int.TryParse(Console.ReadLine(), out int number)) { if (number < 0 || number > 49) { Console.WriteLine($"您輸入的號碼必須是1~49這個範圍"); goto reinput; } } else { Console.WriteLine($"請輸入數字"); goto reinput; } // 檢查有沒有重複輸入 for (int j = 0; j < i; ++j) { if (number == result[j]) { Console.WriteLine($"你輸入了重複的數字"); goto reinput; } } result[i] = number; } return result; } // 回傳中幾個號碼 public int CheckPrize(int[] yourNums, int[] prizeNums) { int result = 0; for (int i = 0; i < yourNums.Length; ++i) { for (int j = 0; j < prizeNums.Length; ++j) { if (yourNums[i] == prizeNums[j]) { ++result; } } } return result; } } ```