2026-04-23 一步步地深入了解 C# 中類別(Classes)的語法構造及其背後的設計理念

2026-04-23 一步步地深入了解 C# 中類別(Classes)的語法構造及其背後的設計理念

我:
妳一步一步來, 然後出題問我, 確定我能否理解. 比如說 為什麼類別型前面的修飾詞為什麼要用 static void?

GPT:
當然,讓我們一步步地深入了解 C# 中類別(Classes)的語法構造及其背後的設計理念。我們將從最基本的編程概念開始,逐漸引入類別和物件,並通過實際範例和問題來確保你能夠理解每個部分。


目錄

  1. 程序式編程的基礎
  2. 程序式編程的限制
  3. 引入類別與物件
  4. 類別的基本構造
  5. 存取修飾符(Access Modifiers)
  6. 方法的修飾符:staticvoid
  7. 繼承(Inheritance)
  8. 多態性(Polymorphism)
  9. 封裝(Encapsulation)
  10. 小結與練習題

1. 程序式編程的基礎

程序式編程(Procedural Programming)

在程序式編程中,程式被組織為一系列的函數或方法,這些函數按照順序執行。這種編程方式強調的是「做什麼」(What to do),而不是「是什麼」(What something is)。

範例:簡單的程序式代碼

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)

為了解決上述問題,面向物件程式設計(Object-Oriented Programming, 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. 存取修飾符(Access Modifiers)

存取修飾符是什麼?

存取修飾符(Access Modifiers) 是用來控制類別及其成員(如方法、屬性、欄位等)的可見性和訪問範圍的關鍵字。透過存取修飾符,你可以決定哪些部分的程式碼可以訪問特定的類別或其成員,從而實現資訊隱藏(Information Hiding)和封裝(Encapsulation)。

C# 中的主要存取修飾符

C# 提供了多種存取修飾符,每種修飾符定義了不同的訪問範圍。以下是主要的存取修飾符及其說明:

a. public

  • 描述:公開的,沒有任何限制。任何其他類別或組件都可以訪問標記為 public 的類別或成員。
  • 使用場景:當你希望某個類別或成員能被所有其他程式碼訪問時使用。

範例:

1
2
3
4
5
6
7
8
public class Person
{
public string Name; // 公開欄位
public void Speak() // 公開方法
{
Console.WriteLine("Hello!");
}
}

b. private

  • 描述:私有的,只能在定義它的類別內部訪問。其他類別無法訪問標記為 private 的成員。
  • 使用場景:當你希望某個成員只在類別內部使用,並且不允許外部直接訪問或修改時使用。

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Person
{
private int age; // 私有欄位

public void SetAge(int value)
{
if (value >= 0)
age = value;
}

public int GetAge()
{
return age;
}
}

c. protected

  • 描述:受保護的,只能在定義它的類別及其派生類別中訪問。
  • 使用場景:當你希望某個成員在類別內部及其子類別中可訪問,但不允許其他外部類別訪問時使用。

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Animal
{
protected string species;

protected void DisplaySpecies()
{
Console.WriteLine($"Species: {species}");
}
}

public class Dog : Animal
{
public void SetSpecies(string sp)
{
species = sp; // 可訪問
}

public void ShowSpecies()
{
DisplaySpecies(); // 可訪問
}
}

d. internal

  • 描述:內部的,只能在同一個組件(Assembly)內部訪問。不同組件之間無法訪問標記為 internal 的成員。
  • 使用場景:當你希望某個類別或成員僅在其所在的組件內部使用,不對外部公開時使用。

範例:

1
2
3
4
5
6
7
internal class Helper
{
internal void Assist()
{
Console.WriteLine("Assisting...");
}
}

e. protected internal

  • 描述:受保護且內部的,表示該成員可以在同一個組件內部訪問,或在任何派生類別中訪問(無論組件是否相同)。
  • 使用場景:當你希望成員在內部組件內部及其派生類別中可訪問時使用。

範例:

1
2
3
4
5
6
7
8
9
public class Vehicle
{
protected internal int speed;

protected internal void SetSpeed(int sp)
{
speed = sp;
}
}

f. private protected(C# 7.2 及以上版本)

  • 描述:私有且受保護的,表示該成員只能在定義它的類別內部,或在同一組件中的派生類別中訪問。
  • 使用場景:當你希望成員僅在類別內部及其派生類別(且必須在同一組件內部)中可訪問時使用。

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BaseClass
{
private protected void DoSomething()
{
Console.WriteLine("Doing something...");
}
}

public class DerivedClass : BaseClass
{
public void Execute()
{
DoSomething(); // 可訪問
}
}

了解存取修飾符的重要性

正確使用存取修飾符有助於:

  • 實現封裝:保護內部數據不被外部直接訪問或修改。
  • 控制依賴:限制哪些類別或模組可以訪問特定的成員,減少程式碼的耦合度。
  • 提高安全性:防止敏感數據被未經授權的部分訪問或更改。

練習題 1

問題:

考慮以下類別定義,哪一行會導致編譯錯誤?為什麼?

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
public class BankAccount
{
private decimal balance;

public decimal Balance
{
get { return balance; }
private set { balance = value; }
}

public void Deposit(decimal amount)
{
if (amount > 0)
{
Balance += amount;
}
}

public void Withdraw(decimal amount)
{
if (amount > 0 && amount <= Balance)
{
Balance -= amount;
}
}
}

class Program
{
static void Main(string[] args)
{
BankAccount account = new BankAccount();
account.Balance = 1000m; // 會導致編譯錯誤
}
}

選項:

A. BankAccount account = new BankAccount();

B. account.Balance = 1000m;

C. public decimal Balance { get; set; }

D. public void Deposit(decimal amount)

答案: B

解釋:

  • Balance 屬性的 set 訪問器被標記為 private,因此只能在 BankAccount 類別內部訪問。Main 方法位於外部,無法訪問 privateset 方法,因此 account.Balance = 1000m; 會導致編譯錯誤。

6. 方法的修飾符:staticvoid

了解 staticvoid

在 C# 中,staticvoid 是用於方法定義的關鍵字,它們有不同的用途和意義。

static 關鍵字

  • 描述static 用於定義靜態方法、屬性、欄位等。靜態成員屬於類別本身,而不是類別的實例。

  • 用途

    • 靜態方法:可以在不創建類別實例的情況下呼叫。
    • 靜態欄位:所有類別實例共享同一個靜態欄位。
    • 靜態類別:包含僅有靜態成員的類別,無法被實例化。

範例:靜態方法和靜態類別

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
using System;

// 靜態類別
static class MathUtilities
{
public static double Pi = 3.141592653589793;

public static double Add(double a, double b)
{
return a + b;
}

public static double Multiply(double a, double b)
{
return a * b;
}
}

class Program
{
static void Main(string[] args)
{
// 訪問靜態成員,不需要實例化類別
Console.WriteLine($"Pi 的值是 {MathUtilities.Pi}");
double sum = MathUtilities.Add(5.5, 3.2);
double product = MathUtilities.Multiply(4.0, 2.5);
Console.WriteLine($"5.5 + 3.2 = {sum}");
Console.WriteLine($"4.0 * 2.5 = {product}");
}
}

輸出:

1
2
3
Pi 的值是 3.141592653589793
5.5 + 3.2 = 8.7
4.0 * 2.5 = 10

void 關鍵字

  • 描述void 表示方法不返回任何值。
  • 用途:用於不需要返回結果的方法,如輸出信息、修改物件狀態等。

範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Logger
{
public void Log(string message) // 返回類型為 void
{
Console.WriteLine($"Log: {message}");
}
}

class Program
{
static void Main(string[] args)
{
Logger logger = new Logger();
logger.Log("應用程式啟動"); // 輸出: Log: 應用程式啟動
}
}

為什麼要使用 staticvoid

  • **static**:

    • 當某個方法的功能不依賴於類別的實例時,可以將其定義為靜態方法,這樣可以直接通過類別名稱呼叫,無需創建物件實例。
    • 範例中的 MathUtilities 類別是一個靜態類別,包含靜態方法 AddMultiply,這些方法不需要任何物件狀態即可運行。
  • **void**:

    • 當一個方法的主要目的是執行某個操作而不需要返回任何結果時,使用 void 作為返回類型。
    • 範例中的 Logger.Log 方法只是記錄一條信息,無需返回任何值,因此使用 void

練習題 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
public class Calculator
{
public static void Add(int a, int b)
{
Console.WriteLine(a + b);
}

public void Multiply(int a, int b)
{
Console.WriteLine(a * b);
}
}

class Program
{
static void Main(string[] args)
{
// 選項:
// A. Calculator.Add(5, 3);
// B. Calculator.Multiply(5, 3);
// C. new Calculator().Add(5, 3);
// D. new Calculator().Multiply(5, 3);
}
}

選項:

A. Calculator.Add(5, 3);

B. Calculator.Multiply(5, 3);

C. new Calculator().Add(5, 3);

D. new Calculator().Multiply(5, 3);

答案: A 和 D

解釋:

  • 選項 A (Calculator.Add(5, 3);) 是正確的,因為 Add 方法是靜態方法,可以直接通過類別名稱呼叫。
  • 選項 B (Calculator.Multiply(5, 3);) 是錯誤的,因為 Multiply 方法是非靜態的,需要通過物件實例呼叫。
  • 選項 C (new Calculator().Add(5, 3);) 是錯誤的,因為 Add 方法是靜態的,不應該通過物件實例呼叫。
  • 選項 D (new Calculator().Multiply(5, 3);) 是正確的,因為 Multiply 方法是非靜態的,需要通過物件實例呼叫。

7. 繼承(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

練習題 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
public class Vehicle
{
public string Brand { get; set; }

public virtual void Start()
{
Console.WriteLine("Vehicle starting...");
}
}

public class Car : Vehicle
{
public override void Start()
{
Console.WriteLine("Car starting...");
}

public void OpenTrunk()
{
Console.WriteLine("Trunk opened.");
}
}

class Program
{
static void Main(string[] args)
{
Vehicle myVehicle = new Vehicle();
Vehicle myCar = new Car();

myVehicle.Start(); // 正確
myCar.Start(); // 正確
// myCar.OpenTrunk(); // 會導致編譯錯誤
}
}

選項:

A. Vehicle myVehicle = new Vehicle();

B. Vehicle myCar = new Car();

C. myVehicle.Start();

D. myCar.OpenTrunk();

答案: D

解釋:

  • myCar 是一個基類 Vehicle 的引用,即使它實際上指向一個 Car 物件,但基類引用無法訪問派生類特有的方法 OpenTrunk。因此,myCar.OpenTrunk(); 會導致編譯錯誤。

8. 多態性(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
88
89
90
91
92
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();
}

// 輸出:
// 圓形A 的半徑是 5,面積是 78.54。
// 矩形A 的寬度是 4,高度是 6,面積是 24.00。
// 圓形B 的半徑是 3.5,面積是 38.48。
}
}

輸出:

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 方法,根據實際物件的類型執行相應的覆蓋方法。

練習題 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
using System;

public class Animal
{
public virtual void Speak()
{
Console.WriteLine("Animal speaks");
}
}

public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Cat meows");
}
}

class Program
{
static void Main(string[] args)
{
Animal myAnimal = new Cat();
myAnimal.Speak();
}
}

選項:

A. Animal speaks

B. Cat meows

C. 會導致編譯錯誤

D. 不會有任何輸出

答案: B

解釋:

  • myAnimal 是一個基類 Animal 的引用,但它指向一個 Cat 物件。
  • 由於 Speak 方法是虛擬的(virtual),並且 Cat 類別覆蓋了它(override),因此呼叫 myAnimal.Speak() 時會執行 Cat 類別中的 Speak 方法,輸出 “Cat meows”。

9. 封裝(Encapsulation)

封裝的概念

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

如何實現封裝

  1. 使用私有欄位(Private Fields)

    • 將類別的欄位設為 private,防止外部直接訪問或修改。
  2. 使用公開屬性(Public Properties)

    • 通過 getset 訪問器來提供對私有欄位的控制性訪問。
  3. 使用方法(Methods)

    • 提供公開的方法來操作和修改私有欄位,確保數據的一致性和有效性。

範例:封裝與存取修飾符

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; } // 私有 set 訪問器
}

// 建構函數
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,只能通過公開的方法進行操作,確保數據的一致性和安全性。
  • 控制權限

    • 使用公開屬性和方法來控制對數據的訪問和修改,防止不當操作。

練習題 5

問題:

考慮以下類別定義,哪一行的代碼會導致編譯錯誤?為什麼?

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 Person
{
private string name;

public string Name
{
get { return name; }
private set { name = value; }
}

public void SetName(string newName)
{
Name = newName;
}
}

class Program
{
static void Main(string[] args)
{
Person person = new Person();
person.SetName("John");
Console.WriteLine(person.Name); // 正確
person.Name = "Doe"; // 會導致編譯錯誤
}
}

選項:

A. Person person = new Person();

B. person.SetName("John");

C. Console.WriteLine(person.Name);

D. person.Name = "Doe";

答案: D

解釋:

  • Name 屬性的 set 訪問器被標記為 private,因此只能在 Person 類別內部訪問。Main 方法位於外部,無法直接修改 Name 屬性,因此 person.Name = "Doe"; 會導致編譯錯誤。

10. 小結與練習

小結

我們已經逐步探討了 C# 中類別的語法構造及其背後的設計理念,從程序式編程的基礎到面向物件程式設計(OOP)的核心概念,包括:

  1. 程序式編程的基礎

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

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

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

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

    • 使用存取修飾符控制類別及其成員的可見性,實現資訊隱藏和封裝。
  6. **方法的修飾符:staticvoid**:

    • 理解 staticvoid 的用途,並學會如何正確使用它們來定義方法。
  7. 繼承(Inheritance)

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

    • 通過基類引用操作派生類物件,實現靈活的代碼設計和減少耦合。
  9. 封裝(Encapsulation)

    • 將數據和操作數據的方法組織在一起,並控制對數據的訪問,確保物件的一致性和安全性。

練習題 6

問題:

請完成以下類別定義,使其符合封裝原則,並能正確顯示餘額。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Wallet
{
// 定義一個私有欄位 balance

// 定義一個公開屬性 Balance,只有 get 訪問器

// 定義一個公開方法 AddMoney,增加 balance

// 定義一個公開方法 SpendMoney,減少 balance
}

class Program
{
static void Main(string[] args)
{
Wallet myWallet = new Wallet();
myWallet.AddMoney(100m);
myWallet.SpendMoney(30m);
Console.WriteLine($"餘額: {myWallet.Balance}"); // 輸出應為: 餘額: 70
}
}

答案:

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
public class Wallet
{
// 定義一個私有欄位 balance
private decimal balance;

// 定義一個公開屬性 Balance,只有 get 訪問器
public decimal Balance
{
get { return balance; }
}

// 定義一個公開方法 AddMoney,增加 balance
public void AddMoney(decimal amount)
{
if (amount > 0)
{
balance += amount;
Console.WriteLine($"已存入: {amount},新餘額: {balance}");
}
}

// 定義一個公開方法 SpendMoney,減少 balance
public void SpendMoney(decimal amount)
{
if (amount > 0 && amount <= balance)
{
balance -= amount;
Console.WriteLine($"已提取: {amount},新餘額: {balance}");
}
else
{
Console.WriteLine("提取金額無效。");
}
}
}

class Program
{
static void Main(string[] args)
{
Wallet myWallet = new Wallet();
myWallet.AddMoney(100m); // 輸出: 已存入: 100, 新餘額: 100
myWallet.SpendMoney(30m); // 輸出: 已提取: 30, 新餘額: 70
Console.WriteLine($"餘額: {myWallet.Balance}"); // 輸出: 餘額: 70
}
}

解析

  • **私有欄位 balance**:保護數據不被外部直接訪問。
  • **公開屬性 Balance**:僅提供讀取權限,防止外部修改。
  • **公開方法 AddMoneySpendMoney**:通過方法來控制 balance 的修改,確保數據的有效性。

下一步

現在,你已經掌握了 C# 類別的基本構造、存取修飾符、靜態與非靜態方法、繼承和多態性的基礎知識。下一步,我們可以進一步探討以下主題:

  1. 屬性(Properties) 的進階使用,如自動實作屬性、只讀屬性等。
  2. 介面(Interfaces) 的深入理解與應用。
  3. 抽象類別(Abstract Classes)封閉類別(Sealed Classes) 的使用場景。
  4. 委派(Delegates)事件(Events) 的基礎知識。
  5. 例外處理(Exception Handling) 的基礎和最佳實踐。

提問

在我們繼續深入之前,請回答以下問題來檢查你的理解:

問題:

為什麼在 BankAccount 類別中,我們將 balance 欄位設為 private,並通過公開的 DepositWithdraw 方法來修改它,而不是直接將 balance 設為 public

選項:

A. 為了讓外部程式碼更容易訪問和修改 balance

B. 為了保護數據不被外部不當修改,確保數據的一致性和安全性。

C. 因為 C# 語法要求所有欄位必須是私有的。

D. 為了讓 balance 可以在派生類別中被訪問。

請選出正確答案並解釋原因。


答案: B

解釋:

balance 欄位設為 private,並通過公開的 DepositWithdraw 方法來修改它,是為了保護數據不被外部不當修改,確保數據的一致性和安全性。這種做法實現了封裝(Encapsulation),避免了外部直接操作 balance,從而可以在方法中添加必要的邏輯來驗證和控制數據的修改。


總結

通過這一系列的步驟,我們從最基本的程序式編程開始,逐漸引入了類別與物件、存取修飾符、靜態與非靜態方法、繼承和多態性等 OOP 的核心概念。這些知識將幫助你設計出結構良好、易於維護和擴展的 C# 程式。

接下來的步驟

  1. 屬性的進階使用

    • 了解自動實作屬性、只讀屬性、計算屬性等。
  2. 介面(Interfaces)

    • 深入理解介面的概念,如何定義和實作介面,介面的應用場景。
  3. 抽象類別與封閉類別

    • 探討何時使用抽象類別,何時使用封閉類別,及其在設計中的作用。
  4. 委派與事件

    • 學習委派的基本概念,如何使用委派來實現事件驅動的程式設計。
  5. 例外處理

    • 理解如何使用 try-catch 塊來處理例外,保證程式的穩定性和可靠性。

如果你對以上內容有任何疑問,或希望深入了解某個特定主題,請隨時告訴我!