C#語法系統性學習
參考教程: 菜鳥教程
頂級語句
- 傳統 C# 代碼 - 在使用頂級語句之前,你必須像這樣編寫一個 C# 程序:
using System;
namespace MyApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
}
}
}
- 使用頂級語句的 C# 代碼 - 使用頂級語句,可以簡化為:
using System;
Console.WriteLine("Hello, World!");
- 頂級語句支持所有常見的 C# 語法,包括聲明變量、定義方法、處理異常等。
using System;
using System.Linq;
// 頂級語句中的變量聲明
int number = 42;
string message = "The answer to life, the universe, and everything is";
// 輸出變量
Console.WriteLine($"{message} {number}.");
// 定義和調用方法
int Add(int a, int b) => a + b;
Console.WriteLine($"Sum of 1 and 2 is {Add(1, 2)}.");
// 使用 LINQ
var numbers = new[] { 1, 2, 3, 4, 5 };
var evens = numbers.Where(n => n % 2 == 0).ToArray();
Console.WriteLine("Even numbers: " + string.Join(", ", evens));
// 異常處理
try
{
int zero = 0;
int result = number / zero;
}
catch (DivideByZeroException ex)
{
Console.WriteLine("Error: " + ex.Message);
}
注意事項
- 文件限制:頂級語句只能在一個源文件中使用。如果在一個項目中有多個使用頂級語句的文件,會導致編譯錯誤。
- 程序入口:如果使用頂級語句,則該文件會隱式地包含 Main 方法,并且該文件將成為程序的入口點。
- 作用域限制:頂級語句中的代碼共享一個全局作用域,這意味著可以在頂級語句中定義的變量和方法可以在整個文件中訪問。
頂級語句在簡化代碼結構、降低學習難度和加快開發速度方面具有顯著優勢,特別適合于編寫簡單程序和腳本。
List列表和Array相關
- 示例1
using System;
using System.Linq;
// 創建包含數字1-5的整數數組
var numbers = new[] { 1, 2, 3, 4, 5 };
// 使用LINQ的Where方法進行篩選,并將結果轉換為數組
var evens = numbers.Where(n => n % 2 == 0).ToArray();
// 拆分數組為字符串并添加','分隔符
Console.WriteLine("Even numbers: " + string.Join(", ", evens));
數據類型
在 C# 中,變量分為以下幾種類型:
- 值類型(Value types): 可以直接分配給一個值,比如
int - 引用類型(Reference types):不包含存儲在變量中的實際數據,但它們包含對變量的引用
- 它們指的是一個內存位置。使用多個變量時,引用類型可以指向一個內存位置。如果內存位置的數據是由一個變量改變的,其他變量會自動反映這種值的變化。內置的 引用類型有:object、dynamic 和 string
- 指針類型(Pointer types)
類型轉換
-
概念: 將一個
數據類型的值轉換為另一個數據類型的過程-
隱式轉換: 不需要編寫代碼來指定的轉換,編譯器會自動進行
- 將一個較小范圍的數據類型轉換為較大范圍的數據類型時,編譯器會自動完成類型轉換,這些轉換是 C# 默認的以安全方式進行的轉換, 不會導致數據丟失(例如,從 int 到 long,從 float 到 double 等)
byte b = 10; int i = b; // 隱式轉換,不需要顯式轉換 int intValue = 42; long longValue = intValue; // 隱式轉換,從 int 到 long -
顯式轉換(強制類型轉換)
- 是指將一個
較大范圍的數據類型轉換為較小范圍的數據類型時,或者將一個對象類型轉換為另一個對象類型時,需要使用強制類型轉換符號進行顯示轉換,強制轉換會造成數據丟失
int i = 10; byte b = (byte)i; // 顯式轉換,需要使用強制類型轉換符號 double doubleValue = 3.14; int intValue = (int)doubleValue; // 強制從 double 到 int,數據可能損失小數部分 int intValue = 42; float floatValue = (float)intValue; // 強制從 int 到 float,數據可能損失精度 int intValue = 123; string stringValue = intValue.ToString(); // 將 int 轉換為字符串-
C#提供了很多內置的類型轉換方法(無法轉換時,會自動拋異常)
- 例如** ToString,ToDouble,ToBoolean...**
string str = "123"; int number = Convert.ToInt32(str); // 轉換成功,number為123 - 注意事項:字符串不是有效的整數表示,Convert.ToInt32 將拋出 FormatException
- 是指將一個
-
C# 還提供了多種類型轉換方法,例如使用 Convert 類、Parse 方法和 TryParse 方法,這些方法可以幫助處理不同的數據類型之間的轉換
string str = "123"; int num = Convert.ToInt32(str); string str = "123.45"; double d = double.Parse(str); string str = "123.45"; double d; bool success = double.TryParse(str, out d); if (success) { Console.WriteLine("轉換成功: " + d); } else { Console.WriteLine("轉換失敗"); }- 自定義類型轉換(暫時先了解一下即可)
-
變量
- 作用域---全局變量: 在整個類中可見,如果在命名空間級別定義,那么它們在整個命名空間中可見
class MyClass
{
int memberVar = 30; // 成員變量,在整個類中可見
}
- 作用域---靜態變量: 在類級別上聲明的,但它們的作用域也受限于其定義的類
class MyClass
{
static int staticVar = 40; // 靜態變量,在整個類中可見
}
封裝
-
概念: "把一個或多個項目封閉在一個物理的或者邏輯的包中,防止對實現細節的訪問。
- C# 封裝根據具體的需要,設置使用者的訪問權限,并通過 訪問修飾符 來實現。
-
一個 訪問修飾符 *定義了一個類成員的范圍和可見性。C# 支持的訪問修飾符如下所示:
-
public:所有對象都可以訪問;
-
private:對象本身在對象內部可以訪問;
- 只有同一個類中的函數可以訪問它的私有成員。即使是類的實例也不能訪問它的私有成員。
using System; namespace RectangleApplication { class Rectangle { // 私有成員變量 private double length; private double width; // 公有方法,用于從用戶輸入獲取矩形的長度和寬度 public void AcceptDetails() { Console.WriteLine("請輸入長度:"); length = Convert.ToDouble(Console.ReadLine()); Console.WriteLine("請輸入寬度:"); width = Convert.ToDouble(Console.ReadLine()); } // 公有方法,用于計算矩形的面積 public double GetArea() { return length * width; } // 公有方法,用于顯示矩形的屬性和面積 public void Display() { Console.WriteLine("長度: {0}", length); Console.WriteLine("寬度: {0}", width); Console.WriteLine("面積: {0}", GetArea()); } }//end class Rectangle class ExecuteRectangle { static void Main(string[] args) { // 創建 Rectangle 類的實例 Rectangle r = new Rectangle(); // 通過公有方法 AcceptDetails() 從用戶輸入獲取矩形的長度和寬度 r.AcceptDetails(); // 通過公有方法 Display() 顯示矩形的屬性和面積 r.Display(); Console.ReadLine(); } } } 說明: - length 和 width 被聲明為私有成員變量,以防止直接在類的外部訪問和修改。 - AcceptDetails 方法允許用戶輸入矩形的長度和寬度,這是通過公有方法來操作私有變量 - 在 ExecuteRectangle 類中,通過創建 Rectangle 類的實例,然后調用其公有方法,來執行操作。這樣,主程序無法直接訪問和修改矩形的長度和寬度,而是通過類提供的公有接口來進行操作,實現了封裝。 -
protected:只有該類對象及其子類對象可以訪問(有助于實現繼承,世襲制)
-
internal:同一個程序集的對象可以訪問;
- 帶有 internal 訪問修飾符的任何成員可以被定義在該成員所定義的應用程序內的任何類或方法訪問
using System; namespace RectangleApplication { class Rectangle { //成員變量 internal double length; internal double width; // 如果沒有指定訪問修飾符,則使用類成員的默認訪問修飾符,即為 private double GetArea() { return length * width; } public void Display() { Console.WriteLine("長度: {0}", length); Console.WriteLine("寬度: {0}", width); Console.WriteLine("面積: {0}", GetArea()); } } class ExecuteRectangle { static void Main(string[] args) { Rectangle r = new Rectangle(); r.length = 4.5; r.width = 3.5; r.Display(); Console.ReadLine(); } } } -
protected internal:訪問限于當前程序集或派生自包含類的類型。
-

按值傳遞和按引用傳遞的區別
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(int x, int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 賦值給 x */
y = temp; /* 把 temp 賦值給 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部變量定義 */
int a = 100;
int b = 200;
Console.WriteLine("在交換之前,a 的值: {0}", a);
Console.WriteLine("在交換之前,b 的值: {0}", b);
/* 調用函數來交換值 */
n.swap(a, b);
Console.WriteLine("在交換之后,a 的值: {0}", a);
Console.WriteLine("在交換之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}
- 結果如下:
在交換之前,a 的值:100
在交換之前,b 的值:200
在交換之后,a 的值:100
在交換之后,b 的值:200
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(ref int x, ref int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 賦值給 x */
y = temp; /* 把 temp 賦值給 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部變量定義 */
int a = 100;
int b = 200;
Console.WriteLine("在交換之前,a 的值: {0}", a);
Console.WriteLine("在交換之前,b 的值: {0}", b);
/* 調用函數來交換值 */
n.swap(ref a, ref b);
Console.WriteLine("在交換之后,a 的值: {0}", a);
Console.WriteLine("在交換之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}
- 結果如下:
在交換之前,a 的值:100
在交換之前,b 的值:200
在交換之后,a 的值:200
在交換之后,b 的值:100
第一段代碼:按值傳遞(無法交換)
public void swap(int x, int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 賦值給 x */
y = temp; /* 把 temp 賦值給 y */
}
問題:這段代碼無法真正交換兩個變量的值。
原因:
- 參數
x和y是按值傳遞的 - 方法內部操作的是原始變量的副本,而不是原始變量本身
- 方法執行完畢后,原始變量
a和b的值保持不變
輸出結果:
在交換之前,a 的值: 100
在交換之前,b 的值: 200
在交換之后,a 的值: 100
在交換之后,b 的值: 200
第二段代碼:按引用傳遞(成功交換)
public void swap(ref int x, ref int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 賦值給 x */
y = temp; /* 把 temp 賦值給 y */
}
關鍵改進:使用了 ref 關鍵字
工作原理:
ref關鍵字表示按引用傳遞參數- 方法內部操作的是原始變量的內存地址,而不是副本
- 對參數的修改會直接影響原始變量
調用方式:
n.swap(ref a, ref b); // 調用時也需要加上 ref 關鍵字
輸出結果:
在交換之前,a 的值: 100
在交換之前,b 的值: 200
在交換之后,a 的值: 200
在交換之后,b 的值: 100
核心概念總結
| 特性 | 按值傳遞 | 按引用傳遞 (ref) |
|---|---|---|
| 傳遞內容 | 變量的值副本 | 變量的內存地址 |
| 對原始變量的影響 | 無影響 | 直接影響 |
| 性能 | 需要復制數據 | 直接操作原數據 |
| 使用場景 | 不希望修改原始值 | 需要修改原始值 |
Python與C#的關鍵區別
| 特性 | C# | Python |
|---|---|---|
| 直接交換變量 | 需要臨時變量和ref關鍵字 | a, b = b, a |
| 參數傳遞 | 明確區分值類型和引用類型 | 都是對象引用傳遞 |
| 修改原始變量 | 需要ref或out關鍵字 |
只能修改可變對象的內容 |
推薦:在Python中,直接使用 a, b = b, a 是最簡單、最Pythonic的交換方式。
按輸出傳遞參數(out參數應用)
- 以下代碼展示了如何使用
out關鍵字在方法中返回一個值,并修改調用方的變量
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void getValue(out int x )
{
int temp = 5;
x = temp;
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部變量定義 */
int a = 100;
Console.WriteLine("在方法調用之前,a 的值: {0}", a);
/* 調用函數來獲取值 */
n.getValue(out a);
Console.WriteLine("在方法調用之后,a 的值: {0}", a);
Console.ReadLine();
}
}
}
- 返回結果如下:
在方法調用之前,a 的值: 100
在方法調用之后,a 的值: 5
這段C#代碼演示了輸出參數(out parameter) 的使用。讓我詳細解釋:
代碼功能概述
這段代碼展示了如何使用 out 關鍵字在方法中返回一個值,并修改調用方的變量。
方法定義部分
public void getValue(out int x)
{
int temp = 5;
x = temp; // 必須對 out 參數賦值
}
out int x:聲明一個輸出參數- 關鍵特性:在方法內部必須對
out參數進行賦值 x = temp:將局部變量temp的值賦給輸出參數x
Main 方法部分
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
int a = 100; // 初始化變量 a
Console.WriteLine("在方法調用之前,a 的值: {0}", a); // 輸出:100
n.getValue(out a); // 調用方法,傳遞 a 作為輸出參數
Console.WriteLine("在方法調用之后,a 的值: {0}", a); // 輸出:5
Console.ReadLine();
}
執行流程
- 初始化:變量
a被賦值為 100 - 方法調用前:輸出
a = 100 - 方法調用:
n.getValue(out a)- 在
getValue方法中,x被賦值為 5 - 由于
x是out參數,這個賦值會直接修改原始變量a
- 在
- 方法調用后:輸出
a = 5
輸出結果
在方法調用之前,a 的值: 100
在方法調用之后,a 的值: 5
out 參數的特點
- 必須賦值:在方法內部必須對
out參數賦值 - 不關心初始值:調用方法前,變量的初始值不會被使用
- 引用傳遞:傳遞的是變量的引用,不是值的副本
- 調用語法:調用時必須使用
out關鍵字
與 ref 的區別
| 特性 | out 參數 |
ref 參數 |
|---|---|---|
| 初始化要求 | 調用前變量不需要初始化 | 調用前變量必須初始化 |
| 方法內賦值 | 必須賦值 | 可以選擇性賦值 |
| 使用場景 | 用于從方法返回數據 | 用于傳入和傳出數據 |
Python 中的類似實現
在Python中,可以通過返回多個值或使用可變對象來模擬:
# 方式1:返回元組
def get_value():
temp = 5
return temp
a = 100
print(f"方法調用之前,a 的值: {a}") # 100
a = get_value() # 重新賦值
print(f"方法調用之后,a 的值: {a}") # 5
# 方式2:使用列表模擬引用
def get_value_out(container):
temp = 5
container[0] = temp
a_container = [100]
print(f"方法調用之前,a 的值: {a_container[0]}") # 100
get_value_out(a_container)
print(f"方法調用之后,a 的值: {a_container[0]}") # 5
這個例子很好地展示了C#中 out 參數的用法,常用于需要從方法返回多個值的場景。
C# 可空類型(Nullable)
-
可空類型就是允許值類型(如 int、bool、DateTime)可以有一個沒有值的狀態(即 null) -
在 C# 中,像 int、float、bool 等都是值類型(Value Types),默認情況下它們不能為 null。
例如以下賦值就會編譯錯誤:
int a = null; // 編譯錯誤但是使用 可空類型之后就可以:
int? a = null; // 合法,a此時的類型為Nullable<int>,int? 是 Nullable<int> 的語法糖(簡寫形式)。 int i; // 默認值為 0 int? ii; // 默認值為 null
可空引用類型 (Nullable Reference Types)
#nullable enable // 啟用可空引用類型檢查
// 傳統引用類型(默認不可為空)
string nonNullableString = null; // 編譯器警告!
string definitelyNotNull = "Hello"; // 正確
// 可空引用類型
string? nullableString = null; // 正確,明確聲明可為null
// 使用時的檢查
Console.WriteLine(nullableString.Length); // 編譯器警告:可能為null
if (nullableString != null)
{
Console.WriteLine(nullableString.Length); // 正確,編譯器知道不為null
}
總結
可空引用類型是C# 8.0引入的編譯時靜態分析特性,目的是:
- 減少
NullReferenceException - 讓代碼意圖更明確
- 通過編譯器警告提前發現問題
而可空值類型是運行時特性,真正允許值類型存儲null值。
兩者解決的問題不同,但都旨在提高代碼的安全性和可讀性。
結構體(輕量級的類)
- demo演示
using System;
using System.Text;
struct Books
{
public string title;
public string author;
public string subject;
public int book_id;
};
public class testStructure
{
public static void Main(string[] args)
{
Books Book1; /* 聲明 Book1,類型為 Books */
Books Book2; /* 聲明 Book2,類型為 Books */
/* book 1 詳述 */
Book1.title = "C Programming";
Book1.author = "Nuha Ali";
Book1.subject = "C Programming Tutorial";
Book1.book_id = 6495407;
/* book 2 詳述 */
Book2.title = "Telecom Billing";
Book2.author = "Zara Ali";
Book2.subject = "Telecom Billing Tutorial";
Book2.book_id = 6495700;
/* 打印 Book1 信息 */
Console.WriteLine( "Book 1 title : {0}", Book1.title);
Console.WriteLine("Book 1 author : {0}", Book1.author);
Console.WriteLine("Book 1 subject : {0}", Book1.subject);
Console.WriteLine("Book 1 book_id :{0}", Book1.book_id);
/* 打印 Book2 信息 */
Console.WriteLine("Book 2 title : {0}", Book2.title);
Console.WriteLine("Book 2 author : {0}", Book2.author);
Console.WriteLine("Book 2 subject : {0}", Book2.subject);
Console.WriteLine("Book 2 book_id : {0}", Book2.book_id);
Console.ReadKey();
}
}
類和結構有以下幾個基本的不同點:
值類型 vs 引用類型:
- 結構是值類型(Value Type): 結構是值類型,它們在棧上分配內存,而不是在堆上。當將結構實例傳遞給方法或賦值給另一個變量時,將復制整個結構的內容。
- 類是引用類型(Reference Type): 類是引用類型,它們在堆上分配內存。當將類實例傳遞給方法或賦值給另一個變量時,實際上是傳遞引用(內存地址)而不是整個對象的副本。
繼承和多態性:
- 結構不能繼承: 結構不能繼承其他結構或類,也不能作為其他結構或類的基類。
- 類支持繼承: 類支持繼承和多態性,可以通過派生新類來擴展現有類的功能。
默認構造函數:
- 結構不能有無參數的構造函數: 結構不能包含無參數的構造函數。
- 類可以有無參數的構造函數: 類可以包含無參數的構造函數,如果沒有提供構造函數,系統會提供默認的無參數構造函數。
性能和內存分配:
- 結構通常更輕量: 由于結構是值類型且在棧上分配內存,它們通常比類更輕量,適用于簡單的數據表示。
- 類可能有更多開銷: 由于類是引用類型,可能涉及更多的內存開銷和管理。
可以理解為輕量級的類嗎?
是的,但需要謹慎理解這個比喻。
結構體確實可以被看作是"輕量級"的數據容器,但更重要的是理解它們的語義差異:
// 結構體 - 值語義
Point p1 = new Point(10, 20);
Point p2 = p1; // 復制值
p2.X = 30; // 不影響p1
// 類 - 引用語義
Customer c1 = new Customer { Name = "John" };
Customer c2 = c1; // 復制引用
c2.Name = "Jane"; // 影響c1,因為它們指向同一對象
實際使用建議
使用結構體的情況:
- 數據量小(通常小于16字節)
- 邏輯上表示一個值(如坐標、顏色)
- 不可變或很少修改
- 性能關鍵路徑,需要避免GC壓力
使用類的情況:
- 需要繼承和多態
- 表示具有身份標識的實體
- 包含大量數據或復雜狀態
- 需要共享引用語義
性能考慮
// 結構體數組 - 內存連續,緩存友好
Point[] points = new Point[1000]; // 內存中連續存儲
// 類數組 - 存儲引用,數據可能分散
Customer[] customers = new Customer[1000]; // 存儲的是引用指針
總結:結構體確實是更輕量的數據容器,但選擇使用結構體還是類應該基于語義需求(值語義vs引用語義)而不僅僅是性能考慮。在正確的場景中使用正確的類型,才能寫出既高效又易于維護的代碼。
枚舉(Enum)
- 枚舉是一組命名整型常量。枚舉類型是使用 enum 關鍵字聲明的
- C# 枚舉是值類型。換句話說,枚舉包含自己的值,且不能繼承或傳遞繼承。
- 枚舉列表中的每個符號代表一個整數值,一個比它前面的符號大的整數值。默認情況下,第一個枚舉符號的值是 0.例如:
enum Days { Sun, Mon, tue, Wed, thu, Fri, Sat };
using System;
public class EnumTest
{
enum Day { Sun, Mon, Tue, Wed, Thu, Fri, Sat };
static void Main()
{
int x = (int)Day.Sun;
int y = (int)Day.Fri;
Console.WriteLine("Sun = {0}", x); // Sun = 0
Console.WriteLine("Fri = {0}", y); // Fri = 5
}
}
析構函數,靜態成員和靜態函數
-
析構函數:用于資源清理(釋放內存)
- 對象被銷毀時自動調用的特殊方法,用于清理非托管資源
public class FileHandler { private FileStream fileStream; // 構造函數 public FileHandler(string filePath) { fileStream = new FileStream(filePath, FileMode.Open); } // 析構函數 ~FileHandler() { Console.WriteLine("析構函數被調用,清理資源"); fileStream?.Close(); } } -
靜態成員:類級別的共享數據,所有實例共享
-
靜態函數:工具方法,無需實例化即可調用
類的繼承
- 一個
類可以繼承多個接口,但只能繼承自一個類。 派生類可以通過關鍵字base來調用基類的構造函數和方法。
class BaseClass
{
public void SomeMethod()
{
......
}
}
class DerivedClass : BaseClass
{
public void AnotherMethod()
{
// 調用父類的方法
base.SomeMethod();
......
}
}
- 演示面向對象的繼承、方法重寫和代碼復用概念
using System;
var topTable = new Tabletop(10, 20);
//Console.WriteLine(topTable.GetCost());
topTable.Display();
class Rectangle
{
protected double length;
protected double width;
public Rectangle(double l, double w)
{
length = l;
width = w;
}
public double GetArea()
{
return length * width;
}
public virtual void Display() // 添加virtual允許重寫
{
Console.WriteLine("長度: {0}", length);
Console.WriteLine("寬度: {0}", width);
Console.WriteLine("面積: {0}", GetArea());
}
}
class Tabletop : Rectangle
{
public Tabletop(double l, double w) : base(l, w) // 繼承父類的構造方法
{
}
public double GetCost()
{
return GetArea() * 70;
}
public override void Display() // 重寫父類的Display方法
{
base.Display();
Console.WriteLine("成本: {0}", GetCost());
}
}
- 程序運行結果:
長度: 10
寬度: 20
面積: 200
成本: 14000
-
子類繼承父類的時候,私有的成員或者方法,不能被繼承或者訪問
- private成員只限類自己訪問,是類的私有財產,即使是兒子也不行
public class Parent { private string privateField = "私有字段"; private void PrivateMethod() { Console.WriteLine("私有方法"); } public void ShowPrivate() { Console.WriteLine(privateField); // 只能在父類內部訪問 PrivateMethod(); // 只能在父類內部調用 } } public class Child : Parent { public void TryAccessPrivate() { // 以下代碼都會編譯錯誤: // Console.WriteLine(privateField); // 錯誤:無法訪問 // PrivateMethod(); // 錯誤:無法訪問 } }- 封裝的示例如下
public class Vehicle { public string model; private string _engineType; public string EngineType { get => _engineType; protected set => _engineType = value; // 子類可以設置,外部只讀 } protected int _speed; public virtual int Speed { get => _speed; set => _speed = value >= 0 ? value : 0; // 添加驗證邏輯 } } // 創建車輛對象 Vehicle car = new Vehicle(); // 可以設置(公共字段) car.model = "Toyota"; // 可以讀取(公共屬性) string engine = car.EngineType; // 不能設置(protected set) // car.EngineType = "V6"; // 編譯錯誤 // 速度自動驗證 car.Speed = 100; // 正常設置 car.Speed = -10; // 自動變為0很好的觀察!你提出了一個重要的概念區分。EngineType和Speed都是屬性(Property),不是方法(Method)。
屬性和方法的區別
屬性 (Property)
public string EngineType { get => _engineType; // 讀取時執行 protected set => _engineType = value; // 寫入時執行 }-
用途:封裝字段的訪問,像"智能字段"
-
調用方式:像字段一樣直接使用
string type = car.EngineType; // 調用get // car.EngineType = "V6"; // 調用set (但這里protected) -
特點:通常不包含復雜邏輯,主要用于數據訪問控制
方法 (Method)
public string GetEngineType() { return _engineType; } protected void SetEngineType(string value) { _engineType = value; }-
用途:執行操作或計算
-
調用方式:需要括號和參數
string type = car.GetEngineType(); // car.SetEngineType("V6"); -
特點:可以包含復雜邏輯,可以有返回值
為什么使用屬性而不是方法?
屬性提供了更自然的語法:
// 屬性語法 (更簡潔) car.Speed = 100; int currentSpeed = car.Speed; // 對應的方法語法 (更繁瑣) car.SetSpeed(100); int currentSpeed = car.GetSpeed();屬性在C#中的特殊地位:
- 可以被數據綁定
- 在Visual Studio中有智能感知支持
- 可以被序列化
- 有專門的元數據
總結
特性 屬性(Property) 方法(Method) 語法 obj.Propertyobj.Method()用途 數據訪問 執行操作 邏輯復雜度 簡單驗證/轉換 可以很復雜 參數 通常無(set有value) 可以有多個 所以你的觀察是正確的:
EngineType和Speed都是屬性,它們使用特殊的get/set語法來提供字段般的訪問體驗,同時具備方法般的邏輯控制能力。 -
C#
多態性和抽象類的示例
using System;
namespace PolymorphismApplication
{
abstract class Shape
{
abstract public int area();
}
class Rectangle: Shape
{
private int length;
private int width;
public Rectangle( int a=0, int b=0)
{
length = a;
width = b;
}
public override int area ()
{
Console.WriteLine("Rectangle 類的面積:");
return (width * length);
}
}
class RectangleTester
{
static void Main(string[] args)
{
Rectangle r = new Rectangle(10, 7);
double a = r.area();
Console.WriteLine("面積: {0}",a);
Console.ReadKey();
}
}
}
代碼結構概覽
using System;
namespace PolymorphismApplication
{
// 代碼分為三個主要部分
}
1. 抽象基類 Shape
abstract class Shape
{
abstract public int area();
}
abstract:表示這是一個抽象類- 不能直接實例化:
Shape s = new Shape();? 會報錯 - 只能被其他類繼承
- 不能直接實例化:
- 抽象方法
area():- 只有方法聲明,沒有方法體(沒有
{}) - 強制所有子類必須實現這個方法
- 這是多態的基礎
- 只有方法聲明,沒有方法體(沒有
2. 具體子類 Rectangle
class Rectangle: Shape // 繼承Shape
{
private int length;
private int width;
// 構造函數
public Rectangle(int a=0, int b=0)
{
length = a;
width = b;
}
// 實現抽象方法
public override int area()
{
Console.WriteLine("Rectangle 類的面積:");
return (width * length);
}
}
關鍵點:
- 繼承關系:
Rectangle : Shape表示Rectangle繼承自Shape - 構造函數參數默認值:
int a=0, int b=0允許不傳參數創建對象 override:必須使用這個關鍵字來重寫抽象方法- 具體實現:提供了計算矩形面積的具體邏輯
3. 測試類 RectangleTester
class RectangleTester
{
static void Main(string[] args)
{
Rectangle r = new Rectangle(10, 7); // 創建對象
double a = r.area(); // 調用方法
Console.WriteLine("面積: {0}",a); // 輸出結果
Console.ReadKey(); // 等待按鍵
}
}
多態性體現
雖然這個簡單例子沒有完全展示多態,但擴展后可以這樣:
// 多態的例子
Shape[] shapes = new Shape[3];
shapes[0] = new Rectangle(10, 7);
shapes[1] = new Circle(5); // 假設有Circle類
shapes[2] = new Triangle(4, 3); // 假設有Triangle類
foreach (Shape shape in shapes)
{
Console.WriteLine($"面積: {shape.area()}"); // 多態調用
}
程序輸出
Rectangle 類的面積:
面積: 70
設計模式分析
模板方法模式:
- 基類定義"要做什么"(計算面積)
- 子類定義"具體怎么做"(如何計算矩形面積)
優點:
- 強制規范:確保所有形狀都有area方法
- 擴展性好:可以輕松添加新的形狀類
- 多態支持:可以用統一接口處理不同形狀
這就是面向對象編程中抽象和多態的經典應用!
Vritual和abstract的區別
核心區別對比
| 特性 | virtual (虛方法) |
abstract (抽象方法) |
|---|---|---|
| 方法體 | 必須有實現 | 不能有實現 |
| 所在類 | 可以在普通類或抽象類中 | 只能在抽象類中 |
| 子類要求 | 可選重寫 | 必須重寫(除非子類也是抽象類) |
| 實例化 | 所在類可以實例化 | 所在類不能實例化 |
代碼示例對比
使用 virtual 的例子:
class Animal
{
// virtual方法有默認實現
public virtual void MakeSound()
{
Console.WriteLine("動物發出聲音");
}
}
class Dog : Animal
{
// 可選重寫
public override void MakeSound()
{
Console.WriteLine("汪汪!");
}
}
class Cat : Animal
{
// 可以不重寫,使用基類的實現
}
使用 abstract 的例子:
abstract class Shape
{
// abstract方法沒有實現
public abstract double Area();
}
class Rectangle : Shape
{
public double Width { get; set; }
public double Height { get; set; }
// 必須重寫
public override double Area()
{
return Width * Height;
}
}
class Circle : Shape
{
public double Radius { get; set; }
// 必須重寫
public override double Area()
{
return Math.PI * Radius * Radius;
}
}
實際使用場景
使用 virtual 的情況:
- 方法有合理的默認實現
- 希望子類可選地定制行為
- 基類本身是有意義的實體
public class Vehicle
{
public virtual void StartEngine()
{
Console.WriteLine("啟動發動機");
// 大多數車輛的啟動邏輯相同
}
}
使用 abstract 的情況:
- 方法沒有有意義的默認實現
- 強制要求子類提供特定實現
- 基類只是概念,不能獨立存在
public abstract class DatabaseConnection
{
public abstract void Connect(); // 每種數據庫連接方式完全不同
public abstract void Disconnect();
}
混合使用示例
abstract class PaymentProcessor
{
// 抽象方法:支付方式完全不同,必須由子類實現
public abstract void ProcessPayment(decimal amount);
// 虛方法:日志記錄有默認實現,但子類可定制
public virtual void LogTransaction(decimal amount)
{
Console.WriteLine($"處理支付: {amount}");
}
// 普通方法:通用邏輯,不希望子類修改
public void ValidateAmount(decimal amount)
{
if (amount <= 0)
throw new ArgumentException("金額必須大于0");
}
}
總結
virtual:"你可以改變我,如果你需要的話"abstract:"你必須實現我,因為我不知道該怎么做"
選擇依據:
- 如果方法有有意義的默認行為 → 用
virtual - 如果方法必須由子類提供具體實現 → 用
abstract - 如果希望確保方法不被修改 → 都不用(普通方法)
多態實例演示
多態性允許我們使用基類類型的引用指向子類的對象,并且調用方法時會調用子類重寫的方法。
using System;
using System.Collections.Generic;
public class Shape
{
public int X { get; private set; }
public int Y { get; private set; }
public int Height { get; set; }
public int Width { get; set; }
// 虛方法
public virtual void Draw()
{
Console.WriteLine("執行基類的畫圖任務");
}
}
class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("畫一個圓形");
base.Draw();
}
}
class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("畫一個長方形");
base.Draw();
}
}
class Triangle : Shape
{
public override void Draw()
{
Console.WriteLine("畫一個三角形");
base.Draw();
}
}
class Program
{
static void Main(string[] args)
{
// 創建一個 List<Shape> 對象,并向該對象添加 Circle、Triangle 和 Rectangle
var shapes = new List<Shape>
{
new Rectangle(),
new Triangle(),
new Circle()
};
// 使用 foreach 循環對該列表的派生類進行循環訪問,并對其中的每個 Shape 對象調用 Draw 方法
foreach (var shape in shapes)
{
shape.Draw();
}
Console.WriteLine("按下任意鍵退出。");
Console.ReadKey();
}
}
代碼結構分析
1. 基類 Shape
public class Shape
{
public int X { get; private set; } // 只讀屬性
public int Y { get; private set; } // 只讀屬性
public int Height { get; set; } // 可讀寫屬性
public int Width { get; set; } // 可讀寫屬性
// 虛方法 - 可以被重寫
public virtual void Draw()
{
Console.WriteLine("執行基類的畫圖任務");
}
}
特點:
- 包含基本的幾何屬性
Draw()方法標記為virtual,提供默認實現
2. 派生類(子類)
class Circle : Shape
{
public override void Draw()
{
Console.WriteLine("畫一個圓形");
base.Draw(); // 調用基類方法
}
}
class Rectangle : Shape
{
public override void Draw()
{
Console.WriteLine("畫一個長方形");
base.Draw(); // 調用基類方法
}
}
class Triangle : Shape
{
public override void Draw()
{
Console.WriteLine("畫一個三角形");
base.Draw(); // 調用基類方法
}
}
關鍵點:
- 都使用
override重寫Draw()方法 - 每個類添加自己特定的繪制邏輯
- 都調用
base.Draw()保留基類的通用邏輯
3. 多態性演示
class Program
{
static void Main(string[] args)
{
// 創建Shape類型的集合,但存儲的是各種子類對象
var shapes = new List<Shape>
{
new Rectangle(),
new Triangle(),
new Circle()
};
// 多態調用:編譯時類型是Shape,運行時調用具體子類的方法
foreach (var shape in shapes)
{
shape.Draw(); // 運行時決定調用哪個版本的Draw
}
}
}
多態性工作原理
編譯時 vs 運行時
- 編譯時類型:
Shape(編譯器看到的類型) - 運行時類型:
Rectangle、Triangle、Circle(實際對象類型)
方法調用過程
shape.Draw(); // 這行代碼在運行時:
// 1. 檢查shape的實際類型
// 2. 調用該類型重寫的Draw方法
// 3. 每個子類方法中又調用base.Draw()
程序輸出結果
畫一個長方形
執行基類的畫圖任務
畫一個三角形
執行基類的畫圖任務
畫一個圓形
執行基類的畫圖任務
按下任意鍵退出。
設計優勢
1. 擴展性
// 可以輕松添加新形狀,無需修改現有代碼
class Hexagon : Shape
{
public override void Draw()
{
Console.WriteLine("畫一個六邊形");
base.Draw();
}
}
// 直接添加到列表中即可工作
shapes.Add(new Hexagon());
2. 統一處理
// 所有形狀都可以用同樣的方式處理
void ProcessShapes(List<Shape> shapes)
{
foreach (var shape in shapes)
{
shape.Draw(); // 繪制
// shape.Move(); // 移動(如果添加了此方法)
// shape.Rotate(); // 旋轉(如果添加了此方法)
}
}
3. 代碼復用
通過 base.Draw() 調用,所有子類都復用了基類的通用繪制邏輯。
實際應用場景
這種設計模式在GUI框架、游戲開發、圖形處理等領域非常常見:
- GUI控件:所有控件繼承自基類Control,都有Draw方法
- 游戲實體:所有游戲對象繼承自GameObject,都有Update和Render方法
- 文檔元素:所有文檔元素都有統一的打印/顯示接口
這就是面向對象編程中多態的強大之處:統一的接口,不同的實現!
又一個多態實例
- 通過參數多態來實現不同形狀的面積計算
using System;
namespace PolymorphismApplication
{
class Shape
{
protected int width, height;
public Shape( int a=0, int b=0)
{
width = a;
height = b;
}
public virtual int area()
{
Console.WriteLine("父類的面積:");
return 0;
}
}
class Rectangle: Shape
{
public Rectangle( int a=0, int b=0): base(a, b)
{
}
public override int area ()
{
Console.WriteLine("Rectangle 類的面積:");
return (width * height);
}
}
class Triangle: Shape
{
public Triangle(int a = 0, int b = 0): base(a, b)
{
}
public override int area()
{
Console.WriteLine("Triangle 類的面積:");
return (width * height / 2);
}
}
class Caller
{
public void CallArea(Shape sh)
{
int a;
a = sh.area();
Console.WriteLine("面積: {0}", a);
}
}
class Tester
{
static void Main(string[] args)
{
Caller c = new Caller();
Rectangle r = new Rectangle(10, 7);
Triangle t = new Triangle(10, 5);
c.CallArea(r);
c.CallArea(t);
Console.ReadKey();
}
}
}
代碼結構分析
1. 基類 Shape
class Shape
{
protected int width, height; // 受保護字段,子類可訪問
// 構造函數,參數有默認值
public Shape(int a=0, int b=0)
{
width = a;
height = b;
}
// 虛方法,提供默認實現(返回0)
public virtual int area()
{
Console.WriteLine("父類的面積:");
return 0;
}
}
2. 派生類 Rectangle 和 Triangle
Rectangle 類:
class Rectangle: Shape
{
// 構造函數,調用基類構造函數
public Rectangle(int a=0, int b=0): base(a, b) { }
// 重寫area方法,計算矩形面積
public override int area()
{
Console.WriteLine("Rectangle 類的面積:");
return (width * height);
}
}
Triangle 類:
class Triangle: Shape
{
// 構造函數,調用基類構造函數
public Triangle(int a=0, int b=0): base(a, b) { }
// 重寫area方法,計算三角形面積
public override int area()
{
Console.WriteLine("Triangle 類的面積:");
return (width * height / 2);
}
}
3. 關鍵類 Caller - 體現多態性
class Caller
{
// 參數類型是基類Shape,但可以接受任何派生類對象
public void CallArea(Shape sh)
{
int a;
a = sh.area(); // 多態調用:運行時決定調用哪個area方法
Console.WriteLine("面積: {0}", a);
}
}
這是多態的核心體現:
- 方法參數是基類類型
Shape - 但可以傳遞任何派生類對象(
Rectangle、Triangle等) - 在運行時根據實際對象類型調用相應的方法
4. 測試類 Tester
class Tester
{
static void Main(string[] args)
{
Caller c = new Caller();
Rectangle r = new Rectangle(10, 7); // 創建矩形:寬10,高7
Triangle t = new Triangle(10, 5); // 創建三角形:底10,高5
c.CallArea(r); // 傳遞Rectangle對象
c.CallArea(t); // 傳遞Triangle對象
Console.ReadKey();
}
}
多態性工作原理
編譯時類型 vs 運行時類型
// 在CallArea方法中:
public void CallArea(Shape sh) // 編譯時類型:Shape
{
sh.area(); // 運行時根據sh的實際類型調用對應方法
}
方法調用過程
c.CallArea(r)→sh實際是Rectangle對象 → 調用Rectangle.area()c.CallArea(t)→sh實際是Triangle對象 → 調用Triangle.area()
程序輸出結果
Rectangle 類的面積:
面積: 70
Triangle 類的面積:
面積: 25
設計優勢分析
1. 擴展性極佳
// 可以輕松添加新形狀,無需修改Caller類
class Circle : Shape
{
private int radius;
public Circle(int r) : base() { radius = r; }
public override int area()
{
Console.WriteLine("Circle 類的面積:");
return (int)(3.14 * radius * radius);
}
}
// 在Main方法中直接使用:
Circle circle = new Circle(5);
c.CallArea(circle); // 無需修改CallArea方法
2. 統一的接口
// 可以創建各種形狀的集合統一處理
List<Shape> shapes = new List<Shape>
{
new Rectangle(10, 7),
new Triangle(10, 5),
new Circle(5)
};
foreach (Shape shape in shapes)
{
c.CallArea(shape); // 統一處理,自動調用正確的方法
}
3. 降低耦合
Caller類不需要知道具體有哪些形狀類型- 新增形狀類型時,
Caller類不需要任何修改 - 符合"開閉原則":對擴展開放,對修改關閉
實際應用場景
這種參數多態在設計模式中廣泛應用:
- 策略模式:不同的算法實現同一接口
- 訪問者模式:對不同類型的元素執行相同操作
- 命令模式:不同的命令對象有相同的執行接口
- 支付系統:不同的支付方式(支付寶、微信、銀行卡)實現相同的支付接口
這個例子完美展示了面向對象編程中多態的核心價值:用統一的接口處理不同的對象,提高代碼的靈活性和可維護性!

浙公網安備 33010602011771號