# 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;
}
}
```