2024-11-02 022-01_Ccharp從最基本的編程概念到面向物件程式設計

2024-11-02 022-01_Ccharp從最基本的編程概念到面向物件程式設計

了解,讓我們從最基本的編程概念開始,一步步深入了解 C# 中類別(Classes)的語法構造及其背後的設計理念。我們將從傳統的程序式編程(Procedural Programming)開始,逐漸過渡到面向物件程式設計(Object-Oriented Programming, OOP),並解釋為什麼要引入類別以及其各種特性。


目錄

  1. 程序式編程的基礎
  2. 程序式編程的限制
  3. 引入類別與物件
  4. 類別的基本構造
  5. 封裝與存取修飾符
  6. 繼承(Inheritance)
  7. 多態性(Polymorphism)
  8. 類別設計的進階概念
  9. 小結

1. 程序式編程的基礎

程序式編程(Procedural Programming) 是一種基於程序(函數或方法)來組織代碼的編程範式。在這種方式下,程序被視為一系列指令,按照順序執行。

範例:簡單的程序式代碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;

class Program
{
static void Main(string[] args)
{
string name = GetName();
int age = GetAge();
DisplayInfo(name, age);
}

static string GetName()
{
Console.Write("請輸入你的名字: ");
return Console.ReadLine();
}

static int GetAge()
{
Console.Write("請輸入你的年齡: ");
return int.Parse(Console.ReadLine());
}

static void DisplayInfo(string name, int age)
{
Console.WriteLine($"你好, {name}! 你今年 {age} 歲。");
}
}

輸出範例:

1
2
3
請輸入你的名字: Alice
請輸入你的年齡: 30
你好, Alice! 你今年 30 歲。

解析:

  • 程式由一個 Main 方法開始執行,依次呼叫 GetNameGetAgeDisplayInfo 方法。
  • 每個方法負責一個特定的功能,如獲取使用者輸入或顯示信息。

2. 程序式編程的限制

隨著程式的規模增大,程序式編程可能會面臨以下挑戰:

  1. 代碼重用困難

    • 當需要多次使用相同的功能時,可能會重複編寫相似的代碼,導致代碼冗餘和難以維護。
  2. 維護困難

    • 大型程序中,找到特定功能的代碼位置變得困難,修改一處代碼可能影響到其他部分。
  3. 數據與功能的分離

    • 數據(變數)和操作數據的函數是分開的,缺乏組織,難以理解數據和功能之間的關係。
  4. 缺乏模組化

    • 難以將程式劃分為獨立的模組或部分,每個部分都可以獨立開發和測試。

範例:代碼重用困難

假設我們有多個不同的地方需要獲取使用者的名字和年齡,可能會重複呼叫 GetNameGetAge 方法,但這些方法並沒有組織在一起,難以管理。


3. 引入類別與物件

為了解決上述問題,面向物件程式設計(OOP) 被引入。OOP 將數據和操作數據的功能組織在一起,形成物件(Objects),並通過類別(Classes)來定義物件的結構和行為。

類別與物件的概念

  • 類別(Class):類別是物件的藍圖,定義了物件的屬性(數據)和方法(功能)。
  • 物件(Object):物件是類別的實例,擁有類別定義的屬性和方法。

為什麼使用類別?

  1. 代碼重用

    • 通過定義類別,可以創建多個物件,重複使用相同的代碼。
  2. 封裝

    • 將數據和操作數據的方法組織在一起,保護數據不被外部直接訪問或修改。
  3. 模組化

    • 將程式劃分為獨立的類別,每個類別負責特定的功能,提升代碼的可維護性和可讀性。
  4. 繼承與多態性

    • 通過繼承,可以創建更具特化性的類別,並實現多態性,增強靈活性。

範例:轉換為類別

讓我們將之前的程序式代碼轉換為面向物件的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using System;

class Person
{
// 屬性
public string Name { get; set; }
public int Age { get; set; }

// 方法
public void GetName()
{
Console.Write("請輸入你的名字: ");
Name = Console.ReadLine();
}

public void GetAge()
{
Console.Write("請輸入你的年齡: ");
Age = int.Parse(Console.ReadLine());
}

public void DisplayInfo()
{
Console.WriteLine($"你好, {Name}! 你今年 {Age} 歲。");
}
}

class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.GetName();
person.GetAge();
person.DisplayInfo();
}
}

輸出範例:

1
2
3
請輸入你的名字: Bob
請輸入你的年齡: 25
你好, Bob! 你今年 25 歲。

解析:

  • 定義了一個 Person 類別,包含 NameAge 屬性,以及 GetNameGetAgeDisplayInfo 方法。
  • Main 方法中,創建了一個 Person 物件,並呼叫其方法來獲取和顯示信息。
  • 這樣的結構更具組織性,且易於擴展和維護。

4. 類別的基本構造

類別的結構

一個完整的 C# 類別通常包含以下部分:

  1. 修飾符(Modifiers)

    • 定義類別的可見性和其他屬性,如 publicprivateabstract 等。
  2. 類別名稱(Class Name)

    • 遵循命名規則,通常使用 Pascal 命名法。
  3. 繼承(Inheritance)

    • 指定類別從哪個基類繼承,使用冒號 :
  4. 介面實作(Interface Implementation)

    • 指定類別實作哪些介面。
  5. 成員(Members)

    • 包含欄位(Fields)、屬性(Properties)、方法(Methods)、建構函數(Constructors)等。

範例:類別的基本構造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Car
{
// 欄位
private string model;

// 屬性
public string Make { get; set; }
public string Model
{
get { return model; }
set { model = value; }
}

// 建構函數
public Car(string make, string model)
{
Make = make;
Model = model;
}

// 方法
public void DisplayInfo()
{
Console.WriteLine($"車輛: {Make} {Model}");
}
}

解析:

  • public class Car:定義了一個公開的 Car 類別。
  • 欄位
    • private string model;:私有欄位,只能在 Car 類別內部訪問。
  • 屬性
    • public string Make { get; set; }:公開屬性,提供對 Make 的讀寫訪問。
    • public string Model:公開屬性,通過 getset 訪問私有欄位 model
  • 建構函數
    • public Car(string make, string model):初始化 MakeModel 屬性。
  • 方法
    • public void DisplayInfo():顯示車輛信息。

5. 封裝與存取修飾符

封裝(Encapsulation) 是 OOP 的一個核心概念,旨在將數據(屬性)和操作數據的方法封裝在一起,並控制對這些數據的訪問。

存取修飾符(Access Modifiers)

C# 提供了多種存取修飾符來控制類別及其成員的可見性:

  1. **public**:

    • 可被任何其他程式碼訪問。
    • 適用於希望公開的類別或成員。
  2. **private**:

    • 僅在定義它的類別內部可訪問。
    • 適用於不希望外部直接訪問的成員。
  3. **protected**:

    • 可在定義它的類別及其派生類別中訪問。
    • 適用於需要在子類中訪問的成員。
  4. **internal**:

    • 僅在同一個組件(Assembly)內部可訪問。
    • 適用於類別或成員只供內部使用的情況。
  5. **protected internal**:

    • 可在同一組件內部及任何派生類別中訪問。
  6. **private protected**(C# 7.2 及以上):

    • 可在同一組件內部及其派生類別中訪問。
    • 更嚴格的修飾符,限制訪問範圍。

範例:封裝與存取修飾符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class BankAccount
{
// 私有欄位
private decimal balance;

// 公開屬性
public decimal Balance
{
get { return balance; }
private set { balance = value; }
}

// 建構函數
public BankAccount(decimal initialBalance)
{
Balance = initialBalance;
}

// 公開方法
public void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount;
Console.WriteLine($"存入: {amount}, 新餘額: {Balance}");
}
}

// 公開方法
public void Withdraw(decimal amount)
{
if (amount > 0 && amount <= Balance)
{
Balance -= amount;
Console.WriteLine($"提取: {amount}, 新餘額: {Balance}");
}
else
{
Console.WriteLine("提取金額無效。");
}
}

// 私有方法
private void LogTransaction(string transaction)
{
Console.WriteLine($"記錄交易: {transaction}");
}
}

class Program
{
static void Main(string[] args)
{
BankAccount account = new BankAccount(1000m);
account.Deposit(500m); // 輸出: 存入: 500, 新餘額: 1500
account.Withdraw(200m); // 輸出: 提取: 200, 新餘額: 1300

// 無法直接訪問私有欄位或方法
// Console.WriteLine(account.balance); // 錯誤
// account.LogTransaction("存入"); // 錯誤
}
}

輸出:

1
2
存入: 500, 新餘額: 1500
提取: 200, 新餘額: 1300

解析:

  • **私有欄位 balance**:
    • 只能在 BankAccount 類別內部訪問,外部無法直接修改。
  • **公開屬性 Balance**:
    • 提供對 balance 的讀取權限(get),但寫入權限(set)是私有的,只能在類別內部修改。
  • **公開方法 DepositWithdraw**:
    • 提供了控制 balance 修改的方法,確保金額的有效性。
  • **私有方法 LogTransaction**:
    • 僅供 BankAccount 類別內部使用,用於記錄交易(此範例中未實作)。

優點:

  • 資訊隱藏
    • 外部程式碼無法直接訪問或修改 balance,只能通過公開的方法進行操作,確保數據的一致性和安全性。
  • 控制權限
    • 使用公開屬性和方法來控制對數據的訪問和修改,防止不當操作。

6. 繼承(Inheritance)

繼承(Inheritance) 是 OOP 的一個重要特性,允許你基於現有的類別創建新的類別。新類別(派生類別)繼承自現有的類別(基類),並可以擴展或修改其行為。

為什麼需要繼承?

  1. 代碼重用

    • 重用基類中的屬性和方法,避免重複編寫相同的代碼。
  2. 建立類別層次結構

    • 將相關的類別組織在一起,形成層次化的結構,提高代碼的可理解性。
  3. 多態性

    • 通過基類引用來操作派生類物件,實現靈活的代碼設計。

繼承的語法

使用冒號 : 來指定繼承的基類。

範例:基類與派生類

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;

// 基類
public class Animal
{
public string Name { get; set; }

public void Eat()
{
Console.WriteLine($"{Name} 正在吃食物。");
}

// 虛擬方法,允許派生類覆蓋
public virtual void Speak()
{
Console.WriteLine($"{Name} 發出聲音。");
}
}

// 派生類
public class Dog : Animal
{
// 覆蓋基類的 Speak 方法
public override void Speak()
{
Console.WriteLine($"{Name} 汪汪叫。");
}

// 派生類的特有方法
public void Fetch()
{
Console.WriteLine($"{Name} 正在取回球。");
}
}

class Program
{
static void Main(string[] args)
{
Dog myDog = new Dog { Name = "Buddy" };
myDog.Eat(); // 輸出: Buddy 正在吃食物。
myDog.Speak(); // 輸出: Buddy 汪汪叫。
myDog.Fetch(); // 輸出: Buddy 正在取回球。

// 基類引用指向派生類物件
Animal myAnimal = myDog;
myAnimal.Eat(); // 輸出: Buddy 正在吃食物。
myAnimal.Speak(); // 輸出: Buddy 汪汪叫。
// myAnimal.Fetch(); // 錯誤,基類引用無法訪問派生類特有方法
}
}

輸出:

1
2
3
4
5
Buddy 正在吃食物。
Buddy 汪汪叫。
Buddy 正在取回球。
Buddy 正在吃食物。
Buddy 汪汪叫。

解析:

  • **基類 Animal**:
    • 定義了 Name 屬性和 Eat 方法。
    • 定義了一個虛擬方法 Speak,允許派生類覆蓋。
  • **派生類 Dog**:
    • 繼承自 Animal
    • 覆蓋了基類的 Speak 方法,提供了特定的實現。
    • 定義了一個派生類特有的方法 Fetch
  • Main 方法中
    • 創建了一個 Dog 物件 myDog,並呼叫其方法。
    • 使用基類引用 myAnimal 指向 myDog 物件,展示了多態性。
    • 注意:基類引用無法訪問派生類特有的方法,如 Fetch

優點:

  • 代碼重用Dog 繼承了 AnimalName 屬性和 Eat 方法,無需重新定義。
  • 多態性:可以使用基類引用來操作派生類物件,實現靈活的代碼設計。
  • 擴展性:可以在派生類中新增特有的屬性和方法,擴展基類的功能。

7. 多態性(Polymorphism)

多態性(Polymorphism) 是 OOP 的另一個核心概念,允許相同的接口在不同的類別中有不同的實現。這使得程式碼更加靈活和可擴展。

為什麼需要多態性?

  1. 靈活性

    • 能夠使用基類引用來操作不同的派生類物件,減少程式碼的耦合度。
  2. 可擴展性

    • 新增派生類時,不需要修改使用基類引用的現有代碼。
  3. 代碼簡潔

    • 通過統一的接口來操作不同的類別,減少了代碼的冗餘。

多態性的實現

在 C# 中,多態性通常通過以下兩個步驟實現:

  1. 基類中的虛擬方法

    • 使用 virtual 關鍵字標記基類中的方法,允許派生類覆蓋。
  2. 派生類中的覆蓋方法

    • 使用 override 關鍵字在派生類中覆蓋基類的方法,提供具體的實現。

範例:多態性的應用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
using System;
using System.Collections.Generic;

// 基類
public class Shape
{
public string Name { get; set; }

// 虛擬方法
public virtual double Area()
{
return 0;
}

public virtual void Display()
{
Console.WriteLine($"{Name} 的面積是 {Area()}。");
}
}

// 派生類:圓形
public class Circle : Shape
{
public double Radius { get; set; }

public Circle(string name, double radius)
{
Name = name;
Radius = radius;
}

// 覆蓋 Area 方法
public override double Area()
{
return Math.PI * Radius * Radius;
}

public override void Display()
{
Console.WriteLine($"{Name} 的半徑是 {Radius},面積是 {Area():F2}。");
}
}

// 派生類:矩形
public class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }

public Rectangle(string name, double width, double height)
{
Name = name;
Width = width;
Height = height;
}

// 覆蓋 Area 方法
public override double Area()
{
return Width * Height;
}

public override void Display()
{
Console.WriteLine($"{Name} 的寬度是 {Width},高度是 {Height},面積是 {Area():F2}。");
}
}

class Program
{
static void Main(string[] args)
{
// 創建 Shape 類型的列表,包含不同的派生類物件
List<Shape> shapes = new List<Shape>
{
new Circle("圓形A", 5),
new Rectangle("矩形A", 4, 6),
new Circle("圓形B", 3.5)
};

// 遍歷並呼叫 Display 方法
foreach (var shape in shapes)
{
shape.Display();
}
}
}

輸出:

1
2
3
圓形A 的半徑是 5,面積是 78.54。
矩形A 的寬度是 4,高度是 6,面積是 24.00。
圓形B 的半徑是 3.5,面積是 38.48。

解析:

  • **基類 Shape**:
    • 定義了 Name 屬性和虛擬方法 AreaDisplay
  • **派生類 CircleRectangle**:
    • 覆蓋了基類的 Area 方法,提供了具體的計算邏輯。
    • 覆蓋了基類的 Display 方法,提供了更具體的信息顯示。
  • Main 方法中
    • 創建了一個包含不同派生類物件的 List<Shape>
    • 遍歷這個列表,呼叫每個物件的 Display 方法,根據實際物件的類型執行相應的覆蓋方法。

優點:

  • 靈活性:可以使用基類引用來處理不同的派生類物件,實現統一的接口。
  • 可擴展性:新增派生類時,無需修改使用基類引用的現有代碼。
  • 代碼簡潔:通過基類的方法呼叫,減少了重複代碼。

8. 類別設計的進階概念

封裝(Encapsulation)

封裝 是將數據(屬性)和操作數據的方法組織在一起,並控制對數據的訪問。這有助於保護數據不被外部不當修改,並確保物件的一致性。

抽象化(Abstraction)

抽象化 是指僅展示物件的必要特性,隱藏其內部實現細節。這有助於簡化複雜性,使使用者更容易理解和使用物件。

類別設計原則

  1. 單一職責原則(Single Responsibility Principle)

    • 每個類別應該只有一個單一的職責,並且該職責應該被完整地封裝在類別中。
  2. 開放封閉原則(Open/Closed Principle)

    • 類別應該對擴展開放,對修改封閉。即應該能夠通過繼承或組合來擴展類別的功能,而不需要修改原有的類別。
  3. 里氏替換原則(Liskov Substitution Principle)

    • 派生類別的物件應該能夠替換其基類別的物件,而不影響程式的正確性。
  4. 接口隔離原則(Interface Segregation Principle)

    • 不應該強迫類別依賴它們不使用的接口。應該將大型接口分割成更小、更具專一性的接口。
  5. 依賴反轉原則(Dependency Inversion Principle)

    • 高層模組不應依賴低層模組,兩者應該依賴於抽象(介面或抽象類別)。

範例:良好設計的類別

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;

// 單一職責原則:每個類別只有一個職責

// 定義介面
public interface IPrinter
{
void Print(string message);
}

// 實作介面
public class ConsolePrinter : IPrinter
{
public void Print(string message)
{
Console.WriteLine(message);
}
}

public class Logger
{
private IPrinter printer;

// 依賴注入:通過建構函數注入介面
public Logger(IPrinter printer)
{
this.printer = printer;
}

public void Log(string message)
{
// 使用介面方法來打印日誌
printer.Print($"Log: {message}");
}
}

class Program
{
static void Main(string[] args)
{
// 創建一個 ConsolePrinter 物件
IPrinter printer = new ConsolePrinter();

// 創建一個 Logger 物件,注入 ConsolePrinter
Logger logger = new Logger(printer);

// 使用 Logger 物件來記錄信息
logger.Log("應用程式啟動");
logger.Log("發生錯誤");
}
}

輸出:

1
2
Log: 應用程式啟動
Log: 發生錯誤

解析:

  • **介面 IPrinter**:
    • 定義了一個 Print 方法,抽象化了打印的行為。
  • **類別 ConsolePrinter**:
    • 實作了 IPrinter 介面,具體實現了將信息打印到控制台。
  • **類別 Logger**:
    • 將打印的責任委託給 IPrinter 介面,遵循了依賴反轉原則。
    • 這樣的設計允許未來更換不同的打印方式(如文件打印)而不需要修改 Logger 類別。
  • Main 方法中
    • 創建了 ConsolePrinterLogger 物件,並使用 Logger 來記錄信息。

優點:

  • 高內聚低耦合:每個類別專注於自己的職責,通過介面進行通信,減少了類別之間的耦合。
  • 可擴展性:未來可以輕鬆添加新的打印方式,只需創建新的 IPrinter 實作,而不需修改 Logger
  • 可測試性:可以輕鬆地為 Logger 類別創建模擬的 IPrinter 來進行單元測試。

9. 小結

通過這一步步的演進,我們從程序式編程的基礎開始,逐漸引入了類別和物件,並深入了解了封裝、繼承和多態性等 OOP 的核心概念。以下是本章內容的快速回顧:

  1. 程序式編程的基礎

    • 基於函數和指令的編程方式,適用於簡單的任務,但隨著程式規模增大,維護和重用變得困難。
  2. 程序式編程的限制

    • 代碼重用困難、維護困難、數據與功能的分離、缺乏模組化等問題。
  3. 引入類別與物件

    • 通過類別組織數據和功能,實現代碼重用、封裝和模組化,提升程式的可維護性和可讀性。
  4. 類別的基本構造

    • 學習類別的修飾符、名稱、繼承、介面實作、成員(欄位、屬性、方法、建構函數等)的結構。
  5. 封裝與存取修飾符

    • 使用存取修飾符控制類別及其成員的可見性,實現資訊隱藏和封裝。
  6. 繼承(Inheritance)

    • 基於基類創建派生類,實現代碼重用和建立類別層次結構。
  7. 多態性(Polymorphism)

    • 透過基類引用操作派生類物件,實現靈活的代碼設計和減少耦合。
  8. 類別設計的進階概念

    • 掌握封裝、抽象化、以及 SOLID 原則等,設計出高內聚低耦合的類別。

總結

了解 C# 中類別的語法構造及其設計理念,有助於你編寫出結構良好、易於維護和擴展的程式碼。通過封裝、繼承和多態性等 OOP 概念,你可以創建出更靈活、可重用和高效的應用程式。隨著你不斷練習和應用這些概念,你將能夠更自信地設計和實現複雜的軟體系統。

如果你有任何進一步的問題,或需要更詳細的範例和解釋,請隨時告訴我!