1. C#語言基礎
1.1 C#語法概覽
歡迎來到C#的世界!對于剛從Java轉過來的開發者來說,你會發現C#和Java有很多相似之處,但C#也有其獨特的魅力和強大之處。讓我們一起來探索C#的基本語法,并比較一下與Java的異同。
程序結構
C#程序的基本結構與Java非常相似。這里是一個簡單的C#程序:
using System;namespace HelloWorld
{class Program{static void Main(string[] args){Console.WriteLine("Hello, World!");}}
}
對比Java的版本:
public class HelloWorld {public static void main(String[] args) {System.out.println("Hello, World!");}
}
你會發現,兩者的結構非常相似。主要的區別在于:
- C#使用
using
關鍵字導入命名空間,而Java使用import
。 - C#的
Main
方法是static void Main(string[] args)
,而Java是public static void main(String[] args)
。 - C#使用
Console.WriteLine()
輸出,Java使用System.out.println()
。
在c# 9的最新語法上還可以更簡潔,是的沒錯,只需要一行代碼,不需要寫命名空間,類,方法,直接編寫代碼,當然這個方式只存在c#9以上的版本。
Console.WriteLine("Hello, World!");
命名約定
C#和Java的命名約定有些許不同:
- C#中,方法名和屬性名通常使用PascalCase(如
CalculateTotal
)。 - 局部變量和參數使用camelCase(如
totalAmount
)。 - 接口名稱以"I"開頭(如
IDisposable
)。
而Java中:
- 方法名和變量名都使用camelCase。
- 接口名稱不需要特殊前綴。
數據類型
C#和Java的基本數據類型很相似,但也有一些區別:
C#:
int x = 10;
long y = 100L;
float f = 3.14f;
double d = 3.14;
decimal m = 100.50m;
bool isTrue = true;
char c = 'A';
string s = "Hello";
Java:
int x = 10;
long y = 100L;
float f = 3.14f;
double d = 3.14;
boolean isTrue = true;
char c = 'A';
String s = "Hello";
注意C#特有的decimal
類型,它提供了更高精度的小數計算,特別適合金融相關的應用。
數組
C#和Java的數組聲明稍有不同:
C#:
int[] numbers = new int[5];
string[] names = { "Alice", "Bob", "Charlie" };
Java:
int[] numbers = new int[5];
String[] names = { "Alice", "Bob", "Charlie" };
控制結構
C#和Java的控制結構幾乎完全相同:
// if語句
if (condition)
{// code
}
else if (anotherCondition)
{// code
}
else
{// code
}// for循環
for (int i = 0; i < 10; i++)
{// code
}// while循環
while (condition)
{// code
}// switch語句
switch (variable)
{case value1:// codebreak;case value2:// codebreak;default:// codebreak;
}
這些結構在Java中的寫法完全相同。
異常處理
C#和Java的異常處理也非常相似:
C#:
try
{// 可能拋出異常的代碼
}
catch (SpecificException ex)
{// 處理特定異常
}
catch (Exception ex)
{// 處理一般異常
}
finally
{// 總是要執行的代碼
}
Java的異常處理結構完全相同。
注釋
C#和Java的注釋方式也是一樣的:
// 這是單行注釋/*
* 這是多行注釋
*//// <summary>
/// 這是XML文檔注釋,類似于Java的Javadoc
/// </summary>
小結
通過這個概覽,你可以看到C#和Java在語法上有很多相似之處。這意味著作為一個Java開發者,你可以相對輕松地過渡到C#。然而,C#也有其獨特的特性和語法糖,使得某些任務更加簡潔和高效。
在接下來的章節中,我們將深入探討C#的各個方面,包括它獨特的特性如屬性、事件、委托等。這些概念可能對Java開發者來說比較新,但它們是C#強大功能的關鍵所在。記住,學習一門新的語言不僅是學習語法,更是學習一種新的思維方式。讓我們繼續我們的C#學習之旅吧!
1.2 變量和數據類型
在C#中,變量和數據類型是編程的基礎。對于從Java轉過來的開發者來說,你會發現很多熟悉的概念,但C#也有一些獨特的特性。讓我們深入探討C#的變量和數據類型,并與Java進行比較。
變量聲明
C#和Java的變量聲明方式非常相似:
C#:
int age = 25;
string name = "Alice";
bool isStudent = true;
Java:
int age = 25;
String name = "Alice";
boolean isStudent = true;
主要區別在于:
- C#使用
string
(小寫),而Java使用String
(大寫)。 - C#使用
bool
,而Java使用boolean
。
基本數據類型
C#和Java都有類似的基本數據類型,但C#提供了更多的選擇:
C# 類型 | Java 類型 | 大小 | 范圍 |
---|---|---|---|
sbyte | byte | 8位 | -128 到 127 |
byte | - | 8位 | 0 到 255 |
short | short | 16位 | -32,768 到 32,767 |
ushort | - | 16位 | 0 到 65,535 |
int | int | 32位 | -2^31 到 2^31-1 |
uint | - | 32位 | 0 到 2^32-1 |
long | long | 64位 | -2^63 到 2^63-1 |
ulong | - | 64位 | 0 到 2^64-1 |
float | float | 32位 | ±1.5x 10^-45 到 ±3.4 x 10^38 |
double | double | 64位 | ±5.0 × 10^-324 到 ±1.7 × 10^308 |
decimal | - | 128位 | ±1.0 x 10^-28 到 ±7.9 x 10^28 |
char | char | 16位 | U+0000 到 U+FFFF |
bool | boolean | 8位 | true或 false |
注意C#提供了無符號整數類型(byte
, ushort
, uint
, ulong
)和decimal
類型,這些在Java中是沒有的。
值類型和引用類型
C#和Java都區分值類型和引用類型,但C#的處理更加靈活:
-
值類型(Value Types):
- 在C#中,所有的基本數據類型(int, float, bool等)和struct都是值類型。
- 值類型直接存儲它們的數據。
-
引用類型(Reference Types):
- 類(class)、接口(interface)、委托(delegate)和數組(array)是引用類型。
- 引用類型存儲對其數據(對象)的引用。
C#獨特之處:
- C#允許使用
struct
關鍵字創建自定義值類型。 - C#的
string
雖然是引用類型,但具有值類型的一些特性(如不可變性)。
可空類型
C#引入了可空類型的概念,這在Java中是沒有的:
int? nullableInt = null;
bool? nullableBool = null;
可空類型允許值類型也可以賦值為null
,這在處理數據庫或用戶輸入時非常有用。
var關鍵字
C#提供了var
關鍵字用于隱式類型聲明:
var x = 10; // 編譯器推斷x為int類型
var name = "Alice"; // 編譯器推斷name為string類型
Java從Java 10開始引入了類似的var
關鍵字,但使用范圍更受限制。
常量
C#使用const
關鍵字聲明常量:
const int MaxValue = 100;
const string AppName = "MyApp";
Java使用final
關鍵字:
final int MAX_VALUE = 100;
final String APP_NAME = "MyApp";
枚舉
C#和Java都支持枚舉,但C#的枚舉更加靈活:
C#:
enum Days
{Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday
}// 可以指定底層類型和值
enum Status : byte
{Active = 1,Inactive = 0,Suspended = 2
}
Java:
enum Days {MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
}
C#的枚舉可以指定底層類型,而Java的枚舉實際上是特殊的類。
類型轉換
C#提供了多種類型轉換方法:
-
隱式轉換:
int x = 10; long y = x; // 隱式轉換,不需要顯式轉換
-
顯式轉換(強制類型轉換):
doubled = 3.14; int i = (int)d; // 顯式轉換,可能會損失精度
-
使用Convert類:
string s = "123"; int i = Convert.ToInt32(s);
-
使用Parse方法:
string s = "3.14"; double d = double.Parse(s);
-
TryParse方法(安全轉換):
string s = "123"; int result; if (int.TryParse(s, out result)) {Console.WriteLine($"Converted value: {result}"); } else {Console.WriteLine("Conversion failed"); }
Java的類型轉換相對簡單一些,主要依賴于強制類型轉換和包裝類的方法。
小結
雖然C#和Java在變量和數據類型方面有很多相似之處,但C#提供了更多的選擇和靈活性。C#的可空類型、更豐富的基本數據類型、更靈活的枚舉和方便的類型轉換方法,都為開發者提供了更多的工具來處理各種數據場景。
作為一個從Java轉向C#的開發者,你會發現這些額外的特性可以讓你的代碼更加簡潔和表達力更強。在實際編程中,合理利用這些特性可以提高代碼的可讀性和性能。
在接下來的學習中,我們將深入探討C#的更多高級特性,如屬性、索引器、泛型等。這些概念將進一步展示C#相對于Java的獨特優勢。繼續保持學習的熱情,你會發現C#是一個功能豐富、富有表現力的語言!
1.3 運算符和表達式
C#的運算符和表達式與Java有很多相似之處,但也有一些獨特的特性。讓我們深入了解C#的運算符和表達式,并與Java進行比較。
算術運算符
C#和Java的算術運算符基本相同:
- 加法 (+)
- 減法 (-)
- 乘法 (*)
- 除法 (/)
- 取模 (%)
示例:
int a = 10, b = 3;
int sum = a + b; // 13
int difference = a - b; // 7
int product = a * b; // 30
int quotient = a / b; // 3 (整數除法)
int remainder = a % b; // 1
注意:C#和Java在整數除法時都會舍去小數部分,如果要得到精確結果,至少有一個操作數應該是浮點數。
賦值運算符
C#和Java的賦值運算符也基本相同:
- 簡單賦值 (=)
- 復合賦值 (+=, -=, *=, /=, %=)
C#特有的復合賦值運算符:
- ??= (空合并賦值運算符,C# 8.0引入)
示例:
int x = 5;
x += 3; // 等同于 x = x + 3
x -= 2; // 等同于 x = x - 2string name = null;
name ??= "John"; // 如果name為null,賦值為"John"
比較運算符
C#和Java的比較運算符完全相同:
- 等于 (==)
- 不等于 (!=)
- 大于 (>)
- 小于 (<)
- 大于等于 (>=)
- 小于等于 (<=)
示例:
int a = 5, b = 7;
bool isEqual = (a == b);// false
bool isNotEqual = (a !=b); // true
bool isGreater = (a > b);// false
bool isLess = (a < b); // true
bool isGreaterOrEqual = (a >= b); // false
bool isLessOrEqual = (a <= b);// true
邏輯運算符
C#和Java的邏輯運算符也是相同的:
- 邏輯與 (&&)
- 邏輯或 (||)
- 邏輯非 (!)
示例:
bool a = true, b = false;
bool andResult = a && b; // false
bool orResult = a || b; // true
bool notResult = !a; // false
位運算符
C#和Java的位運算符也基本相同:
- 按位與 (&)
- 按位或 (|)
- 按位異或 (^)
- 按位取反 (~)
- 左移 (<<)
- 右移 (>>)
C#特有的位運算符:
- 無符號右移 (>>>)
示例:
int a = 60;// 二進制: 0011 1100
int b = 13; // 二進制: 0000 1101int c = a & b; // 12(二進制: 0000 1100)
int d = a | b; // 61 (二進制: 0011 1101)
int e = a ^ b; // 49 (二進制: 0011 0001)
int f = ~a; // -61 (二進制: 1100 0011, 補碼表示)
int g = a << 2; // 240 (二進制: 1111 0000)
int h = a >> 2; // 15 (二進制: 0000 1111)
條件運算符
C#和Java都有三元條件運算符:
int a = 10, b = 20;
int max = (a > b) ? a : b; // 20
C#特有的條件運算符:
- 空合并運算符 (??)
- 空條件運算符(?.)
示例:
string name = null;
string displayName = name ?? "Guest"; // "Guest"class Person
{public string Name { get; set; }
}Person person = null;
int? nameLength = person?.Name?.Length; // null
類型測試運算符
C#提供了一些Java中沒有的類型測試運算符:
- is 運算符:檢查對象是否與特定類型兼容
- as 運算符:執行類型轉換,如果轉換失敗,返回null
示例:
object obj = "Hello";
if (obj is string)
{Console.WriteLine("obj is a string");
}string str = obj as string;
if (str != null)
{Console.WriteLine($"The string is: {str}");
}
Lambda 表達式
C#和Java都支持Lambda表達式,但語法略有不同:
C#:
Func<int, int> square = x => x * x;
int result = square(5); // 25
Java:
Function<Integer, Integer> square = x -> x * x;
int result = square.apply(5); // 25
空合并運算符(??)
C#特有的空合并運算符可以簡化處理可能為null的情況:
string name = null;
string displayName = name ?? "Guest"; // "Guest"
在Java中,你可能需要這樣寫:
String name = null;
String displayName = (name != null) ? name : "Guest";
表達式體成員 (Expression-bodied members)
C#允許使用更簡潔的語法來定義屬性和方法:
public class Circle
{public double Radius { get; set; }public double Diameter => Radius * 2;public double CalculateArea() => Math.PI * Radius * Radius;
}
這種語法在Java中是不存在的。
字符串插值
C#提供了非常方便的字符串插值語法:
string name = "Alice";
int age = 30;
string message = $"My name is {name} and I am {age} years old.";
Java在較新的版本中也引入了類似的功能,但語法不同:
String name = "Alice";
int age = 30;
String message = String.format("My name is %s and I am %d years old.", name, age);
小結
雖然C#和Java在運算符和表達式方面有很多相似之處,但C#提供了一些額外的特性,如空合并運算符、空條件運算符、表達式體成員等,這些可以讓代碼更加簡潔和表達力更強。
作為一個從Java轉向C#的開發者,你會發現這些額外的特性可以讓你的代碼更加優雅和易讀。在實際編程中,合理利用這些特性可以提高代碼質量和開發效率。
在接下來的學習中,我們將深入探討C#的更多高級特性,如LINQ、異步編程等。這些概念將進一步展示C#相對于Java的獨特優勢。繼續保持學習的熱情,你會發現C#是一個功能豐富、表達力強的語言!
1.4 控制流語句
控制流語句是編程語言的基本構建塊,用于控制程序的執行路徑。C#和Java在這方面非常相似,但C#也有一些獨特的特性。讓我們深入了解C#的控制流語句,并與Java進行比較。
if-else 語句
C#和Java的if-else語句幾乎完全相同:
int x = 10;
if (x > 5)
{Console.WriteLine("x is greater than 5");
}
else if (x < 5)
{Console.WriteLine("x is less than 5");
}
else
{Console.WriteLine("x is equal to 5");
}
C#特有的特性:
- 可空類型的使用:
int? x = null;
if (x.HasValue)
{Console.WriteLine($"x has a value: {x.Value}");
}
else
{Console.WriteLine("x is null");
}
- 模式匹配(C# 7.0+):
object obj = "Hello";
if (obj is string s)
{Console.WriteLine($"The string is: {s}");
}
switch 語句
C#的switch語句比Java的更加靈活:
int day = 3;
switch (day)
{case 1:Console.WriteLine("Monday");break;case 2:Console.WriteLine("Tuesday");break;case 3:case 4:case 5:Console.WriteLine("Midweek");break;default:Console.WriteLine("Weekend");break;
}
C#特有的特性:
- 模式匹配(C# 7.0+):
object obj = 123;
switch (obj)
{case int i when i > 100:Console.WriteLine($"Large integer: {i}");break;case string s:Console.WriteLine($"String value: {s}");break;case null:Console.WriteLine("Null value");break;default:Console.WriteLine("Unknown type");break;
}
- switch 表達式(C# 8.0+):
string GetDayType(int day) => day switch
{1 => "Monday",2 => "Tuesday",3 or 4 or 5 => "Midweek",_ => "Weekend"
};
循環語句
C#和Java的循環語句非常相似:
- for循環:
for (int i = 0; i < 5; i++)
{Console.WriteLine(i);
}
- while 循環:
int i = 0;
while (i < 5)
{Console.WriteLine(i);i++;
}
- do-while 循環:
int i = 0;
do
{Console.WriteLine(i);i++;
} while (i < 5);
- foreach 循環:
string[] fruits = { "apple", "banana", "cherry" };
foreach (string fruit in fruits)
{Console.WriteLine(fruit);
}
C#特有的特性:
- LINQ與foreach的結合:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
foreach (var num in numbers.Where(n => n % 2 == 0))
{Console.WriteLine(num);
}
跳轉語句
C#和Java都支持以下跳轉語句:
- break:跳出當前循環或switch語句
- continue:跳過當前循環的剩余部分,開始下一次迭代
- return:從方法中返回,并可選擇返回一個值
C#特有的跳轉語句:
- goto:雖然不推薦使用,但C#保留了goto語句
int i = 0;
start:if (i < 5){Console.WriteLine(i);i++;goto start;}
異常處理
C#和Java的異常處理機制非常相似:
try
{int result = 10 / 0;
}
catch (DivideByZeroException ex)
{Console.WriteLine($"Division by zero error: {ex.Message}");
}
catch (Exception ex)
{Console.WriteLine($"An error occurred: {ex.Message}");
}
finally
{Console.WriteLine("This always executes");
}
C#特有的特性:
- 異常過濾器(C# 6.0+):
try
{// 可能拋出異常的代碼
}
catch (Exception ex) when (ex.InnerException != null)
{Console.WriteLine($"Inner exception: {ex.InnerException.Message}");
}
- using 語句(簡化資源管理):
using (var file = new System.IO.StreamReader("file.txt"))
{string content = file.ReadToEnd();Console.WriteLine(content);
}
// file自動關閉
- using 聲明(C# 8.0+):
using var file = new System.IO.StreamReader("file.txt");
string content = file.ReadToEnd();
Console.WriteLine(content);
// file 在作用域結束時自動關閉
小結
雖然C#和Java在控制流語句方面有很多相似之處,但C#提供了一些額外的特性,如模式匹配、switch表達式、異常過濾器等,這些可以讓代碼更加簡潔和表達力更強。
作為一個從Java轉向C#的開發者,你會發現這些額外的特性可以讓你的代碼更加優雅和易讀。特別是模式匹配和switch表達式,它們可以大大簡化復雜的條件邏輯。
在實際編程中,合理利用這些特性可以提高代碼質量和開發效率。例如,使用模式匹配可以使類型檢查和轉換更加簡潔,使用switch表達式可以使復雜的條件判斷更加清晰。
在接下來的學習中,我們將深入探討C#的更多高級特性,如LINQ、異步編程等。這些概念將進一步展示C#相對于Java的獨特優勢。繼續保持學習的熱情,你會發現C#是一個功能豐富、表達力強的語言!
1.5 方法和參數
方法(在Java中稱為函數)是編程中最基本的代碼組織單元。C#和Java在方法定義和使用上有很多相似之處,但C#提供了一些額外的特性,使得方法定義和調用更加靈活。讓我們深入探討C#的方法和參數,并與Java進行比較。
方法定義
C#和Java的基本方法定義非常相似:
public int Add(int a, int b)
{return a + b;
}
Java中的等效代碼:
public int add(int a, int b) {return a + b;
}
主要區別:
- C#方法名通常使用PascalCase,而Java使用camelCase。
- C#支持方法重載,Java也支持。
參數傳遞
C#和Java都支持值傳遞和引用傳遞,但C#提供了更多選項:
- 值參數(默認):
public void IncrementValue(int x)
{x++; // 不影響原始值
}
- 引用參數(ref 關鍵字):
public void IncrementRef(ref int x)
{x++; // 修改原始值
}// 調用
int num = 5;
IncrementRef(ref num);
Console.WriteLine(num); // 輸出 6
Java沒有直接等效的引用參數,但可以通過包裝類或數組實現類似效果。
- 輸出參數(out 關鍵字):
public bool TryParse(string s, out int result)
{return int.TryParse(s, out result);
}// 調用
if (TryParse("123", out int number))
{Console.WriteLine($"Parsed number: {number}");
}
Java沒有直接等效的輸出參數。
- 參數數組(params 關鍵字):
public int Sum(params int[] numbers)
{return numbers.Sum();
}// 調用
int total = Sum(1, 2, 3, 4, 5);
Java使用可變參數(varargs)實現類似功能:
public int sum(int... numbers) {return Arrays.stream(numbers).sum();
}
方法重載
C#和Java都支持方法重載,允許在同一個類中定義多個同名但參數列表不同的方法:
public class Calculator
{public int Add(int a, int b){return a + b;}public double Add(double a, double b){return a + b;}
}
Java的方法重載與C#基本相同。
可選參數
C#支持可選參數,這在Java中直到最近才引入:
public void Greet(string name, string greeting = "Hello")
{Console.WriteLine($"{greeting}, {name}!");
}// 調用
Greet("Alice"); // 輸出: Hello, Alice!
Greet("Bob", "Hi"); // 輸出: Hi, Bob!
在Java中,你通常需要使用方法重載來實現類似功能:
public void greet(String name) {greet(name, "Hello");
}public void greet(String name, String greeting) {System.out.println(greeting + ", " + name + "!");
}
命名參數
C#支持命名參數,可以提高代碼的可讀性:
public void CreateUser(string name, int age, bool isAdmin = false)
{//方法實現
}// 調用
CreateUser(name: "Alice", age: 30, isAdmin: true);
CreateUser(age: 25, name: "Bob"); // 可以改變參數順序
Java不支持命名參數,但可以使用建造者模式來實現類似的效果。
表達式體方法
C# 6.0引入了表達式體方法,可以使簡單方法的定義更加簡潔:
public int Add(int a, int b) => a + b;public string GetFullName(string firstName, string lastName) => $"{firstName} {lastName}";
Java不支持這種語法糖。
本地函數
C# 7.0引入了本地函數,允許在方法內定義函數:
public int Factorial(int n)
{int LocalFactorial(int x){return x <= 1 ? 1 : x * LocalFactorial(x - 1);}return LocalFactorial(n);
}
Java不直接支持本地函數,但可以使用匿名內部類或lambda表達式來實現類似功能。
異步方法
C#對異步編程的支持非常強大,使用async和await關鍵字:
public async Task<string> FetchDataAsync(string url)
{using var client = new HttpClient();return await client.GetStringAsync(url);
}// 調用
string data = await FetchDataAsync("https://api.example.com");
Java也支持異步編程,但語法和使用方式與C#不同,通常使用CompletableFuture:
public CompletableFuture<String> fetchDataAsync(String url) {return CompletableFuture.supplyAsync(() -> {// 使用HttpClient獲取數據return "data";});
}// 調用
String data = fetchDataAsync("https://api.example.com").join();
擴展方法
C#允許你為現有類型添加新方法,而不需要修改原始類型的定義:
public static class StringExtensions
{public static bool IsNullOrEmpty(this string str){return string.IsNullOrEmpty(str);}
}// 使用
string name = "Alice";
bool isEmpty = name.IsNullOrEmpty();
Java不支持擴展方法,但可以使用靜態工具類來實現類似功能。
泛型方法
C#和Java都支持泛型方法,允許你編寫可以處理多種類型的方法:
public T Max<T>(T a, T b) where T : IComparable<T>
{return a.CompareTo(b) > 0 ? a : b;
}// 使用
int maxInt = Max(5, 10);
string maxString = Max("apple", "banana");
Java的泛型方法語法略有不同:
public <T extends Comparable<T>> T max(T a, T b) {return a.compareTo(b) > 0 ? a : b;
}
方法組合與函數式編程
C#對函數式編程有很好的支持,可以輕松組合和傳遞方法:
Func<int, int> square = x => x * x;
Func<int, int> addOne = x => x + 1;Func<int, int> squareThenAddOne = x => addOne(square(x));int result = squareThenAddOne(5); // 26
Java也支持函數式編程,但語法略有不同:
Function<Integer, Integer> square = x -> x * x;
Function<Integer, Integer> addOne = x -> x + 1;Function<Integer, Integer> squareThenAddOne = square.andThen(addOne);int result = squareThenAddOne.apply(5); // 26
小結
雖然C#和Java在方法和參數的基本概念上很相似,但C#提供了更多的特性和靈活性。C#的引用參數、輸出參數、命名參數、可選參數等特性可以讓方法定義和調用更加靈活和清晰。此外,C#的異步方法、擴展方法和表達式體方法等特性可以讓代碼更加簡潔和易讀。
作為一個從Java轉向C#的開發者,你會發現這些額外的特性可以大大提高你的編程效率和代碼質量。例如,命名參數和可選參數可以減少方法重載的需求,擴展方法可以讓你更容易地擴展現有類型的功能,而async/await則可以大大簡化異步編程的復雜性。
在實際編程中,合理利用這些特性可以讓你的代碼更加清晰、簡潔和易于維護。例如,使用命名參數可以提高代碼的可讀性,使用擴展方法可以使你的代碼更加模塊化,而使用異步方法可以提高應用程序的響應性。
隨著你對C#的深入學習,你會發現更多強大的特性和用法。保持學習和實踐的熱情,你將能夠充分利用C#的強大功能,成為一個高效的.NET開發者!
1.6 類和對象
類和對象是面向對象編程的核心概念,C#和Java在這方面有很多相似之處,但C#提供了一些額外的特性和語法糖,使得類的定義和使用更加靈活和簡潔。讓我們深入探討C#的類和對象,并與Java進行比較。
類的定義
C#和Java的基本類定義非常相似:
public class Person
{public string Name { get; set; }public int Age { get; set; }public void SayHello(){Console.WriteLine($"Hello, my name is {Name} and I'm {Age} years old.");}
}
Java中的等效代碼:
public class Person {private String name;private int age;public String getName() { return name; }public void setName(String name) { this.name = name; }public int getAge() { return age; }public void setAge(int age) { this.age = age; }public void sayHello() {System.out.println("Hello, my name is " + name + " and I'm " + age + " years old.");}
}
主要區別:
- C#使用屬性(Properties)代替了Java的getter和setter方法。
- C#的方法名通常使用PascalCase,而Java使用camelCase。
構造函數
C#和Java的構造函數定義類似:
public class Person
{public string Name { get; set; }public int Age { get; set; }public Person(string name, int age){Name = name;Age = age;}
}
C#特有的特性:
- 構造函數初始化器:
public class Employee : Person
{public string Company { get; set; }public Employee(string name, int age, string company) : base(name, age){Company = company;}
}
- 主構造函數(C# 9.0+):
public class Person(string name, int age)
{public string Name { get; set; } = name;public int Age { get; set; } = age;
}
屬性
C#的屬性是一個強大的特性,可以替代Java中的getter和setter方法:
public class Person
{private string name;public string Name{get { return name; }set { name = value; }}// 自動實現的屬性public int Age { get; set; }// 只讀屬性public bool IsAdult => Age >= 18;
}
C# 6.0+引入了更簡潔的屬性語法:
public class Person
{public string Name { get; set; } = "John Doe";public int Age { get; set; }public bool IsAdult => Age >= 18;
}
靜態成員
C#和Java都支持靜態成員:
public class MathHelper
{public static double PI = 3.14159;public static int Add(int a, int b){return a + b;}
}// 使用
double pi = MathHelper.PI;
int sum = MathHelper.Add(5, 3);
繼承
C#和Java的繼承語法略有不同:
public class Animal
{public virtual void MakeSound(){Console.WriteLine("The animal makes a sound");}
}public class Dog : Animal
{public override void MakeSound(){Console.WriteLine("The dog barks");}
}
Java中的等效代碼:
public class Animal {public void makeSound() {System.out.println("The animal makes a sound");}
}public class Dog extends Animal {@Overridepublic void makeSound() {System.out.println("The dog barks");}
}
主要區別:
- C#使用冒號(
:
)表示繼承,Java使用extends
關鍵字。 - C#需要使用
virtual
和override
關鍵字來實現方法重寫,Java只需要使用@Override
注解。
接口
C#和Java的接口定義類似,但C#允許接口包含默認實現(C# 8.0+):
public interface IAnimal
{void MakeSound();void Move() => Console.WriteLine("The animal moves");
}public class Dog : IAnimal
{public void MakeSound(){Console.WriteLine("The dog barks");}// Move方法使用接口的默認實現
}
Java8+也支持接口默認方法:
public interface Animal {void makeSound();default void move() {System.out.println("The animal moves");}
}
匿名類型
C#支持匿名類型,可以快速創建簡單的對象:
var person = new { Name = "Alice", Age = 30 };
Console.WriteLine($"{person.Name} is {person.Age} years old");
Java也支持匿名類,但主要用于創建接口或抽象類的匿名實現。
Record類型(C# 9.0+)
C# 9.0引入了Record類型,用于創建不可變的引用類型:
public record Person(string Name, int Age);var alice = new Person("Alice", 30);
var bob = alice with { Name = "Bob" }; // 創建一個新記錄,只修改Name
Java 14+引入了類似的Record特性:
public record Person(String name, int age) {}
對象初始化器
C#支持對象初始化器,可以在創建對象時直接設置屬性:
var person = new Person
{Name = "Alice",Age = 30
};
Java不直接支持這種語法,通常使用建造者模式來實現類似效果。
擴展方法
C#允許為現有類型添加新方法,而不需要修改原始類型:
public static class StringExtensions
{public static bool IsNullOrEmpty(this string str){return string.IsNullOrEmpty(str);}
}// 使用
string name = "Alice";
bool isEmpty = name.IsNullOrEmpty();
Java不支持擴展方法,但可以使用靜態工具類來實現類似功能,或者使用manifold 插件支持。
部分類(Partial Classes)
C#支持部分類,允許將一個類的定義分散到多個文件中:
// File1.cs
public partial class MyClass
{public void Method1() { }
}// File2.cs
public partial class MyClass
{public void Method2() { }
}
Java不支持部分類的概念。
索引器(Indexers)
C#支持索引器,允許類像數組一樣通過索引訪問:
public class StringCollection
{private List<string> items = new List<string>();public string this[int index]{get { return items[index]; }set { items[index] = value; }}
}// 使用
var collection = new StringCollection();
collection[0] = "Hello";
Console.WriteLine(collection[0]); // 輸出: Hello
Java沒有直接等效的特性,通常需要定義專門的get和set方法。
運算符重載
C#允許為自定義類型定義運算符的行為:
public struct Complex
{public double Real { get; set; }public double Imaginary { get; set; }public static Complex operator +(Complex c1, Complex c2){return new Complex { Real = c1.Real + c2.Real,Imaginary = c1.Imaginary + c2.Imaginary};}
}// 使用
var c1 = new Complex { Real = 1, Imaginary = 2 };
var c2 = new Complex { Real = 3, Imaginary = 4 };
var result = c1 + c2;
Java不支持運算符重載。
嵌套類型
C#和Java都支持嵌套類型,但C#的訪問規則更加靈活:
public class OuterClass
{private int outerField = 10;public class InnerClass{public void AccessOuterField(OuterClass outer){Console.WriteLine(outer.outerField);}}
}
在C#中,嵌套類可以訪問外部類的私有成員,而在Java中,內部類需要外部類的實例才能訪問其私有成員。
密封類和方法
C#使用sealed
關鍵字來防止類被繼承或方法被重寫:
public sealed class FinalClass
{// 這個類不能被繼承
}public class BaseClass
{public virtual void VirtualMethod() { }
}public class DerivedClass : BaseClass
{public sealed override void VirtualMethod() { }// 這個方法不能在子類中被重寫
}
Java使用final
關鍵字實現類似功能。
析構函數和終結器
C#支持析構函數(用于結構體)和終結器(用于類):
public class ResourceHolder
{private IntPtr resource;public ResourceHolder(){resource = AllocateResource();}~ResourceHolder(){FreeResource(resource);}private IntPtr AllocateResource() { /*分配資源 */ }private void FreeResource(IntPtr handle) { /* 釋放資源 */ }
}
Java不支持析構函數,但有類似的finalize()
方法(雖然不推薦使用)。
屬性訪問器的可訪問性
C#允許為屬性的getter 和 setter 單獨設置訪問級別:
public class Person
{public string Name { get; private set; }public Person(string name){Name = name;}
}
Java不支持這種細粒度的訪問控制。
init only setters(C#9.0+)
C#9.0引入了init only setters
,允許在對象初始化時設置屬性值,而之后這些屬性是不可變的:
var circle = new Circle { Radius = 5 };
var bigCircle = new Circle { Diameter = 20 };public class Circle
{private double _radius;public double Radius{get => _radius;init => _radius = value;}public double Diameter{get => 2 * _radius;init => _radius = value / 2;}
}
Java沒有直接等效的特性。
頂級語句(C# 9.0+)
從C# 9.0開始,可以在文件級別直接編寫代碼,而不需要顯式的Main方法:
Console.WriteLine("Hello, World!");
這個特性簡化了小型程序和腳本的編寫。Java仍然需要一個包含main方法的類。
模式匹配(C# 7.0+)
C#支持高級的模式匹配,可以在switch語句和is表達式中使用:
object obj = "Hello";if (obj is string s && s.Length > 5)
{Console.WriteLine($"It's a long string: {s}");
}var result = obj switch
{string s => $"It's a string: {s}",int i => $"It's an int: {i}",_ => "It's something else"
};
Java支持有限形式的模式匹配(從Java 14開始),但不如C#靈活。
小結
C#提供了豐富的特性來定義和使用類和對象,許多這些特性在Java中是沒有直接等價物的。這些特性不僅可以讓代碼更加簡潔和表達力更強,還可以提高開發效率和代碼質量。
作為一個從Java轉向C#的開發者,你會發現這些額外的特性可以讓你以新的方式思考和組織代碼。例如,索引器可以讓你的自定義類型像數組一樣使用,運算符重載可以讓你的類型更自然地參與數學運算,而模式匹配則可以簡化復雜的類型檢查和轉換邏輯。
在實際編程中,合理利用這些特性可以大大提高代碼的可讀性和可維護性。例如,使用屬性可以簡化數據封裝,使用記錄類型可以簡化不可變數據模型的創建,而使用模式匹配可以使復雜的條件邏輯更加清晰。
隨著你對C#的深入學習和實踐,你會發現更多強大的特性和用法。保持學習和實踐的熱情,你將能夠充分利用C#的強大功能,成為一個高效的.NET開發者!記住,編程語言只是工具,關鍵是要理解背后的概念和原理,并能夠在實際問題中靈活應用這些知識。
1.7 繼承和多態
繼承和多態是面向對象編程的核心概念,C#和Java在這方面有許多相似之處,但C#提供了一些額外的特性和語法,使得繼承和多態的實現更加靈活和強大。讓我們深入探討C#的繼承和多態,并與Java進行比較。
基本繼承
C#和Java的基本繼承語法略有不同:
C#:
public class Animal
{public string Name { get; set; }public virtual void MakeSound(){Console.WriteLine("The animal makes a sound");}
}public class Dog : Animal
{public override void MakeSound(){Console.WriteLine("The dog barks");}
}
Java:
public class Animal {private String name;public String getName() { return name; }public void setName(String name) { this.name = name; }public void makeSound() {System.out.println("The animal makes a sound");}
}public class Dog extends Animal {@Overridepublic void makeSound() {System.out.println("The dog barks");}
}
主要區別:
- C#使用冒號(
:
)表示繼承,Java使用extends
關鍵字。 - C#需要使用
virtual
和override
關鍵字來實現方法重寫,Java只需要使用@Override
注解。 - C#默認使用屬性(Properties)而不是getter和setter方法。
構造函數和繼承
在C#中,派生類的構造函數可以顯式調用基類的構造函數:
public class Animal
{public string Name { get; set; }public Animal(string name){Name = name;}
}public class Dog : Animal
{public string Breed { get; set; }public Dog(string name, string breed) : base(name){Breed = breed;}
}
Java中的等效代碼:
public class Animal {private String name;public Animal(String name) {this.name = name;}
}public class Dog extends Animal {private String breed;public Dog(String name, String breed) {super(name);this.breed = breed;}
}
密封類和方法
C#使用sealed
關鍵字來防止類被繼承或方法被重寫:
public sealed class FinalClass
{// 這個類不能被繼承
}public class BaseClass
{public virtual void VirtualMethod() { }
}public class DerivedClass : BaseClass
{public sealed override void VirtualMethod() { }// 這個方法不能在子類中被重寫
}
Java使用final
關鍵字實現類似功能:
public final class FinalClass {// 這個類不能被繼承
}public class BaseClass {public void virtualMethod() { }
}public class DerivedClass extends BaseClass {@Overridepublic final void virtualMethod() { }// 這個方法不能在子類中被重寫
}
抽象類和方法
C#和Java都支持抽象類和方法:
C#:
public abstract class Shape
{public abstract doubleCalculateArea();
}public class Circle : Shape
{public double Radius { get; set; }public override double CalculateArea(){return Math.PI * Radius * Radius;}
}
Java:
public abstract class Shape {public abstract double calculateArea();
}public class Circle extends Shape {private double radius;public void setRadius(double radius) { this.radius = radius; }@Overridepublic double calculateArea() {return Math.PI * radius * radius;}
}
接口
C#和Java都支持接口,但C# 8.0+允許接口定義默認實現:
C#:
public interface IDrawable
{void Draw();void Erase() => Console.WriteLine("Default erase behavior");
}public class Square : IDrawable
{public void Draw(){Console.WriteLine("Drawing a square");}// Erase方法使用默認實現
}
Java 8+也支持接口默認方法:
public interface Drawable {void draw();default void erase() {System.out.println("Default erase behavior");}
}public class Square implements Drawable {@Overridepublic void draw() {System.out.println("Drawing a square");}// erase方法使用默認實現
}
多重繼承
C#和Java都不支持類的多重繼承,但都允許一個類實現多個接口:
public interface IDrawable
{void Draw();
}public interface IResizable
{void Resize(int width, int height);
}public class Rectangle : IDrawable, IResizable
{public void Draw(){Console.WriteLine("Drawing a rectangle");}public void Resize(int width, int height){Console.WriteLine($"Resizing to {width}x{height}");}
}
泛型約束
C#支持泛型約束,可以限制泛型參數的類型:
public class GenericRepository<T> where T : class, new()
{public T CreateNew(){return new T();}
}
Java也支持泛型約束,但語法略有不同:
public class GenericRepository<T extends Object & Serializable> {public T createNew() throws InstantiationException, IllegalAccessException {return T.class.newInstance();}
}
協變和逆變
C#支持泛型接口和委托的協變和逆變:
public interface IEnumerable<out T> { /* ... */ }
public interface IComparer<in T> { /* ... */ }IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // 協變IComparer<object> objectComparer = /* ... */;
IComparer<string> stringComparer = objectComparer; // 逆變
Java也支持泛型的協變和逆變,但語法不同:
List<String> strings = new ArrayList<>();
List<? extends Object> objects = strings; // 協變Comparator<Object> objectComparator = /* ... */;
Comparator<? super String> stringComparator = objectComparator; // 逆變
隱藏基類成員
C#使用new
關鍵字來隱藏基類成員:
public class BaseClass
{public void Method(){Console.WriteLine("BaseClass.Method");}
}public class DerivedClass : BaseClass
{public new void Method(){Console.WriteLine("DerivedClass.Method");}
}
Java沒有直接等效的語法,但可以通過重新定義方法來實現類似效果。
基類訪問
C#使用base
關鍵字來訪問基類成員,類似于Java中的super
:
public class DerivedClass : BaseClass
{public override void Method(){base.Method(); // 調用基類方法Console.WriteLine("Additional behavior in derived class");}
}
構造函數鏈
C#允許在一個類中定義多個構造函數,并使用this
關鍵字鏈接它們:
public class Person
{public string Name { get; set; }public int Age { get; set; }public Person(string name) : this(name, 0){}public Person(string name, int age){Name = name;Age = age;}
}
顯式接口實現
C#允許顯式實現接口方法,這在實現多個具有相同方法簽名的接口時特別有用:
public interfaceIA
{void Method();
}public interface IB
{void Method();
}public class MyClass : IA, IB
{void IA.Method(){Console.WriteLine("IA.Method");}void IB.Method(){Console.WriteLine("IB.Method");}public void Method(){Console.WriteLine("MyClass.Method");}
}
Java不支持這種顯式接口實現。
抽象屬性
C#允許在抽象類中定義抽象屬性:
public abstract class Animal
{public abstract string Sound { get; }
}public class Dog : Animal
{public override string Sound => "Woof";
}
派生類中的新成員
C#允許在派生類中添加新的成員,而不需要特殊語法:
public class Animal
{public virtual void MakeSound() { }
}public class Dog : Animal
{public override void MakeSound(){Console.WriteLine("Woof");}public void Fetch() //新方法{Console.WriteLine("Dog is fetching");}
}
泛型繼承
C#支持泛型類的繼承,并允許在派生類中指定或保持開放泛型類型參數:
public class GenericBase<T>
{public T Data { get; set; }
}public class StringDerived : GenericBase<string>
{public void PrintUpperCase(){Console.WriteLine(Data.ToUpper());}
}public class GenericDerived<T> : GenericBase<T>
{public void PrintType(){Console.WriteLine(typeof(T).Name);}
}
接口的多重繼承雖然C#不支持類的多重繼承,但接口可以繼承多個接口:
public interfaceIA
{void MethodA();
}public interface IB
{void MethodB();
}public interface IC : IA, IB
{void MethodC();
}public class MyClass : IC
{public void MethodA() { }public void MethodB() { }public void MethodC() { }
}
繼承鏈中的構造函數調用順序
在C#中,當創建一個派生類的實例時,構造函數的調用順序是從最基礎的類開始,一直到最派生的類:
public class A
{public A() { Console.WriteLine("A"); }
}public class B : A
{public B() { Console.WriteLine("B"); }
}public class C : B
{public C() { Console.WriteLine("C"); }
}// 使用
var c = new C(); // 輸出: A B C
虛方法表(VMT)和動態分發
C#使用虛方法表來實現多態。當你調用一個虛方法時,實際調用的方法是在運行時根據對象的實際類型決定的:
Animal animal = new Dog();
animal.MakeSound(); // 調用Dog的MakeSound方法
這種機制稱為動態分發,它是多態的核心實現方式。
接口默認實現中的鉆石問題解決
當一個類實現多個具有相同默認方法的接口時,C#要求顯式實現該方法:
public interfaceIA
{void Method() => Console.WriteLine("IA.Method");
}public interface IB
{void Method() => Console.WriteLine("IB.Method");
}public class MyClass : IA, IB
{public void Method() // 必須實現,否則編譯錯誤{((IA)this).Method(); // 調用IA的默認實現// 或((IB)this).Method(); // 調用IB的默認實現}
}
最佳實踐
-
優先使用組合而不是繼承:繼承創建了強耦合,而組合更靈活。
-
遵循里氏替換原則(LSP):子類應該可以替換其基類,而不改變程序的正確性。
-
謹慎使用密封類和方法:雖然它們可以提高性能和安全性,但也限制了擴展性。
-
使用接口進行抽象:接口提供了更好的解耦和靈活性。
-
避免深層繼承層次:深層繼承可能導致復雜性和維護困難。
-
合理使用抽象類:當你需要在基類中提供一些實現時,使用抽象類而不是接口。
-
正確使用virtual和override關鍵字:這些關鍵字明確表達了你的意圖,使代碼更易理解和維護。
抽象類vs接口
雖然抽象類和接口都可以用來定義抽象類型,但它們有一些關鍵區別:
- 抽象類可以包含實現,接口(在C# 8.0之前)只能包含方法簽名。
- 一個類只能繼承一個抽象類,但可以實現多個接口。
- 抽象類可以有構造函數,接口不能。
- 抽象類可以包含字段,接口不能。
選擇使用抽象類還是接口取決于你的設計需求:
public abstract class Animal
{protected string name;public Animal(string name){this.name = name;}public abstract void MakeSound();
}public interface IMovable
{void Move();
}public class Dog : Animal, IMovable
{public Dog(string name) : base(name) { }public override void MakeSound(){Console.WriteLine("Woof!");}public void Move(){Console.WriteLine("Dog is running");}
}
多接口繼承與默認實現
C# 8.0引入的接口默認實現允許我們在不破壞現有代碼的情況下向接口添加新方法:
public interface ILogger
{void Log(string message);void LogError(string message) => Log($"ERROR: {message}");
}public class ConsoleLogger : ILogger
{public void Log(string message){Console.WriteLine(message);}// 可以選擇重寫或使用默認的LogError實現
}
泛型約束中的繼承
C#允許在泛型約束中使用繼承關系:
public class Animal { }
public class Dog : Animal { }public class Kennel<T> where T : Animal
{public void AddAnimal(T animal) { }
}var dogKennel = new Kennel<Dog>(); // 有效
// var intKennel = new Kennel<int>(); // 編譯錯誤
隱藏繼承成員
有時你可能想在派生類中隱藏基類的成員,而不是覆蓋它。C#使用new
關鍵字來實現這一點:
public class Base
{public virtual void Method(){Console.WriteLine("Base.Method");}
}public class Derived : Base
{public new void Method(){Console.WriteLine("Derived.Method");}
}Base b = new Derived();
b.Method(); // 輸出: Base.MethodDerived d = new Derived();
d.Method(); // 輸出: Derived.Method
協變和逆變在委托中的應用
C#支持在委托中使用協變和逆變:
delegate T Factory<out T>();
delegate void Action<in T>(T obj);class Animal { }
class Dog : Animal { }class Program
{static Dog CreateDog() => new Dog();static void HandleAnimal(Animal a) { }static void Main(){Factory<Dog> dogFactory = CreateDog;Factory<Animal> animalFactory = dogFactory; // 協變Action<Animal> animalHandler = HandleAnimal;Action<Dog> dogHandler = animalHandler; // 逆變}
}
虛擬屬性和索引器
C#允許將屬性和索引器聲明為虛擬的,這樣它們可以在派生類中被重寫:
public class Base
{public virtual int Property { get; set; }public virtual int this[int index]{get =>0;set { }}
}public class Derived : Base
{private int[] data = new int[10];public override int Property{get => base.Property;set => base.Property = value * 2;}public override int this[int index]{get => data[index];set => data[index] = value;}
}
抽象類中的密封方法
雖然看起來有點矛盾,但C#允許在抽象類中定義密封方法。這可以用來提供一個不能被重寫的實現:
public abstract class Base
{public abstract void AbstractMethod();public virtual void VirtualMethod() { }public sealed void SealedMethod() { }
}public class Derived : Base
{public override void AbstractMethod() { }public override void VirtualMethod() { } // 可以重寫//無法重寫SealedMethod
}
接口中的靜態成員
從C# 8.0開始,接口可以包含靜態成員,包括字段、方法、屬性等:
public interface IMyInterface
{static int MyProperty { get; set; }static void MyMethod() => Console.WriteLine("Hello from interface!");
}
繼承鏈中的構造函數和字段初始化順序
了解C#中構造函數和字段初始化的順序非常重要:
- 派生類的字段初始化器
- 派生類的構造函數
- 基類的字段初始化器
- 基類的構造函數
public class Base
{public int BaseField = 1;public Base(){Console.WriteLine($"Base constructor, BaseField = {BaseField}");}
}public class Derived : Base
{public int DerivedField = BaseField + 1;public Derived(){Console.WriteLine($"Derived constructor, DerivedField = {DerivedField}");}
}// 輸出:
// Base constructor, BaseField = 1
// Derived constructor, DerivedField = 2
使用接口作為類型參數
接口可以用作方法的類型參數,這提供了更大的靈活性:
public void ProcessItems<T>(IEnumerable<T> items) where T : IComparable<T>
{foreach (var item in items.OrderBy(i => i)){Console.WriteLine(item);}
}
小結
C#提供了豐富而靈活的繼承和多態機制,允許開發者創建復雜的類層次結構和接口設計。相比Java,C#在某些方面提供了更細粒度的控制和更多的語法糖,如顯式接口實現、抽象屬性等。
作為一個從Java轉向C#的開發者,你會發現這些特性可以幫助你創建更加清晰、靈活和可維護的代碼。例如,顯式接口實現可以解決方法名沖突問題,抽象屬性可以更優雅地定義必須由派生類實現的屬性。
然而,重要的是要記住,這些強大的特性也帶來了復雜性。在使用繼承和多態時,始終要考慮代碼的可維護性和可讀性。遵循SOLID原則,合理使用組合和繼承,避免創建過于復雜的繼承層次結構。
通過深入理解和合理使用這些特性,你可以充分利用C#的強大功能,創建出更加靈活、可擴展和易于維護的代碼。在實際項目中,根據具體需求和設計目標來選擇合適的特性和模式,將幫助你成為一個更出色的C#開發者。
當然,讓我們繼續深入探討C#中繼承和多態的一些高級概念和最佳實踐:
抽象工廠模式
抽象工廠模式是一種創建型設計模式,它使用繼承和多態來提供一種創建相關對象家族的方式,而無需指定它們的具體類。這在C#中可以優雅地實現:
public interface IButton { void Paint(); }
public interface ICheckbox { void Paint(); }public interface IGUIFactory
{IButton CreateButton();ICheckbox CreateCheckbox();
}public class WinButton : IButton
{public void Paint() => Console.WriteLine("Windows Button");
}public class MacButton : IButton
{public void Paint() => Console.WriteLine("Mac Button");
}public class WinCheckbox : ICheckbox
{public void Paint() => Console.WriteLine("Windows Checkbox");
}public class MacCheckbox : ICheckbox
{public void Paint() => Console.WriteLine("Mac Checkbox");
}public class WinFactory : IGUIFactory
{public IButton CreateButton() => new WinButton();public ICheckbox CreateCheckbox() => new WinCheckbox();
}public class MacFactory : IGUIFactory
{public IButton CreateButton() => new MacButton();public ICheckbox CreateCheckbox() => new MacCheckbox();
}// 使用
IGUIFactory factory = new WinFactory();
var button = factory.CreateButton();
button.Paint(); //輸出: Windows Button
模板方法模式
模板方法模式定義了一個算法的骨架,允許子類重新定義算法的某些步驟,而不改變算法的結構:
public abstract class DataProcessor
{public void ProcessData(){OpenFile();ExtractData();ParseData();AnalyzeData();SendReport();CloseFile();}protected abstract void ExtractData();protected abstract void ParseData();protected abstract void AnalyzeData();protected virtual void OpenFile(){Console.WriteLine("Opening file...");}protected virtual void SendReport(){Console.WriteLine("Sending report...");}protected virtual void CloseFile(){Console.WriteLine("Closing file...");}
}public class PDFProcessor : DataProcessor
{protected override void ExtractData(){Console.WriteLine("Extracting data from PDF...");}protected override void ParseData(){Console.WriteLine("Parsing PDF data...");}protected override void AnalyzeData(){Console.WriteLine("Analyzing PDF data...");}
}
策略模式
策略模式定義了一系列算法,并使它們可以互相替換。這個模式利用了多態性:
public interface ISortStrategy
{void Sort(List<int> data);
}public class BubbleSort : ISortStrategy
{public void Sort(List<int> data){Console.WriteLine("Performing bubble sort");// 實現冒泡排序}
}public class QuickSort : ISortStrategy
{public void Sort(List<int> data){Console.WriteLine("Performing quick sort");// 實現快速排序}
}public class Sorter
{private ISortStrategy _strategy;public Sorter(ISortStrategy strategy){_strategy = strategy;}public void SetStrategy(ISortStrategy strategy){_strategy = strategy;}public void SortData(List<int> data){_strategy.Sort(data);}
}// 使用
var sorter = new Sorter(new BubbleSort());
sorter.SortData(new List<int> { 3, 1, 4, 1, 5, 9});
sorter.SetStrategy(new QuickSort());
sorter.SortData(new List<int> { 3, 1, 4, 1, 5, 9 });
裝飾器模式
裝飾器模式允許你動態地給對象添加新的行為,而不改變其結構:
public interface ICoffee
{string GetDescription();double GetCost();
}public class SimpleCoffee : ICoffee
{public string GetDescription() => "Simple Coffee";public double GetCost() => 1;
}public abstract class CoffeeDecorator : ICoffee
{protected ICoffee _coffee;public CoffeeDecorator(ICoffee coffee){_coffee = coffee;}public virtual string GetDescription() => _coffee.GetDescription();public virtual double GetCost() => _coffee.GetCost();
}public class MilkDecorator : CoffeeDecorator
{public MilkDecorator(ICoffee coffee) : base(coffee) { }public override string GetDescription() => $"{base.GetDescription()}, Milk";public override double GetCost() => base.GetCost() + 0.5;
}public class SugarDecorator : CoffeeDecorator
{public SugarDecorator(ICoffee coffee) : base(coffee) { }public override string GetDescription() => $"{base.GetDescription()}, Sugar";public override double GetCost() => base.GetCost() + 0.2;
}// 使用
ICoffee coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
Console.WriteLine($"Description: {coffee.GetDescription()}");
Console.WriteLine($"Cost: ${coffee.GetCost()}");
使用協變和逆變優化接口設計
協變和逆變可以使得泛型接口更加靈活:
// 協變接口
public interface IProducer<out T>
{T Produce();
}// 逆變接口
public interface IConsumer<in T>
{void Consume(T item);
}public class Animal { }
public class Dog : Animal { }public class DogProducer : IProducer<Dog>
{public Dog Produce() => new Dog();
}public class AnimalConsumer : IConsumer<Animal>
{public void Consume(Animal animal) { }
}// 使用
IProducer<Animal> animalProducer = new DogProducer(); // 協變
IConsumer<Dog> dogConsumer = new AnimalConsumer(); // 逆變
使用接口隔離原則(ISP)
接口隔離原則建議將大接口分割成更小、更具體的接口:
// 不好的設計
public interface IWorker
{void Work();void Eat();void Sleep();
}// 更好的設計
public interface IWorkable
{void Work();
}public interface IEatable
{void Eat();
}public interface ISleepable
{void Sleep();
}public class Human : IWorkable, IEatable, ISleepable
{public void Work() { }public void Eat() { }public void Sleep() { }
}public class Robot : IWorkable
{public void Work() { }
}
使用組合優于繼承雖然繼承是一個強大的工具,但它也可能導致緊耦合。
在許多情況下,組合可能是一個更好的選擇:
// 使用繼承
public class Bird
{public virtual void Fly(){Console.WriteLine("Flying...");}
}public class Penguin : Bird
{public override void Fly(){throw new NotSupportedException("Penguins can't fly");}
}// 使用組合
public interface IFlyable
{void Fly();
}public class FlyingBehavior : IFlyable
{public void Fly(){Console.WriteLine("Flying...");}
}public class Bird
{private IFlyable _flyingBehavior;public Bird(IFlyable flyingBehavior){_flyingBehavior = flyingBehavior;}public void Fly(){_flyingBehavior.Fly();}
}public class Penguin : Bird
{public Penguin() : base(new NullFlyingBehavior()) { }
}public class NullFlyingBehavior : IFlyable
{public void Fly() { } // Do nothing
}
小結
這些高級概念和模式展示了如何在實際應用中利用C#的繼承和多態特性來創建靈活、可擴展的代碼。設計模式如抽象工廠、策略、裝飾器等,都是繼承和多態的具體應用,可以幫助你解決常見的設計問題。
同時,我們也看到了一些重要的設計原則,如接口隔離原則和組合優于繼承。這些原則可以幫助你創建更加模塊化、易于維護的代碼。
在實際開發中,選擇使用繼承還是組合,或者選擇哪種設計模式,都需要根據具體的問題和上下文來決定。重要的是要理解這些概念背后的原理,并能夠靈活地應用它們。
作為一個從Java轉向C#的開發者,你會發現這些概念在兩種語言中都是適用的,但C#提供了一些額外的特性(如顯式接口實現、擴展方法等)可以讓這些模式的實現更加優雅。
繼續深入學習和實踐這些概念,你將能夠設計出更加健壯、靈活的軟件系統,成為一個更優秀的C#開發者。
1.8 接口和抽象類
接口和抽象類都是C#中用于定義抽象類型的重要工具,但它們有不同的用途和特性。讓我們詳細探討這兩個概念,并比較它們與Java中對應概念的異同。
接口(Interfaces)
接口在C#中定義了一組方法、屬性、事件或索引器的簽名,但不提供實現。它們用于定義一個類應該具有的能力,而不關心這些能力如何實現。
C#中接口的基本語法:
public interface IAnimal
{string Name { get; set; }void MakeSound();
}public class Dog : IAnimal
{public string Name { get; set; }public void MakeSound(){Console.WriteLine("Woof!");}
}
C# 8.0中的接口新特性:
- 默認實現:
public interface IAnimal
{string Name { get; set; }void MakeSound();void Eat() => Console.WriteLine($"{Name} is eating.");
}
- 靜態成員:
public interface IAnimal
{static int Count { get; set; }static void IncrementCount() => Count++;
}
- 私有成員:
public interface IAnimal
{private static int count;public static int Count => count;static void IncrementCount() => count++;
}
與Java的比較:
Java8引入了接口默認方法,類似于C# 8.0的默認實現。但C#的接口更加靈活,允許靜態成員和私有成員。
抽象類(Abstract Classes)
抽象類是一種不能被實例化的類,用于定義其他類的共同特征。它可以包含抽象方法(沒有實現的方法)和具體方法(有實現的方法)。
C#中抽象類的基本語法:
public abstract class Animal
{public string Name { get; set; }public abstract void MakeSound();public virtual void Eat(){Console.WriteLine($"{Name} is eating.");}
}public class Dog : Animal
{public override void MakeSound(){Console.WriteLine("Woof!");}
}
與Java的比較:
C#和Java的抽象類概念非常相似。兩種語言中,抽象類都可以包含抽象方法和具體方法,都不能被實例化,都可以被其他類繼承。
接口 vs 抽象類
讓我們比較一下接口和抽象類的主要區別:
-
多重繼承:
- C#類可以實現多個接口,但只能繼承一個類(包括抽象類)。
- Java的情況相同。
-
成員類型:
- C# 8.0之前,接口只能包含方法、屬性、事件和索引器的簽名。
- C# 8.0及以后,接口可以包含默認實現、靜態成員和私有成員。
-抽象類可以包含字段、構造函數、析構函數,以及實現方法。
-
訪問修飾符:
- 接口成員默認是公共的,不能有私有成員(C# 8.0之前)。
- 抽象類可以有各種訪問修飾符的成員。
-
構造函數:
- 接口不能包含構造函數。
- 抽象類可以包含構造函數。
-
密封成員:
- 接口不能有密封成員。
- 抽象類可以包含密封方法,防止它們被重寫。
何時使用接口,何時使用抽象類?
-
使用接口當:
- 你想定義一個能力或行為,而不關心實現細節。
- 你需要多重繼承。
- 你想在不相關的類之間定義共同的行為。
-
使用抽象類當:
- 你想定義一個模板,其中包含一些共同的實現。
- 你需要在相關的類之間共享代碼。
- 你需要定義非公共的成員。
實際應用示例
讓我們看一個更復雜的例子,展示接口和抽象類的結合使用:
public interface IPayable
{decimalCalculatePay();
}public abstract class Employee : IPayable
{public string Name { get; set; }public int Id { get; set; }protected Employee(string name, int id){Name = name;Id = id;}public abstract decimal CalculatePay();public virtual void DisplayInfo(){Console.WriteLine($"Employee: {Name}, ID: {Id}");}
}public class FullTimeEmployee : Employee
{public decimal MonthlySalary { get; set; }public FullTimeEmployee(string name, int id, decimal monthlySalary) : base(name, id){MonthlySalary = monthlySalary;}public override decimal CalculatePay(){return MonthlySalary;}
}public class PartTimeEmployee : Employee
{public decimal HourlyRate { get; set; }public int HoursWorked { get; set; }public PartTimeEmployee(string name, int id, decimal hourlyRate) : base(name, id){HourlyRate = hourlyRate;}public override decimal CalculatePay(){return HourlyRate * HoursWorked;}public override void DisplayInfo(){base.DisplayInfo();Console.WriteLine($"Hourly Rate: {HourlyRate}");}
}public class Contractor : IPayable
{public string Name { get; set; }public decimal ContractAmount { get; set; }public Contractor(string name, decimal contractAmount){Name = name;ContractAmount = contractAmount;}public decimal CalculatePay(){return ContractAmount;}
}// 使用示例
public class PayrollSystem
{public void ProcessPayroll(List<IPayable> payables){foreach (var payable in payables){Console.WriteLine($"Paying {payable.CalculatePay():C}");if (payable is Employee employee){employee.DisplayInfo();}}}
}// 主程序
class Program
{static void Main(string[] args){var payables = new List<IPayable>{new FullTimeEmployee("Alice", 1, 5000m),new PartTimeEmployee("Bob", 2, 20m) { HoursWorked = 80 },new Contractor("Charlie", 3000m)};var payrollSystem = new PayrollSystem();payrollSystem.ProcessPayroll(payables);}
}
在這個例子中:
IPayable
接口定義了一個通用的支付計算方法。Employee
抽象類實現了IPayable
接口,并提供了一些共同的功能。FullTimeEmployee
和PartTimeEmployee
繼承自Employee
,實現了特定的薪資計算邏輯。Contractor
直接實現了IPayable
接口,因為它與雇員有不同的特征。PayrollSystem
利用多態性處理不同類型的可支付對象。
設計技巧
- 接口隔離原則:創建小而專注的接口,而不是一個大而全的接口。
public interface IWorkable
{void Work();
}public interface IEatable
{void Eat();
}public class Human : IWorkable, IEatable
{public void Work() { /* ... */ }public void Eat() { /* ... */ }
}public class Robot : IWorkable
{public void Work() { /* ... */ }
}
- 使用抽象類作為模板:
public abstract class ReportGenerator
{public void GenerateReport(){CollectData();FormatData();OutputReport();}protected abstract void CollectData();protected abstract void FormatData();protected virtual void OutputReport(){Console.WriteLine("Outputting report...");}
}public class PDFReportGenerator : ReportGenerator
{protected override void CollectData() { /* ... */ }protected override void FormatData() { /* ... */ }//使用基類的OutputReport
}
- 組合接口創建更復雜的契約:
public interface IReadable
{string Read();
}public interface IWritable
{void Write(string content);
}public interface IFile : IReadable, IWritable
{string Name { get; set; }
}public class TextFile : IFile
{public string Name { get; set; }public string Read() { /* ... */ }public void Write(string content) { /* ... */ }
}
最佳實踐
-
優先使用接口:接口提供了更好的靈活性和可測試性。
-
抽象類用于定義模板:當你有一組相關的類需要共享一些實現時,使用抽象類。
-
利用C# 8.0的新特性:默認實現可以幫助你在不破壞現有代碼的情況下擴展接口。
-
考慮可測試性:接口更容易模擬(mock),有利于單元測試。
-
避免過度抽象:只有當你確實需要抽象時才創建接口或抽象類。
6.遵循SOLID原則:特別是單一責任原則和接口隔離原則。
小結
接口和抽象類是C#中兩個強大的抽象工具,它們各有優勢和適用場景。接口提供了更大的靈活性和多重繼承的能力,而抽象類則允許你定義共同的實現和狀態。在設計中合理使用這兩種工具,可以創建出更加靈活、可維護和可擴展的代碼結構。
作為一個從Java轉向C#的開發者,你會發現這兩個概念在兩種語言中有很多相似之處。但C# 8.0引入的接口新特性,如默認實現和私有成員,為接口提供了更多的功能,使得接口和抽象類之間的界限變得更加模糊。這為代碼設計提供了更多的選擇和靈活性。
在實際開發中,根據具體需求選擇合適的抽象工具,并遵循良好的設計原則,將幫助你創建出高質量的C#代碼。隨著你對這些概念的深入理解和實踐,你將能夠更好地利用C#語言的特性,設計出優雅而強大的軟件架構。
很好,讓我們繼續深入探討接口和抽象類的高級應用,以及它們在現代C#開發中的最佳實踐。
高級應用場景
1. 依賴注入與接口
依賴注入是現代軟件開發中的一個重要概念,特別是在使用Asp.Net Core等框架時。接口在這里扮演著關鍵角色。
public interface IUserRepository
{Task<User> GetUserByIdAsync(int id);Task<IEnumerable<User>> GetAllUsersAsync();Task AddUserAsync(User user);
}public class UserRepository : IUserRepository
{private readonly ApplicationDbContext _context;public UserRepository(ApplicationDbContext context){_context = context;}public async Task<User> GetUserByIdAsync(int id){return await _context.Users.FindAsync(id);}public async Task<IEnumerable<User>> GetAllUsersAsync(){return await _context.Users.ToListAsync();}public async Task AddUserAsync(User user){await _context.Users.AddAsync(user);await _context.SaveChangesAsync();}
}// 在Startup.cs中注冊服務
public void ConfigureServices(IServiceCollection services)
{services.AddScoped<IUserRepository, UserRepository>();
}// 在控制器中使用
public class UserController :ControllerBase
{private readonly IUserRepository _userRepository;public UserController(IUserRepository userRepository){_userRepository = userRepository;}[HttpGet("{id}")]public async Task<ActionResult<User>> GetUser(int id){var user = await _userRepository.GetUserByIdAsync(id);if (user == null){return NotFound();}return user;}
}
這種方法允許我們輕松地替換實現,例如,我們可以創建一個模擬版本用于測試:
public class MockUserRepository : IUserRepository
{private readonly List<User> _users = new();public async Task<User> GetUserByIdAsync(int id){return await Task.FromResult(_users.FirstOrDefault(u => u.Id == id));}// 實現其他方法...
}// 在測試中使用
[Fact]
public async Task GetUser_ReturnsUser_WhenUserExists()
{// Arrangevar mockRepo = new MockUserRepository();var controller = new UserController(mockRepo);var testUser = new User { Id = 1, Name = "Test User" };await mockRepo.AddUserAsync(testUser);// Actvar result = await controller.GetUser(1);// Assertvar actionResult = Assert.IsType<ActionResult<User>>(result);var returnValue = Assert.IsType<User>(actionResult.Value);Assert.Equal(testUser.Id, returnValue.Id);Assert.Equal(testUser.Name, returnValue.Name);
}
2. 抽象類作為領域模型的基類
在領域驅動設計(DDD)中,抽象類常常用作實體或值對象的基類。
public abstract class Entity
{public int Id { get; protected set; }public override bool Equals(object obj){var other = obj as Entity;if (ReferenceEquals(other, null))return false;if (ReferenceEquals(this, other))return true;if (GetType() != other.GetType())return false;if (Id ==0 || other.Id == 0)return false;return Id == other.Id;}public static bool operator ==(Entity a, Entity b){if (ReferenceEquals(a, null) && ReferenceEquals(b, null))return true;if (ReferenceEquals(a, null) || ReferenceEquals(b, null))return false;return a.Equals(b);}public static bool operator !=(Entity a, Entity b){return !(a == b);}public override int GetHashCode(){return (GetType().ToString() + Id).GetHashCode();}
}public class User : Entity
{public string Name { get; set; }public string Email { get; set; }
}public class Product : Entity
{public string Name { get; set; }public decimal Price { get; set; }
}
這個抽象基類提供了通用的相等性比較邏輯,所有繼承自它的實體都會自動獲得這些功能。
3. 接口默認實現的高級應用
C# 8.0引入的接口默認實現可以用于創建可選功能或提供默認行為。
public interface ILogger
{void Log(string message);void LogError(string message) => Log($"ERROR: {message}");void LogWarning(string message) => Log($"WARNING: {message}");void LogInfo(string message) => Log($"INFO: {message}");
}public class ConsoleLogger : ILogger
{public void Log(string message){Console.WriteLine($"[{DateTime.Now}] {message}");}// 可以選擇性地重寫默認實現public void LogError(string message){Console.ForegroundColor = ConsoleColor.Red;Log($"ERROR: {message}");Console.ResetColor();}
}public class FileLogger : ILogger
{private readonly string _path;public FileLogger(string path){_path = path;}public void Log(string message){File.AppendAllText(_path, $"[{DateTime.Now}] {message}\n");}// 使用默認實現的其他方法
}
這種方法允許我們在接口中定義通用行為,同時保留了實現類專它的靈活性。
最佳實踐和設計模式
1. 策略模式
策略模式是一個典型的使用接口的設計模式:
public interface IPaymentStrategy
{void Pay(decimal amount);
}public class CreditCardPayment : IPaymentStrategy
{public void Pay(decimal amount){Console.WriteLine($"Paying {amount} using Credit Card");}
}public class PayPalPayment : IPaymentStrategy
{public void Pay(decimal amount){Console.WriteLine($"Paying {amount} using PayPal");}
}public class ShoppingCart
{private readonly IPaymentStrategy _paymentStrategy;public ShoppingCart(IPaymentStrategy paymentStrategy){_paymentStrategy = paymentStrategy;}public void Checkout(decimal amount){_paymentStrategy.Pay(amount);}
}// 使用
var cart = new ShoppingCart(new CreditCardPayment());
cart.Checkout(100.50m);cart = new ShoppingCart(new PayPalPayment());
cart.Checkout(200.75m);
2. 模板方法模式
模板方法模式是抽象類的一個經典應用:
public abstract class PizzaMaker
{public void MakePizza(){PrepareDough();AddSauce();AddToppings();Bake();Cut();}protected abstract void AddToppings();protected virtual void PrepareDough(){Console.WriteLine("Preparing pizza dough");}protected virtual void AddSauce(){Console.WriteLine("Adding tomato sauce");}protected virtual void Bake(){Console.WriteLine("Baking for 15 minutes at 200°C");}protected virtual void Cut(){Console.WriteLine("Cutting the pizza into 8 slices");}
}public class MargheritaPizza : PizzaMaker
{protected override void AddToppings(){Console.WriteLine("Adding mozzarella and basil");}
}public class PepperoniPizza : PizzaMaker
{protected override void AddToppings(){Console.WriteLine("Adding pepperoni and extra cheese");}protected override void Bake(){Console.WriteLine("Baking for 20 minutes at 180°C");}
}// 使用
var margherita = new MargheritaPizza();
margherita.MakePizza();var pepperoni = new PepperoniPizza();
pepperoni.MakePizza();
高級技巧
- 使用泛型約束與接口:
public interface IEntity
{int Id { get; set; }
}public interface IRepository<T> where T : class, IEntity
{T GetById(int id);void Add(T entity);void Update(T entity);void Delete(int id);
}public class GenericRepository<T> : IRepository<T> where T : class, IEntity
{private readonly DbContext _context;private readonly DbSet<T> _dbSet;public GenericRepository(DbContext context){_context = context;_dbSet = context.Set<T>();}public T GetById(int id){return _dbSet.Find(id);}public void Add(T entity){_dbSet.Add(entity);_context.SaveChanges();}public void Update(T entity){_dbSet.Attach(entity);_context.Entry(entity).State = EntityState.Modified;_context.SaveChanges();}public void Delete(int id){var entity = GetById(id);_dbSet.Remove(entity);_context.SaveChanges();}
}
- 使用接口進行多重繼承:
public interface IFlying
{void Fly();
}public interface ISwimming
{void Swim();
}public class Duck : IFlying, ISwimming
{public void Fly(){Console.WriteLine("Duck is flying");}public void Swim(){Console.WriteLine("Duck is swimming");}
}public class FlyingFish : IFlying, ISwimming
{public void Fly(){Console.WriteLine("Flying fish is gliding through the air");}public void Swim(){Console.WriteLine("Flying fish is swimming");}
}
- 使用顯式接口實現來解決方法名沖突:
public interface IWork
{void Start();
}public interface IEngine
{void Start();
}public class Car : IWork, IEngine
{void IWork.Start(){Console.WriteLine("Starting work");}void IEngine.Start(){Console.WriteLine("Starting engine");}public void Start(){Console.WriteLine("Car is starting");}
}// 使用
Car car = new Car();
car.Start(); // 輸出: Car is startingIWork work = car;
work.Start(); // 輸出: Starting workIEngine engine = car;
engine.Start(); // 輸出: Starting engine
結論
接口和抽象類是C#中強大的抽象化工具,它們在軟件設計中扮演著關鍵角色。通過正確使用這些工具,我們可以創建更加靈活、可維護和可擴展的代碼結構。
- 接口適用于定義對象的能力或行為,特別是在需要多重繼承或定義跨類型契約時。
- 抽象類適用于定義一組相關類的共同特征和行為,特別是在需要共享實現代碼時。
在實際開發中,你可能會發現同時使用這兩種工具的情況。例如,你可能會創建一個抽象基類來實現一個或多個接口,然后讓具體類繼承這個抽象基類。
記住,好的設計不僅僅是使用正確的工具,還包括遵循SOLID原則、設計模式和最佳實踐。隨著你在C#開發中積累更多經驗,你將能夠更加自如地在不同場景中選擇和應用這些抽象化工具。
最后,隨著C#語言的不斷發展(特別是C# 8.0引入的接口新特性),保持對語言新特性的了解和學習也很重要。這些新特性可能會為你提供更多的設計選擇,幫助你寫出更好的代碼。
1.9 委托和事件
委托是C#中的一種類型,它表示對具有特定參數列表和返回類型的方法的引用。事件則是基于委托的一種機制,用于對象之間的通信。讓我們深入了解這兩個概念。
1.9.1 委托(Delegates)
委托允許將方法作為參數傳遞,這為實現回調和事件處理提供了基礎。
基本語法
// 定義委托
public delegate void SimpleDelegate(string message);// 使用委托
public class DelegateDemo
{public void DisplayMessage(string message){Console.WriteLine($"Message: {message}");}public void Run(){SimpleDelegate del = DisplayMessage;del("Hello, Delegates!");}
}// 使用
var demo = new DelegateDemo();
demo.Run();// 輸出: Message: Hello, Delegates!
多播委托
委托可以指向多個方法,這被稱為多播委托。
public class MulticastDelegateDemo
{public static void Method1(string message){Console.WriteLine($"Method1: {message}");}public static void Method2(string message){Console.WriteLine($"Method2: {message}");}public void Run(){SimpleDelegate del = Method1;del += Method2; // 添加另一個方法del("Hello, Multicast!");}
}// 使用
var demo = new MulticastDelegateDemo();
demo.Run();
// 輸出:
// Method1: Hello, Multicast!
// Method2: Hello, Multicast!
匿名方法和Lambda 表達式
C# 2.0 引入了匿名方法,C# 3.0 進一步引入了 Lambda 表達式,它們都可以用來創建內聯委托。
public class AnonymousAndLambdaDemo
{public void Run(){// 匿名方法SimpleDelegate anonymousDelegate = delegate(string message){Console.WriteLine($"Anonymous: {message}");};// Lambda 表達式SimpleDelegate lambdaDelegate = (message) => Console.WriteLine($"Lambda: {message}");anonymousDelegate("Hello from anonymous method!");lambdaDelegate("Hello from lambda expression!");}
}
1.9.2 事件(Events)
事件提供了一種方式,使得一個類可以通知其他類發生了某些事情。事件使用委托來實現。
基本語法
public class Publisher
{// 定義一個委托類型public delegate void EventHandler(string message);// 聲明一個事件public event EventHandler SomethingHappened;public void DoSomething(){// 觸發事件SomethingHappened?.Invoke("Something just happened!");}
}public class Subscriber
{public void HandleEvent(string message){Console.WriteLine($"Event handled: {message}");}
}// 使用
var publisher = new Publisher();
var subscriber = new Subscriber();publisher.SomethingHappened += subscriber.HandleEvent;
publisher.DoSomething();// 輸出: Event handled: Something just happened!
標準事件模式
.NET Framework 定義了一個標準的事件模式,使用EventHandler
和 EventArgs
。
public class CustomEventArgs : EventArgs
{public string Message { get; set; }
}public class StandardEventPublisher
{public event EventHandler<CustomEventArgs> CustomEvent;protected virtual void OnCustomEvent(CustomEventArgs e){CustomEvent?.Invoke(this, e);}public void TriggerEvent(){OnCustomEvent(new CustomEventArgs { Message = "Custom event triggered" });}
}public class StandardEventSubscriber
{public void HandleCustomEvent(object sender, CustomEventArgs e){Console.WriteLine($"Event received: {e.Message}");}
}// 使用
var publisher = new StandardEventPublisher();
var subscriber = new StandardEventSubscriber();publisher.CustomEvent += subscriber.HandleCustomEvent;
publisher.TriggerEvent(); // 輸出: Event received: Custom event triggered
1.9.3 委托和事件的高級應用
1. 異步編程
委托在異步編程中扮演重要角色,盡管現在更常用的是 Task-based 異步模式。
public class AsyncDelegateDemo
{public delegate int AsyncCalculation(int x, int y);public static int Add(int x, int y){Thread.Sleep(1000); // 模擬耗時操作return x + y;}public async Task RunAsync(){AsyncCalculation del = Add;var result = await Task.Run(() => del(5, 3));Console.WriteLine($"Async result: {result}");}
}
2. LINQ 和函數式編程
委托是LINQ和函數式編程風格的基礎。
public class LinqDemo
{public void Run(){var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };var evenNumbers = numbers.Where(n => n % 2 == 0).Select(n => n * n).ToList();Console.WriteLine(string.Join(", ", evenNumbers));}
}
3. 事件聚合器
在復雜的應用程序中,可以使用事件聚合器模式來集中管理事件。
public class EventAggregator
{private Dictionary<Type, List<object>> eventHandlers = new Dictionary<Type, List<object>>();public void Subscribe<TEvent>(Action<TEvent> handler){var eventType = typeof(TEvent);if (!eventHandlers.ContainsKey(eventType)){eventHandlers[eventType] = new List<object>();}eventHandlers[eventType].Add(handler);}public void Publish<TEvent>(TEvent eventToPublish){var eventType = typeof(TEvent);if (eventHandlers.ContainsKey(eventType)){foreach (var handler in eventHandlers[eventType].Cast<Action<TEvent>>()){handler(eventToPublish);}}}
}// 使用
public class EventAggregatorDemo
{public class UserLoggedInEvent{public string Username { get; set; }}public void Run(){var aggregator = new EventAggregator();aggregator.Subscribe<UserLoggedInEvent>(e =>Console.WriteLine($"User logged in: {e.Username}"));aggregator.Publish(new UserLoggedInEvent { Username = "JohnDoe" });}
}
1.10.4 最佳實踐
- 使用
EventHandler
和EventHandler<TEventArgs>
作為事件委托類型。 - 總是檢查事件是否為null 再調用(使用
?.
操作符)。 - 考慮使用
System.Action
和System.Func
代替自定義委托類型。 - 在多線程環境中使用事件時要小心,考慮線程安全問題。
- 避免在循環中頻繁觸發事件,考慮批處理或節流。
// 好的實踐
public event EventHandler<CustomEventArgs> GoodEvent;protected virtual void OnGoodEvent(CustomEventArgs e)
{GoodEvent?.Invoke(this, e);
}// 避免的做法
public delegate void BadEventDelegate(int someParameter);
public event BadEventDelegate BadEvent;public void TriggerBadEvent()
{if (BadEvent != null){BadEvent(42); // 不安全,可能在檢查和調用之間變為null}
}
結論
委托和事件是C#中非常強大的特性,它們為實現靈活的、松耦合的代碼提供了基礎。委托允許方法作為參數傳遞,這為回調、異步編程和LINQ等功能提供了支持。事件則建立在委托的基礎上,提供了一種標準的、類型安全的方式來實現發布-訂閱模式。
通過使用委托和事件,你可以創建更加模塊化和可擴展的代碼。它們在圖形用戶界面編程、異步編程、事件驅動系統等多個領域都有廣泛應用。
然而,使用委托和事件也需要謹慎。過度使用可能導致代碼難以追蹤和調試。在設計系統時,應該仔細考慮何時使用委托和事件,以及如何最好地組織和管理它們。
隨著你在C#開發中積累更多經驗,你會發現委托和事件在各種場景中的巧妙應用。掌握這些概念將幫助你編寫更加靈活、高效的代碼,并更好地理解和利用.NET框架的許多高級特性。
1.10泛型
歡迎來到我們的C#泛型深度探索之旅!對于從Java轉向.NET Core的開發者來說,泛型可能是一個熟悉而又陌生的概念。雖然兩種語言都支持泛型,但C#的泛型實現在某些方面更加強大和靈活。讓我們一起來探討這些差異,并通過實際案例來加深理解。
1.10.1 泛型基礎
首先,讓我們回顧一下泛型的基本概念。泛型允許我們編寫可以處理多種數據類型的代碼,而不需要為每種類型都編寫重復的代碼。這不僅提高了代碼的復用性,也增強了類型安全性。
C#示例:
public class GenericList<T>
{private List<T> items = new();public void Add(T item) => items.Add(item);public T GetItem(int index) => items[index];
}// 使用示例
var intList = new GenericList<int>();
intList.Add(10);
Console.WriteLine(intList.GetItem(0)); // 輸出: 10var stringList = new GenericList<string>();
stringList.Add("Hello");
Console.WriteLine(stringList.GetItem(0)); // 輸出: Hello
Java示例:
public class GenericList<T> {private List<T> items = new ArrayList<>();public void add(T item) {items.add(item);}public T getItem(int index) {return items.get(index);}
}// 使用示例
GenericList<Integer> intList = new GenericList<>();
intList.add(10);
System.out.println(intList.getItem(0)); // 輸出: 10GenericList<String> stringList = new GenericList<>();
stringList.add("Hello");
System.out.println(stringList.getItem(0)); // 輸出: Hello
看起來很相似,對吧?但是,讓我們深入探討一下C#泛型的一些獨特特性。
1.10.2 泛型約束
C#允許我們對泛型類型參數添加約束,這在Java中是沒有直接對應的功能。
C#示例:
public class Calculator<T> where T : struct, IComparable<T>
{public T Max(T a, T b) => a.CompareTo(b) > 0 ? a : b;
}// 使用示例
var calc = new Calculator<int>();
Console.WriteLine(calc.Max(5, 10)); // 輸出: 10// 下面的代碼會導致編譯錯誤,因為string不是值類型
// var stringCalc = new Calculator<string>();
在這個例子中,我們限制了T必須是值類型(struct)并且實現了IComparable接口。
Java的近似實現:
Java沒有直接的泛型約束,但可以通過接口來實現類似的功能:
public class Calculator<T extends Comparable<T>> {public T max(T a, T b) {return a.compareTo(b) > 0 ? a : b;}
}// 使用示例
Calculator<Integer> calc = new Calculator<>();
System.out.println(calc.max(5, 10)); // 輸出: 10// 這是允許的,因為Java沒有值類型的概念
Calculator<String> stringCalc = new Calculator<>();
System.out.println(stringCalc.max("apple", "banana")); // 輸出: banana
注意,Java的泛型約束相對較弱,無法像C#那樣限制為值類型。
1.10.3 泛型協變和逆變
C#支持泛型接口和委托的協變和逆變,這為類型系統帶來了更大的靈活性。Java從1.5版本開始也支持協變,但范圍較小。
C#示例:
// 協變
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // 這是允許的// 逆變
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // 這是允許的// 同時使用協變和逆變
interface IConverter<in TInput, out TOutput>
{TOutput Convert(TInput input);
}class StringToIntConverter : IConverter<string, int>
{public int Convert(string input) => int.Parse(input);
}IConverter<object, int> converter = new StringToIntConverter();
int result = converter.Convert("42");
Console.WriteLine(result); // 輸出: 42
Java示例:
Java的泛型協變較為有限,主要通過通配符實現:
// 協變(只讀)
List<String> strings = new ArrayList<>();
List<? extends Object> objects = strings; // 這是允許的// Java不支持泛型方法的逆變
// 下面的代碼在Java中是不允許的:
// Consumer<Object> objectConsumer = obj -> System.out.println(obj);
// Consumer<String> stringConsumer = objectConsumer;// Java中沒有直接對應C#的in和out關鍵字的概念
interface Converter<T, R> {R convert(T input);
}class StringToIntConverter implements Converter<String, Integer> {@Overridepublic Integer convert(String input) {return Integer.parseInt(input);}
}// 在Java中,不能像C#那樣直接賦值
// Converter<Object, Integer> converter = new StringToIntConverter(); // 這在Java中是不允許的Converter<String, Integer> converter = new StringToIntConverter();
int result = converter.convert("42");
System.out.println(result); // 輸出: 42
1.10.4 泛型方法
C#和Java都支持泛型方法,但C#的語法更加簡潔。
C#示例:
public static class Utilities
{public static T[] CreateArray<T>(params T[] elements) => elements;public static void Swap<T>(ref T a, ref T b){T temp = a;a = b;b = temp;}
}// 使用示例
int[] numbers = Utilities.CreateArray(1, 2, 3);
Console.WriteLine(string.Join(", ", numbers)); // 輸出: 1, 2, 3int x = 5, y = 10;
Utilities.Swap(ref x, ref y);
Console.WriteLine($"x = {x}, y = {y}"); // 輸出: x = 10, y = 5
Java示例:
public class Utilities {@SafeVarargspublic static <T> T[] createArray(T... elements) {return elements;}public static <T> void swap(T[] arr, int i, int j) {T temp = arr[i];arr[i] = arr[j];arr[j] = temp;}
}// 使用示例
Integer[] numbers = Utilities.createArray(1, 2, 3);
System.out.println(Arrays.toString(numbers)); // 輸出: [1, 2, 3]Integer[] arr = {5, 10};
Utilities.swap(arr, 0, 1);
System.out.println("x = " + arr[0] + ", y = " + arr[1]); // 輸出: x = 10, y = 5
注意,Java不支持引用參數,所以我們不能像C#那樣直接交換兩個變量的值。
1.10.5 默認值和約束
C#允許我們為泛型類型參數指定默認值,這在Java中是不可能的。
C#示例:
public class DefaultValueExample<T> where T : struct
{public T GetDefaultValue() => default(T);
}// 使用示例
var example = new DefaultValueExample<int>();
Console.WriteLine(example.GetDefaultValue()); // 輸出: 0var dateExample = new DefaultValueExample<DateTime>();
Console.WriteLine(dateExample.GetDefaultValue()); // 輸出: 1/1/0001 12:00:00 AM
Java示例:
Java沒有直接對應的功能,但可以通過反射或其他方式模擬:
public class DefaultValueExample<T> {private final Class<T> type;public DefaultValueExample(Class<T> type) {this.type = type;}@SuppressWarnings("unchecked")public T getDefaultValue() {if (type.equals(int.class)) return (T) Integer.valueOf(0);if (type.equals(boolean.class)) return (T) Boolean.FALSE;// ... 其他基本類型的處理return null; // 對于引用類型,返回null}
}// 使用示例
DefaultValueExample<Integer> example = new DefaultValueExample<>(int.class);
System.out.println(example.getDefaultValue()); // 輸出: 0DefaultValueExample<Boolean> boolExample = new DefaultValueExample<>(boolean.class);
System.out.println(boolExample.getDefaultValue()); // 輸出: false
結論
通過這些示例,我們可以看到C#的泛型在某些方面比Java的更加強大和靈活。C#提供了更豐富的類型約束、協變和逆變支持,以及默認值處理等特性。這些特性使得C#在處理復雜的泛型場景時更加得心應手。
對于從Java轉向.NET Core的開發者,熟悉這些差異不僅能幫助你更好地理解和使用C#的泛型,還能讓你在設計API時充分利用C#的獨特優勢。記住,雖然基本概念相似,但細節上的差異可能會對代碼的結構和性能產生重大影響。
在實際開發中,你會發現C#的泛型能夠幫助你編寫出更加類型安全、更加靈活的代碼。特別是在處理集合、算法實現、依賴注入等場景時,C#的泛型優勢會更加明顯。
最后,不要忘記C#還有一些其他與泛型相關的高級特性,比如泛型協變和逆變在異步編程中的應用、泛型特化等。這些topics可能需要更深入的學習和實踐。繼續探索,你會發現C#泛型的更多精彩之處!
1.11 LINQ (Language Integrated Query)
歡迎來到LINQ的世界!對于從Java轉向.NET Core的開發者來說,LINQ可能是最令人興奮的特性之一。LINQ為C#帶來了強大的查詢能力,使得處理集合和數據變得異常簡單和優雅。讓我們深入探討LINQ,并與Java中的相似功能進行比較。
1.11.1LINQ基礎
LINQ允許我們使用類似SQL的語法直接在代碼中查詢數據,無論是內存中的集合、數據庫還是XML文檔。
C#示例:
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };// 查詢語法
var evenNumbers = from num in numberswhere num % 2 == 0select num;// 方法語法
var evenNumbersMethod = numbers.Where(num => num % 2 == 0);Console.WriteLine(string.Join(", ", evenNumbers)); // 輸出: 2, 4, 6, 8, 10
Java示例:
Java8引入的Stream API提供了類似的功能,但語法和能力有所不同:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);List<Integer> evenNumbers = numbers.stream().filter(num -> num % 2 == 0).collect(Collectors.toList());System.out.println(String.join(", ", evenNumbers.stream().map(Object::toString).collect(Collectors.toList())));
// 輸出: 2, 4, 6, 8, 10
1.11.2 復雜查詢
LINQ的強大之處在于它能夠輕松處理復雜的查詢,包括排序、分組和連接操作。
C#示例:
var people = new List<Person>
{new Person { Name = "Alice", Age = 25, City = "New York" },new Person { Name = "Bob", Age = 30, City = "London" },new Person { Name = "Charlie", Age = 35, City = "New York" },new Person { Name = "David", Age = 40, City = "London" }
};var query = from person in peoplewhere person.Age > 30orderby person.Namegroup person by person.City into cityGroupselect new{City = cityGroup.Key,Count = cityGroup.Count(),AverageAge = cityGroup.Average(p => p.Age)};foreach (var result in query)
{Console.WriteLine($"City: {result.City}, Count: {result.Count}, Average Age: {result.AverageAge}");
}
// 輸出:
// City: London, Count: 1, Average Age: 40
// City: New York, Count: 1, Average Age: 35
Java示例:
Java的Stream API也可以實現類似的功能,但語法會更加復雜:
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;class Person {String name;int age;String city;//構造函數、getter和setter省略
}List<Person> people = Arrays.asList(new Person("Alice", 25, "New York"),new Person("Bob", 30, "London"),new Person("Charlie", 35, "New York"),new Person("David", 40, "London")
);Map<String, List<Person>> groupedByCity = people.stream().filter(person -> person.age > 30).sorted((p1, p2) -> p1.name.compareTo(p2.name)).collect(Collectors.groupingBy(person -> person.city));groupedByCity.forEach((city, cityGroup) -> {double averageAge = cityGroup.stream().mapToInt(p -> p.age).average().orElse(0);System.out.printf("City: %s, Count: %d, Average Age: %.2f%n", city, cityGroup.size(), averageAge);
});
// 輸出:
// City: London, Count: 1, Average Age: 40.00
// City: New York, Count: 1, Average Age: 35.00
1.11.3 延遲執行
LINQ查詢默認是延遲執行的,這意味著查詢只在結果被實際使用時才執行。這與Java的Stream API類似。
C#示例:
var numbers = new List<int> { 1, 2, 3, 4, 5 };var query = numbers.Select(n => {Console.WriteLine($"Processing {n}");return n * 2;
});Console.WriteLine("Query defined.");
foreach (var num in query)
{Console.WriteLine($"Result: {num}");
}// 輸出:
// Query defined.
// Processing 1
// Result: 2
// Processing 2
// Result: 4
// Processing 3
// Result: 6
// Processing 4
// Result: 8
// Processing 5
// Result: 10
Java示例:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Stream;List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);Stream<Integer> stream = numbers.stream().map(n -> {System.out.println("Processing " + n);return n * 2;
});System.out.println("Stream defined.");
stream.forEach(num -> System.out.println("Result: " + num));// 輸出:
// Stream defined.
// Processing 1
// Result: 2
// Processing 2
// Result: 4
// Processing 3
// Result: 6
// Processing 4
// Result: 8
// Processing 5
// Result: 10
1.11.4 查詢提供者
LINQ的一個強大特性是它可以針對不同的數據源使用相同的查詢語法。例如,LINQ to SQL允許我們使用LINQ語法直接查詢數據庫。
C#示例(使用Entity Framework Core):
using (var context = new MyDbContext())
{var query = from user in context.Userswhere user.Age > 18orderby user.Nameselect new { user.Name, user.Age };foreach (var item in query){Console.WriteLine($"Name: {item.Name}, Age: {item.Age}");}
}
這個查詢會被轉換為SQL并在數據庫中執行。
Java示例:
Java沒有直接等價的功能。JPA(Java Persistence API)提供了類似的查詢能力,但語法不同:
EntityManager em = // 獲取EntityManager
TypedQuery<User> query = em.createQuery("SELECT NEW com.example.UserDTO(u.name, u.age) " +"FROM User u WHERE u.age >18 ORDER BY u.name", User.class);List<UserDTO> results = query.getResultList();
for (UserDTO user : results) {System.out.println("Name: " + user.getName() + ", Age: " + user.getAge());
}
1.11.5 LINQ的其他特性
LINQ還有許多其他強大的特性,例如:
- 即時查詢(使用ToList()、ToArray()等方法)
- 自定義查詢操作符
- 并行LINQ(PLINQ)用于并行查詢處理
C#示例(PLINQ):
var numbers = Enumerable.Range(1, 1000000);var evenSquares = numbers.AsParallel().Where(n => n % 2 == 0).Select(n => n * n);Console.WriteLine($"Count of even squares: {evenSquares.Count()}");
Java示例(并行流):
import java.util.stream.IntStream;long count = IntStream.range(1, 1000001).parallel().filter(n -> n % 2 == 0).mapToLong(n -> (long)n * n).count();System.out.println("Count of even squares: " + count);
結論
LINQ是C#中一個非常強大和靈活的特性,它大大簡化了數據查詢和處理的代碼。雖然Java8引入的Stream API提供了類似的功能,但LINQ在語法簡潔性和統一性方面仍然具有優勢。
對于從Java轉向.NET Core的開發者,掌握LINQ將極大地提高你的生產力。LINQ不僅可以用于內存中的集合,還可以無縫地應用于各種數據源,如數據庫和XML。
在實際開發中,你會發現LINQ在處理復雜數據查詢、數據轉換和數據分析時特別有用。它可以幫助你編寫更加簡潔、可讀性更強的代碼,同時保持良好的性能。
記住,雖然LINQ強大,但也要注意合理使用。過度復雜的LINQ查詢可能會影響代碼的可讀性和性能。在編寫LINQ查詢時,始終要考慮查詢的效率和可維護性。
最后,隨著你對LINQ的深入了解,你會發現它不僅僅是一個查詢工具,而是一種強大的編程范式,可以改變你思考和解決問題的方式。繼續探索LINQ的高級特性,你會發現更多令人興奮的可能性!
1.12 異步編程 (async/await)
在現代軟件開發中,異步編程已經成為提高應用程序性能和響應性的關鍵技術之一。C# 中的 async/await 語法提供了一種簡潔而強大的方式來處理異步操作,使得編寫非阻塞代碼變得更加容易。對于從Java 轉向 .NET Core 的開發者來說,理解 C# 中的異步編程模型及其與 Java 的差異是非常重要的。讓我們深入探討 C# 的異步編程,并與 Java 進行對比。
異步編程的基礎概念
在開始之前,我們需要理解幾個關鍵概念:
- 異步操作:不會立即完成的操作,通常涉及 I/O 或長時間運行的計算。
- 非阻塞:允許程序在等待異步操作完成時繼續執行其他代碼。
- Task:表示異步操作的對象。
- async/await:C# 中用于簡化異步編程的關鍵字。
C# 中的async/await
C# 的 async/await 模型提供了一種直觀的方式來編寫異步代碼,使其看起來像同步代碼。這大大簡化了異步編程,提高了代碼的可讀性和可維護性。
基本語法
public async Task<string> FetchDataAsync()
{// 模擬異步操作await Task.Delay(1000);return "Data fetched!";
}// 調用異步方法
public async Task UseDataAsync()
{string result = await FetchDataAsync();Console.WriteLine(result);
}
在這個例子中:
async
關鍵字標記方法為異步方法。Task<T>
表示異步操作,其中T
是返回值的類型。await
關鍵字用于等待異步操作完成,而不阻塞線程。
與 Java 的對比
Java 8 引入了 CompletableFuture 來處理異步操作,但語法和使用方式與 C# 的async/await 有很大不同。讓我們看一個 Java 的例子:
public CompletableFuture<String> fetchDataAsync() {return CompletableFuture.supplyAsync(() -> {try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}return "Data fetched!";});
}// 調用異步方法
public void useDataAsync() {fetchDataAsync().thenAccept(result -> System.out.println(result));
}
對比可以看出:
- C# 的 async/await 語法更加直觀,代碼結構更接近同步代碼。
- Java 需要使用 CompletableFuture 和 lambda 表達式,這可能導致代碼嵌套和復雜性增加。
- C# 的異常處理更自然,可以直接使用 try-catch,而 Java 通常需要在 CompletableFuture 中處理異常。
深入理解 Task 和 ValueTask
在 C# 的異步編程中,Task 和 ValueTask 是兩個核心概念,它們都用于表示異步操作,但有一些重要的區別。
Task
Task 是.NET 中表示異步操作的標準方式。它是一個引用類型,可以表示無返回值的操作(Task)或有返回值的操作(Task)。
public async Task DoSomethingAsync()
{await Task.Delay(1000);Console.WriteLine("Task completed");
}public async Task<int> CalculateAsync()
{await Task.Delay(1000);return 42;
}
Task 的優點:
- 可以表示復雜的異步操作。
- 支持取消、延續和異常處理。
- 可以輕松組合多個異步操作。
ValueTask
ValueTask 是一個較新的概念,設計用于優化性能,特別是在異步操作可能立即完成的情況下。它是一個值類型,可以避免在某些情況下分配額外的內存。
public ValueTask<int> GetValueAsync()
{if (cachedValue.HasValue){return new ValueTask<int>(cachedValue.Value);}return new ValueTask<int>(FetchValueAsync());
}private async Task<int> FetchValueAsync()
{await Task.Delay(1000);return 42;
}
ValueTask 的優點:
- 在同步完成的情況下可以避免內存分配。
- 適用于頻繁調用且經常同步完成的方法。
何時使用 ValueTask
- 當方法頻繁被調用,并且大部分情況下可以同步完成時。
- 當你需要優化性能,減少內存分配時。
注意事項
- ValueTask 不應該被多次使用,這可能導致不可預知的行為。
- Task 更適合長時間運行或復雜的異步操作。
異步編程最佳實踐
- 避免異步死鎖:不要在同步上下文中等待異步方法,特別是在 UI 線程中。
// 錯誤示范private void Button_Click(object sender, EventArgs e){var result = DoSomethingAsync().Result; // 可能導致死鎖}// 正確做法private async void Button_Click(object sender, EventArgs e){var result = await DoSomethingAsync();}
-
使用 ConfigureAwait(false):在不需要同步上下文的地方,使用 ConfigureAwait(false) 可以提高性能。
public async Task<string> FetchDataAsync() {var result = await httpClient.GetStringAsync(url).ConfigureAwait(false);return result.ToUpper(); }
-
正確處理異常:使用 try-catch 塊來處理異步方法中的異常。
public async Task ProcessDataAsync() {try{var data = await FetchDataAsync();await ProcessAsync(data);}catch (HttpRequestException ex){Console.WriteLine($"Failed to fetch data: {ex.Message}");}catch (Exception ex){Console.WriteLine($"An error occurred: {ex.Message}");} }
-
合理使用 Task.WhenAll 和 Task.WhenAny:當需要并行執行多個異步操作時,這些方法非常有用。
public async Task ProcessMultipleAsync() {var task1 = FetchDataAsync("url1");var task2 = FetchDataAsync("url2");var task3 = FetchDataAsync("url3");var results = await Task.WhenAll(task1, task2, task3);foreach (var result in results){Console.WriteLine(result);} }
-
避免不必要的異步:如果操作是快速完成的,使用同步方法可能更合適。
-
使用異步流:在C# 8.0 及以后版本中,可以使用異步流(IAsyncEnumerable)來處理異步數據序列。
public async IAsyncEnumerable<int> GenerateSequenceAsync() {for (int i = 0; i < 10; i++){await Task.Delay(100);yield return i;} }public async Task ConsumeSequenceAsync() {await foreach (var item in GenerateSequenceAsync()){Console.WriteLine(item);} }
結論
C# 的 async/await 模型為異步編程提供了一個強大而直觀的工具。相比 Java 的 CompletableFuture,C# 的方法更加簡潔和易于理解。Task 和 ValueTask 的引入進一步增強了異步編程的靈活性和性能。
對于從 Java 轉向 .NET Core 的開發者來說,掌握這些概念和最佳實踐將大大提高編寫高效、可維護的異步代碼的能力。記住,異步編程不僅僅是about使用正確的關鍵字,更是關于理解并發和異步操作的本質,以及如何在實際應用中最好地利用它們。
通過不斷練習和實踐,你將能夠充分利用 C# 異步編程的強大功能,創建響應更快、更高效的應用程序。
異步編程 - C# |Microsoft學習
異步編程模式 | Microsoft Learn
1.13 異常處理
異常處理是任何編程語言中的關鍵概念,對于從Java轉向.NET Core的開發者來說,理解兩種語言在異常處理上的異同點至關重要。雖然C#和Java在異常處理的基本概念上有很多相似之處,但C#提供了一些獨特的特性和語法糖,可以使異常處理更加靈活和強大。讓我們深入探討C#的異常處理機制,并與Java進行對比。
1.13.1 基本異常處理結構
C#和Java的基本異常處理結構非常相似,都使用try-catch-finally塊。
C#示例:
try
{// 可能拋出異常的代碼int result = 10 / 0;
}
catch (DivideByZeroException ex)
{Console.WriteLine($"除零錯誤:{ex.Message}");
}
catch (Exception ex)
{Console.WriteLine($"發生了一個錯誤:{ex.Message}");
}
finally
{Console.WriteLine("這里的代碼總是會執行");
}
Java示例:
try {// 可能拋出異常的代碼int result = 10 / 0;
} catch (ArithmeticException ex) {System.out.println("除零錯誤:" + ex.getMessage());
} catch (Exception ex) {System.out.println("發生了一個錯誤:" + ex.getMessage());
} finally {System.out.println("這里的代碼總是會執行");
}
主要區別:
- C#使用大括號{}來包圍try, catch和finally塊,而Java使用小括號()。
- C#的異常類型名稱略有不同,例如DivideByZeroException而不是ArithmeticException。
- C#使用$進行字符串插值,這是一個更現代和方便的字符串格式化方法。
1.13.2 異常過濾器(C# 6.0+)
C#引入了異常過濾器,這是Java中沒有的特性。異常過濾器允許你在catch塊中添加額外的條件。
try
{// 可能拋出異常的代碼
}
catch (Exception ex) when (ex.Message.Contains("specific error"))
{Console.WriteLine("捕獲到特定錯誤");
}
catch (Exception ex)
{Console.WriteLine($"其他錯誤:{ex.Message}");
}
這個特性在Java中是不存在的,Java需要在catch塊內部使用if語句來實現類似的功能。
1.13.3 使用when關鍵字進行模式匹配(C# 7.0+)
C# 7.0引入了模式匹配,可以在catch塊中使用when關鍵字結合模式匹配來處理異常。
try
{// 可能拋出異常的代碼
}
catch (Exception ex) when (ex is FileNotFoundException fnf)
{Console.WriteLine($"文件未找到:{fnf.FileName}");
}
catch (Exception ex) when (ex is UnauthorizedAccessException uae)
{Console.WriteLine($"訪問被拒絕:{uae.Message}");
}
這種方式比Java的instanceof檢查更加簡潔和強大。
1.13.4 拋出異常
C#和Java在拋出異常方面非常相似,都使用throw關鍵字。
C#示例:
if (someCondition)
{throw new ArgumentException("無效的參數");
}
Java示例:
if (someCondition) {throw new IllegalArgumentException("無效的參數");
}
主要區別在于異常類的名稱可能略有不同。
1.13.5 自定義異常
在C#和Java中創建自定義異常的方式也很相似,但C#提供了更多的構造函數選項。
C#示例:
public class CustomException : Exception
{public CustomException() { }public CustomException(string message) : base(message) { }public CustomException(string message, Exception inner) : base(message, inner) { }protected CustomException(System.Runtime.Serialization.SerializationInfo info,System.Runtime.Serialization.StreamingContext context) : base(info, context) { }
}
Java示例:
public class CustomException extends Exception {public CustomException() { }public CustomException(String message) { super(message); }public CustomException(String message, Throwable cause) { super(message, cause); }public CustomException(Throwable cause) { super(cause); }
}
C#的自定義異常類通常包括一個用于序列化的構造函數,這在Java中是不需要的。
1.13.6 異常處理中的性能考慮
C#和Java在異常處理的性能開銷上有相似之處,但C#提供了一些獨特的優化機會。
- 使用Nullable類型避免異常: C#的Nullable可以用來表示可能不存在的值,而不是拋出異常。
int? result = SomeMethod();if (result.HasValue){Console.WriteLine(result.Value);}else{Console.WriteLine("No value");}
-
使用TryParse模式: C#的許多內置類型提供TryParse方法,可以避免拋出異常。
if (int.TryParse(input, out int number)) {Console.WriteLine($"解析成功:{number}"); } else {Console.WriteLine("解析失敗"); }
-
異常過濾器的性能: C#的異常過濾器不會展開堆棧,這可能比在catch塊內部進行條件檢查更高效。
1.13.7 異步方法中的異常處理
C#在異步方法中處理異常時特別強大,這要歸功于async/await語法。
public async Task ProcessAsync()
{try{await SomeAsyncMethod();}catch (SpecificException ex){await HandleSpecificExceptionAsync(ex);}catch (Exception ex){await LogExceptionAsync(ex);throw;}
}
相比之下,Java的CompletableFuture需要使用不同的方法來處理異常:
public CompletableFuture<Void> processAsync() {return someAsyncMethod().exceptionally(ex -> {if (ex instanceof SpecificException) {return handleSpecificException((SpecificException) ex);} else {logException(ex);throw new CompletionException(ex);}});
}
C#的方法更加直觀,并且更容易與同步代碼保持一致的結構。
1.13.8 新的C# 8.0特性:Using聲明
C# 8.0引入了using聲明,這是一種簡化資源管理和異常處理的新方法。
public void ProcessFile(string path)
{using var file = new StreamReader(path);//文件會在方法結束時自動關閉// 即使發生異常也是如此
}
這比Java的try-with-resources語句更加簡潔。
結論
雖然C#和Java在異常處理的基本概念上有很多相似之處,但C#提供了一些獨特的特性,如異常過濾器、模式匹配和更簡潔的資源管理語法。這些特性使得C#的異常處理更加靈活和強大。
對于從Java轉向.NET Core的開發者來說,熟悉這些差異和新特性將有助于編寫更加健壯和高效的代碼。記住,好的異常處理不僅僅是捕獲和拋出異常,還包括合理使用異常、優化性能以及提高代碼的可讀性和可維護性。
在實際開發中,建議充分利用C#提供的這些特性,同時保持對性能影響的警惑。適當的異常處理策略可以顯著提高應用程序的穩定性和用戶體驗。繼續探索和實踐這些概念,你將能夠在.NET Core環境中更加自如地處理各種異常情況。
1.14 文件I/O操作
文件I/O操作是幾乎所有應用程序中的基本功能。對于從Java轉向.NET Core的開發者來說,了解兩個平臺在文件處理上的異同點非常重要。雖然基本概念相似,但.NET Core提供了一些獨特的特性和語法糖,使文件操作更加簡潔和高效。讓我們深入探討.NET Core的文件I/O操作,并與Java進行對比。
1.14.1 基本文件操作
讀取文件
C#示例:
string content = File.ReadAllText("path/to/file.txt");// 或者逐行讀取
string[] lines = File.ReadAllLines("path/to/file.txt");// 使用流讀取
using var stream = new StreamReader("path/to/file.txt");
string line;
while ((line = stream.ReadLine()) != null)
{Console.WriteLine(line);
}
Java示例:
String content = new String(Files.readAllBytes(Paths.get("path/to/file.txt")));// 或者逐行讀取
List<String> lines = Files.readAllLines(Paths.get("path/to/file.txt"));// 使用緩沖讀取器
try (BufferedReader reader = new BufferedReader(new FileReader("path/to/file.txt"))) {String line;while ((line = reader.readLine()) != null) {System.out.println(line);}
}
主要區別:
- C#的
File
類提供了更多便捷的靜態方法。 - C#使用
using
語句自動管理資源,而Java使用try-with-resources。 - C#的
StreamReader
類似于Java的BufferedReader
。
寫入文件
C#示例:
File.WriteAllText("path/to/file.txt", "Hello, World!");// 追加內容
File.AppendAllText("path/to/file.txt", "Additional content");// 使用流寫入
using var writer = new StreamWriter("path/to/file.txt");
writer.WriteLine("Hello, World!");
writer.WriteLine("Another line");
Java示例:
Files.write(Paths.get("path/to/file.txt"), "Hello, World!".getBytes());// 追加內容
Files.write(Paths.get("path/to/file.txt"), "Additional content".getBytes(), StandardOpenOption.APPEND);// 使用緩沖寫入器
try (BufferedWriter writer = new BufferedWriter(new FileWriter("path/to/file.txt"))) {writer.write("Hello, World!");writer.newLine();writer.write("Another line");
}
主要區別:
- C#的
File
類方法更加直觀。 - Java需要顯式指定字符編碼和打開選項。
- C#的
StreamWriter
類似于Java的BufferedWriter
。
1.14.2 文件和目錄操作
文件操作
C#示例:
// 檢查文件是否存在
bool exists = File.Exists("path/to/file.txt");// 復制文件
File.Copy("source.txt", "destination.txt", overwrite: true);// 移動文件
File.Move("oldpath.txt", "newpath.txt");// 刪除文件
File.Delete("path/to/file.txt");
Java示例:
// 檢查文件是否存在
boolean exists = Files.exists(Paths.get("path/to/file.txt"));// 復制文件
Files.copy(Paths.get("source.txt"), Paths.get("destination.txt"), StandardCopyOption.REPLACE_EXISTING);// 移動文件
Files.move(Paths.get("oldpath.txt"), Paths.get("newpath.txt"), StandardCopyOption.REPLACE_EXISTING);// 刪除文件
Files.delete(Paths.get("path/to/file.txt"));
主要區別:
- C#使用靜態
File
類方法,而Java使用Files
類的靜態方法。 - Java需要顯式指定復制和移動的選項。
目錄操作
C#示例:
// 創建目錄
Directory.CreateDirectory("path/to/new/directory");// 獲取文件列表
string[] files = Directory.GetFiles("path/to/directory");// 獲取子目錄列表
string[] subdirectories = Directory.GetDirectories("path/to/directory");// 刪除目錄
Directory.Delete("path/to/directory", recursive: true);
Java示例:
// 創建目錄
Files.createDirectories(Paths.get("path/to/new/directory"));// 獲取文件列表
try (Stream<Path> paths = Files.list(Paths.get("path/to/directory"))) {paths.filter(Files::isRegularFile).forEach(System.out::println);
}// 獲取子目錄列表
try (Stream<Path> paths = Files.list(Paths.get("path/to/directory"))) {paths.filter(Files::isDirectory).forEach(System.out::println);
}// 刪除目錄
Files.walk(Paths.get("path/to/directory")).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete);
主要區別:
- C#的
Directory
類提供了更直觀的方法。 - Java的目錄操作更加靈活,但可能需要更多代碼。
- C#刪除目錄可以通過一個方法調用完成,而Java需要遍歷刪除。
1.14.3 異步文件操作
.NET Core對異步文件操作的支持非常出色,這是相對于Java的一個顯著優勢。
C#示例:
public async Task ProcessFileAsync(string filePath)
{string content = await File.ReadAllTextAsync(filePath);// 處理內容await File.WriteAllTextAsync("output.txt", processedContent);
}
Java示例:
Java的文件I/O操作主要是同步的。雖然可以使用CompletableFuture
來模擬異步操作,但并不如C#原生支持那么方便:
public CompletableFuture<Void> processFileAsync(String filePath) {return CompletableFuture.supplyAsync(() -> {try {String content = new String(Files.readAllBytes(Paths.get(filePath)));// 處理內容Files.write(Paths.get("output.txt"), processedContent.getBytes());return null;} catch (IOException e) {throw new CompletionException(e);}});
}
主要區別:
- C#提供了原生的異步文件I/O方法。
- Java需要手動將同步操作包裝在
CompletableFuture
中。
1.14.4 文件流和內存流
文件流
C#示例:
using var fileStream = new FileStream("path/to/file.txt", FileMode.Open, FileAccess.Read);
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = await fileStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
{// 處理讀取的數據
}
Java示例:
try (FileInputStream fis = new FileInputStream("path/to/file.txt")) {byte[] buffer = new byte[1024];int bytesRead;while ((bytesRead = fis.read(buffer)) != -1) {// 處理讀取的數據}
}
內存流
C#示例:
using var memoryStream = new MemoryStream();
byte[] data = Encoding.UTF8.GetBytes("Hello, World!");
await memoryStream.WriteAsync(data, 0, data.Length);memoryStream.Position = 0;
string result = Encoding.UTF8.GetString(memoryStream.ToArray());
Java示例:
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] data = "Hello, World!".getBytes(StandardCharsets.UTF_8);
baos.write(data);String result = baos.toString(StandardCharsets.UTF_8);
主要區別:
- C#的
Stream
類提供了異步方法,而Java的流操作主要是同步的。 - C#的
MemoryStream
可以直接重置位置,而Java的ByteArrayOutputStream
需要轉換為ByteArrayInputStream
來重新讀取。
1.14.5 文件監視
.NET Core和Java都提供了文件系統監視功能,但實現方式略有不同。
C#示例:
using var watcher = new FileSystemWatcher("path/to/directory");
watcher.Created += OnFileCreated;
watcher.Changed += OnFileChanged;
watcher.Deleted += OnFileDeleted;
watcher.Renamed += OnFileRenamed;watcher.EnableRaisingEvents = true;// 事件處理方法
private static void OnFileCreated(object sender, FileSystemEventArgs e)
{Console.WriteLine($"File created: {e.FullPath}");
}
Java示例:
Path path = Paths.get("path/to/directory");
try (WatchService watchService = FileSystems.getDefault().newWatchService()) {path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE,StandardWatchEventKinds.ENTRY_MODIFY, StandardWatchEventKinds.ENTRY_DELETE);while (true) {WatchKey key = watchService.take();for (WatchEvent<?> event : key.pollEvents()) {System.out.println("Event kind: " + event.kind() + ". File affected: " + event.context() + ".");}key.reset();}
} catch (IOException | InterruptedException e) {e.printStackTrace();
}
主要區別:
- C#使用事件驅動模型,而Java使用輪詢模型。
- C#的實現更加簡潔和直觀。
- Java的實現需要顯式的循環和異常處理。
1.14.6 文件壓縮
.NET Core和Java都提供了文件壓縮功能,但API略有不同。
C#示例:
using System.IO.Compression;// 創建zip文件
using (var zipArchive = ZipFile.Open("archive.zip", ZipArchiveMode.Create))
{zipArchive.CreateEntryFromFile("file1.txt", "file1.txt");zipArchive.CreateEntryFromFile("file2.txt", "file2.txt");
}// 解壓zip文件
ZipFile.ExtractToDirectory("archive.zip", "extractPath");
Java示例:
import java.util.zip.*;// 創建zip文件
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream("archive.zip"))) {addToZipFile("file1.txt", zos);addToZipFile("file2.txt", zos);
}// 解壓zip文件
try (ZipInputStream zis = new ZipInputStream(new FileInputStream("archive.zip"))) {ZipEntry zipEntry = zis.getNextEntry();while (zipEntry != null) {String fileName = zipEntry.getName();File newFile = new File("extractPath/" + fileName);// 提取文件// ...zipEntry = zis.getNextEntry();}
}private static void addToZipFile(String fileName, ZipOutputStream zos) throws IOException {File file = new File(fileName);try (FileInputStream fis = new FileInputStream(file)) {ZipEntry zipEntry = new ZipEntry(fileName);zos.putNextEntry(zipEntry);byte[] bytes = new byte[1024];int length;while ((length = fis.read(bytes)) >= 0) {zos.write(bytes, 0, length);}zos.closeEntry();}
}
主要區別:
- C#提供了更高級的
ZipFile
類,使壓縮和解壓操作更加簡單。 - Java需要手動處理每個zip條目,代碼較為冗長。
- C#的API設計更加用戶友好,而Java的API提供了更底層的控制。
結論
雖然.NET Core和Java在文件I/O操作的基本概念上有許多相似之處,但.NET Core提供了一些獨特的特性和更簡潔的API,使得文件操作更加方便和高效。對于從Java轉向.NET Core的開發者來說,主要需要注意以下幾點:
-
靜態方法vs對象方法:C#更傾向于使用靜態方法(如
File
和Directory
類),而Java則更多地使用對象方法。 -
異步支持:.NET Core對異步文件I/O的原生支持是一個顯著優勢,可以更容易地編寫高性能的文件操作代碼。
-
資源管理:C#的
using
語句和Java的try-with-resources語句都提供了自動資源管理,但C#的語法更加簡潔。 -
API設計:總體而言,.NET Core的文件I/O API設計更加直觀和用戶友好,而Java的API則提供了更多的底層控制。
-
文件監視:C#的事件驅動模型比Java的輪詢模型更加簡潔和易用。
-
壓縮操作:C#提供了更高級的API來處理文件壓縮,使得操作更加簡單。
在實際開發中,熟悉這些差異將有助于更快地適應.NET Core環境,并充分利用其提供的特性來編寫高效、簡潔的文件I/O代碼。同時,.NET Core的跨平臺特性也使得文件操作代碼可以在不同的操作系統上運行,提供了更大的靈活性。
隨著技術的不斷發展,建議持續關注.NET Core在文件I/O方面的新特性和改進,以便在項目中充分利用這些優勢。通過實踐和深入學習,你將能夠在.NET Core環境中更加得心應手地處理各種文件操作任務。
文章部分內容來著AI編寫可能存在錯誤或過期內容,如果有問題請評論
.NET 新人入門技術QQ交流群:157210263