47. 设计模式与 C# 中的应用
设计模式是编写可重用和可维护代码的最佳实践,它们帮助你解决常见的软件设计问题。在 C# 中,常见的设计模式包括:
1. 工厂模式(Factory Pattern)
工厂模式通过提供一个创建对象的接口来允许子类决定实例化哪个类。它将对象的创建逻辑与使用逻辑解耦,提高了系统的灵活性和扩展性。
示例:简单工厂模式
using System;
public interface IProduct
{
void Operate();
}
public class ProductA : IProduct
{
public void Operate()
{
Console.WriteLine("Product A is working");
}
}
public class ProductB : IProduct
{
public void Operate()
{
Console.WriteLine("Product B is working");
}
}
public class ProductFactory
{
public static IProduct CreateProduct(string productType)
{
return productType.ToLower() switch
{
"a" => new ProductA(),
"b" => new ProductB(),
_ => throw new ArgumentException("Invalid product type")
};
}
}
class Program
{
static void Main()
{
var product = ProductFactory.CreateProduct("A");
product.Operate(); // Output: Product A is working
}
}
解释:
ProductFactory
类提供了一个静态方法CreateProduct()
,根据传入的字符串决定返回哪种类型的产品。- 这种方法允许根据需要在不同情况下创建不同的对象,而不需要显式地构造每个产品类。
2. 单例模式(Singleton Pattern)
单例模式确保一个类只有一个实例,并提供全局访问点。它通常用于需要控制资源访问的场景,如数据库连接、配置管理等。
示例:线程安全的单例模式
using System;
public class Singleton
{
private static Singleton _instance;
private static readonly object _lock = new object();
private Singleton() { }
public static Singleton Instance
{
get
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton();
}
return _instance;
}
}
}
public void DoSomething()
{
Console.WriteLine("Doing something...");
}
}
class Program
{
static void Main()
{
Singleton.Instance.DoSomething();
}
}
解释:
Singleton
类使用了双重锁定机制(double-check locking)来确保线程安全,并且只创建一个实例。lock
关键字用于同步代码块,避免多个线程同时创建实例。
3. 观察者模式(Observer Pattern)
观察者模式允许一个对象(被观察者)在其状态改变时通知所有依赖它的对象(观察者)。它适用于实现事件处理和消息广播的场景。
示例:观察者模式
using System;
using System.Collections.Generic;
public interface IObserver
{
void Update(string message);
}
public class ConcreteObserver : IObserver
{
private readonly string _name;
public ConcreteObserver(string name)
{
_name = name;
}
public void Update(string message)
{
Console.WriteLine($"{_name} received message: {message}");
}
}
public class Subject
{
private readonly List<IObserver> _observers = new List<IObserver>();
public void Attach(IObserver observer)
{
_observers.Add(observer);
}
public void Detach(IObserver observer)
{
_observers.Remove(observer);
}
public void Notify(string message)
{
foreach (var observer in _observers)
{
observer.Update(message);
}
}
}
class Program
{
static void Main()
{
var subject = new Subject();
var observer1 = new ConcreteObserver("Observer 1");
var observer2 = new ConcreteObserver("Observer 2");
subject.Attach(observer1);
subject.Attach(observer2);
subject.Notify("Hello Observers!");
// Output:
// Observer 1 received message: Hello Observers!
// Observer 2 received message: Hello Observers!
}
}
解释:
Subject
类维护了一个观察者列表,并提供Attach
、Detach
和Notify
方法来管理观察者。- 每当
Notify
被调用时,所有的观察者都会收到更新。
48. 性能优化技巧
性能优化是开发高效、响应快速应用程序的重要方面。在 C# 中,以下是一些常见的优化技术:
1. 使用值类型(Value Types)而非引用类型(Reference Types)
值类型通常比引用类型性能更高,因为它们在栈上分配内存,而引用类型在堆上分配。避免不必要的堆分配可以提高性能。
示例:结构体(Value Type)
public struct Point
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
Point point = new Point { X = 10, Y = 20 };
Console.WriteLine($"Point: ({point.X}, {point.Y})");
}
}
解释:
Point
结构体是一个值类型,它存储在栈中而不是堆中。值类型适合存储小的数据,避免不必要的内存分配。
2. 避免频繁的垃圾回收
垃圾回收会导致性能开销,因此减少堆上的对象创建频率和管理内存的方式非常重要。尽量复用对象,减少内存碎片。
示例:使用对象池
using System;
using System.Collections.Generic;
public class ObjectPool<T> where T : new()
{
private readonly Stack<T> _pool = new Stack<T>();
public T GetObject()
{
return _pool.Count > 0 ? _pool.Pop() : new T();
}
public void ReturnObject(T obj)
{
_pool.Push(obj);
}
}
public class ExampleClass
{
public string Name { get; set; }
}
class Program
{
static void Main()
{
var pool = new ObjectPool<ExampleClass>();
var obj1 = pool.GetObject();
obj1.Name = "Object 1";
Console.WriteLine(obj1.Name);
pool.ReturnObject(obj1);
}
}
解释:
ObjectPool<T>
提供了一个对象池来管理ExampleClass
对象的复用,避免频繁的对象创建和销毁。- 通过
GetObject
和ReturnObject
方法,可以高效地管理对象。
3. 选择合适的数据结构
选择合适的数据结构可以大幅提高算法的效率。例如,使用哈希表(Dictionary
)进行查找比使用列表(List
)更高效。
示例:查找操作优化
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
var dictionary = new Dictionary<int, string>();
dictionary[1] = "One";
dictionary[2] = "Two";
dictionary[3] = "Three";
Console.WriteLine(dictionary[2]); // 输出:Two
}
}
解释:
Dictionary
是一个哈希表,它通过键值对存储数据,提供常数时间的查找效率(O(1))。- 与列表相比,查找数据的效率更高,尤其是数据量较大时。
49. 反射(Reflection)与动态类型
反射允许程序在运行时获取对象的类型信息和动态调用方法。反射对于框架和库开发非常有用,但由于其性能开销较大,应该谨慎使用。
1. 基本的反射操作
using System;
using System.Reflection;
class Program
{
static void Main()
{
var type = typeof(Person);
var instance = Activator.CreateInstance(type);
var property = type.GetProperty("Name");
property.SetValue(instance, "John Doe");
var name = property.GetValue(instance);
Console.WriteLine(name); // Output: John Doe
}
}
public class Person
{
public string Name { get; set; }
}
解释:
typeof()
用于获取类型信息。Activator.CreateInstance()
动态创建对象实例。- 使用反射可以动态访问对象的属性和方法。
2. 动态类型(dynamic
)
dynamic
类型允许你在运行时决定数据的类型,它绕过了编译时的类型检查,非常适合处理不确定类型的场景。
using System;
class Program
{
static void Main()
{
dynamic obj = "Hello, dynamic!";
Console.WriteLine(obj); // Output: Hello, dynamic!
}
}
解释:
dynamic
类型的变量在运行时才能确定类型,这意味着你可以随时改变它的类型。- 尽管
dynamic
提供了灵活性,但使用时要小心,因为它绕过了编译时的类型安全检查。
50. 与数据库交互:Entity Framework Core
在 C# 中,Entity Framework Core(EF Core)是一个非常强大的对象关系映射(ORM)工具,用于与数据库进行交互。它允许你以对象的方式操作数据库,而不需要直接编写 SQL 查询。EF Core 支持多种数据库,如 SQL Server、SQLite、PostgreSQL 等。
1. 创建模型并映射到数据库
首先,我们需要定义一些模型类,并使用 EF Core 来自动创建数据库表。以下是一个简单的例子,展示如何设置 EF Core 和定义模型:
安装 Entity Framework Core NuGet 包
你可以使用以下命令安装 EF Core:
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Design
创建模型类
using Microsoft.EntityFrameworkCore;
using System;
public class Product
{
public int ProductId { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}
public class ApplicationDbContext : DbContext
{
public DbSet<Product> Products { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder options)
=> options.UseSqlServer("YourConnectionStringHere");
}
2. 创建数据库
EF Core 提供了迁移(Migrations)功能,可以根据模型类自动生成数据库。
dotnet ef migrations add InitialCreate
dotnet ef database update
这些命令将创建数据库,并根据你的模型类生成相应的表结构。
3. 数据库操作
一旦数据库和模型准备好,你就可以使用 EF Core 提供的 API 来进行数据的增、删、改、查操作。
using (var context = new ApplicationDbContext())
{
// 增加数据
var product = new Product { Name = "Laptop", Price = 1000 };
context.Products.Add(product);
context.SaveChanges();
// 查询数据
var products = context.Products.ToList();
foreach (var prod in products)
{
Console.WriteLine($"{prod.ProductId}: {prod.Name} - {prod.Price}");
}
}
解释:
DbSet<Product>
代表数据库中的一张表。context.SaveChanges()
用于将更改保存到数据库。- 通过
ToList()
方法,你可以从数据库中查询所有的产品。
51. 异步编程的高级技巧
在 C# 中,异步编程(async
/await
)不仅能提高应用程序的响应性,还能有效地处理 I/O 密集型操作。虽然我们之前讨论过基本的异步编程,但在实际应用中,可能还会遇到一些更复杂的异步操作场景。
1. 异步与并行化结合:处理多个异步任务
有时你需要同时启动多个异步任务并等待它们的结果。Task.WhenAll()
和 Task.WhenAny()
可以帮助你有效管理多个任务。
示例:并行执行多个任务
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
var task1 = FetchDataAsync("https://api.example.com/data1");
var task2 = FetchDataAsync("https://api.example.com/data2");
// 等待两个任务都完成
await Task.WhenAll(task1, task2);
Console.WriteLine("Both tasks completed.");
}
static async Task FetchDataAsync(string url)
{
await Task.Delay(1000); // 模拟异步请求
Console.WriteLine($"Data fetched from {url}");
}
}
解释:
Task.WhenAll()
等待多个任务完成后才继续执行。Task.WhenAny()
用于等待第一个完成的任务,但这里的例子使用了WhenAll
来等待所有任务完成。
2. 异常处理与异步任务
在处理异步任务时,异常的捕获和处理也变得尤为重要。你可以通过 try-catch
语句来捕获异步任务中的异常。
示例:捕获异步任务的异常
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
try
{
await FetchDataAsync("https://api.example.com/invalid-url");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
}
}
static async Task FetchDataAsync(string url)
{
if (url.Contains("invalid"))
{
throw new InvalidOperationException("Invalid URL");
}
await Task.Delay(1000); // 模拟异步操作
Console.WriteLine($"Data fetched from {url}");
}
}
解释:
- 如果
FetchDataAsync
方法中发生异常,我们可以通过try-catch
捕获并处理它。 - 异步方法中抛出的异常会传递给调用者,并且需要在调用
await
时捕获。
52. C# 8.0 及更高版本的新特性
C# 8.0 引入了多个有用的特性,进一步增强了语言的表达能力和易用性。这些新特性可以帮助你编写更简洁、优雅和健壮的代码。
1. 空引用类型(Nullable Reference Types)
C# 8.0 引入了对空引用类型的支持,使得在代码中更加显式地处理空值问题,从而减少了 NullReferenceException
的发生。
示例:启用和使用空引用类型
在项目文件中启用空引用类型:
<Nullable>enable</Nullable>
然后,在代码中使用:
public class Person
{
public string? Name { get; set; }
public int Age { get; set; }
}
class Program
{
static void Main()
{
Person? person = new Person { Name = null, Age = 30 };
// 检查 Name 是否为空
if (person?.Name != null)
{
Console.WriteLine(person.Name);
}
}
}
解释:
- 使用
string?
表示Name
可以为null
,启用空引用类型后,编译器会要求你显式地处理可能为空的引用类型。 - 通过
?.
操作符,可以安全地访问可能为空的对象成员。
2. 异步流(Asynchronous Streams)
异步流允许你以异步的方式从数据源中流式获取数据。这对于从远程 API 获取大量数据或在后台加载数据时非常有用。
示例:异步流(IAsyncEnumerable
)
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
await foreach (var number in GetNumbersAsync())
{
Console.WriteLine(number);
}
}
static async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 1; i <= 5; i++)
{
await Task.Delay(1000); // 模拟异步操作
yield return i;
}
}
}
解释:
IAsyncEnumerable<int>
表示一个异步枚举器,它允许你异步地迭代数据。- 使用
await foreach
循环来异步获取数据,每次获取一个数据项并等待。
3. Switch 表达式
C# 8.0 引入了 switch
表达式,它比传统的 switch
语句更加简洁和强大,支持更复杂的模式匹配。
示例:Switch 表达式
using System;
class Program
{
static void Main()
{
var day = 3;
var result = day switch
{
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
4 => "Thursday",
5 => "Friday",
6 => "Saturday",
7 => "Sunday",
_ => "Invalid day"
};
Console.WriteLine(result);
}
}
解释:
switch
表达式返回一个值,而不是仅仅执行一段代码。- 语法更加简洁,并且允许你轻松地为每个案例返回结果。
53. C# 9.0 和 10.0 新特性
随着 C# 9.0 和 10.0 的发布,更多的新特性被加入到语言中,继续简化代码编写并增强功能。
1. 记录类型(Record Types)
C# 9.0 引入了 record
类型,这是一种引用类型,它自带了值比较功能,适用于数据传输对象(DTO)等场景。
示例:记录类型
public record Person(string Name, int Age);
class Program
{
static void Main()
{
var person1 = new Person("Alice", 30);
var person2 = person1 with { Name = "Bob" };
Console.WriteLine(person1); // Output: Person { Name = Alice, Age = 30 }
Console.WriteLine(person2); // Output: Person { Name = Bob, Age = 30 }
}
}
解释:
record
类型会自动生成一个值比较的方法和一个with
表达式,方便创建修改后的副本。
54. 内存管理与性能优化
在 C# 中,内存管理由垃圾回收器(GC)负责,但这并不意味着我们完全不需要关心内存的使用。了解如何优化内存管理可以帮助你编写更高效的程序,尤其是在处理大量数据或高并发操作时。
1. 值类型与引用类型的区别
在 C# 中,值类型和引用类型有显著的区别:
- 值类型(如
int
、struct
)存储在栈上,通常具有较小的内存占用,且复制时会创建独立的副本。 - 引用类型(如
class
、string
)存储在堆上,复制时只会复制引用,不会复制数据本身。
在优化时,合理使用值类型和引用类型可以显著提升性能。
示例:值类型与引用类型的内存分配
public struct Point
{
public int X;
public int Y;
}
public class PointClass
{
public int X;
public int Y;
}
class Program
{
static void Main()
{
Point p1 = new Point { X = 10, Y = 20 }; // 值类型,存储在栈上
PointClass p2 = new PointClass { X = 10, Y = 20 }; // 引用类型,存储在堆上
// 值类型赋值会复制整个对象
Point p3 = p1;
p3.X = 50; // 修改副本
// 引用类型赋值只复制引用,原对象会受影响
PointClass p4 = p2;
p4.X = 50; // 修改原对象
}
}
2. 使用内存池和对象池
频繁创建和销毁对象会导致频繁的垃圾回收,进而影响性能。对象池可以复用对象,减少堆上的内存分配,降低垃圾回收的负担。
示例:使用 ArrayPool
对象池
ArrayPool<T>
是 .NET 中的一个内存池类,用于管理数组的复用,避免频繁分配新数组。
using System;
using System.Buffers;
class Program
{
static void Main()
{
// 从对象池中租用一个数组
var pool = ArrayPool<int>.Shared;
int[] array = pool.Rent(100); // 租用大小为 100 的数组
// 使用数组
array[0] = 10;
array[1] = 20;
// 归还数组
pool.Return(array);
}
}
解释:
ArrayPool<T>
提供了一个共享的对象池来复用数组,减少内存分配和垃圾回收。- 使用
Rent()
方法从池中租用一个数组,使用完后,调用Return()
方法将数组归还给池。
3. 内存泄漏和避免内存泄漏
虽然 C# 有垃圾回收机制,但如果你的应用程序持有对不再需要的对象的引用,垃圾回收器就无法回收这些对象,导致内存泄漏。以下是一些常见的内存泄漏原因和解决方法:
-
事件订阅没有解除订阅:当对象订阅事件时,必须在不再需要时取消订阅,否则这些对象会被持有在事件列表中。
-
过度使用静态字段:静态字段会在应用程序生命周期内持续存在,可能导致内存不被及时释放。
示例:取消事件订阅
using System;
class Publisher
{
public event EventHandler SomethingHappened;
public void OnSomethingHappened() => SomethingHappened?.Invoke(this, EventArgs.Empty);
}
class Subscriber
{
public void Respond(object sender, EventArgs e) => Console.WriteLine("Event triggered!");
}
class Program
{
static void Main()
{
var publisher = new Publisher();
var subscriber = new Subscriber();
publisher.SomethingHappened += subscriber.Respond;
// 取消订阅,防止内存泄漏
publisher.SomethingHappened -= subscriber.Respond;
}
}
解释:
- 通过
-=
操作符取消事件订阅,避免对象因事件订阅而无法被垃圾回收。
55. 设计模式进阶应用
在实际应用中,设计模式的使用远不仅仅是理论上的好习惯,它们能帮助你在开发复杂系统时处理常见问题。以下是一些设计模式的进阶应用和优化。
1. 装饰者模式(Decorator Pattern)
装饰者模式允许你在不改变对象自身的情况下,动态地为其添加额外的行为。这对于需要在运行时修改对象行为的场景特别有效。
示例:装饰者模式
using System;
public interface ICar
{
void Drive();
}
public class BasicCar : ICar
{
public void Drive() => Console.WriteLine("Driving a basic car.");
}
public class CarDecorator : ICar
{
protected readonly ICar _car;
public CarDecorator(ICar car)
{
_car = car;
}
public virtual void Drive() => _car.Drive();
}
public class SportsCar : CarDecorator
{
public SportsCar(ICar car) : base(car) { }
public override void Drive()
{
base.Drive();
Console.WriteLine("Driving a sports car with more speed!");
}
}
class Program
{
static void Main()
{
ICar basicCar = new BasicCar();
ICar sportsCar = new SportsCar(basicCar);
basicCar.Drive(); // Output: Driving a basic car.
sportsCar.Drive(); // Output: Driving a basic car. Driving a sports car with more speed!
}
}
解释:
CarDecorator
类为任何实现ICar
接口的对象提供基础装饰功能。SportsCar
继承自CarDecorator
,通过重写Drive()
方法,添加特定的行为。
2. 适配器模式(Adapter Pattern)
适配器模式用于将一个类的接口转换成另一个类的接口,使得不兼容的接口能够协同工作。在处理不同接口的系统集成时,这非常有用。
示例:适配器模式
using System;
public interface ITarget
{
void Request();
}
public class Adaptee
{
public void SpecificRequest() => Console.WriteLine("Specific request from Adaptee.");
}
public class Adapter : ITarget
{
private readonly Adaptee _adaptee;
public Adapter(Adaptee adaptee) => _adaptee = adaptee;
public void Request() => _adaptee.SpecificRequest();
}
class Program
{
static void Main()
{
Adaptee adaptee = new Adaptee();
ITarget target = new Adapter(adaptee);
target.Request(); // Output: Specific request from Adaptee.
}
}
解释:
Adapter
类通过实现ITarget
接口,委托Request()
方法给Adaptee
的SpecificRequest()
方法,从而实现了接口的兼容。
56. 开发大型应用的架构设计
当你开始开发复杂的应用程序时,合理的架构设计至关重要。以下是一些常见的架构设计模式,帮助你构建可维护、可扩展的大型应用。
1. 分层架构(Layered Architecture)
分层架构通常包括以下几层:
- 表示层(Presentation Layer):负责与用户交互。
- 业务逻辑层(Business Logic Layer):处理核心业务逻辑。
- 数据访问层(Data Access Layer):与数据库交互。
通过分层架构,可以将不同的功能模块分离,使得代码更易于维护和扩展。
示例:简单的分层架构
// Data Access Layer
public class ProductRepository
{
public List<Product> GetAllProducts()
{
return new List<Product>
{
new Product { Id = 1, Name = "Product1" },
new Product { Id = 2, Name = "Product2" }
};
}
}
// Business Logic Layer
public class ProductService
{
private readonly ProductRepository _repository;
public ProductService(ProductRepository repository)
{
_repository = repository;
}
public List<Product> GetProducts()
{
return _repository.GetAllProducts();
}
}
// Presentation Layer
class Program
{
static void Main()
{
var productRepository = new ProductRepository();
var productService = new ProductService(productRepository);
var products = productService.GetProducts();
foreach (var product in products)
{
Console.WriteLine($"Product: {product.Name}");
}
}
}
解释:
- 数据访问层负责与数据库交互,业务逻辑层负责处理业务,表示层则负责展示数据。
- 分层架构确保了各层之间的独立性和灵活性,使得更改某一层的实现不会影响其他层。