C# 集合类
本文档详细介绍了 C# 中常用的集合类,包括描述、初始化方法、常见操作和使用示例。
1. List<T>
描述
List<T>
是一个动态数组,允许存储特定类型的对象,提供了按索引访问元素、动态调整大小等功能。它是 C# 中最常用的泛型集合类。
初始化
- 空列表:
List<int> numbers = new List<int>();
- 带有初始容量的列表:
List<int> numbers = new List<int>(capacity: 10);
- 通过初始元素列表初始化:
List<int> numbers = new List<int> { 1, 2, 3 };
- 通过数组初始化:
List<int> numbers = new List<int>(new int[] { 1, 2, 3 });
常见操作
- Add(T item): 添加元素到列表末尾。
numbers.Add(4);
- Remove(T item): 移除指定的元素。
numbers.Remove(2);
- Insert(int index, T item): 在指定位置插入元素。
numbers.Insert(1, 10);
- Count: 获取列表中的元素数量。
int count = numbers.Count;
- Sort(): 对列表中的元素进行排序。
numbers.Sort();
使用示例
List<string> names = new List<string> { "Alice", "Bob", "Charlie" };
names.Add("Dave");
names.Remove("Bob");
names.Insert(1, "Eve");
Console.WriteLine(string.Join(", ", names)); // 输出: Alice, Eve, Charlie, Dave
2. Dictionary<TKey, TValue>
描述
Dictionary<TKey, TValue>
是一种键值对集合,允许通过键快速查找值。每个键必须是唯一的,值可以重复。
初始化
- 空字典:
Dictionary<int, string> employees = new Dictionary<int, string>();
- 通过初始键值对列表初始化:
var employees = new Dictionary<int, string> { { 1, "John" }, { 2, "Jane" }, { 3, "Alice" } };
常见操作
- Add(TKey key, TValue value): 添加键值对。
employees.Add(4, "Bob");
- Remove(TKey key): 移除指定键的元素。
employees.Remove(2);
- ContainsKey(TKey key): 检查字典是否包含指定键。
bool hasKey = employees.ContainsKey(1);
- TryGetValue(TKey key, out TValue value): 获取指定键的值,如果键存在返回
true
。if (employees.TryGetValue(3, out string name)) { Console.WriteLine(name); // 输出: Alice }
使用示例
var students = new Dictionary<int, string>
{
{ 101, "John" },
{ 102, "Jane" },
{ 103, "Tom" }
};
students[104] = "Jerry";
students.Remove(102);
Console.WriteLine(students[101]); // 输出: John
3. Queue<T>
描述
Queue<T>
实现了先进先出(FIFO)结构,适用于按顺序处理数据的场景。
初始化
- 空队列:
Queue<string> queue = new Queue<string>();
- 通过初始元素列表初始化:
Queue<string> queue = new Queue<string>(new[] { "Alice", "Bob", "Charlie" });
常见操作
- Enqueue(T item): 将元素添加到队列末尾。
queue.Enqueue("Dave");
- Dequeue(): 移除并返回队列中的第一个元素。
string first = queue.Dequeue();
- Peek(): 返回队列中的第一个元素但不移除。
string peek = queue.Peek();
- Count: 获取队列中元素的数量。
int count = queue.Count;
使用示例
Queue<int> numbers = new Queue<int>();
numbers.Enqueue(1);
numbers.Enqueue(2);
numbers.Enqueue(3);
int first = numbers.Dequeue(); // 输出: 1
int peek = numbers.Peek(); // 输出: 2
4. Stack<T>
描述
Stack<T>
实现了后进先出(LIFO)结构,适用于需要倒序处理数据的场景。
初始化
- 空栈:
Stack<int> stack = new Stack<int>();
- 通过初始元素列表初始化:
Stack<int> stack = new Stack<int>(new[] { 1, 2, 3 });
常见操作
- Push(T item): 将元素添加到栈顶。
stack.Push(4);
- Pop(): 移除并返回栈顶元素。
int top = stack.Pop();
- Peek(): 返回栈顶元素但不移除。
int peek = stack.Peek();
- Count: 获取栈中元素的数量。
int count = stack.Count;
使用示例
Stack<string> books = new Stack<string>();
books.Push("Book 1");
books.Push("Book 2");
string topBook = books.Pop(); // 输出: Book 2
string nextBook = books.Peek(); // 输出: Book 1
5. HashSet<T>
描述
HashSet<T>
是一种不允许重复元素的集合,基于哈希表实现,提供了高效的查找和集合操作。
初始化
- 空 HashSet:
HashSet<int> set = new HashSet<int>();
- 通过初始元素列表初始化:
HashSet<int> set = new HashSet<int> { 1, 2, 3 };
常见操作
- Add(T item): 添加元素到集合。
set.Add(4);
- Remove(T item): 移除指定的元素。
set.Remove(2);
- Contains(T item): 检查集合中是否包含指定的元素。
bool exists = set.Contains(1);
- UnionWith(IEnumerable<T> other): 对当前集合和指定集合进行并集运算。
set.UnionWith(new int[] { 4, 5, 6 });
- IntersectWith(IEnumerable<T> other): 对当前集合和指定集合进行交集运算。
set.IntersectWith(new int[] { 1, 4, 7 });
使用示例
HashSet<string> fruits = new HashSet<string> { "Apple", "Banana", "Orange" };
fruits.Add("Grape");
fruits.Remove("Banana");
bool hasApple = fruits.Contains("Apple"); // 输出: true
6. Concurrent Collections
描述
System.Collections.Concurrent
命名空间中的集合类(如 ConcurrentDictionary<TKey, TValue>
)提供了线程安全的集合操作,避免了手动锁机制的复杂性。
初始化
- 空 ConcurrentDictionary:
ConcurrentDictionary<int, string> concurrentDict = new ConcurrentDictionary<int, string>();
- 通过初始键值对列表初始化:
var concurrentDict = new ConcurrentDictionary<int, string>( new[] { new KeyValuePair<int, string>(1, "John"), new KeyValuePair<int, string>(2, "Jane") });
常见操作
- AddOrUpdate(TKey key, TValue value, Func<TKey, TValue, TValue> updateValueFactory): 添加或更新元素。
concurrentDict.AddOrUpdate(3, "Alice", (key, oldValue) => "Updated Alice");
- TryGetValue(TKey key, out TValue value): 尝试获取指定键的值。
if (concurrentDict.TryGetValue(2, out string name)) { Console.WriteLine(name);
Console.WriteLine(name); // 输出: Jane
}
- TryRemove(TKey key, out TValue value): 尝试移除指定键的元素。
if (concurrentDict.TryRemove(1, out string removedName)) { Console.WriteLine(removedName); // 输出: John }
使用示例
ConcurrentDictionary<int, string> concurrentDict = new ConcurrentDictionary<int, string>();
concurrentDict.TryAdd(1, "John");
concurrentDict.AddOrUpdate(2, "Jane", (key, oldValue) => "Updated Jane");
if (concurrentDict.TryGetValue(1, out string name))
{
Console.WriteLine(name); // 输出: John
}
C# 泛型
本文档涵盖了 C# 中的泛型(Generics),包括泛型的概念、泛型类型、泛型方法、常见使用场景及其优势,供查阅和参考。
1. 什么是泛型
概念
泛型(Generics)是 C# 中的一项强大功能,允许你定义类、接口和方法时,可以延迟指定数据类型,直到你在实例化时再指定具体的数据类型。使用泛型,可以创建更通用且类型安全的代码。
优势
- 类型安全: 泛型允许在编译时检查类型,避免了运行时类型转换的错误。
- 代码复用: 通过泛型,你可以编写一次代码,适用于不同的数据类型,提高了代码的复用性。
- 性能: 避免了装箱和拆箱操作,提高了程序的性能。
2. 泛型类
定义
泛型类是使用泛型参数定义的类。使用泛型类,你可以将类的定义与特定的数据类型解耦。
初始化
- 定义泛型类:
public class GenericList<T> { private T[] items; private int count; public GenericList(int capacity) { items = new T[capacity]; } public void Add(T item) { if (count < items.Length) { items[count++] = item; } } public T this[int index] => items[index]; }
- 实例化泛型类:
GenericList<int> intList = new GenericList<int>(10); intList.Add(1);
使用示例
GenericList<string> stringList = new GenericList<string>(5);
stringList.Add("Hello");
stringList.Add("World");
Console.WriteLine(stringList[0]); // 输出: Hello
3. 泛型方法
定义
泛型方法是定义在泛型类或普通类中的方法,可以在方法级别上使用泛型参数。这样的方法允许在定义时并不指定具体的数据类型。
初始化
-
定义泛型方法:
public T GetMax<T>(T x, T y) where T : IComparable<T> { return x.CompareTo(y) > 0 ? x : y; }
-
调用泛型方法:
int maxInt = GetMax<int>(10, 20); string maxString = GetMax<string>("Apple", "Banana");
使用示例
public class Utility
{
public static T GetMax<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
}
int maxInt = Utility.GetMax(100, 200);
string maxString = Utility.GetMax("Cat", "Dog");
Console.WriteLine(maxInt); // 输出: 200
Console.WriteLine(maxString); // 输出: Dog
4. 泛型接口
定义
泛型接口允许你定义与特定数据类型无关的接口,从而为实现该接口的类提供更大的灵活性。
初始化
-
定义泛型接口:
public interface IRepository<T> { void Add(T item); T GetById(int id); }
-
实现泛型接口:
public class Repository<T> : IRepository<T> { private List<T> items = new List<T>(); public void Add(T item) { items.Add(item); } public T GetById(int id) { return items[id]; } }
使用示例
IRepository<string> stringRepo = new Repository<string>();
stringRepo.Add("Hello");
Console.WriteLine(stringRepo.GetById(0)); // 输出: Hello
5. 泛型委托
定义
泛型委托允许你定义与特定类型无关的委托,使得相同的委托可以应用于不同的数据类型。
初始化
-
定义泛型委托:
public delegate T Transformer<T>(T arg);
-
使用泛型委托:
Transformer<int> square = x => x * x; int squareOfTwo = square(2); // 输出: 4
使用示例
Transformer<string> capitalize = s => s.ToUpper();
Console.WriteLine(capitalize("hello")); // 输出: HELLO
6. 泛型约束
定义
泛型约束用于限制泛型参数的类型,确保泛型参数满足某些特定的条件,例如必须实现某个接口或必须具有无参构造函数。
初始化
- 泛型约束示例:
public class GenericClass<T> where T : class, new() { public T CreateInstance() { return new T(); } }
使用示例
GenericClass<MyClass> myClassInstance = new GenericClass<MyClass>();
MyClass obj = myClassInstance.CreateInstance();
C# 委托、Func 和 Lambda 表达式
本文档详细介绍了 C# 中的委托(Delegates)、Func 委托和 Lambda 表达式的概念、定义方式及其在实际开发中的使用示例。
1. 什么是委托(Delegates)
概念
委托是 C# 中的一种类型,类似于函数指针。它允许将方法作为参数传递,并在运行时动态调用这些方法。委托是类型安全的,意味着方法签名必须与委托定义匹配。
定义
public delegate int Operation(int x, int y);
使用示例
public int Add(int x, int y)
{
return x + y;
}
public int Subtract(int x, int y)
{
return x - y;
}
Operation op = Add;
int result = op(3, 2); // 输出: 5
op = Subtract;
result = op(3, 2); // 输出: 1
多播委托
一个委托可以指向多个方法,并依次调用这些方法。
Operation op = Add;
op += Subtract;
int result = op(3, 2); // 最后一个方法的结果:1
2. Func 委托
概念
Func
是 .NET 框架中内置的委托类型,专门用于包含返回值的方法。Func
委托可以接受 0 到 16 个输入参数,并返回一个结果。最后一个泛型参数是返回类型,前面的泛型参数是输入参数类型。
定义
Func<int, int, int> operation;
使用示例
Func<int, int, int> add = (x, y) => x + y;
Func<int, int, int> subtract = (x, y) => x - y;
int sum = add(10, 20); // 输出: 30
int difference = subtract(20, 10); // 输出: 10
匿名方法与 Lambda 表达式
Func<int, int> square = delegate(int x) { return x * x; };
Console.WriteLine(square(5)); // 输出: 25
Func<int, int> cube = x => x * x * x;
Console.WriteLine(cube(3)); // 输出: 27
3. Lambda 表达式
概念
Lambda 表达式是一种匿名函数,使用更简洁的语法定义委托或表达式树。它可以包含表达式或语句块,允许将方法作为委托的实例传递。
语法
(parameters) => expression
使用示例
Func<int, int, int> multiply = (x, y) => x * y;
Console.WriteLine(multiply(3, 4)); // 输出: 12
4. 委托、Func 与 Lambda 表达式在实际开发中的应用
回调机制
委托经常用于实现回调机制,使方法可以在特定事件发生后执行。
public delegate void Notify(string message);
public void Process(Notify notifyDelegate)
{
// 处理过程
notifyDelegate("Processing completed.");
}
Process(message => Console.WriteLine(message)); // 输出: Processing completed.
事件处理
委托用于事件处理程序的定义,Func 和 Lambda 表达式可以简化事件处理程序的定义。
public event Func<int, int, int> OnCalculate;
public void RaiseEvent()
{
int result = OnCalculate?.Invoke(5, 10) ?? 0;
Console.WriteLine(result); // 输出: 15
}
OnCalculate += (x, y) => x + y;
RaiseEvent();
LINQ 查询
Lambda 表达式在 LINQ 查询中应用广泛,使得对集合的操作更加简洁。
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0).ToList();
Console.WriteLine(string.Join(", ", evenNumbers)); // 输出: 2, 4
C# 多线程、多进程与协程
本文档详细探讨了 C# 中的多线程(Multithreading)、多进程(Multiprocessing)与协程(Coroutines)的概念、详细使用方法及实际应用场景,帮助开发者在处理并发和异步任务时做出最佳选择。
1. 多线程(Multithreading)
概念
多线程是并发编程的一种实现方式,允许多个线程同时执行不同的代码片段。线程共享同一个进程的内存空间,但有各自的执行路径和堆栈。C# 中的线程通过 System.Threading.Thread
类实现。
初始化和使用
1.1 创建和启动线程
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
- 参数:
ThreadStart
是一个无参数且无返回值的委托。可以使用带参数的ParameterizedThreadStart
。 - 示例:
Thread thread = new Thread(() => Console.WriteLine("Thread is working")); thread.Start();
1.2 线程优先级设置
可以通过 Thread.Priority
属性设置线程的优先级:
thread.Priority = ThreadPriority.Highest;
1.3 线程同步
在线程之间共享数据时,需要使用同步机制来避免竞态条件和数据不一致。常用的同步机制包括 lock
语句、Monitor
类、Mutex
、Semaphore
和 AutoResetEvent
等。
private static readonly object _lock = new object();
public void SafeWork()
{
lock (_lock)
{
// 线程安全的代码块
}
}
1.4 使用线程池
线程池是管理线程的高级机制,它避免了频繁创建和销毁线程的开销,适合短时间任务和高并发场景。
ThreadPool.QueueUserWorkItem(state => DoWork());
- 线程池管理: 线程池自动管理线程的创建和销毁,简化了多线程编程。
1.5 线程取消和超时
在线程中处理长时间运行的任务时,可以通过设置标志位或使用 CancellationToken
来取消线程的执行。
CancellationTokenSource cts = new CancellationTokenSource();
Thread thread = new Thread(() => DoWork(cts.Token));
thread.Start();
cts.Cancel(); // 取消线程
使用场景
- I/O 密集型任务: 例如文件读写、网络请求,在这些场景中,多线程可以避免阻塞主线程,提高程序的响应速度。
- 并行计算: 对大量数据的并行处理,使用多线程可以显著缩短计算时间。
- 用户界面应用程序: 在 UI 线程中执行长时间操作可能导致界面卡顿,使用后台线程来处理这些操作可以保持界面的响应性。
2. 多进程(Multiprocessing)
概念
多进程指的是在多个独立的进程中同时执行任务,每个进程都有独立的内存空间,进程之间的数据隔离更彻底。C# 可以通过 System.Diagnostics.Process
类启动和管理进程。
初始化和使用
2.1 启动新进程
Process process = new Process();
process.StartInfo.FileName = "notepad.exe";
process.Start();
- 启动参数: 通过
ProcessStartInfo
可以指定启动参数和环境变量。 - 示例:
ProcessStartInfo startInfo = new ProcessStartInfo("cmd.exe", "/C dir"); startInfo.RedirectStandardOutput = true; Process process = Process.Start(startInfo); string output = process.StandardOutput.ReadToEnd(); Console.WriteLine(output);
2.2 进程间通信(IPC)
进程间通信常用的方法有管道(Pipes)、消息队列、共享内存和文件等。在 C# 中,可以通过 NamedPipeServerStream
和 NamedPipeClientStream
实现管道通信。
使用场景
- 隔离性强的任务: 例如运行不受信任的代码或需要高可靠性的任务,可以通过多进程来隔离失败对系统的影响。
- 并行执行独立任务: 在多核 CPU 上可以通过多进程并行执行多个计算密集型任务。
3. 协程(Coroutines)
概念
协程是一种比线程更轻量级的并发机制,通常用于实现非阻塞的异步操作。C# 中的协程通过 async
和 await
关键字实现,允许在任务执行过程中挂起并在稍后继续执行。
初始化和使用
3.1 定义和使用异步方法
public async Task DoAsyncWork()
{
await Task.Delay(1000); // 模拟异步操作
Console.WriteLine("Async work completed.");
}
await DoAsyncWork();
3.2 异步方法返回值
- 无返回值的异步方法: 使用
async void
定义,但一般用于事件处理程序中。 - 返回 Task 的异步方法: 使用
async Task
定义,用于无返回值的异步操作。 - 返回结果的异步方法: 使用
async Task<T>
定义,T
是返回的结果类型。public async Task<int> CalculateAsync(int value) { await Task.Delay(500); // 模拟异步操作 return value * value; }
3.3 并行执行多个异步任务
可以使用 Task.WhenAll
或 Task.WhenAny
并行执行多个异步任务。
var task1 = DoAsyncWork();
var task2 = CalculateAsync(10);
await Task.WhenAll(task1, task2);
3.4 异步流(Async Streams)
C# 8.0 引入了异步流,通过 IAsyncEnumerable<T>
和 await foreach
可以在异步方法中返回数据流。
public async IAsyncEnumerable<int> GenerateNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(500);
yield return i;
}
}
await foreach (var number in GenerateNumbersAsync())
{
Console.WriteLine(number);
}
使用场景
- I/O 密集型任务: 例如文件操作、网络请求和数据库查询等场景,异步编程能避免线程阻塞。
- 提高 UI 响应性: 在 UI 线程中使用异步操作可以防止界面卡顿,提升用户体验。
- 并发任务处理: 适用于需要同时处理多个任务的场景,特别是当任务之间没有依赖关系时。
4. 多线程、多进程与协程的比较
性能与资源消耗
- 多线程: 线程共享同一内存空间,资源消耗较低,适合 CPU 密集型任务,但需要处理线程同步问题。
- 多进程: 进程隔离性强,每个进程独立拥有内存空间,适合高可靠性任务,但资源消耗较大。
- 协程: 协程是最轻量级的并发模型,资源消耗最少,适合 I/O 密集型任务。
编程复杂度
- 多线程: 需要管理线程的生命周期和同步,编程复杂度较高。
- 多进程: 需要处理进程间通信和进程管理,编程复杂度中等。
- 协程: 使用
async
和await
使异步编程更加简单直观,编程复杂度最低。
使用场景总结
- 多线程: 适合需要并行处理的复杂计算任务或需要同时处理多个操作的情况。
- 多进程: 适合需要隔离任务的场景,如处理不可靠代码或需要分离资源的任务。
- 协程: 适合 I/O 密集型任务以及需要提高应用程序响应速度的场景。
C# 属性(Properties)
本文档详细介绍了 C# 中的属性(Properties),包括其概念、定义方式、使用示例和实际应用场景,帮助开发者更好地理解和使用属性来封装数据和控制访问。
1. 什么是属性(Properties)
概念
属性是 C# 中的一种成员,用于为类的字段提供受控的访问。属性允许在读取或写入字段时执行附加操作,并且通过封装字段,提高了数据的完整性和安全性。属性通常用于替代直接访问类的私有字段,提供更灵活和安全的访问机制。
优势
- 数据封装: 属性隐藏了类的内部实现细节,仅通过公共接口暴露字段。
- 控制访问: 属性可以添加逻辑来控制读取或写入操作,如验证数据、触发事件等。
- 提高代码可维护性: 通过属性的使用,类的字段可以在不影响外部代码的情况下进行修改。
2. 定义和使用属性
定义属性
2.1 自动属性(Auto-Implemented Properties)
自动属性简化了属性的定义,当不需要自定义逻辑时非常有用。
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
2.2 带逻辑的属性
带逻辑的属性允许在获取或设置属性值时执行额外的操作,例如验证数据或调整格式。
public class Person
{
private int _age;
public int Age
{
get { return _age; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException("Age cannot be negative.");
_age = value;
}
}
}
访问器(Accessors)
2.3 get 访问器
get
访问器用于读取属性值。
public int Age
{
get { return _age; }
}
2.4 set 访问器
set
访问器用于写入属性值。
public int Age
{
set
{
if (value < 0)
throw new ArgumentOutOfRangeException("Age cannot be negative.");
_age = value;
}
}
2.5 只读和只写属性
- 只读属性: 只有
get
访问器,没有set
访问器。public string Name { get; }
- 只写属性: 只有
set
访问器,没有get
访问器。public string Password { set { /* Set the password */ } }
使用示例
public class Product
{
private decimal _price;
public decimal Price
{
get { return _price; }
set
{
if (value < 0)
throw new ArgumentOutOfRangeException("Price cannot be negative.");
_price = value;
}
}
public string Name { get; set; } // 自动属性
}
在上述代码中,Price
属性在设置值时进行了验证,以确保价格不为负数,而 Name
属性则是一个简单的自动属性。
3. 属性的高级用法
3.1 计算属性(Calculated Properties)
计算属性通过计算得到,不需要存储在字段中。
public class Rectangle
{
public double Width { get; set; }
public double Height { get; set; }
public double Area
{
get { return Width * Height; }
}
}
3.2 静态属性(Static Properties)
静态属性属于类本身,而不是类的实例。可以使用 static
关键字定义。
public class Configuration
{
public static string ApplicationName { get; set; }
}
3.3 索引器属性(Indexer Properties)
索引器允许通过索引访问类的内部集合,类似于数组。
public class SampleCollection<T>
{
private T[] arr = new T[100];
public T this[int i]
{
get { return arr[i]; }
set { arr[i] = value; }
}
}
3.4 只读字段的初始化(C# 6.0及以上)
C# 6.0 及以上版本允许在定义只读属性时直接初始化值。
public string Name { get; } = "Default Name";
4. 实际应用场景
4.1 数据验证
属性允许在设置值时执行数据验证,从而确保对象状态的合法性。
4.2 数据计算
属性可以通过动态计算返回值,这样可以避免存储冗余数据,并确保数据的实时性。
4.3 配置设置
静态属性可以用于全局配置设置的存储和访问,例如应用程序的配置项。
4.4 简化代码
自动属性简化了类的实现,当不需要额外的逻辑时,避免了冗长的代码。
C# 模式匹配(Pattern Matching)
本文档详细介绍了 C# 中的模式匹配(Pattern Matching),包括其概念、使用场景和示例,帮助开发者充分利用这一功能来简化代码逻辑和提高代码可读性。
1. 什么是模式匹配(Pattern Matching)
概念
模式匹配是一种在代码中通过匹配对象类型或结构来执行不同操作的技术。C# 的模式匹配使得对数据类型和结构的检查变得更加简洁和直观,尤其在处理复杂条件分支时尤为有用。
模式匹配主要用于 switch
语句和 is
表达式中,通过检测对象的类型、值或结构,来执行特定的代码逻辑。
优势
- 代码简洁性: 模式匹配可以简化
if-else
语句,减少冗余代码。 - 类型安全: 提供了类型检查和转换的简洁语法,避免手动类型转换的潜在错误。
- 增强可读性: 通过直观的模式匹配表达,代码逻辑更加清晰易懂。
2. 基本使用
2.1 is
表达式的模式匹配
is
表达式不仅可以检查对象是否为某一类型,还可以将其转换为该类型。
示例
public void Process(object obj)
{
if (obj is string s)
{
Console.WriteLine($"The string is: {s}");
}
}
在上述代码中,is
表达式不仅检查 obj
是否是 string
类型,还将 obj
转换为 string
类型并赋值给变量 s
。
2.2 switch
语句中的模式匹配
C# 7.0 及以上版本的 switch
语句引入了模式匹配功能,可以直接在 case
子句中进行类型匹配。
示例
public void ProcessShape(object shape)
{
switch (shape)
{
case Circle c:
Console.WriteLine($"Circle with radius: {c.Radius}");
break;
case Rectangle r when r.Length == r.Width:
Console.WriteLine($"Square with side length: {r.Length}");
break;
case Rectangle r:
Console.WriteLine($"Rectangle with length: {r.Length} and width: {r.Width}");
break;
case null:
Console.WriteLine("Shape is null");
break;
default:
Console.WriteLine("Unknown shape");
break;
}
}
在这个例子中,switch
语句不仅匹配类型,还通过 when
子句引入了额外的条件判断。
2.3 常量模式匹配
常量模式允许你检查对象是否与某个特定的常量值相等。
示例
public string GetColorName(int colorCode)
{
return colorCode switch
{
1 => "Red",
2 => "Green",
3 => "Blue",
_ => "Unknown",
};
}
在这个例子中,switch
表达式使用常量模式来匹配整数值,并返回对应的字符串。
3. 高级模式匹配
3.1 元组模式匹配
元组模式匹配允许你匹配多个值的组合情况。
示例
public string DescribePoint(int x, int y)
{
return (x, y) switch
{
(0, 0) => "Origin",
(_, 0) => "On the X axis",
(0, _) => "On the Y axis",
_ => "Somewhere else",
};
}
3.2 递归模式匹配
递归模式匹配允许你在匹配对象时,对其内部的属性或字段进一步进行模式匹配。
示例
public string DescribeShape(object shape)
{
return shape switch
{
Rectangle { Length: var l, Width: var w } when l == w => "Square",
Rectangle { Length: var l, Width: var w } => $"Rectangle with length {l} and width {w}",
Circle { Radius: var r } => $"Circle with radius {r}",
_ => "Unknown shape",
};
}
在此示例中,模式匹配不仅匹配 Rectangle
对象,还进一步匹配了其 Length
和 Width
属性。
4. 使用场景
4.1 复杂条件判断
模式匹配可以简化复杂的 if-else
逻辑,使代码更具可读性。例如,在处理不同类型的输入或不同状态的对象时,模式匹配能有效减少嵌套结构。
4.2 数据解构
通过模式匹配,开发者可以直接从对象中提取所需的数据,而无需手动拆解对象的结构。
4.3 类型检查与转换
在需要对对象进行类型检查并立即转换为目标类型的场景中,模式匹配提供了更优雅的解决方案。
4.4 状态机实现
在状态机的实现中,模式匹配可以用于根据当前状态和输入条件,简洁地处理状态转换逻辑。
C# 异常处理
本文档详细介绍了 C# 中的异常处理机制,包括其概念、使用方法、最佳实践及常见的应用场景。异常处理在编写健壮和可靠的代码中至关重要,了解其细节有助于提高代码的可维护性和稳定性。
1. 什么是异常(Exception)
概念
异常是程序运行过程中发生的非预期事件或错误,它会导致程序的正常执行流程中断。在 C# 中,异常是通过 System.Exception
类及其派生类来表示的。当异常发生时,程序会生成异常对象并抛出该异常。
异常分类
- 系统异常: 由 .NET 运行时抛出的异常,例如
NullReferenceException
、IndexOutOfRangeException
等。 - 应用程序异常: 由应用程序逻辑抛出的异常,例如自定义异常类或使用
throw
关键字显式抛出的异常。
2. 异常处理结构
2.1 try-catch 语句
try-catch
语句用于捕获和处理异常。try
块包含可能抛出异常的代码,而 catch
块用于捕获异常并处理。
示例
try
{
int[] numbers = { 1, 2, 3 };
int number = numbers[5]; // 这里会抛出 IndexOutOfRangeException
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine($"Exception caught: {ex.Message}");
}
2.2 多个 catch 块
你可以为一个 try
块定义多个 catch
块,每个 catch
块处理不同类型的异常。C# 8.0 之后,支持使用模式匹配来处理异常。
示例
try
{
// 可能抛出多种类型的异常的代码
}
catch (NullReferenceException ex)
{
Console.WriteLine("NullReferenceException caught.");
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("IndexOutOfRangeException caught.");
}
catch (Exception ex)
{
Console.WriteLine("General exception caught.");
}
2.3 finally 块
finally
块用于执行无论是否发生异常都要执行的代码,通常用于释放资源,如关闭文件或数据库连接。
示例
try
{
// 代码可能抛出异常
}
catch (Exception ex)
{
Console.WriteLine("Exception caught.");
}
finally
{
Console.WriteLine("Finally block executed.");
}
2.4 throw 关键字
throw
关键字用于显式抛出异常。可以在 catch
块中使用 throw
重新抛出捕获的异常,保留其堆栈跟踪信息。
示例
try
{
throw new InvalidOperationException("This is an invalid operation.");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Exception caught: {ex.Message}");
throw; // 重新抛出异常
}
3. 异常处理的最佳实践
3.1 只捕获你能处理的异常
捕获异常时,应只捕获你能够处理的异常类型,不要随意捕获所有异常,避免掩盖其他潜在的编程错误。
示例
try
{
// 代码可能抛出特定类型的异常
}
catch (SpecificException ex)
{
// 处理特定类型的异常
}
3.2 使用 finally 块释放资源
确保在 finally
块中释放资源,即使发生异常,也要保证资源被正确释放,如关闭文件或数据库连接。
示例
StreamReader reader = null;
try
{
reader = new StreamReader("file.txt");
// 读取文件内容
}
catch (IOException ex)
{
Console.WriteLine("File handling exception.");
}
finally
{
reader?.Dispose(); // 确保释放资源
}
3.3 避免在 catch
块中进行复杂操作
在 catch
块中尽量避免执行复杂的操作或逻辑,保持处理简单明了,建议记录异常或返回错误状态,而非尝试修复问题。
3.4 使用自定义异常
在应用程序中,使用自定义异常类可以更清晰地表达错误语义。继承自 Exception
类的自定义异常应提供足够的上下文信息。
示例
public class CustomException : Exception
{
public CustomException(string message) : base(message)
{
}
}
throw new CustomException("This is a custom exception.");
3.5 日志记录
在异常处理过程中,记录异常日志是非常重要的,它能帮助你在问题发生时快速找到根源。使用日志框架(如 NLog
、log4net
等)可以实现异常的详细记录。
示例
catch (Exception ex)
{
Logger.Error(ex, "An error occurred.");
throw; // 重新抛出异常
}
4. 高级异常处理
4.1 异常过滤器(C# 6.0及以上)
异常过滤器允许在 catch
块中定义条件,仅在特定条件满足时捕获异常。它使用 when
关键字。
示例
try
{
// 代码可能抛出异常
}
catch (Exception ex) when (ex.Message.Contains("specific condition"))
{
Console.WriteLine("Exception with specific condition caught.");
}
4.2 全局异常处理
对于控制台应用程序,可以通过设置全局异常处理来捕获未处理的异常,确保程序不会因为未处理的异常崩溃。
示例
AppDomain.CurrentDomain.UnhandledException += (sender, args) =>
{
Exception ex = (Exception)args.ExceptionObject;
Console.WriteLine($"Unhandled exception: {ex.Message}");
};
对于 ASP.NET Core 或 Windows 窗体应用程序,可以通过设置全局异常处理中间件或 Application.ThreadException
事件来实现类似的功能。
4.3 异常的链式捕获(Inner Exceptions)
在处理多层次的异常时,可以使用 InnerException
属性捕获和分析异常的链式原因。
示例
try
{
try
{
// 可能抛出异常的代码
}
catch (Exception ex)
{
throw new InvalidOperationException("Operation failed", ex);
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Outer exception: {ex.Message}");
Console.WriteLine($"Inner exception: {ex.InnerException?.Message}");
}
5. 使用场景
5.1 数据库操作
在数据库操作中,异常处理非常重要。通过捕获数据库连接、查询等过程中的异常,能够有效防止数据损坏或丢失。
5.2 文件操作
在文件读写操作中,可能会发生文件不存在、权限不足等异常,必须通过异常处理来确保应用程序的健壮性。
5.3 网络操作
在网络通信中,常常会遇到网络连接失败、超时等问题。通过异常处理可以在发生错误时提供友好的用户提示,并尝试重新连接或取消操作。
C# 事件与委托
本文档详细探讨了 C# 中的事件(Events)和委托(Delegates),包括它们的概念、用法、实现细节、最佳实践和实际应用场景。这些知识对构建响应式、可扩展的应用程序至关重要,特别是在事件驱动编程中。
1. 什么是委托(Delegates)
概念
委托是 C# 中的一种类型,用于封装具有特定签名的方法。它类似于 C/C++ 中的函数指针,但委托是类型安全的。委托可以存储方法的引用,允许在运行时动态调用这些方法。
委托的定义与使用
1.1 定义委托
public delegate int Operation(int x, int y);
- 解释: 这个委托
Operation
表示任何接受两个整数并返回一个整数的方法。
1.2 使用委托
Operation add = (x, y) => x + y;
int result = add(3, 4); // result 为 7
- 解释: 此示例中,Lambda 表达式被赋给了
add
委托,用于执行加法操作。
多播委托(Multicast Delegates)
委托可以指向多个方法,并按顺序调用它们。这种委托称为多播委托。
示例
public delegate void Notify(string message);
public void ShowMessage(string message)
{
Console.WriteLine(message);
}
public void LogMessage(string message)
{
// 将信息记录到日志
}
Notify notifier = ShowMessage;
notifier += LogMessage;
notifier("Hello World!");
- 解释:
notifier
委托同时指向ShowMessage
和LogMessage
方法,调用notifier
会依次执行这两个方法。
2. 什么是事件(Events)
概念
事件是委托的特殊形式,用于将对象状态的改变通知给其他对象。在 C# 中,事件通常用于实现发布-订阅模式。事件本质上是委托的封装,允许将方法订阅到事件上。当事件被触发时,所有订阅的方法都会被调用。
事件的定义与使用
2.1 定义事件
public class Process
{
public delegate void ProcessCompletedHandler(string message);
public event ProcessCompletedHandler ProcessCompleted;
public void StartProcess()
{
// 模拟一些处理
Console.WriteLine("Process started...");
// 处理完成后触发事件
OnProcessCompleted("Process completed successfully.");
}
protected virtual void OnProcessCompleted(string message)
{
ProcessCompleted?.Invoke(message);
}
}
- 解释: 这里定义了一个
Process
类,其中包含一个ProcessCompleted
事件。当StartProcess
方法执行完后,ProcessCompleted
事件被触发,通知所有订阅者。
2.2 订阅事件
Process process = new Process();
process.ProcessCompleted += MessageHandler;
void MessageHandler(string message)
{
Console.WriteLine(message);
}
process.StartProcess();
- 解释: 在此示例中,
MessageHandler
方法订阅了ProcessCompleted
事件。当事件被触发时,MessageHandler
将被调用。
自动事件属性
为了简化事件的定义,C# 提供了自动事件属性,它可以简化事件的声明和使用。
示例
public event EventHandler ProcessCompleted;
- 解释: 使用
EventHandler
类型的自动事件属性可以简化事件定义,EventHandler
是 .NET 中广泛使用的一个委托类型。
3. 事件和委托的高级用法
3.1 泛型委托与事件
C# 中的泛型委托(如 Func
和 Action
)可以与事件结合使用,以提供更灵活的事件处理机制。
示例
public event Action<int> NumberChanged;
protected void OnNumberChanged(int number)
{
NumberChanged?.Invoke(number);
}
- 解释: 这里使用
Action<int>
泛型委托定义了一个事件,当数值改变时,该事件会通知所有订阅者。
3.2 自定义事件数据
在某些情况下,事件需要传递更多的信息,可以通过定义事件参数类来实现。
示例
public class NumberChangedEventArgs : EventArgs
{
public int OldNumber { get; }
public int NewNumber { get; }
public NumberChangedEventArgs(int oldNumber, int newNumber)
{
OldNumber = oldNumber;
NewNumber = newNumber;
}
}
public event EventHandler<NumberChangedEventArgs> NumberChanged;
protected void OnNumberChanged(int oldNumber, int newNumber)
{
NumberChanged?.Invoke(this, new NumberChangedEventArgs(oldNumber, newNumber));
}
- 解释: 在这个示例中,自定义的
NumberChangedEventArgs
类用于传递更多的事件数据。
4. 委托与事件的使用场景
4.1 UI 事件处理
事件在用户界面编程中非常重要。例如,按钮的点击事件可以通过事件机制轻松处理。
示例
button.Click += Button_Click;
private void Button_Click(object sender, EventArgs e)
{
MessageBox.Show("Button clicked!");
}
4.2 发布-订阅模式
在复杂系统中,发布-订阅模式是一种常见的设计模式,允许对象间松散耦合。发布者通过事件通知订阅者,而不需要关心订阅者的具体实现。
4.3 日志记录与监控
事件与委托可以用于实现日志记录与系统监控。当特定操作发生时,系统可以触发事件,并记录相关信息。
5. 最佳实践
5.1 使用 EventHandler 和 EventHandler<T>
推荐使用 .NET 提供的标准委托类型 EventHandler
和 EventHandler<T>
来定义事件,这样可以提高代码的一致性和可读性。
5.2 始终检查事件是否为 null
在触发事件之前,始终检查事件是否为 null
,以避免 NullReferenceException
。
示例
protected virtual void OnSomethingHappened()
{
SomethingHappened?.Invoke(this, EventArgs.Empty);
}
5.3 使用弱引用订阅事件
避免内存泄漏,可以使用弱引用订阅事件,特别是在事件源生命周期比订阅者长的情况下。
C# 特性(Attributes)
本文档详细介绍了 C# 中的特性(Attributes),包括其概念、使用方法、内置特性、如何创建自定义特性,以及常见应用场景。特性在元编程和应用程序配置中扮演着重要角色,通过理解和使用特性,开发者可以更灵活地控制和扩展代码行为。
1. 什么是特性(Attributes)
概念
特性是用于向代码元素(如类、方法、属性等)添加元数据的一种机制。特性提供了一种在编译时、运行时或设计时获取元数据的方法,允许开发者通过反射(Reflection)读取这些元数据,并根据这些元数据改变程序的行为。
特性通过在代码元素前使用方括号 []
的形式来应用。特性可以包含参数,以提供更多的上下文信息。
作用
- 代码注释和描述: 特性可用于描述代码的行为或附加信息。
- 控制编译行为: 某些特性影响编译器的行为,如
Obsolete
特性标记已过时的代码。 - 运行时行为控制: 特性可用于改变运行时的行为,如
Serializable
特性标记类可以序列化。 - 元数据驱动编程: 通过反射读取特性信息,可以实现更加动态和灵活的程序逻辑。
2. 使用内置特性
C# 提供了大量的内置特性,涵盖了编译、序列化、测试、版本控制等多个方面。
2.1 Obsolete
特性
Obsolete
特性用于标记已过时的代码段,提示开发者使用新方法或新功能。
示例
[Obsolete("Use NewMethod instead")]
public void OldMethod()
{
// 已过时的代码
}
- 解释: 当调用
OldMethod
时,编译器会发出警告,提示开发者使用NewMethod
。
2.2 Serializable
特性
Serializable
特性用于标记一个类可以序列化,这对于将对象状态保存到文件或通过网络传输非常有用。
示例
[Serializable]
public class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
- 解释:
Person
类现在可以被序列化和反序列化。
2.3 DllImport
特性
DllImport
特性用于声明从非托管代码库(如 DLL)导入的外部方法,主要用于与 C/C++ 代码交互。
示例
[DllImport("user32.dll")]
public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
- 解释: 通过
DllImport
特性,可以在 C# 中调用 Windows API 函数。
2.4 Conditional
特性
Conditional
特性用于指示编译器在特定条件下才编译该方法的调用,这通常用于调试或日志记录。
示例
[Conditional("DEBUG")]
public void DebugLog(string message)
{
Console.WriteLine(message);
}
- 解释:
DebugLog
方法只有在编译时定义了DEBUG
常量的情况下才会被编译。
3. 自定义特性
3.1 定义自定义特性
开发者可以创建自定义特性,通过继承 System.Attribute
类,并为特性类添加必要的构造函数和属性。
示例
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DocumentationAttribute : Attribute
{
public string Description { get; }
public string Version { get; }
public DocumentationAttribute(string description, string version)
{
Description = description;
Version = version;
}
}
- 解释: 该示例定义了一个
DocumentationAttribute
特性,用于描述类或方法的文档信息。
3.2 应用自定义特性
一旦定义了自定义特性,可以将其应用到类、方法、属性等代码元素上。
示例
[Documentation("This class performs data processing", "1.0")]
public class DataProcessor
{
[Documentation("Processes data input", "1.0")]
public void Process()
{
// 处理逻辑
}
}
- 解释:
Documentation
特性现在应用在DataProcessor
类和Process
方法上,用于提供文档描述信息。
3.3 通过反射读取特性
使用反射可以在运行时读取应用在代码元素上的特性,进而根据特性信息调整程序行为。
示例
Type type = typeof(DataProcessor);
var attributes = type.GetCustomAttributes(typeof(DocumentationAttribute), false);
foreach (DocumentationAttribute attr in attributes)
{
Console.WriteLine($"Description: {attr.Description}, Version: {attr.Version}");
}
- 解释: 该示例展示了如何通过反射获取
DocumentationAttribute
的值,并输出到控制台。
4. 应用场景
4.1 代码注释和文档生成
通过自定义特性,可以将文档信息直接嵌入代码中,并利用工具生成 API 文档。
4.2 运行时行为控制
特性可以用来标注可序列化对象、数据验证规则、事务处理等,从而在运行时通过反射获取这些信息,控制程序的执行逻辑。
4.3 测试和调试
通过 Conditional
特性,可以根据编译配置(如 DEBUG 模式)控制调试代码的编译。使用 TestMethod
特性可以标记单元测试方法,供测试框架自动识别和执行。
4.4 与非托管代码的交互
特性在与非托管代码(如 C/C++ DLL)交互时扮演重要角色,通过 DllImport
特性可以方便地调用非托管函数。
5. 最佳实践
5.1 合理使用自定义特性
自定义特性应该用于需要在多个位置复用的元数据标注,避免过度使用特性,使代码变得复杂。
5.2 结合反射动态调整行为
在编写需要高灵活性的应用程序时,可以结合特性和反射动态调整程序的行为,如根据特性应用不同的验证规则、序列化策略等。
5.3 利用内置特性简化开发
充分利用 C# 提供的内置特性(如 Serializable
, Obsolete
, DllImport
等),这些特性涵盖了常见的开发场景,可以大幅简化代码和配置。
C# LINQ
1. 什么是 LINQ?
LINQ(Language Integrated Query)是 Microsoft .NET Framework 引入的一种查询语言,它使开发人员能够使用 C# 或 VB.NET 等 .NET 语言直接在代码中查询和操作数据。LINQ 提供了一种统一的查询方法,适用于各种数据源,例如内存中集合、数据库、XML 文档等。
1.1 LINQ 的优势
- 统一数据查询模型:LINQ 提供了统一的数据访问方式,无论数据源是集合、SQL 数据库还是 XML,都可以用相同的查询语法来访问。
- 类型安全:LINQ 查询在编译时进行类型检查,减少了运行时错误。
- 可读性高:LINQ 语法简洁明了,类似于 SQL 语句,易于阅读和维护。
- 延迟执行:LINQ 查询在实际使用数据时才执行,从而提高了性能。
2. 使用 LINQ 查询
LINQ 查询可以用于不同的数据源,如数组、列表、字典、数据库等。基本的 LINQ 查询由以下三个部分组成:
- 数据源:要查询的数据集合。
- 查询表达式:定义查询的内容。
- 执行查询:实际运行查询并返回结果。
2.1 基本的 LINQ 查询结构
// 数据源
int[] numbers = { 2, 5, 8, 3, 7, 1 };
// 查询表达式
var query = from num in numbers
where num > 3
orderby num
select num;
// 执行查询
foreach (var n in query)
{
Console.WriteLine(n);
}
2.2 方法语法和查询语法
LINQ 支持两种查询语法:
- 查询语法:类似于 SQL 语法,更加直观。
- 方法语法:调用方法链来构建查询,适用于复杂的查询场景。
示例:查询语法
var query = from num in numbers
where num > 3
select num;
示例:方法语法
var query = numbers.Where(num => num > 3);
3. LINQ 操作符
LINQ 操作符分为不同的类别,用于对数据进行过滤、投影、排序、连接、分组等操作。
3.1 过滤操作符
- Where:用于从数据源中过滤数据。
var evenNumbers = numbers.Where(n => n % 2 == 0);
3.2 投影操作符
- Select:用于将数据投影到新形式。
var squaredNumbers = numbers.Select(n => n * n);
3.3 排序操作符
- OrderBy:用于对数据进行升序排序。
- OrderByDescending:用于对数据进行降序排序。
var orderedNumbers = numbers.OrderBy(n => n);
3.4 分组操作符
- GroupBy:用于对数据进行分组。
var groupedNumbers = numbers.GroupBy(n => n % 2 == 0 ? "Even" : "Odd");
3.5 连接操作符
- Join:用于连接两个数据源。
var query = from p in products
join c in categories on p.CategoryID equals c.CategoryID
select new { p.ProductName, c.CategoryName };
4. LINQ 与不同的数据源
4.1 LINQ to Objects
LINQ to Objects 允许对内存中的数据进行查询,如数组、列表等。
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
4.2 LINQ to SQL
LINQ to SQL 允许使用 LINQ 查询关系数据库,返回的结果是 IQueryable<T>
类型,支持延迟执行。
using (var db = new DataContext())
{
var query = from c in db.Customers
where c.City == "London"
select c;
}
4.3 LINQ to XML
LINQ to XML 允许使用 LINQ 查询和操作 XML 数据。
XDocument doc = XDocument.Load("data.xml");
var elements = from el in doc.Descendants("Element")
where (int)el.Attribute("ID") == 1
select el;
5. 最佳实践
- 延迟执行:充分利用 LINQ 的延迟执行特性,避免不必要的计算。
- 使用方法语法进行复杂查询:方法语法在处理复杂查询时比查询语法更灵活。
- 分步执行:对于复杂的查询,可以将查询拆分为多个步骤,提高代码的可读性和维护性。
- 注意性能问题:对大数据集使用 LINQ 时,要注意性能问题,必要时可以使用
AsParallel()
来进行并行化处理。
C# 异常处理
1. 什么是异常处理?
异常处理是编程语言中用来处理程序运行时发生的错误或异常情况的一种机制。在 C# 中,异常处理通过 try
, catch
, finally
, 和 throw
关键字实现。使用异常处理可以提高程序的健壮性和稳定性,使程序在面对不可预见的错误时能够采取适当的行动。
1.1 异常的来源
异常通常由以下几种情况引起:
- 运行时错误:如除以零、空引用、数组越界等。
- 硬件故障:如磁盘读写错误。
- 用户输入错误:如文件不存在、格式不正确等。
2. C# 异常处理的基本结构
C# 的异常处理通过 try-catch-finally
结构来实现。try
块包含可能引发异常的代码,catch
块捕获并处理异常,finally
块包含无论是否发生异常都要执行的代码。
2.1 基本异常处理示例
try
{
int[] numbers = { 1, 2, 3 };
int number = numbers[5]; // 可能引发异常:IndexOutOfRangeException
}
catch (IndexOutOfRangeException ex)
{
Console.WriteLine("发生异常:数组索引超出范围。");
Console.WriteLine($"异常消息: {ex.Message}");
}
finally
{
Console.WriteLine("这个代码块总是会被执行。");
}
2.2 多个 catch
块
当一个 try
块可能引发多种不同类型的异常时,可以使用多个 catch
块来分别处理这些异常。
try
{
int a = 10;
int b = 0;
int result = a / b; // 可能引发异常:DivideByZeroException
}
catch (DivideByZeroException ex)
{
Console.WriteLine("发生异常:试图除以零。");
}
catch (Exception ex)
{
Console.WriteLine("发生了一个未知异常。");
Console.WriteLine($"异常消息: {ex.Message}");
}
finally
{
Console.WriteLine("这个代码块总是会被执行。");
}
2.3 finally
块的使用
finally
块中的代码无论是否发生异常都会执行,通常用于释放资源,如关闭文件、断开数据库连接等。
StreamReader reader = null;
try
{
reader = new StreamReader("file.txt");
string content = reader.ReadToEnd(); // 可能引发异常:FileNotFoundException
Console.WriteLine(content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine("文件未找到。");
}
finally
{
if (reader != null)
{
reader.Close();
Console.WriteLine("文件流已关闭。");
}
}
3. 自定义异常
C# 允许开发人员创建自定义的异常类,以提供更具体的异常信息。自定义异常类通常继承自 System.Exception
类。
3.1 创建自定义异常类
public class InvalidAgeException : Exception
{
public InvalidAgeException(string message) : base(message)
{
}
}
3.2 使用自定义异常
public void ValidateAge(int age)
{
if (age < 18)
{
throw new InvalidAgeException("年龄必须大于或等于 18 岁。");
}
}
try
{
ValidateAge(16); // 可能引发自定义异常:InvalidAgeException
}
catch (InvalidAgeException ex)
{
Console.WriteLine($"自定义异常:{ex.Message}");
}
4. 异常处理的最佳实践
4.1 使用特定的异常类型
尽量捕获特定的异常类型,而不是使用通用的 Exception
类型。这有助于更精确地处理异常。
try
{
// 代码块
}
catch (ArgumentNullException ex)
{
// 处理特定异常
}
catch (Exception ex)
{
// 处理其他异常
}
4.2 避免吞掉异常
在 catch
块中不要忽略异常或空捕获(即捕获后什么也不做),这会导致难以排查的错误。
try
{
// 代码块
}
catch (Exception ex)
{
// 记录异常日志
Console.WriteLine($"异常: {ex.Message}");
// 或者重新抛出异常
throw;
}
4.3 使用异常过滤器
C# 6.0 引入了异常过滤器,可以在捕获异常之前对其进行筛选。
try
{
// 代码块
}
catch (Exception ex) when (ex.Message.Contains("关键字"))
{
// 仅在异常消息包含 "关键字" 时捕获异常
Console.WriteLine("捕获到关键字相关的异常。");
}
4.4 避免过度使用异常
异常处理应仅用于处理真正的异常情况,而不应将其用作普通的程序控制流。
// 不推荐
try
{
int number = int.Parse("NotANumber"); // 这会引发 FormatException
}
catch (FormatException)
{
number = 0; // 使用异常来处理普通的逻辑流
}
// 推荐
bool success = int.TryParse("NotANumber", out int number);
if (!success)
{
number = 0; // 使用合理的控制流来处理逻辑
}
5. 常见的 C# 异常类型
- System.ArgumentNullException: 当传递了一个 null 参数,但该参数不允许为 null 时引发。
- System.ArgumentOutOfRangeException: 当参数值超出允许的范围时引发。
- System.InvalidOperationException: 当方法调用对于对象的当前状态无效时引发。
- System.NotImplementedException: 当尝试调用未实现的方法或操作时引发。
- System.NullReferenceException: 当尝试对 null 对象进行操作时引发。
C# 文件操作
1. 概述
文件操作是开发过程中非常常见的需求,包括文件的创建、读取、写入、删除等操作。C# 提供了丰富的类和方法来处理文件操作,主要的类包括 System.IO.File
, System.IO.FileInfo
, System.IO.StreamReader
, System.IO.StreamWriter
, System.IO.FileStream
等。
2. 文件的基本操作
2.1 创建文件
使用 File.Create()
方法可以创建一个新文件,如果文件已经存在,则会覆盖该文件。
string filePath = "example.txt";
// 创建文件
FileStream fs = File.Create(filePath);
// 关闭文件流
fs.Close();
2.2 写入文件
可以使用 StreamWriter
或 File.WriteAllText
来将文本写入文件。
示例:使用 StreamWriter
写入文件
string filePath = "example.txt";
using (StreamWriter writer = new StreamWriter(filePath))
{
writer.WriteLine("Hello, World!");
writer.WriteLine("这是第二行文本。");
}
示例:使用 File.WriteAllText
写入文件
string filePath = "example.txt";
string content = "这是使用 File.WriteAllText 写入的内容。";
File.WriteAllText(filePath, content);
2.3 读取文件
可以使用 StreamReader
或 File.ReadAllText
来读取文件内容。
示例:使用 StreamReader
读取文件
string filePath = "example.txt";
using (StreamReader reader = new StreamReader(filePath))
{
string line;
while ((line = reader.ReadLine()) != null)
{
Console.WriteLine(line);
}
}
示例:使用 File.ReadAllText
读取文件
string filePath = "example.txt";
string content = File.ReadAllText(filePath);
Console.WriteLine(content);
2.4 追加文件内容
使用 StreamWriter
的 AppendText
方法可以将新内容追加到现有文件中。
string filePath = "example.txt";
using (StreamWriter writer = File.AppendText(filePath))
{
writer.WriteLine("这是追加的内容。");
}
2.5 删除文件
使用 File.Delete()
方法可以删除指定的文件。
string filePath = "example.txt";
// 删除文件
if (File.Exists(filePath))
{
File.Delete(filePath);
Console.WriteLine("文件已删除。");
}
else
{
Console.WriteLine("文件不存在。");
}
3. 文件信息操作
C# 提供了 FileInfo
类,用于获取文件的详细信息和执行文件操作。与 File
类相比,FileInfo
是面向对象的,它允许您通过实例方法来操作文件。
3.1 获取文件信息
使用 FileInfo
类可以获取文件的各种属性,如文件大小、创建时间、最后修改时间等。
string filePath = "example.txt";
FileInfo fileInfo = new FileInfo(filePath);
Console.WriteLine($"文件名: {fileInfo.Name}");
Console.WriteLine($"文件大小: {fileInfo.Length} 字节");
Console.WriteLine($"创建时间: {fileInfo.CreationTime}");
Console.WriteLine($"最后修改时间: {fileInfo.LastWriteTime}");
3.2 移动或重命名文件
使用 FileInfo.MoveTo()
方法可以移动文件到新位置或重命名文件。
string sourcePath = "example.txt";
string destinationPath = "new_example.txt";
FileInfo fileInfo = new FileInfo(sourcePath);
// 移动文件或重命名
fileInfo.MoveTo(destinationPath);
Console.WriteLine("文件已移动或重命名。");
3.3 复制文件
使用 FileInfo.CopyTo()
方法可以复制文件到指定位置。
string sourcePath = "example.txt";
string destinationPath = "copy_example.txt";
FileInfo fileInfo = new FileInfo(sourcePath);
// 复制文件
fileInfo.CopyTo(destinationPath);
Console.WriteLine("文件已复制。");
4. 高级文件操作
4.1 文件流操作
对于大文件或需要精细控制读写过程的情况,可以使用 FileStream
类进行文件流操作。
示例:使用 FileStream
进行文件写入
string filePath = "example.bin";
// 创建文件流并写入数据
using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
byte[] data = new byte[] { 0x0A, 0x0B, 0x0C, 0x0D };
fs.Write(data, 0, data.Length);
}
示例:使用 FileStream
进行文件读取
string filePath = "example.bin";
// 打开文件流并读取数据
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
byte[] data = new byte[fs.Length];
fs.Read(data, 0, data.Length);
Console.WriteLine("读取的数据:");
foreach (byte b in data)
{
Console.Write($"{b:X2} ");
}
}
4.2 异步文件操作
C# 还支持异步文件操作,可以提高文件操作的性能,特别是在处理大文件或需要保持 UI 响应时。
示例:异步写入文件
string filePath = "async_example.txt";
string content = "这是异步写入的内容。";
// 异步写入文件
await File.WriteAllTextAsync(filePath, content);
Console.WriteLine("文件已异步写入。");
示例:异步读取文件
string filePath = "async_example.txt";
// 异步读取文件
string content = await File.ReadAllTextAsync(filePath);
Console.WriteLine("文件内容已异步读取:");
Console.WriteLine(content);
5. 最佳实践
5.1 使用 using
语句自动释放资源
在操作文件流或 StreamReader
, StreamWriter
等对象时,推荐使用 using
语句,这样可以确保在使用完成后自动释放资源,避免资源泄漏。
using (StreamWriter writer = new StreamWriter("example.txt"))
{
writer.WriteLine("使用 using 语句自动管理资源。");
}
5.2 检查文件存在性
在对文件进行操作之前,先检查文件是否存在,可以避免 FileNotFoundException
或其他异常。
if (File.Exists("example.txt"))
{
// 进行文件操作
}
else
{
Console.WriteLine("文件不存在。");
}
5.3 使用异步方法进行大文件操作
在处理大文件时,尽量使用异步方法,以避免阻塞主线程,特别是在 GUI 应用中,这可以保持界面的响应性。
await File.WriteAllTextAsync("large_file.txt", largeContent);
C# 并发与多线程
1. 概述
并发和多线程是提高应用程序性能和响应速度的重要技术手段。在 C# 中,多线程支持通过 System.Threading
命名空间中的类和方法来实现。使用多线程,可以让应用程序同时执行多个操作,从而更好地利用多核处理器的能力。
2. 创建和管理线程
C# 中有多种方法可以创建和管理线程,主要包括使用 Thread
类、使用线程池和使用任务并行库(TPL)。
2.1 使用 Thread
类创建线程
最基本的多线程实现方式是直接使用 Thread
类来创建和管理线程。
示例:创建并启动一个线程
using System;
using System.Threading;
class Program
{
static void Main()
{
// 创建线程
Thread thread = new Thread(new ThreadStart(DoWork));
// 启动线程
thread.Start();
// 主线程继续执行
Console.WriteLine("主线程继续执行...");
}
static void DoWork()
{
Console.WriteLine("线程工作开始...");
Thread.Sleep(2000); // 模拟一些工作
Console.WriteLine("线程工作结束...");
}
}
2.2 线程的生命周期控制
Thread
类提供了控制线程生命周期的方法,如 Start()
, Join()
, Abort()
等。
Start()
: 启动线程的执行。Join()
: 阻塞调用线程,直到目标线程完成。Abort()
: 强制终止线程(不推荐使用,因为会导致资源未释放问题)。
示例:使用 Join
方法等待线程完成
Thread thread = new Thread(new ThreadStart(DoWork));
thread.Start();
// 等待子线程完成
thread.Join();
Console.WriteLine("子线程已完成,主线程继续...");
3. 线程同步
在多线程环境中,多个线程可能同时访问共享资源,这可能导致数据竞争和不一致性。C# 提供了多种同步机制来解决这些问题,包括 lock
关键字、Monitor
, Mutex
, Semaphore
, AutoResetEvent
, ManualResetEvent
等。
3.1 使用 lock
关键字
lock
关键字用于确保同一时刻只有一个线程可以访问某个共享资源。
示例:使用 lock
进行线程同步
class Program
{
private static object lockObj = new object();
private static int counter = 0;
static void Main()
{
Thread thread1 = new Thread(IncrementCounter);
Thread thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"最终计数值: {counter}");
}
static void IncrementCounter()
{
for (int i = 0; i < 100000; i++)
{
lock (lockObj)
{
counter++;
}
}
}
}
3.2 使用 Monitor
类
Monitor
类提供了比 lock
更灵活的同步机制,可以显式地进入和退出临界区。
示例:使用 Monitor
进行同步
class Program
{
private static int counter = 0;
private static object lockObj = new object();
static void Main()
{
Thread thread1 = new Thread(IncrementCounter);
Thread thread2 = new Thread(IncrementCounter);
thread1.Start();
thread2.Start();
thread1.Join();
thread2.Join();
Console.WriteLine($"最终计数值: {counter}");
}
static void IncrementCounter()
{
for (int i = 0; i < 100000; i++)
{
Monitor.Enter(lockObj);
try
{
counter++;
}
finally
{
Monitor.Exit(lockObj);
}
}
}
}
4. 线程池
线程池是一个包含多个线程的池,这些线程可以被重用来执行多个任务,而不需要频繁创建和销毁线程。使用线程池可以提高应用程序的性能,特别是在需要频繁执行小任务的场景下。
4.1 使用 ThreadPool
执行任务
ThreadPool.QueueUserWorkItem
方法可以将任务提交到线程池执行。
示例:使用线程池执行任务
class Program
{
static void Main()
{
// 向线程池提交任务
ThreadPool.QueueUserWorkItem(DoWork);
Console.WriteLine("主线程继续执行...");
// 保持主线程运行
Thread.Sleep(1000);
}
static void DoWork(object state)
{
Console.WriteLine("线程池中的线程正在工作...");
Thread.Sleep(500);
Console.WriteLine("工作完成。");
}
}
5. 任务并行库 (Task Parallel Library, TPL)
任务并行库提供了更高层次的并行编程模型,使用 Task
类可以更方便地管理异步操作和并行计算。Task
提供了丰富的功能,如等待任务完成、处理异常、取消任务等。
5.1 使用 Task
类
Task
类用于表示异步操作,可以替代传统的线程管理方式。
示例:使用 Task
创建和启动异步任务
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
// 创建并启动任务
Task task = Task.Run(() => DoWork());
// 等待任务完成
task.Wait();
Console.WriteLine("主线程继续执行...");
}
static void DoWork()
{
Console.WriteLine("任务正在执行...");
Task.Delay(1000).Wait(); // 模拟异步工作
Console.WriteLine("任务完成。");
}
}
5.2 使用 Task
进行并行计算
Task
也可以用于并行计算,将多个任务同时执行。
示例:使用 Task
并行执行多个任务
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Task[] tasks = new Task[3];
tasks[0] = Task.Run(() => DoWork("任务1"));
tasks[1] = Task.Run(() => DoWork("任务2"));
tasks[2] = Task.Run(() => DoWork("任务3"));
// 等待所有任务完成
Task.WaitAll(tasks);
Console.WriteLine("所有任务已完成。");
}
static void DoWork(string taskName)
{
Console.WriteLine($"{taskName} 开始执行...");
Task.Delay(1000).Wait(); // 模拟异步工作
Console.WriteLine($"{taskName} 完成。");
}
}
5.3 异常处理与任务取消
Task
支持异常处理和任务取消,可以更好地控制任务的生命周期。
示例:任务异常处理
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
try
{
Task task = Task.Run(() => { throw new InvalidOperationException("任务中发生异常"); });
task.Wait();
}
catch (AggregateException ex)
{
Console.WriteLine($"捕获到异常: {ex.InnerException.Message}");
}
Console.WriteLine("主线程继续执行...");
}
}示例:任务取消using System;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
CancellationTokenSource cts = new CancellationTokenSource();
Task task = Task.Run(() => DoWork(cts.Token), cts.Token);
// 模拟用户取消操作
Thread.Sleep(500);
cts.Cancel();
try
{
task.Wait();
}
catch (AggregateException ex)
{
Console.WriteLine("任务被取消。");
}
Console.WriteLine("主线程继续执行...");
}
static void DoWork(CancellationToken token)
{
for (int i = 0; i < 10; i++)
{
if (token.IsCancellationRequested)
{
Console.WriteLine("检测到取消请求...");
token.ThrowIfCancellationRequested();
}
Console.WriteLine("正在执行工作...");
Thread.Sleep(200);
}
}
}
### 6. 并行 LINQ (PLINQ)
PLINQ 是 LINQ (Language Integrated Query) 的并行实现,允许将 LINQ 查询转换为并行执行。通过 PLINQ,可以更好地利用多核处理器的能力,从而提高计算密集型任务的执行效率。
#### 6.1 使用 PLINQ 进行并行查询
PLINQ 提供了一个简单的方法来将现有的 LINQ 查询转换为并行查询。可以通过调用 `AsParallel()` 方法将一个序列转换为并行序列。
**示例:将 LINQ 查询转换为并行执行**
```csharp
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = Enumerable.Range(1, 10000000).ToArray();
// 使用 PLINQ 进行并行查询
var evenNumbers = numbers.AsParallel()
.Where(n => n % 2 == 0)
.ToArray();
Console.WriteLine($"总共有 {evenNumbers.Length} 个偶数。");
}
}
在这个示例中,AsParallel()
方法将数组转换为并行处理的序列,Where
过滤条件将被多个线程并行执行,从而加快了查询的速度。
6.2 控制 PLINQ 并行度
PLINQ 提供了控制并行度的选项,例如最大并行度和强制顺序执行。
示例:设置最大并行度
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = Enumerable.Range(1, 10000000).ToArray();
// 设置最大并行度为 4
var evenNumbers = numbers.AsParallel()
.WithDegreeOfParallelism(4)
.Where(n => n % 2 == 0)
.ToArray();
Console.WriteLine($"总共有 {evenNumbers.Length} 个偶数。");
}
}
在这个示例中,WithDegreeOfParallelism(4)
设置了最大并行度为 4,也就是说最多允许 4 个线程同时执行查询。
示例:强制顺序执行
在某些情况下,您可能希望并行查询的输出顺序与输入顺序一致。可以通过调用 AsOrdered()
方法来实现这一点。
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = Enumerable.Range(1, 100).ToArray();
// 强制顺序执行
var orderedSquares = numbers.AsParallel()
.AsOrdered()
.Select(n => n * n)
.ToArray();
Console.WriteLine("按顺序输出平方数:");
foreach (var square in orderedSquares)
{
Console.WriteLine(square);
}
}
}
在这个示例中,AsOrdered()
强制 PLINQ 保持结果的顺序与输入顺序一致。
6.3 捕获 PLINQ 异常
PLINQ 在并行查询时可能会引发多个异常。可以通过捕获 AggregateException
来处理这些异常。
示例:捕获 PLINQ 异常
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] numbers = { 1, 2, 0, 4, 5 };
try
{
var results = numbers.AsParallel()
.Select(n => 100 / n)
.ToArray();
}
catch (AggregateException ex)
{
foreach (var innerEx in ex.InnerExceptions)
{
Console.WriteLine($"捕获到异常: {innerEx.Message}");
}
}
}
}
在这个示例中,PLINQ 查询会尝试对数组中的每个元素执行除法操作,因为数组中包含零,所以会引发 DivideByZeroException
。通过捕获 AggregateException
,可以处理所有并行操作引发的异常。
7. 并发集合
在多线程环境中,对集合的并发访问是常见的需求。C# 提供了一些线程安全的集合类,这些集合可以在多线程环境中安全地进行添加、删除和遍历操作。常用的并发集合包括:
ConcurrentDictionary<TKey, TValue>
ConcurrentBag<T>
ConcurrentQueue<T>
ConcurrentStack<T>
7.1 使用 ConcurrentDictionary
ConcurrentDictionary
是一个线程安全的字典,允许多个线程同时读写数据。
示例:使用 ConcurrentDictionary
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static void Main()
{
ConcurrentDictionary<int, int> dictionary = new ConcurrentDictionary<int, int>();
// 启动多个任务并发操作字典
Parallel.For(0, 1000, i =>
{
dictionary[i] = i * i;
});
Console.WriteLine($"字典中共有 {dictionary.Count} 个键值对。");
}
}
7.2 使用 ConcurrentBag
ConcurrentBag
是一个线程安全的无序集合,适用于需要快速频繁添加和读取的场景。
示例:使用 ConcurrentBag
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
class Program
{
static void Main()
{
ConcurrentBag<int> bag = new ConcurrentBag<int>();
// 启动多个任务并发操作集合
Parallel.For(0, 1000, i =>
{
bag.Add(i);
});
Console.WriteLine($"集合中共有 {bag.Count} 个元素。");
}
}
8. 最佳实践
8.1 合理使用线程数
在并行编程中,使用过多的线程可能会导致性能下降,因为线程上下文切换会增加系统开销。一般情况下,线程数应与系统的物理核心数接近,具体可根据任务类型和系统配置进行调整。
8.2 避免死锁
在多线程编程中,死锁是一个常见的问题。通过合理设计锁的顺序、减少锁的持有时间以及避免嵌套锁,可以降低死锁的风险。
8.3 使用异步编程模型
对于 I/O 密集型任务,如文件读写、网络通信等,使用异步编程模型(如 async
/await
)可以避免阻塞线程,提升应用程序的响应性。
8.4 使用并发集合
在多线程环境中,使用并发集合代替普通集合,可以避免手动实现复杂的同步逻辑,简化代码的同时提高性能。
C# async
和 await
异步编程
1. 概述
异步编程是处理 I/O 密集型任务(如文件操作、网络请求、数据库查询等)的一种重要技术。异步编程可以在不阻塞主线程的情况下执行耗时操作,从而提高应用程序的响应性和性能。在 C# 中,async
和 await
关键字提供了一种简单而强大的方式来实现异步编程。
2. async
和 await
的基本用法
2.1 async
关键字
async
关键字用于修饰一个方法,表示该方法包含异步操作。被 async
修饰的方法通常返回一个 Task
或 Task<TResult>
对象。
public async Task DoWorkAsync()
{
// 异步操作
}
2.2 await
关键字
await
关键字用于等待一个异步操作的完成。在等待期间,await
不会阻塞当前线程,而是允许线程继续执行其他任务,直到异步操作完成。
public async Task DoWorkAsync()
{
await Task.Delay(1000); // 异步等待 1 秒
Console.WriteLine("异步操作完成");
}
3. 使用 async
和 await
实现异步操作
异步编程通常用于 I/O 操作,比如文件读写、网络请求等。以下是一些常见的异步操作示例。
3.1 异步读取文件
示例:使用 async
和 await
进行异步文件读取
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string filePath = "example.txt";
string content = await ReadFileAsync(filePath);
Console.WriteLine("文件内容:");
Console.WriteLine(content);
}
static async Task<string> ReadFileAsync(string path)
{
using (StreamReader reader = new StreamReader(path))
{
// 异步读取文件内容
return await reader.ReadToEndAsync();
}
}
}
在这个示例中,ReadFileAsync
方法使用 async
关键字修饰,并且使用 await
等待 StreamReader.ReadToEndAsync()
的完成,而不会阻塞主线程。
3.2 异步网络请求
示例:使用 async
和 await
进行异步 HTTP 请求
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string url = "https://api.github.com/repos/dotnet/runtime";
string content = await FetchDataAsync(url);
Console.WriteLine("响应数据:");
Console.WriteLine(content);
}
static async Task<string> FetchDataAsync(string url)
{
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; async-await)");
// 异步发送 HTTP GET 请求
HttpResponseMessage response = await client.GetAsync(url);
// 异步读取响应内容
return await response.Content.ReadAsStringAsync();
}
}
}
在这个示例中,FetchDataAsync
方法使用 HttpClient
异步发送 HTTP 请求,并使用 await
等待请求和响应的完成。
4. 异常处理
在异步方法中,可以使用 try-catch
块来捕获和处理异步操作中发生的异常。异步方法中引发的异常会被包装在 Task
对象中,因此使用 await
时,异常会在 await
行被抛出。
示例:在异步方法中处理异常
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
try
{
string content = await ReadFileAsync("nonexistentfile.txt");
Console.WriteLine(content);
}
catch (FileNotFoundException ex)
{
Console.WriteLine($"文件未找到: {ex.Message}");
}
}
static async Task<string> ReadFileAsync(string path)
{
using (StreamReader reader = new StreamReader(path))
{
// 异步读取文件内容
return await reader.ReadToEndAsync();
}
}
}
在这个示例中,ReadFileAsync
方法尝试读取一个不存在的文件,会抛出 FileNotFoundException
,并在 Main
方法中被捕获和处理。
5. 返回值与 Task
类型
异步方法的返回值通常是 Task
或 Task<TResult>
,Task
表示一个不返回结果的异步操作,而 Task<TResult>
表示一个返回结果的异步操作。
5.1 返回 Task
类型
示例:异步方法返回 Task
类型
public async Task DoWorkAsync()
{
await Task.Delay(1000);
Console.WriteLine("工作完成");
}
5.2 返回 Task<TResult>
类型
示例:异步方法返回 Task<TResult>
类型
public async Task<int> CalculateSumAsync(int a, int b)
{
await Task.Delay(1000); // 模拟异步工作
return a + b;
}
// 使用异步方法
int result = await CalculateSumAsync(3, 4);
Console.WriteLine($"结果: {result}");
在这个示例中,CalculateSumAsync
方法返回一个 Task<int>
,表示异步计算的结果是一个整数。
6. 使用 async
和 await
优化 UI 应用
在 GUI 应用程序中,异步编程尤为重要,因为它可以防止阻塞 UI 线程,从而保持界面的响应性。
示例:在 WPF 应用中使用 async
和 await
private async void OnButtonClick(object sender, RoutedEventArgs e)
{
// 在 UI 线程上禁用按钮
myButton.IsEnabled = false;
// 执行异步操作
string content = await FetchDataAsync("https://example.com");
// 更新 UI
myTextBox.Text = content;
// 重新启用按钮
myButton.IsEnabled = true;
}
在这个示例中,当按钮被点击时,异步方法 FetchDataAsync
会被调用来获取数据。在异步操作完成之前,UI 线程不会被阻塞,因此界面仍然保持响应。操作完成后,UI 会被更新。
7. 异步编程的最佳实践
7.1 避免阻塞
在异步方法中,不应使用 Task.Wait()
或 Task.Result
来阻塞等待结果,因为这会抵消异步编程的优势,并可能导致死锁。应该始终使用 await
。
// 不推荐
Task<int> task = DoWorkAsync();
int result = task.Result; // 阻塞
// 推荐
int result = await DoWorkAsync(); // 不阻塞
7.2 避免使用异步 void
除非是事件处理程序,否则异步方法应返回 Task
或 Task<TResult>
,避免使用 void
返回类型,因为 async void
方法的异常不能被捕获。
// 不推荐
public async void DoWorkAsync()
{
await Task.Delay(1000);
}
// 推荐
public async Task DoWorkAsync()
{
await Task.Delay(1000);
}
7.3 使用配置性等待
在特定场景下,可以使用 ConfigureAwait(false) 来避免上下文切换,从而提高性能,尤其是在不需要在 UI 线程上执行后续代码时。
await Task.Delay(1000).ConfigureAwait(false);
C# 网络编程
1. 概述
网络编程是指通过计算机网络进行数据通信的技术。C# 提供了丰富的类库来实现网络编程功能,常用的命名空间包括 System.Net
, System.Net.Http
, System.Net.Sockets
等。C# 的网络编程涵盖了从简单的 HTTP 请求到复杂的套接字编程等多个层面。
2. HTTP 客户端编程
HttpClient
类是 .NET 中推荐使用的用于发送 HTTP 请求和接收响应的类。它支持同步和异步操作,并且可以处理各种类型的 HTTP 请求(GET、POST、PUT、DELETE 等)。
2.1 发送 GET 请求
示例:使用 HttpClient
发送异步 GET 请求
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string url = "https://api.github.com/repos/dotnet/runtime";
using (HttpClient client = new HttpClient())
{
// 设置请求头
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; NetworkClient)");
// 发送 GET 请求
HttpResponseMessage response = await client.GetAsync(url);
// 确保响应成功
response.EnsureSuccessStatusCode();
// 读取响应内容
string content = await response.Content.ReadAsStringAsync();
Console.WriteLine("响应内容:");
Console.WriteLine(content);
}
}
}
在这个示例中,我们使用 HttpClient
发送了一个异步 GET 请求,并读取了响应内容。EnsureSuccessStatusCode
方法用于确保请求成功,否则会抛出异常。
2.2 发送 POST 请求
示例:使用 HttpClient
发送异步 POST 请求
using System;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string url = "https://jsonplaceholder.typicode.com/posts";
string json = "{\"title\": \"foo\", \"body\": \"bar\", \"userId\": 1}";
using (HttpClient client = new HttpClient())
{
// 设置请求头
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (compatible; NetworkClient)");
// 创建 HttpContent
HttpContent content = new StringContent(json, Encoding.UTF8, "application/json");
// 发送 POST 请求
HttpResponseMessage response = await client.PostAsync(url, content);
// 确保响应成功
response.EnsureSuccessStatusCode();
// 读取响应内容
string responseBody = await response.Content.ReadAsStringAsync();
Console.WriteLine("响应内容:");
Console.WriteLine(responseBody);
}
}
}
在这个示例中,我们使用 HttpClient
发送了一个异步 POST 请求,并将 JSON 数据作为请求体发送到服务器。
3. WebSocket 编程
WebSocket 是一种通信协议,它在单个 TCP 连接上提供全双工的通信。C# 提供了 ClientWebSocket
类来实现 WebSocket 客户端。
3.1 使用 ClientWebSocket
进行 WebSocket 通信
示例:使用 ClientWebSocket
建立 WebSocket 连接并发送消息
using System;
using System.Net.WebSockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
using (ClientWebSocket ws = new ClientWebSocket())
{
Uri serverUri = new Uri("wss://echo.websocket.org");
await ws.ConnectAsync(serverUri, CancellationToken.None);
Console.WriteLine("已连接到 WebSocket 服务器");
string message = "Hello, WebSocket!";
byte[] buffer = Encoding.UTF8.GetBytes(message);
// 发送消息
await ws.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
Console.WriteLine($"已发送消息: {message}");
// 接收消息
buffer = new byte[1024];
WebSocketReceiveResult result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);
Console.WriteLine($"接收到消息: {Encoding.UTF8.GetString(buffer, 0, result.Count)}");
await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "关闭连接", CancellationToken.None);
Console.WriteLine("WebSocket 连接已关闭");
}
}
}
在这个示例中,我们使用 ClientWebSocket
类连接到一个 WebSocket 服务器(wss://echo.websocket.org
),并发送和接收消息。
4. TCP 套接字编程
TCP 套接字编程是一种低级别的网络编程方式,允许开发者直接控制数据的传输。C# 中的 System.Net.Sockets
命名空间提供了用于实现 TCP 客户端和服务器的类。
4.1 创建 TCP 客户端
示例:使用 TcpClient
创建 TCP 客户端
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string server = "127.0.0.1";
int port = 5000;
using (TcpClient client = new TcpClient())
{
await client.ConnectAsync(server, port);
Console.WriteLine("已连接到服务器");
NetworkStream stream = client.GetStream();
string message = "Hello, Server!";
byte[] data = Encoding.UTF8.GetBytes(message);
// 发送数据
await stream.WriteAsync(data, 0, data.Length);
Console.WriteLine($"已发送: {message}");
// 接收响应
data = new byte[256];
int bytes = await stream.ReadAsync(data, 0, data.Length);
Console.WriteLine($"接收到: {Encoding.UTF8.GetString(data, 0, bytes)}");
}
}
}
在这个示例中,我们使用 TcpClient
类连接到本地的 TCP 服务器,并发送和接收数据。
4.2 创建 TCP 服务器
示例:使用 TcpListener
创建 TCP 服务器
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
int port = 5000;
TcpListener server = new TcpListener(IPAddress.Any, port);
server.Start();
Console.WriteLine($"服务器已启动,监听端口 {port}");
while (true)
{
TcpClient client = await server.AcceptTcpClientAsync();
Console.WriteLine("客户端已连接");
_ = Task.Run(async () =>
{
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[256];
int bytes = await stream.ReadAsync(buffer, 0, buffer.Length);
string message = Encoding.UTF8.GetString(buffer, 0, bytes);
Console.WriteLine($"接收到: {message}");
// 发送响应
byte[] response = Encoding.UTF8.GetBytes("Hello, Client!");
await stream.WriteAsync(response, 0, response.Length);
client.Close();
});
}
}
}
在这个示例中,我们使用 TcpListener
类创建了一个 TCP 服务器,监听客户端连接并处理每个客户端的请求。
5. UDP 套接字编程
UDP 是一种无连接的、不可靠的传输协议。C# 提供了 UdpClient
类来实现 UDP 客户端和服务器。
5.1 创建 UDP 客户端
示例:使用 UdpClient
创建 UDP 客户端
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string server = "127.0.0.1";
int port = 5000;
using (UdpClient client = new UdpClient())
{
string message = "Hello, UDP Server!";
byte[] data = Encoding.UTF8.GetBytes(message);
// 发送数据
await client.SendAsync(data, data.Length, server, port);
Console.WriteLine($"已发送: {message}");
// 接收响应
UdpReceiveResult result = await client.ReceiveAsync();
Console.WriteLine($"接收到: {Encoding.UTF8.GetString(result.Buffer)}");
}
}
}
在这个示例中,我们使用 UdpClient
类创建了一个 UDP 客户端,向服务器发送数据并接收响应。
5.2 创建 UDP 服务器
示例:使用 UdpClient
创建 UDP 服务器
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
int port = 5000;
using (UdpClient server = new UdpClient(port))
{
Console.WriteLine($"UDP 服务器已启动,监听端口 {port}");
while (true)
{
// 接收来自客户端的数据
UdpReceiveResult result = await server.ReceiveAsync();
string receivedMessage = Encoding.UTF8.GetString(result.Buffer);
Console.WriteLine($"接收到: {receivedMessage} 来自 {result.RemoteEndPoint}");
// 发送响应给客户端
string responseMessage = "Hello, UDP Client!";
byte[] responseData = Encoding.UTF8.GetBytes(responseMessage);
await server.SendAsync(responseData, responseData.Length, result.RemoteEndPoint);
Console.WriteLine("响应已发送");
}
}
}
}
在这个示例中,我们创建了一个简单的 UDP 服务器,监听来自客户端的消息并进行响应。服务器在接收到客户端的数据后,打印出收到的消息并返回一个响应消息。
6. 使用 Dns
进行域名解析
C# 中的 System.Net.Dns
类提供了与 DNS 服务器进行交互的方法,用于解析域名或 IP 地址。
6.1 获取主机 IP 地址
示例:使用 Dns.GetHostAddresses
获取主机 IP 地址
using System;
using System.Net;
class Program
{
static void Main()
{
string hostName = "www.google.com";
// 获取主机的 IP 地址
IPAddress[] addresses = Dns.GetHostAddresses(hostName);
Console.WriteLine($"主机 {hostName} 的 IP 地址:");
foreach (var address in addresses)
{
Console.WriteLine(address);
}
}
}
在这个示例中,Dns.GetHostAddresses
方法用于获取指定主机名的 IP 地址。
6.2 反向 DNS 查找
示例:使用 Dns.GetHostEntry
进行反向 DNS 查找
using System;
using System.Net;
class Program
{
static void Main()
{
string ipAddress = "8.8.8.8";
// 进行反向 DNS 查找
IPHostEntry hostEntry = Dns.GetHostEntry(ipAddress);
Console.WriteLine($"IP 地址 {ipAddress} 对应的主机名: {hostEntry.HostName}");
}
}
在这个示例中,Dns.GetHostEntry
方法用于执行反向 DNS 查找,从 IP 地址获取对应的主机名。
7. 网络编程的最佳实践
7.1 使用异步方法
在网络编程中,使用异步方法可以避免阻塞主线程或其他重要线程,从而保持应用程序的响应性。尤其是在处理 I/O 密集型任务时,推荐使用 async
和 await
。
7.2 处理网络异常
网络通信过程中常常会遇到各种异常,如网络超时、连接失败等。必须妥善处理这些异常,以提高应用程序的健壮性。
try
{
string content = await client.GetStringAsync("https://example.com");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"请求失败: {ex.Message}");
}
7.3 使用 using
语句管理资源
网络编程中经常使用 TcpClient
, HttpClient
, UdpClient
等类,这些类实现了 IDisposable
接口,建议使用 using
语句来管理它们的生命周期,确保资源在使用后被正确释放。
using (HttpClient client = new HttpClient())
{
// 使用 client 进行网络操作
}
7.4 考虑安全性
在进行网络编程时,考虑安全性是至关重要的,特别是在传输敏感数据时。使用 HTTPS 而不是 HTTP,确保数据在传输过程中是加密的。此外,验证服务器证书的合法性也是防止中间人攻击的重要步骤。
// 配置 HttpClientHandler 验证服务器证书
HttpClientHandler handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
};
HttpClient client = new HttpClient(handler);
C# 数据库编程
1. 概述
C# 提供了多种方式与数据库进行交互,常见的方式包括使用 ADO.NET、Entity Framework 和 Dapper 等。通过这些工具和技术,您可以在 C# 应用程序中执行数据库查询、更新数据、管理事务等操作。
2. 使用 ADO.NET 进行数据库编程
ADO.NET 是 .NET Framework 提供的用于访问数据源的基本工具。它包含了一组类,用于执行 SQL 查询和存储过程、检索和管理数据。
2.1 连接到数据库
首先,我们需要使用 SqlConnection
类来连接到数据库。
示例:连接到 SQL Server 数据库
using System;
using System.Data.SqlClient;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (SqlConnection connection = new SqlConnection(connectionString))
{
try
{
connection.Open();
Console.WriteLine("连接成功!");
}
catch (SqlException ex)
{
Console.WriteLine($"连接失败: {ex.Message}");
}
}
}
}
在这个示例中,我们通过指定连接字符串连接到 SQL Server 数据库。SqlConnection
对象用于管理与数据库的连接,并且使用 using
语句确保在使用完毕后连接会被关闭。
2.2 执行 SQL 查询
示例:执行 SELECT 查询
using System;
using System.Data.SqlClient;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
string query = "SELECT TOP 10 * FROM your_table";
using (SqlCommand command = new SqlCommand(query, connection))
{
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine($"{reader["ColumnName1"]}, {reader["ColumnName2"]}");
}
}
}
}
}
}
在这个示例中,我们使用 SqlCommand
类执行 SQL 查询,并使用 SqlDataReader
读取结果。ExecuteReader()
方法返回一个 SqlDataReader
对象,用于逐行读取查询结果。
2.3 插入、更新和删除数据
示例:插入数据
using System;
using System.Data.SqlClient;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
string query = "INSERT INTO your_table (ColumnName1, ColumnName2) VALUES (@Value1, @Value2)";
using (SqlCommand command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Value1", "SomeValue");
command.Parameters.AddWithValue("@Value2", "AnotherValue");
int rowsAffected = command.ExecuteNonQuery();
Console.WriteLine($"插入了 {rowsAffected} 行数据");
}
}
}
}
示例:更新数据
using System;
using System.Data.SqlClient;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
string query = "UPDATE your_table SET ColumnName1 = @Value WHERE ColumnName2 = @Condition";
using (SqlCommand command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Value", "UpdatedValue");
command.Parameters.AddWithValue("@Condition", "ConditionValue");
int rowsAffected = command.ExecuteNonQuery();
Console.WriteLine($"更新了 {rowsAffected} 行数据");
}
}
}
}
示例:删除数据
using System;
using System.Data.SqlClient;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
string query = "DELETE FROM your_table WHERE ColumnName1 = @Condition";
using (SqlCommand command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Condition", "ConditionValue");
int rowsAffected = command.ExecuteNonQuery();
Console.WriteLine($"删除了 {rowsAffected} 行数据");
}
}
}
}
在这些示例中,我们使用 SqlCommand
的 ExecuteNonQuery
方法来执行插入、更新和删除操作。Parameters.AddWithValue
用于添加 SQL 参数,从而防止 SQL 注入攻击。
2.4 使用事务
事务用于确保一组数据库操作要么全部成功,要么全部失败,从而保证数据的一致性。
示例:使用事务
using System;
using System.Data.SqlClient;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (SqlConnection connection = new SqlConnection(connectionString))
{
connection.Open();
SqlTransaction transaction = connection.BeginTransaction();
try
{
string query1 = "INSERT INTO your_table (ColumnName1, ColumnName2) VALUES (@Value1, @Value2)";
using (SqlCommand command1 = new SqlCommand(query1, connection, transaction))
{
command1.Parameters.AddWithValue("@Value1", "SomeValue");
command1.Parameters.AddWithValue("@Value2", "AnotherValue");
command1.ExecuteNonQuery();
}
string query2 = "UPDATE your_table SET ColumnName1 = @Value WHERE ColumnName2 = @Condition";
using (SqlCommand command2 = new SqlCommand(query2, connection, transaction))
{
command2.Parameters.AddWithValue("@Value", "UpdatedValue");
command2.Parameters.AddWithValue("@Condition", "ConditionValue");
command2.ExecuteNonQuery();
}
transaction.Commit();
Console.WriteLine("事务提交成功");
}
catch (Exception ex)
{
transaction.Rollback();
Console.WriteLine($"事务回滚: {ex.Message}");
}
}
}
}
在这个示例中,我们将多条 SQL 命令封装在一个事务中。如果所有操作都成功,则提交事务;如果发生异常,则回滚事务。
3. 使用 Entity Framework 进行数据库编程
Entity Framework (EF) 是一个对象关系映射 (ORM) 框架,它使开发人员能够使用 C# 类来表示数据库中的数据,并通过 LINQ 查询来操作这些数据。
3.1 配置 Entity Framework
首先,确保在项目中安装了 Entity Framework 包。可以使用 NuGet Package Manager 或者 Package Manager Console:
Install-Package Microsoft.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer
接下来,定义数据模型和数据库上下文。
示例:定义数据模型和上下文
using Microsoft.EntityFrameworkCore;
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class AppDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("Server=your_server;Database=your_database;User Id=your_username;Password=your_password;");
}
}
在这个示例中,Product
类表示数据库中的 Products
表,AppDbContext
类用于管理实体与数据库的映射关系。
3.2 使用 Entity Framework 进行 CRUD 操作
示例:插入数据
using System;
class Program
{
static void Main()
{
using (var context = new AppDbContext())
{
var product = new Product { Name = "Sample Product", Price = 9.99M };
context.Products.Add(product);
context.SaveChanges();
Console.WriteLine("产品已添加");
}
}
}
示例:读取数据
using System;
using System.Linq;
class Program
{
static void Main()
{
using (var context = new AppDbContext())
{
var products = context.Products.ToList();
foreach (var product in products)
{
Console.WriteLine($"{product.ProductId}: {product.Name} - {product.Price:C}");
}
}
}
}
示例:更新数据
using System;
using System.Linq;
class Program
{
static void Main()
{
using (var context = new AppDbContext())
{
// 查找要更新的产品
var product = context.Products.FirstOrDefault(p => p.Name == "Sample Product");
if (product != null)
{
// 更新产品价格
product.Price = 19.99M;
context.SaveChanges();
Console.WriteLine("产品已更新");
}
else
{
Console.WriteLine("产品未找到");
}
}
}
}
在这个示例中,我们首先从数据库中查找要更新的产品,并修改其价格,然后调用 SaveChanges()
方法将更改保存到数据库。
示例:删除数据
using System;
using System.Linq;
class Program
{
static void Main()
{
using (var context = new AppDbContext())
{
// 查找要删除的产品
var product = context.Products.FirstOrDefault(p => p.Name == "Sample Product");
if (product != null)
{
// 删除产品
context.Products.Remove(product);
context.SaveChanges();
Console.WriteLine("产品已删除");
}
else
{
Console.WriteLine("产品未找到");
}
}
}
}
在这个示例中,我们使用 Remove()
方法将产品从上下文中删除,然后通过 SaveChanges()
方法将删除操作应用到数据库。
4. 使用 Dapper 进行数据库编程
Dapper 是一个轻量级的 ORM 库,与 Entity Framework 相比,它更加灵活且性能更高。Dapper 直接映射 SQL 查询结果到 C# 对象。
4.1 安装 Dapper
首先,通过 NuGet Package Manager 安装 Dapper 包:
Install-Package Dapper
4.2 使用 Dapper 进行 CRUD 操作
示例:查询数据
using System;
using System.Data.SqlClient;
using Dapper;
using System.Linq;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (var connection = new SqlConnection(connectionString))
{
string query = "SELECT * FROM Products WHERE Price > @Price";
var products = connection.Query<Product>(query, new { Price = 10.0M }).ToList();
foreach (var product in products)
{
Console.WriteLine($"{product.ProductId}: {product.Name} - {product.Price:C}");
}
}
}
}
在这个示例中,我们使用 Dapper 的 Query<T>
方法执行 SQL 查询,并将结果映射到 Product
对象列表中。
示例:插入数据
using System;
using System.Data.SqlClient;
using Dapper;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (var connection = new SqlConnection(connectionString))
{
string query = "INSERT INTO Products (Name, Price) VALUES (@Name, @Price)";
var parameters = new { Name = "New Product", Price = 14.99M };
int rowsAffected = connection.Execute(query, parameters);
Console.WriteLine($"插入了 {rowsAffected} 行数据");
}
}
}
示例:更新数据
using System;
using System.Data.SqlClient;
using Dapper;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (var connection = new SqlConnection(connectionString))
{
string query = "UPDATE Products SET Price = @Price WHERE Name = @Name";
var parameters = new { Price = 18.99M, Name = "New Product" };
int rowsAffected = connection.Execute(query, parameters);
Console.WriteLine($"更新了 {rowsAffected} 行数据");
}
}
}
示例:删除数据
using System;
using System.Data.SqlClient;
using Dapper;
class Program
{
static void Main()
{
string connectionString = "Server=your_server;Database=your_database;User Id=your_username;Password=your_password;";
using (var connection = new SqlConnection(connectionString))
{
string query = "DELETE FROM Products WHERE Name = @Name";
var parameters = new { Name = "New Product" };
int rowsAffected = connection.Execute(query, parameters);
Console.WriteLine($"删除了 {rowsAffected} 行数据");
}
}
}
在这些示例中,我们使用 Dapper 的 Execute
方法执行插入、更新和删除操作。Dapper 通过直接映射参数,简化了 SQL 操作的执行过程。
5. 数据库编程的最佳实践
5.1 使用参数化查询
在执行 SQL 查询时,始终使用参数化查询来避免 SQL 注入攻击。无论是使用 ADO.NET 还是 ORM 框架,都应该确保所有 SQL 参数都是通过参数化方式传递的。
string query = "SELECT * FROM Products WHERE Name = @Name";
var parameters = new { Name = "Product1" };
5.2 管理数据库连接
使用 using
语句管理数据库连接,确保在操作完成后连接自动关闭和释放资源,避免连接泄漏。
using (var connection = new SqlConnection(connectionString))
{
connection.Open();
// 数据库操作
}
5.3 处理数据库异常
数据库操作可能会引发异常,如连接失败、查询错误等。必须在代码中妥善处理这些异常,并提供友好的错误信息。
try
{
// 数据库操作
}
catch (SqlException ex)
{
Console.WriteLine($"数据库操作失败: {ex.Message}");
}
5.4 使用异步操作
在处理大量数据或长时间运行的查询时,使用异步方法可以避免阻塞主线程,从而提高应用程序的响应性。
var products = await connection.QueryAsync<Product>(query, parameters);
C# 测试
1. 概述
软件测试是确保应用程序质量的关键环节。通过测试,我们可以验证代码的正确性、稳定性和性能。C# 中有多种测试框架和工具可用于编写和执行测试,常见的包括 MSTest、NUnit 和 xUnit。这些测试框架都支持单元测试、集成测试和其他类型的测试。
2. 单元测试
单元测试是最基本的测试类型,旨在验证应用程序中单一模块或单一功能的行为。通常,单元测试是对方法或类的测试,用于确保其在各种输入条件下的输出是正确的。
2.1 使用 MSTest 进行单元测试
MSTest 是 Microsoft 提供的测试框架,集成在 Visual Studio 中,易于使用。
示例:编写一个简单的单元测试
首先,确保在项目中添加了测试项目。如果没有,可以通过 Visual Studio 添加一个新的“单元测试项目”。
示例代码:待测试的类
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
public int Subtract(int a, int b)
{
return a - b;
}
}
示例代码:MSTest 单元测试
using Microsoft.VisualStudio.TestTools.UnitTesting;
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_TwoNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
int a = 5;
int b = 3;
// Act
int result = calculator.Add(a, b);
// Assert
Assert.AreEqual(8, result);
}
[TestMethod]
public void Subtract_TwoNumbers_ReturnsDifference()
{
// Arrange
var calculator = new Calculator();
int a = 5;
int b = 3;
// Act
int result = calculator.Subtract(a, b);
// Assert
Assert.AreEqual(2, result);
}
}
在这个示例中,我们编写了两个简单的单元测试,分别测试 Add
和 Subtract
方法。[TestMethod]
特性用于标记每个测试方法,而 Assert.AreEqual
方法用于验证结果是否符合预期。
2.2 使用 NUnit 进行单元测试
NUnit 是另一个流行的测试框架,功能强大,支持各种测试模式和约定。
示例代码:NUnit 单元测试
首先,确保安装了 NUnit 和 NUnit3TestAdapter 包:
Install-Package NUnit
Install-Package NUnit3TestAdapter
然后,编写测试类:
using NUnit.Framework;
[TestFixture]
public class CalculatorTests
{
[Test]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator();
int result = calculator.Add(5, 3);
Assert.AreEqual(8, result);
}
[Test]
public void Subtract_TwoNumbers_ReturnsDifference()
{
var calculator = new Calculator();
int result = calculator.Subtract(5, 3);
Assert.AreEqual(2, result);
}
}
与 MSTest 类似,NUnit 使用 [TestFixture]
标记测试类,使用 [Test]
标记测试方法,并使用 Assert.AreEqual
进行断言。
2.3 使用 xUnit 进行单元测试
xUnit 是一个相对较新的测试框架,设计简洁,易于使用,且与 .NET Core 集成良好。
示例代码:xUnit 单元测试
首先,确保安装了 xUnit 和 xUnit.runner.visualstudio 包:
Install-Package xUnit
Install-Package xUnit.runner.visualstudio
然后,编写测试类:
using Xunit;
public class CalculatorTests
{
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
var calculator = new Calculator();
int result = calculator.Add(5, 3);
Assert.Equal(8, result);
}
[Fact]
public void Subtract_TwoNumbers_ReturnsDifference()
{
var calculator = new Calculator();
int result = calculator.Subtract(5, 3);
Assert.Equal(2, result);
}
}
在 xUnit 中,我们使用 [Fact]
特性标记测试方法,并使用 Assert.Equal
进行断言。xUnit 的风格简洁且易读,是现代 .NET 项目的推荐选择之一。
3. 集成测试
集成测试旨在验证多个模块或系统的协同工作。通常,集成测试比单元测试更加复杂,因为它需要模拟真实的环境,并且可能涉及数据库、网络请求等外部依赖。
3.1 设置集成测试环境
集成测试通常需要一个类似于生产环境的设置。可以使用 SQLite 等内存数据库来模拟数据库操作,或使用 Mocking 框架(如 Moq)来模拟外部依赖。
示例:集成测试设置
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using NUnit.Framework;
public class DatabaseFixture
{
public AppDbContext Context { get; private set; }
[SetUp]
public void SetUp()
{
var connection = new SqliteConnection("DataSource=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseSqlite(connection)
.Options;
Context = new AppDbContext(options);
Context.Database.EnsureCreated();
}
[TearDown]
public void TearDown()
{
Context.Dispose();
}
}
在这个示例中,我们创建了一个 DatabaseFixture
类,用于设置和清理测试数据库环境。这里使用 SQLite 的内存数据库来模拟真实的数据库操作。
3.2 编写集成测试
集成测试通常包括多个步骤,如准备数据、执行操作、验证结果等。
示例:编写集成测试
using NUnit.Framework;
[TestFixture]
public class ProductServiceTests : DatabaseFixture
{
[Test]
public void AddProduct_SavesToDatabase()
{
var service = new ProductService(Context);
var product = new Product { Name = "Test Product", Price = 9.99M };
service.AddProduct(product);
var savedProduct = Context.Products.FirstOrDefault(p => p.Name == "Test Product");
Assert.IsNotNull(savedProduct);
Assert.AreEqual(9.99M, savedProduct.Price);
}
}
在这个示例中,我们编写了一个简单的集成测试,验证 ProductService.AddProduct
方法是否正确地将产品保存到数据库中。
4. Mocking 和依赖注入
在单元测试和集成测试中,有时我们需要隔离测试对象与其依赖项。Mocking 和依赖注入是实现这一目的的有效方法。
4.1 使用 Moq 进行 Mocking
Moq 是一个流行的 .NET Mocking 框架,它允许开发者轻松创建和配置模拟对象。
示例:使用 Moq 进行依赖注入
首先,确保安装了 Moq 包:
Install-Package Moq
然后,编写 Mock 对象:
using Moq;
using Xunit;
public class ProductServiceTests
{
[Fact]
public void AddProduct_CallsRepositorySave()
{
var mockRepo = new Mock<IProductRepository>();
var service = new ProductService(mockRepo.Object);
var product = new Product { Name = "Mock Product", Price = 19.99M };
service.AddProduct(product);
mockRepo.Verify(r => r.Save(product), Times.Once);
}
}
在这个示例中,我们使用 Moq 创建了一个 IProductRepository
的 Mock 对象,并验证 ProductService.AddProduct
是否调用了 Save
方法。
5. 测试驱动开发 (TDD)
测试驱动开发(TDD)是一种软件开发方法,强调在编写代码之前先编写测试。TDD 的基本流程包括:
- 编写失败的测试。
- 编写最少量的代码使测试通过。
- 重构代码,确保所有测试继续通过。
示例:TDD 流程
[TestFixture]
public class FizzBuzzTests
{
[Test]
public void GetFizzBuzz_ReturnsBuzzForMultiplesOfFive()
{
var fizzBuzz = new FizzBuzz();
string result = fizzBuzz.GetFizzBuzz(5);
Assert.AreEqual("Buzz", result);
}
[Test]
public void GetFizzBuzz_ReturnsFizzBuzzForMultiplesOfThreeAndFive()
{
var fizzBuzz = new FizzBuzz();
string result = fizzBuzz.GetFizzBuzz(15);
Assert.AreEqual("FizzBuzz", result);
}
[Test]
public void GetFizzBuzz_ReturnsNumberForNonMultiplesOfThreeOrFive()
{
var fizzBuzz = new FizzBuzz();
string result = fizzBuzz.GetFizzBuzz(2);
Assert.AreEqual("2", result);
}
}
public class FizzBuzz
{
public string GetFizzBuzz(int number)
{
if (number % 3 == 0 && number % 5 == 0)
return "FizzBuzz";
if (number % 3 == 0)
return "Fizz";
if (number % 5 == 0)
return "Buzz";
return number.ToString();
}
}
在这个示例中,我们按照 TDD 的步骤实现了一个简单的 FizzBuzz
逻辑:
-
编写失败的测试:
- 我们首先编写了
GetFizzBuzz_ReturnsFizzForMultiplesOfThree
测试方法,但此时FizzBuzz
类尚未实现,因此测试会失败。 - 接着,我们依次编写了
GetFizzBuzz_ReturnsBuzzForMultiplesOfFive
和GetFizzBuzz_ReturnsFizzBuzzForMultiplesOfThreeAndFive
测试方法,测试的目标是确保返回正确的字符串。
- 我们首先编写了
-
编写最少量的代码使测试通过:
- 我们实现了
GetFizzBuzz
方法,使其逐步满足各个测试条件。 - 每次添加代码后,我们重新运行测试,直到所有测试通过。
- 我们实现了
-
重构代码:
- 由于代码结构已经比较简单,因此不需要进一步重构。
- 在更复杂的项目中,重构是确保代码简洁和可维护的重要步骤。
通过 TDD 方法,我们能够逐步构建出符合需求的功能,同时确保代码的正确性。TDD 强调在开发的早期阶段发现问题,并在整个开发过程中保持代码的高质量。
6. 自动化测试和持续集成
自动化测试和持续集成 (CI) 是现代软件开发的重要组成部分。通过自动化测试,开发团队可以在每次代码更改时自动运行所有测试,从而快速发现和修复问题。持续集成是一种开发实践,强调频繁地将代码集成到共享代码库,并通过自动化构建和测试来验证代码。
6.1 使用 CI 工具
常见的 CI 工具包括 Jenkins、GitHub Actions、Azure DevOps 等。这些工具可以配置为在每次代码提交时自动运行测试并生成报告。
示例:使用 GitHub Actions 配置 CI
在 GitHub 中,可以通过 .github/workflows
文件夹下的配置文件设置自动化测试。
name: .NET Core
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup .NET Core
uses: actions/setup-dotnet@v1
with:
dotnet-version: '6.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --no-restore
- name: Run tests
run: dotnet test --no-build --verbosity normal
在这个示例中,GitHub Actions 配置了一个简单的 CI 流程:
- 当代码推送到
main
分支时,自动触发构建。 - 使用最新的 .NET Core SDK 恢复依赖项并构建项目。
- 运行所有单元测试,并在发现问题时生成报告。
7. 性能测试和负载测试
除了功能性测试,性能测试和负载测试也是确保应用程序稳定性的重要环节。这些测试可以帮助开发者了解系统在高负载情况下的表现,并找出潜在的性能瓶颈。
7.1 使用 BenchmarkDotNet 进行性能测试
BenchmarkDotNet 是一个强大的性能测试库,专门用于测量和比较 .NET 代码的性能。
示例:使用 BenchmarkDotNet 测试性能
首先,确保安装了 BenchmarkDotNet 包:
Install-Package BenchmarkDotNet
然后,编写性能测试类:
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System;
public class Benchmarks
{
private readonly Calculator calculator = new Calculator();
[Benchmark]
public int AddNumbers()
{
return calculator.Add(100, 200);
}
[Benchmark]
public int SubtractNumbers()
{
return calculator.Subtract(200, 100);
}
}
class Program
{
static void Main()
{
var summary = BenchmarkRunner.Run<Benchmarks>();
}
}
在这个示例中,BenchmarkDotNet
会自动运行测试并生成详细的性能报告,包括执行时间、分配内存等数据。
7.2 使用 Apache JMeter 进行负载测试
Apache JMeter 是一个流行的负载测试工具,可以模拟大量用户访问,测试应用程序的性能。
示例:使用 Apache JMeter
- 下载并安装 Apache JMeter。
- 配置测试计划,添加 HTTP 请求、线程组等。
- 运行负载测试,生成报告,分析结果。
JMeter 提供了丰富的可视化报告,帮助开发者分析系统的性能瓶颈和改进空间。
8. 测试覆盖率和代码质量分析
测试覆盖率是衡量测试质量的重要指标,表示代码中被测试覆盖的比例。工具如 coverlet
可以帮助开发者计算测试覆盖率,并生成详细的报告。
8.1 使用 coverlet 测试覆盖率
示例:使用 coverlet 计算测试覆盖率
- 安装 coverlet 包:
dotnet add package coverlet.collector
- 运行测试并生成覆盖率报告:
dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=cobertura
- 查看生成的覆盖率报告,了解哪些部分的代码尚未被测试覆盖。
通过使用这些工具,开发者可以持续监控和提升代码的测试覆盖率,确保代码质量达到预期标准。
C# 预处理
1. 概述
预处理指的是在编译器编译源代码之前,处理代码中的一些特殊指令。这些指令称为预处理指令,在 C# 中以 #
开头。C# 预处理指令并不像 C/C++ 中的预处理器那样功能丰富,但它们在条件编译、定义符号、区域划分和错误处理方面非常有用。
2. 常见的预处理指令
2.1 #define
和 #undef
#define
用于定义一个符号,而 #undef
则用于取消定义一个符号。这些符号通常用于条件编译。
示例:定义和取消定义符号
#define DEBUG
using System;
class Program
{
static void Main()
{
#if DEBUG
Console.WriteLine("调试模式下的代码");
#endif
#if RELEASE
Console.WriteLine("发布模式下的代码");
#endif
}
}
在这个示例中,如果定义了 DEBUG
符号,那么编译器将在编译时包含 #if DEBUG
块中的代码。
2.2 #if
, #elif
, #else
, #endif
这些指令用于条件编译,它们根据符号是否定义来决定是否编译对应的代码块。
示例:条件编译
#define FEATURE_A
using System;
class Program
{
static void Main()
{
#if FEATURE_A
Console.WriteLine("启用了 FEATURE_A");
#elif FEATURE_B
Console.WriteLine("启用了 FEATURE_B");
#else
Console.WriteLine("未启用任何特性");
#endif
}
}
在这个示例中,只有在定义了 FEATURE_A
或 FEATURE_B
时,对应的代码块才会被编译。
2.3 #region
和 #endregion
#region
和 #endregion
用于定义代码的折叠区域,通常用于组织和简化大段代码,使其更易读。
示例:使用区域
using System;
class Program
{
static void Main()
{
#region Initialization
int a = 10;
int b = 20;
#endregion
int result = a + b;
Console.WriteLine($"结果: {result}");
}
}
在这个示例中,#region Initialization
和 #endregion
定义了一个折叠区域,通常用于在代码编辑器中更方便地管理代码。
2.4 #warning
和 #error
#warning
生成一个编译器警告,而 #error
则生成一个编译器错误。这在代码中标记特定的编译问题时非常有用。
示例:生成警告和错误
#define OLD_API
using System;
class Program
{
static void Main()
{
#if OLD_API
#warning 使用了过时的 API,请更新代码
#endif
#error 编译终止:遇到严重错误
}
}
在这个示例中,#warning
会生成编译器警告,提醒开发者需要更新代码,而 #error
则会中止编译并生成错误。
2.5 #line
#line
指令用于改变编译器报告的行号和文件名。这在生成代码或包含文件时可能会有用,但在日常开发中很少使用。
示例:改变行号和文件名
#line 200 "MyFile.cs"
using System;
class Program
{
static void Main()
{
Console.WriteLine("这行代码在 MyFile.cs 的第 200 行");
}
}
在这个示例中,编译器将认为 Console.WriteLine
位于 MyFile.cs
的第 200 行。
2.6 #pragma
#pragma
指令提供了编译器特定的功能。C# 支持一些常用的 #pragma
指令,例如 #pragma warning
。
示例:使用 #pragma warning
控制警告
using System;
class Program
{
static void Main()
{
#pragma warning disable CS0168 // 禁用特定警告
int unusedVariable;
#pragma warning restore CS0168 // 恢复警告
Console.WriteLine("警告控制示例");
}
}
在这个示例中,#pragma warning disable
禁用了 CS0168
警告(未使用的变量),而 #pragma warning restore
恢复了该警告。
3. 预处理指令的应用场景
3.1 条件编译用于多环境构建
条件编译可以在不同的构建环境中选择性地包含或排除代码。例如,您可以在调试和发布模式下编译不同的日志代码。
#if DEBUG
Console.WriteLine("调试模式下的详细日志");
#else
Console.WriteLine("发布模式下的简洁日志");
#endif
3.2 区域划分提高代码可读性
#region
指令用于组织大段代码,使其更易读和维护,尤其是在涉及多个功能或步骤时。
#region Initialization
// 初始化代码
#endregion
#region Processing
// 处理代码
#endregion
3.3 警告和错误用于代码审查
通过使用 #warning
和 #error
,可以在代码审查过程中突出显示需要关注的部分,或在某些条件下阻止编译。
#if LEGACY_CODE
#error 此部分代码已过时,禁止编译
#endif
4. 预处理指令的注意事项
-
简洁性:预处理指令虽然强大,但也可能使代码变得复杂和难以维护。应谨慎使用,避免过度依赖条件编译。
-
可读性:尽量使用
#region
来组织代码,而不是使用#define
创建大量条件编译分支,这样可以保持代码的可读性和一致性。 -
调试友好:当使用
#error
和#warning
时,确保在开发过程中不要遗漏处理这些错误或警告,以免影响后续的调试和维护。
5. 实践示例
假设我们有一个应用程序,需要在不同的操作系统平台上进行不同的编译和处理。在 Windows 和 Linux 平台上,我们可能需要编译不同的代码。
using System;
class Program
{
static void Main()
{
#if WINDOWS
Console.WriteLine("这是 Windows 平台的代码");
#elif LINUX
Console.WriteLine("这是 Linux 平台的代码");
#else
Console.WriteLine("这是未定义的平台代码");
#endif
}
}
在这个示例中,我们通过预处理指令为不同的操作系统平台编译不同的代码。这种方式常用于跨平台开发中。
C# 内存管理
1. 概述
内存管理是指在程序运行期间,动态分配、使用和释放内存的过程。在 C# 中,内存管理主要由 .NET 的垃圾回收器 (Garbage Collector, GC) 自动管理。尽管垃圾回收器能够自动处理大部分内存管理任务,但了解其工作原理和内存管理的基本概念对开发高效和稳定的应用程序非常重要。
2. 内存分配
在 C# 中,内存分为两种主要类型:栈内存 (Stack) 和 堆内存 (Heap)。
2.1 栈内存
栈内存用于存储值类型 (Value Types) 和方法调用的局部变量。栈内存的分配和释放是自动且快速的,当方法调用结束时,分配给该方法的栈内存会自动释放。
示例:值类型的栈内存分配
int a = 10;
int b = 20;
在这个示例中,a
和 b
是整数(值类型),它们被分配在栈内存中。
2.2 堆内存
堆内存用于存储引用类型 (Reference Types),如对象和数组。当一个对象被创建时,它会在堆内存中分配空间,并返回一个引用地址,这个地址存储在栈上。
示例:引用类型的堆内存分配
class Person
{
public string Name { get; set; }
}
Person person = new Person();
person.Name = "John";
在这个示例中,Person
对象被分配在堆内存中,而 person
变量(存储在栈上)持有对该对象的引用。
3. 垃圾回收 (Garbage Collection)
垃圾回收器 (GC) 是 .NET 运行时的一个内置机制,用于自动管理堆内存。它会跟踪堆上对象的引用情况,并在对象不再被使用时释放这些对象占用的内存。
3.1 GC 的工作原理
GC 使用代数回收 (Generational Collection) 的策略,将托管堆内存分为以下几代:
- 第 0 代 (Generation 0):存储短期存活的对象,垃圾回收会最频繁地清理这部分内存。
- 第 1 代 (Generation 1):存储稍长期存活的对象,通常是从第 0 代晋升而来。
- 第 2 代 (Generation 2):存储长期存活的对象,例如应用程序启动后持续存在的对象。
GC 通过以下步骤工作:
- 标记 (Marking):标记所有存活的对象。
- 清理 (Sweeping):清理未被标记的对象,释放内存。
- 压缩 (Compacting):压缩内存,以减少碎片并提高内存使用效率。
3.2 触发垃圾回收
垃圾回收通常由 .NET 运行时自动触发,但也可以手动触发,尽管手动触发通常是不推荐的。
示例:手动触发垃圾回收
GC.Collect();
GC.WaitForPendingFinalizers();
在这个示例中,GC.Collect()
触发垃圾回收,GC.WaitForPendingFinalizers()
等待所有终结器线程执行完毕。
4. IDisposable 接口与 using
语句
虽然 GC 能够自动回收大部分内存,但对于非托管资源(如文件句柄、数据库连接等),我们需要手动释放。这可以通过实现 IDisposable
接口并使用 using
语句来完成。
4.1 实现 IDisposable
接口
实现 IDisposable
接口的类需要提供 Dispose()
方法,该方法用于释放非托管资源。
示例:实现 IDisposable
接口
public class ResourceHolder : IDisposable
{
private bool disposed = false;
public void UseResource()
{
if (disposed)
throw new ObjectDisposedException("ResourceHolder");
Console.WriteLine("使用资源");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
// 释放托管资源
}
// 释放非托管资源
disposed = true;
}
}
~ResourceHolder()
{
Dispose(false);
}
}
在这个示例中,ResourceHolder
类实现了 IDisposable
接口,并在 Dispose()
方法中释放资源。同时实现了析构函数 (~ResourceHolder()
),以防忘记手动调用 Dispose()
时仍然可以释放资源。
4.2 使用 using
语句
using
语句可以简化资源的管理,并确保资源在使用完毕后自动释放。
示例:使用 using
语句
using (var resource = new ResourceHolder())
{
resource.UseResource();
}
在这个示例中,using
语句确保了在 UseResource()
执行完毕后,Dispose()
方法会被自动调用,释放资源。
5. 内存泄漏与管理技巧
尽管有垃圾回收机制,内存泄漏仍然可能发生,通常由以下原因引起:
- 事件处理器未取消订阅:当对象订阅了事件但未在对象销毁前取消订阅时,可能导致内存泄漏。
- 未释放非托管资源:未正确调用
Dispose()
方法释放非托管资源。 - 静态引用:对象被静态变量持有引用,导致它无法被回收。
5.1 取消事件订阅
示例:正确取消事件订阅
public class EventPublisher
{
public event EventHandler MyEvent;
public void RaiseEvent()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}
}
public class EventSubscriber : IDisposable
{
private EventPublisher publisher;
public EventSubscriber(EventPublisher publisher)
{
this.publisher = publisher;
this.publisher.MyEvent += HandleEvent;
}
public void HandleEvent(object sender, EventArgs e)
{
Console.WriteLine("处理事件");
}
public void Dispose()
{
if (publisher != null)
{
publisher.MyEvent -= HandleEvent; // 取消订阅
}
}
}
在这个示例中,EventSubscriber
通过 Dispose()
方法取消订阅事件,防止内存泄漏。
5.2 避免静态引用导致的内存泄漏
静态变量会在应用程序生命周期内一直存在,持有引用的对象也不会被垃圾回收。因此,应谨慎使用静态变量。
示例:静态变量的使用
public class Cache
{
private static Dictionary<string, object> _cache = new Dictionary<string, object>();
public static void AddToCache(string key, object value)
{
_cache[key] = value;
}
public static void ClearCache()
{
_cache.Clear();
}
}
在这个示例中,我们提供了 ClearCache()
方法,以便在不再需要时释放缓存中的对象,避免内存泄漏。
6. 大对象堆 (Large Object Heap, LOH)
.NET 中的大对象(通常大于 85,000 字节)会被分配到大对象堆 (LOH)。LOH 不会像普通堆那样被频繁压缩,因此可能会导致内存碎片。优化 LOH 的使用可以减少内存碎片,提高应用程序性能。
6.1 优化大对象的使用
- 避免频繁分配大对象:尽量重用大对象或使用内存池技术来减少频繁的分配和释放。
- 优化数组和字符串操作:大数组和长字符串可能会被分配到 LOH,尽量避免频繁创建大数组或字符串。
示例:优化数组使用
class ArrayPoolExample
{
private static readonly ArrayPool<byte> pool = ArrayPool<byte>.Shared;
public void ProcessData()
{
byte[] buffer = pool.Rent(1024 * 1024); // 租借 1MB 的数组
try
{
// 使用 buffer 进行处理
}
finally
{
pool.Return(buffer); // 归还数组到池中
}
}
}
在这个示例中,我们使用 ArrayPool
来重用大数组,减少内存碎片和频繁的内存分配操作,提高应用程序的内存使用效率。
7. 值类型和引用类型的内存管理
理解值类型和引用类型的内存管理对编写高效的 C# 程序非常重要。值类型通常存储在栈上,而引用类型存储在堆上,这两种类型的内存分配和管理有着显著的区别。
7.1 值类型的内存管理
值类型 (如 int
, float
, struct
) 在声明时直接在栈上分配内存。由于栈内存是自动管理的,值类型的分配和释放速度非常快。当方法结束时,栈上的所有局部变量都会自动释放,因此值类型不会引起内存泄漏。
示例:值类型的栈内存分配
struct Point
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
Point p = new Point { X = 10, Y = 20 };
Console.WriteLine($"Point: {p.X}, {p.Y}");
}
}
在这个示例中,Point
结构体是一个值类型,它的内存直接分配在栈上,当 Main
方法结束时,p
的内存会自动释放。
7.2 引用类型的内存管理
引用类型 (如 class
, array
, string
) 在堆上分配内存。它们的生命周期由垃圾回收器管理。当引用类型不再被任何变量引用时,垃圾回收器会将其内存标记为可回收,并在合适的时机释放。
示例:引用类型的堆内存分配
class Person
{
public string Name { get; set; }
}
class Program
{
static void Main()
{
Person person = new Person { Name = "John" };
Console.WriteLine($"Person: {person.Name}");
}
}
在这个示例中,Person
类是引用类型,person
对象被分配在堆上。当 Main
方法结束后,person
的引用被销毁,垃圾回收器将在适当的时候回收 person
对象所占用的内存。
8. 内存管理的最佳实践
8.1 尽量减少内存分配
频繁的内存分配和释放会增加垃圾回收的负担,从而影响应用程序的性能。尽量重用对象,尤其是大对象,可以有效减少内存分配的频率。
示例:对象重用
class ResourcePool
{
private static readonly List<MyObject> pool = new List<MyObject>();
public static MyObject GetObject()
{
if (pool.Count > 0)
{
var obj = pool[0];
pool.RemoveAt(0);
return obj;
}
return new MyObject();
}
public static void ReleaseObject(MyObject obj)
{
pool.Add(obj);
}
}
在这个示例中,我们通过对象池的方式来重用对象,减少了频繁的对象分配和垃圾回收。
8.2 谨慎使用静态变量
静态变量的生命周期与应用程序相同,如果它们持有对大对象的引用,会导致这些对象无法被垃圾回收,从而造成内存泄漏。应谨慎使用静态变量,并在不再需要时及时释放资源。
示例:释放静态变量资源
class GlobalCache
{
private static Dictionary<string, byte[]> cache = new Dictionary<string, byte[]>();
public static void AddToCache(string key, byte[] data)
{
cache[key] = data;
}
public static void ClearCache()
{
cache.Clear();
}
}
在这个示例中,我们提供了 ClearCache()
方法,以确保在不再需要时清空缓存,释放内存。
8.3 使用弱引用 (WeakReference)
对于一些不强制要求保留的对象,可以使用弱引用 (WeakReference),允许垃圾回收器回收这些对象,即使它们仍然被引用。
示例:使用弱引用
class CacheItem
{
private WeakReference dataRef;
public CacheItem(byte[] data)
{
dataRef = new WeakReference(data);
}
public byte[] GetData()
{
return dataRef.Target as byte[];
}
}
在这个示例中,WeakReference
允许 byte[]
数据在内存压力较大时被垃圾回收,即使 CacheItem
仍然存在引用。
8.4 避免频繁的字符串连接
字符串是不可变类型,每次修改都会创建一个新字符串对象,从而可能导致大量的短期对象分配。使用 StringBuilder
可以有效减少这种情况。
示例:使用 StringBuilder
拼接字符串
using System.Text;
class Program
{
static void Main()
{
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
builder.Append("Hello ");
}
string result = builder.ToString();
Console.WriteLine(result);
}
}
在这个示例中,StringBuilder
在拼接字符串时避免了创建多个临时字符串对象,从而提高了内存效率。
结论
内存管理是 C# 编程中的一个重要方面,尽管 .NET 提供了强大的垃圾回收机制来自动管理内存,但理解内存分配的工作原理以及掌握一些最佳实践,能够帮助开发者编写更高效、更稳定的应用程序。
通过合理的内存管理技术,例如重用对象、谨慎使用静态变量、使用弱引用以及优化大对象的使用,开发者可以显著减少内存使用量和提高应用程序的性能。
C# 安全性技术
1. 概述
在现代应用程序开发中,安全性是一个至关重要的方面。C# 提供了多种安全性机制,帮助开发者保护应用程序免受各种安全威胁,包括数据泄露、未授权访问、SQL 注入、跨站脚本攻击(XSS)等。本文档将介绍 C# 中常用的安全性技术和最佳实践。
2. 加密与解密
加密是保护敏感数据的一种有效方法。C# 提供了丰富的类库,用于对数据进行加密和解密。常用的加密算法包括对称加密(如 AES)、非对称加密(如 RSA)以及哈希算法(如 SHA-256)。
2.1 对称加密
对称加密使用相同的密钥进行加密和解密。AES(高级加密标准)是常用的对称加密算法之一。
示例:使用 AES 进行对称加密
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
class Program
{
static void Main()
{
string original = "Sensitive Data";
using (Aes aes = Aes.Create())
{
aes.Key = GenerateKey();
aes.IV = GenerateIV();
byte[] encrypted = EncryptStringToBytes_Aes(original, aes.Key, aes.IV);
Console.WriteLine($"Encrypted: {Convert.ToBase64String(encrypted)}");
string decrypted = DecryptStringFromBytes_Aes(encrypted, aes.Key, aes.IV);
Console.WriteLine($"Decrypted: {decrypted}");
}
}
static byte[] GenerateKey() => Encoding.UTF8.GetBytes("A very secure key");
static byte[] GenerateIV() => Encoding.UTF8.GetBytes("An init vector123");
static byte[] EncryptStringToBytes_Aes(string plainText, byte[] Key, byte[] IV)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = Key;
aesAlg.IV = IV;
ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(plainText);
}
return msEncrypt.ToArray();
}
}
}
}
static string DecryptStringFromBytes_Aes(byte[] cipherText, byte[] Key, byte[] IV)
{
using (Aes aesAlg = Aes.Create())
{
aesAlg.Key = Key;
aesAlg.IV = IV;
ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);
using (MemoryStream msDecrypt = new MemoryStream(cipherText))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
return srDecrypt.ReadToEnd();
}
}
}
}
}
}
在这个示例中,EncryptStringToBytes_Aes
方法用于加密字符串,DecryptStringFromBytes_Aes
方法用于解密。这种方法确保了敏感数据的传输和存储安全。
2.2 非对称加密
非对称加密使用一对密钥:公钥和私钥。公钥用于加密,私钥用于解密。RSA 是一种常用的非对称加密算法。
示例:使用 RSA 进行非对称加密
using System;
using System.Security.Cryptography;
using System.Text;
class Program
{
static void Main()
{
string original = "Sensitive Data";
using (RSA rsa = RSA.Create())
{
rsa.KeySize = 2048;
byte[] encrypted = rsa.Encrypt(Encoding.UTF8.GetBytes(original), RSAEncryptionPadding.OaepSHA256);
Console.WriteLine($"Encrypted: {Convert.ToBase64String(encrypted)}");
byte[] decrypted = rsa.Decrypt(encrypted, RSAEncryptionPadding.OaepSHA256);
Console.WriteLine($"Decrypted: {Encoding.UTF8.GetString(decrypted)}");
}
}
}
在这个示例中,使用 RSA 算法对数据进行加密和解密。RSA 通常用于加密较小的敏感数据或加密对称加密的密钥。
2.3 哈希算法
哈希算法用于生成数据的唯一标识符。常用的哈希算法包括 SHA-256、SHA-512 等。哈希通常用于数据完整性验证或存储密码的散列值。
示例:使用 SHA-256 生成数据哈希
using System;
using System.Security.Cryptography;
using System.Text;
class Program
{
static void Main()
{
string original = "Sensitive Data";
using (SHA256 sha256Hash = SHA256.Create())
{
byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(original));
StringBuilder builder = new StringBuilder();
foreach (byte b in bytes)
{
builder.Append(b.ToString("x2"));
}
Console.WriteLine($"Hash: {builder.ToString()}");
}
}
}
在这个示例中,我们使用 SHA-256 算法生成了字符串的哈希值,常用于密码存储和数据完整性验证。
3. 输入验证与防御性编码
输入验证是防止恶意数据注入和跨站脚本攻击的重要手段。防御性编码则是在编写代码时,假设所有输入都是不可信的,并采取措施保护系统免受攻击。
3.1 SQL 注入攻击防护
SQL 注入是一种常见的攻击方式,通过向 SQL 查询中注入恶意代码,攻击者可以访问、修改或删除数据库中的数据。使用参数化查询可以有效防止 SQL 注入。
示例:防止 SQL 注入
using System;
using System.Data.SqlClient;
class Program
{
static void Main()
{
string userInput = "'; DROP TABLE Users; --";
using (SqlConnection connection = new SqlConnection("your_connection_string"))
{
connection.Open();
string query = "SELECT * FROM Users WHERE Username = @Username";
using (SqlCommand command = new SqlCommand(query, connection))
{
command.Parameters.AddWithValue("@Username", userInput);
using (SqlDataReader reader = command.ExecuteReader())
{
while (reader.Read())
{
Console.WriteLine($"{reader["Username"]}");
}
}
}
}
}
}
在这个示例中,通过使用参数化查询,我们防止了可能的 SQL 注入攻击。用户输入即使包含恶意代码,也不会影响查询的结构。
3.2 防御跨站脚本攻击 (XSS)
XSS 攻击是通过在网页中注入恶意脚本,攻击者可以窃取用户信息或执行恶意操作。确保所有用户输入在显示前进行转义和编码,是防御 XSS 攻击的重要方法。
示例:防御 XSS 攻击
using System;
using System.Web;
class Program
{
static void Main()
{
string userInput = "<script>alert('Hacked!');</script>";
string sanitizedInput = HttpUtility.HtmlEncode(userInput);
Console.WriteLine($"Sanitized Input: {sanitizedInput}");
}
}
在这个示例中,HttpUtility.HtmlEncode
方法对用户输入进行编码,将 <
和 >
等特殊字符转换为 HTML 实体,从而防止恶意脚本执行。
3.3 输入验证
输入验证是防止恶意输入的第一道防线。通过验证输入数据的格式、长度、范围等,可以减少恶意数据进入系统的可能性。
示例:验证用户输入
using System;
using System.Text.RegularExpressions;
class Program
{
static void Main()
{
string email = "user@example.com";
if (IsValidEmail(email))
{
Console.WriteLine("Valid email address.");
}
else
{
Console.WriteLine("Invalid email address.");
}
}
static bool IsValidEmail(string email)
{
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
}
在这个示例中,我们使用正则表达式验证电子邮件地址的格式,确保用户输入符合预期。
4. 认证与授权
认证和授权是确保用户访问权限的重要组成部分。认证是验证用户身份的过程,而授权是基于身份授予特定操作权限的过程。
4.1 使用 ASP.NET Core Identity 进行用户认证
ASP.NET Core Identity 是一个完整的用户管理系统,支持注册、登录、角色管理等功能。ASP.NET Core Identity 可以帮助开发者轻松实现用户认证和授权,确保应用程序的安全性。
4.1 使用 ASP.NET Core Identity 进行用户认证(续)
4.1.1 安装和配置 ASP.NET Core Identity
首先,在 ASP.NET Core 项目中,确保安装了 Microsoft.AspNetCore.Identity.EntityFrameworkCore
包:
dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore
接下来,在 Startup.cs
中配置 Identity 服务:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddControllersWithViews();
}
在这个示例中,我们配置了 Identity,并将其与 ApplicationDbContext
关联,用于管理用户和角色的数据存储。
4.1.2 创建用户模型和数据库上下文
需要创建一个继承自 IdentityUser
的用户模型和一个继承自 IdentityDbContext
的数据库上下文。
示例:创建用户模型和数据库上下文
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
public class ApplicationUser : IdentityUser
{
// 可以在这里添加额外的用户属性
}
public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
4.1.3 添加身份验证中间件
在 Startup.cs
的 Configure
方法中添加身份验证中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication(); // 添加身份验证中间件
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
UseAuthentication
和 UseAuthorization
中间件确保了应用程序可以正确处理用户的认证和授权请求。
4.1.4 用户注册和登录
接下来,我们可以创建控制器和视图,用于处理用户的注册和登录。
示例:用户注册
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
public class AccountController : Controller
{
private readonly UserManager<ApplicationUser> _userManager;
private readonly SignInManager<ApplicationUser> _signInManager;
public AccountController(UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
{
_userManager = userManager;
_signInManager = signInManager;
}
[HttpGet]
public IActionResult Register()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (ModelState.IsValid)
{
var user = new ApplicationUser { UserName = model.Email, Email = model.Email };
var result = await _userManager.CreateAsync(user, model.Password);
if (result.Succeeded)
{
await _signInManager.SignInAsync(user, isPersistent: false);
return RedirectToAction("Index", "Home");
}
foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}
}
return View(model);
}
}
在这个示例中,Register
方法处理用户注册。UserManager
用于创建新用户,而 SignInManager
用于在用户注册后立即登录。
示例:用户登录
[HttpGet]
public IActionResult Login()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (ModelState.IsValid)
{
var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false);
if (result.Succeeded)
{
return RedirectToAction("Index", "Home");
}
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
}
return View(model);
}
在这个示例中,Login
方法处理用户登录请求,并验证用户的电子邮件和密码。
4.1.5 角色管理与授权
角色管理允许管理员创建不同的用户角色,并基于角色来控制用户访问特定的资源或功能。
示例:创建角色和分配角色
public class RoleController : Controller
{
private readonly RoleManager<IdentityRole> _roleManager;
private readonly UserManager<ApplicationUser> _userManager;
public RoleController(RoleManager<IdentityRole> roleManager, UserManager<ApplicationUser> userManager)
{
_roleManager = roleManager;
_userManager = userManager;
}
public async Task<IActionResult> CreateRole(string roleName)
{
if (!await _roleManager.RoleExistsAsync(roleName))
{
await _roleManager.CreateAsync(new IdentityRole(roleName));
}
return RedirectToAction("Index");
}
public async Task<IActionResult> AssignRole(string userEmail, string roleName)
{
var user = await _userManager.FindByEmailAsync(userEmail);
if (user != null)
{
await _userManager.AddToRoleAsync(user, roleName);
}
return RedirectToAction("Index");
}
}
在这个示例中,我们提供了创建角色和分配角色的功能。RoleManager
用于管理角色,而 UserManager
用于将角色分配给用户。
示例:基于角色的授权
[Authorize(Roles = "Admin")]
public IActionResult AdminOnlyAction()
{
return View();
}
在这个示例中,Authorize
特性确保只有具有 Admin
角色的用户才能访问 AdminOnlyAction
方法。
4.2 OAuth 和 OpenID Connect
OAuth 和 OpenID Connect 是常用的身份验证和授权协议,支持通过第三方提供者(如 Google、Facebook)进行用户认证。ASP.NET Core 提供了对这些协议的支持。
示例:使用 Google 登录
首先,安装 Microsoft.AspNetCore.Authentication.Google
包:
dotnet add package Microsoft.AspNetCore.Authentication.Google
然后,在 Startup.cs
中配置 Google 认证:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication()
.AddGoogle(options =>
{
options.ClientId = Configuration["Authentication:Google:ClientId"];
options.ClientSecret = Configuration["Authentication:Google:ClientSecret"];
});
services.AddControllersWithViews();
}
在这个示例中,我们配置了 Google OAuth 认证。用户可以使用 Google 账户登录到您的应用程序。
5. 安全性最佳实践
5.1 强密码策略
确保用户使用强密码是保护账户安全的基本手段。可以在应用程序中设置密码策略,例如最小长度、复杂性要求等。
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 8;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
5.2 使用 HTTPS 加密传输
HTTPS 确保数据在客户端和服务器之间传输时是加密的,防止中间人攻击。确保您的应用程序使用 HTTPS,并在生产环境中强制使用 HSTS(HTTP 严格传输安全)。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts(); // 强制使用 HSTS
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
5.3 定期审计与日志记录
记录用户活动和系统事件是检测潜在安全威胁的重要手段。确保应用程序实现了详细的日志记录和定期的安全审计。
示例:日志记录
private readonly ILogger<Program> _logger;
public Program(ILogger<Program> logger)
{
_logger = logger;
}
public void LogAction(string action)
{
_logger.LogInformation($"Action performed: {action}");
}
在这个示例中,使用 ILogger
记录用户操作,帮助追踪和分析可能的安全事件。
5.4 防止跨站请求伪造 (CSRF)
跨站请求伪造 (CSRF) 是一种通过伪造用户请求来执行未授权操作的攻击。ASP.NET Core 自动为表单生成防御 CSRF 的防伪令牌,确保表单提交的请求来自合法用户。
示例:启用防伪令牌
在视图中使用 @Html.AntiForgeryToken()
生成防伪令牌,并确保控制器中启用了验证。
// 在视图中
<form asp-action="Submit">
@Html.AntiForgeryToken()
<button type="submit">Submit</button>
</form>
// 在控制器中
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Submit()
{
// 处理表单提交
return View();
}
在这个示例中,@Html.AntiForgeryToken()
生成了一个防伪令牌,并将其包含在表单中。[ValidateAntiForgeryToken]
特性确保只有包含有效防伪令牌的请求才能被处理。
5.5 使用内容安全策略 (CSP)
内容安全策略 (CSP) 是一种防御 XSS 攻击的强大工具,通过定义允许加载的内容源,限制恶意脚本的执行。您可以在 HTTP 响应头中添加 CSP 规则。
示例:启用内容安全策略
在 ASP.NET Core 中,您可以通过中间件添加 CSP 头。
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.Use(async (context, next) =>
{
context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'");
await next();
});
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
在这个示例中,我们设置了一个简单的 CSP 规则,只允许从同一源加载脚本和样式。这有效地防止了外部恶意脚本的执行。
5.6 数据库安全性
在数据库操作中,确保数据的安全性和完整性至关重要。除了使用参数化查询防止 SQL 注入外,还应采取其他措施,如使用加密存储敏感数据、定期备份和使用安全的连接字符串。
示例:加密敏感数据
在数据库中存储敏感数据时,使用加密存储而非明文存储。
public class UserService
{
private readonly string _encryptionKey = "Your_Secret_Key";
public void SaveUserPassword(string username, string password)
{
var encryptedPassword = EncryptPassword(password);
// 保存加密后的密码到数据库
}
private string EncryptPassword(string password)
{
using (var aes = Aes.Create())
{
aes.Key = Encoding.UTF8.GetBytes(_encryptionKey);
aes.IV = new byte[16]; // 初始化向量
var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
{
using (var sw = new StreamWriter(cs))
{
sw.Write(password);
}
return Convert.ToBase64String(ms.ToArray());
}
}
}
}
}
在这个示例中,我们使用 AES 加密用户密码,并将加密后的密码存储在数据库中,从而提高了敏感数据的安全性。
5.7 使用安全的依赖库
确保使用的第三方库和框架是安全的,且定期更新以防止已知的安全漏洞。可以使用工具如 OWASP Dependency-Check
来扫描项目中的依赖库并发现潜在的安全问题。
示例:使用 .NET CLI 更新依赖项
dotnet outdated
dotnet add package <Package-Name> --version <Version>
dotnet outdated
工具可以帮助检查项目中的依赖项是否过时,并提示进行更新。
5.8 适当的异常处理和日志记录
不应在生产环境中暴露详细的异常信息,因为这些信息可能被攻击者利用来发现系统的漏洞。应当捕获和处理异常,并使用适当的日志记录机制记录详细信息以供内部分析。
示例:适当的异常处理
public IActionResult ProcessOrder(int orderId)
{
try
{
// 处理订单逻辑
}
catch (Exception ex)
{
_logger.LogError(ex, "Order processing failed.");
return View("Error", new ErrorViewModel { Message = "An error occurred while processing your request." });
}
}
在这个示例中,异常处理确保了用户不会看到敏感的错误信息,而详细的错误记录则保存在日志中以供分析。
6. 安全开发生命周期
安全开发生命周期 (SDL) 是一种在软件开发过程中集成安全实践的框架。SDL 的目标是识别和修复安全漏洞,并通过安全设计和编码实践减少漏洞的数量。
6.1 威胁建模
威胁建模是识别系统潜在安全威胁的过程,并为这些威胁制定对策。威胁建模通常在项目的设计阶段进行。
步骤:进行威胁建模
- 识别资产:识别系统中需要保护的资产,如用户数据、交易信息等。
- 确定威胁:识别可能影响这些资产的威胁,如数据泄露、未授权访问等。
- 评估漏洞:评估系统中的潜在漏洞,这些漏洞可能被利用来攻击资产。
- 实施对策:制定和实施对策来减少或消除这些威胁。
6.2 安全代码审查
代码审查是确保代码安全性的重要步骤。通过定期的代码审查,可以发现和修复潜在的安全漏洞。
示例:代码审查清单
- 确保所有输入都经过验证和清理。
- 检查 SQL 查询是否使用了参数化查询。
- 确保敏感数据在存储和传输时被加密。
- 检查异常处理,确保敏感信息不会暴露给最终用户。
6.3 安全性测试
在项目的不同阶段执行安全性测试,以发现和修复安全问题。常见的安全性测试包括渗透测试、漏洞扫描和代码静态分析。
示例:使用 OWASP ZAP 进行渗透测试
OWASP ZAP 是一款开源的渗透测试工具,主要用于发现 Web 应用程序中的安全漏洞。
- 设置 OWASP ZAP:下载安装 OWASP ZAP,并配置目标应用程序的 URL。
- 运行测试:使用 OWASP ZAP 对目标应用程序进行自动化扫描,识别潜在的安全漏洞。
- 分析结果:查看扫描报告,分析并修复发现的安全问题。
C# 框架与库
1. 概述
C# 作为一门强大的编程语言,拥有丰富的框架和库生态系统,涵盖了从 Web 开发到桌面应用、从数据库访问到并行计算的各个领域。正确选择和使用这些框架和库,可以大大提升开发效率和代码质量。本文档将介绍一些常用的 C# 框架和库,及其应用场景。
2. Web 开发框架
2.1 ASP.NET Core
ASP.NET Core 是一个跨平台的、高性能的开源框架,用于构建现代的、基于云的、基于互联网的应用程序。ASP.NET Core 支持 RESTful API、MVC、Razor Pages 等开发模式,并且通过依赖注入、身份验证、授权等机制,提供了构建企业级 Web 应用的强大功能。
示例:ASP.NET Core MVC 应用程序
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
在这个示例中,我们展示了如何使用 ASP.NET Core 配置和启动一个 MVC 应用程序。ASP.NET Core 提供了丰富的功能,可以轻松构建复杂的 Web 应用。
2.2 Blazor
Blazor 是微软推出的一种使用 C# 进行 Web 开发的框架,支持运行在浏览器中的 WebAssembly 版本(Blazor WebAssembly)和服务器端版本(Blazor Server)。Blazor 允许开发者使用 C# 和 Razor 组件构建交互式的、SPA(单页面应用程序)风格的应用。
示例:Blazor 组件
@page "/counter"
<h3>Counter</h3>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}
在这个示例中,我们定义了一个简单的 Blazor 组件,用于展示一个计数器。Blazor 使得使用 C# 构建现代 Web 应用成为可能,减少了前后端语言切换的复杂性。
3. 数据库访问框架
3.1 Entity Framework Core
Entity Framework Core (EF Core) 是一个轻量级、可扩展的开源 ORM(对象关系映射)框架,支持在 .NET 中使用 C# 类与数据库进行交互。EF Core 提供了数据库迁移、LINQ 查询等功能,使得数据库操作更加直观和简洁。
示例:使用 EF Core 进行数据库访问
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
public class ApplicationDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("your_connection_string");
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ProductService
{
private readonly ApplicationDbContext _context;
public ProductService(ApplicationDbContext context)
{
_context = context;
}
public IEnumerable<Product> GetAllProducts()
{
return _context.Products.ToList();
}
}
在这个示例中,ApplicationDbContext
类配置了 EF Core 与 SQL Server 的连接,并定义了一个 Product
实体。ProductService
类演示了如何使用 EF Core 进行数据查询。
3.2 Dapper
Dapper 是一个轻量级的 ORM 框架,专注于性能和易用性。与 EF Core 不同,Dapper 更接近 SQL 原生操作,适合对性能有较高要求的应用场景。
示例:使用 Dapper 进行数据库访问
using System.Collections.Generic;
using System.Data.SqlClient;
using Dapper;
public class ProductService
{
private readonly string _connectionString;
public ProductService(string connectionString)
{
_connectionString = connectionString;
}
public IEnumerable<Product> GetAllProducts()
{
using (var connection = new SqlConnection(_connectionString))
{
return connection.Query<Product>("SELECT * FROM Products");
}
}
}
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
在这个示例中,Dapper
用于直接执行 SQL 查询并将结果映射到 Product
对象列表中。Dapper 的轻量级和高性能使其成为许多开发者的首选。
4. 并行与异步编程库
4.1 Task Parallel Library (TPL)
Task Parallel Library (TPL) 是 .NET 中用于并行编程的核心库,它简化了并发和并行编程的实现。TPL 允许开发者使用任务 (Task
) 和并行循环 (Parallel.For
/ Parallel.ForEach
) 来编写并行代码。
示例:使用 TPL 实现并行任务
using System;
using System.Threading.Tasks;
class Program
{
static void Main()
{
Task[] tasks = new Task[3];
tasks[0] = Task.Run(() => DoWork(1));
tasks[1] = Task.Run(() => DoWork(2));
tasks[2] = Task.Run(() => DoWork(3));
Task.WaitAll(tasks);
Console.WriteLine("All tasks completed.");
}
static void DoWork(int taskId)
{
Console.WriteLine($"Task {taskId} is working...");
Task.Delay(1000).Wait();
Console.WriteLine($"Task {taskId} is done.");
}
}
在这个示例中,我们创建了三个并行执行的任务,使用 Task.Run
来异步执行工作负载,并通过 Task.WaitAll
等待所有任务完成。
4.2 Async / Await
Async / Await 是 C# 中实现异步编程的关键字,它简化了异步方法的调用和异常处理,帮助开发者编写更清晰和可读的异步代码。
示例:使用 Async / Await 实现异步操作
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
string content = await FetchDataAsync("https://api.github.com/");
Console.WriteLine(content);
}
static async Task<string> FetchDataAsync(string url)
{
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0");
return await client.GetStringAsync(url);
}
}
}
在这个示例中,我们使用 async
和 await
关键字异步获取一个 URL 的内容。HttpClient.GetStringAsync
方法异步执行 HTTP GET 请求,避免阻塞主线程。
5. 测试框架
5.1 xUnit
xUnit 是一个常用的开源测试框架,用于编写单元测试和集成测试。它与 .NET Core 紧密集成,并支持数据驱动测试、并行测试等高级特性。
示例:使用 xUnit 编写单元测试
using Xunit;
public class CalculatorTests
{
[Fact]
public void Add_ReturnsCorrectSum()
{
// Arrange
var calculator = new Calculator();
// Act
int result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
}
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
在这个示例中,我们使用 xUnit 编写了一个简单的单元测试,验证 Calculator.Add
方法是否返回正确的结果。
5.2 NUnit
NUnit 是另一个流行的测试框架,具有强大的断言功能和灵活的测试用例支持。NUnit 广泛用于单元测试、集成测试和接受测试,并且在 .NET 开发者中拥有广泛的社区支持。
示例:使用 NUnit 编写单元测试
using NUnit.Framework;
[TestFixture]
public class CalculatorTests
{
[Test]
public void Add_ReturnsCorrectSum()
{
// Arrange
var calculator = new Calculator();
// Act
int result = calculator.Add(2, 3);
// Assert
Assert.AreEqual(5, result);
}
}
public class Calculator
{
public int Add(int a, int b)
{
return a + b;
}
}
在这个示例中,NUnit
使用 [TestFixture]
特性标记测试类,使用 [Test]
特性标记测试方法,并使用 Assert.AreEqual
进行断言。与 xUnit 类似,NUnit 也非常易用,并提供了强大的测试功能。
6. 桌面应用开发框架
6.1 Windows Presentation Foundation (WPF)
Windows Presentation Foundation (WPF) 是一个用于构建 Windows 桌面应用程序的框架。WPF 提供了强大的图形功能、数据绑定、样式和控件模板,适用于构建现代的、具有丰富用户界面的桌面应用程序。
示例:使用 WPF 构建简单的桌面应用
<Window x:Class="WpfApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="200" Width="400">
<Grid>
<Button Name="btnClickMe" Content="Click Me" HorizontalAlignment="Center" VerticalAlignment="Center" Click="btnClickMe_Click"/>
</Grid>
</Window>
using System.Windows;
namespace WpfApp
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void btnClickMe_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Hello, WPF!");
}
}
}
在这个示例中,我们创建了一个简单的 WPF 应用程序,其中包含一个按钮,当用户点击按钮时,会弹出一个消息框。WPF 强大的数据绑定和模板功能使其非常适合构建复杂的桌面应用程序。
6.2 Windows Forms
Windows Forms 是另一种用于构建 Windows 桌面应用程序的框架。虽然相对于 WPF,Windows Forms 更加简单和轻量级,但它仍然在许多业务应用场景中得到广泛应用。
示例:使用 Windows Forms 构建简单的桌面应用
using System;
using System.Windows.Forms;
public class MainForm : Form
{
private Button btnClickMe;
public MainForm()
{
btnClickMe = new Button();
btnClickMe.Text = "Click Me";
btnClickMe.Location = new System.Drawing.Point(100, 100);
btnClickMe.Click += BtnClickMe_Click;
Controls.Add(btnClickMe);
}
private void BtnClickMe_Click(object sender, EventArgs e)
{
MessageBox.Show("Hello, Windows Forms!");
}
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
在这个示例中,我们使用 Windows Forms 创建了一个简单的桌面应用程序,同样包含一个按钮,点击按钮会弹出一个消息框。Windows Forms 提供了简单的控件和布局系统,适合快速开发桌面应用程序。
7. 库与工具
7.1 AutoMapper
AutoMapper 是一个用于对象到对象映射的库,能够简化 DTO(数据传输对象)和业务模型之间的映射工作。它可以通过配置,自动映射具有相同属性的类,从而减少手动编写映射代码的负担。
示例:使用 AutoMapper 进行对象映射
using AutoMapper;
public class Source
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Destination
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
static void Main()
{
var config = new MapperConfiguration(cfg => cfg.CreateMap<Source, Destination>());
var mapper = config.CreateMapper();
var source = new Source { Id = 1, Name = "SourceName" };
var destination = mapper.Map<Destination>(source);
Console.WriteLine($"Id: {destination.Id}, Name: {destination.Name}");
}
}
在这个示例中,AutoMapper
自动将 Source
对象映射到 Destination
对象,减少了手动映射的工作量,尤其在大型项目中,这种工具可以显著提高生产力。
7.2 Serilog
Serilog 是一个灵活的、可扩展的日志记录库,它允许将日志记录到多个目标(如文件、控制台、数据库等),并支持结构化日志记录。Serilog 以其简单的配置和强大的扩展能力而广受欢迎。
示例:使用 Serilog 记录日志
using Serilog;
class Program
{
static void Main()
{
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.WriteTo.File("logs/log.txt", rollingInterval: RollingInterval.Day)
.CreateLogger();
Log.Information("This is an information message.");
Log.Warning("This is a warning message.");
Log.Error("This is an error message.");
Log.CloseAndFlush();
}
}
在这个示例中,Serilog
被配置为将日志记录到控制台和文件。Serilog 的强大之处在于它支持丰富的日志格式和目标,能够满足各种复杂的日志需求。
7.3 Polly
Polly 是一个用于实现瞬态故障处理和弹性策略的库,例如重试、断路器、超时、回退等策略。Polly 可以帮助构建更稳定和弹性的应用程序。
示例:使用 Polly 实现重试策略
using Polly;
using System;
using System.Net.Http;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var httpClient = new HttpClient();
var retryPolicy = Policy
.Handle<HttpRequestException>()
.RetryAsync(3, onRetry: (exception, retryCount) =>
{
Console.WriteLine($"Retry {retryCount} due to {exception.Message}");
});
await retryPolicy.ExecuteAsync(async () =>
{
var response = await httpClient.GetStringAsync("https://api.github.com/");
Console.WriteLine(response);
});
}
}
在这个示例中,我们使用 Polly
实现了一个重试策略,在 HTTP 请求失败时自动重试多次。Polly 提供了丰富的策略,可以轻松应对分布式系统中的各种瞬态故障。
7.4 Hangfire
Hangfire 是一个用于在 .NET 应用程序中管理和执行后台任务的库。它支持定时任务、延迟任务和长时间运行的任务,并且可以将任务持久化到数据库中,以便在应用程序重启后继续执行。
示例:使用 Hangfire 创建定时任务
using Hangfire;
using Microsoft.Owin;
using Owin;
using System;
[assembly: OwinStartup(typeof(HangfireExample.Startup))]
namespace HangfireExample
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
GlobalConfiguration.Configuration.UseSqlServerStorage("your_connection_string");
app.UseHangfireDashboard();
app.UseHangfireServer();
RecurringJob.AddOrUpdate(() => Console.WriteLine("Recurring job executed"), Cron.Minutely);
}
}
}
在这个示例中,Hangfire
被配置为使用 SQL Server 存储任务信息,并设置了一个每分钟执行一次的定时任务。Hangfire 提供了丰富的任务管理功能,可以轻松处理各种后台任务需求。
8. 总结与选择指南
选择合适的框架和库是构建高效、可维护应用程序的关键。以下是一些选择指南:
-
Web 开发:如果需要构建现代 Web 应用程序,ASP.NET Core 是首选;如果偏好使用 C# 构建单页应用,Blazor 是不错的选择。
-
数据库访问:Entity Framework Core 提供了强大的 ORM 支持,适合多数数据库操作需求;Dapper 适合需要高性能的场景。
-
异步与并行编程:Task Parallel Library (TPL) 和 Async / Await 是 C# 中标准的并行和异步编程模型。
-
测试:xUnit 和 NUnit 都是强大的测试框架,选择其一主要取决于团队习惯。xUnit 更加简洁,默认支持并行测试,NUnit 则具有更为丰富的特性和断言选项。
-
桌面应用开发:如果需要构建 Windows 桌面应用程序,WPF 是适合构建复杂、现代 UI 的工具,支持丰富的数据绑定和样式配置。Windows Forms 更简单,适合快速开发或维护传统应用程序。
-
对象映射:对于需要在不同层次之间进行对象映射的应用,AutoMapper 是一种高效的选择,减少了手动编写映射代码的繁琐工作。
-
日志记录:Serilog 是一种非常灵活且功能丰富的日志库,支持结构化日志记录和多种输出目标,非常适合需要详细日志管理的应用。
-
故障处理:在构建需要高稳定性和弹性的应用程序时,Polly 提供了丰富的瞬态故障处理策略,包括重试、断路器、回退、超时等,能够帮助应用程序更好地应对不稳定的外部依赖。
-
后台任务处理:对于需要处理定时任务、长时间运行任务或任务队列的应用,Hangfire 是一种简单易用且功能强大的解决方案,支持任务的持久化和管理。
9. 框架与库的集成与扩展
9.1 集成第三方库
在构建现代 C# 应用程序时,通常需要集成多个第三方库来完成复杂的功能。通过适当的设计和架构,可以使这些库无缝集成并协同工作。
示例:集成 AutoMapper 与依赖注入
using AutoMapper;
using Microsoft.Extensions.DependencyInjection;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 配置 AutoMapper
services.AddAutoMapper(typeof(Startup));
// 注册其他服务
services.AddScoped<IProductService, ProductService>();
}
}
// 配置映射配置文件
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<Product, ProductDto>();
}
}
在这个示例中,我们展示了如何在 ASP.NET Core 中通过依赖注入集成 AutoMapper,并配置了一个简单的映射配置文件。这种集成方式可以确保各个库之间的协作更加顺畅。
9.2 扩展框架功能
大多数框架和库都提供了扩展点,允许开发者根据需要自定义功能。例如,可以通过编写中间件扩展 ASP.NET Core 的功能,或通过编写自定义策略扩展 Polly 的故障处理能力。
示例:扩展 Polly 的自定义策略
using Polly;
public static class CustomPollyExtensions
{
public static ISyncPolicy AddCustomRetryPolicy(this PolicyBuilder policyBuilder)
{
return policyBuilder.Retry(3, onRetry: (exception, retryCount) =>
{
Console.WriteLine($"Custom retry {retryCount} due to {exception.Message}");
});
}
}
在这个示例中,我们通过扩展方法为 Polly 添加了一个自定义的重试策略,这样在应用程序中可以轻松复用这一策略。
10. 总结
C# 生态系统提供了丰富的框架与库,涵盖了从前端到后端、从数据访问到并行处理的各个领域。开发者可以根据项目需求选择合适的工具,并通过合理的设计与架构,实现不同框架和库的无缝集成,构建高效、稳定和可维护的应用程序。
在选择框架与库时,除了考虑技术功能之外,还应综合考虑团队的熟悉程度、社区支持、长期维护成本等因素。通过不断学习和实践,开发者可以更好地掌握这些工具,并在实际项目中发挥它们的最大价值。