七、DDD(领域驱动设计)
领域驱动设计(Domain-Driven Design,简称DDD)是一种软件开发方法,旨在将业务领域的复杂性和需求直观地映射到软件代码中,以便更好地理解、设计和维护复杂的应用程序。DDD强调将软件开发过程中的关注点从技术转移到业务领域,并提供了一组概念和模式来实现这一目标。DDD是一套思想方法论,核心在于 建模 ,提供了一套 落地的一些实践,核心在于战略设计。
以下是一些与DDD相关的关键概念:
- 实体(Entity):实体是业务领域中具有标识的对象,通常对应于数据库中的表。实体具有生命周期,可以具有状态和行为。例如,一个订单(Order)可以是一个实体。
- 值对象(Value Object):值对象是没有自己的标识,通常是不可变的对象,用于表示概念上的值。例如,订单中的金额可以作为值对象。
- 聚合根(Aggregate Root):聚合根是实体的根,它负责管理相关实体和值对象。它充当访问和维护整个聚合的入口点。
- 仓储(Repository):仓储是用于访问和持久化领域对象的接口,它隐藏了数据访问细节,提供了对领域对象的抽象访问方式。
- 领域事件(Domain Events):领域事件是在领域中发生的事件,它们用于表示领域的状态变化,通常与业务规则和行为相关。
- 领域服务(Domain Services):领域服务是一种不属于实体或值对象的对象,它包含与特定领域相关的业务逻辑。
下面是一个简单的C#示例,演示如何使用DDD的一些基本概念:
// 实体类-首发于jhrs.com(江湖人士网) public class Order { public int Id { get; private set; } public decimal TotalAmount { get; private set; } // 构造函数 public Order(int id, decimal totalAmount) { Id = id; TotalAmount = totalAmount; } // 领域事件 public void MarkAsPaid() { // 发出领域事件 DomainEvents.Publish(new OrderPaidEvent(this)); } } // 值对象类-首发于jhrs.com(江湖人士网) public class Money { public decimal Amount { get; private set; } public string Currency { get; private set; } public Money(decimal amount, string currency) { Amount = amount; Currency = currency; } } // 领域事件类-首发于jhrs.com(江湖人士网) public class OrderPaidEvent { public Order Order { get; private set; } public OrderPaidEvent(Order order) { Order = order; } } // 领域事件发布器-首发于jhrs.com(江湖人士网) public static class DomainEvents { public static event Action<object> OnDomainEvent; public static void Publish(object domainEvent) { OnDomainEvent?.Invoke(domainEvent); } } // 使用示例-首发于jhrs.com(江湖人士网) public class Program { public static void Main() { var order = new Order(1, new Money(100, "USD")); order.MarkAsPaid(); // 订阅领域事件 DomainEvents.OnDomainEvent += HandleOrderPaidEvent; } public static void HandleOrderPaidEvent(object domainEvent) { if (domainEvent is OrderPaidEvent orderPaidEvent) { Console.WriteLine($"jhrs.com友情提示:狗日的,订单ID{orderPaidEvent.Order.Id}已经支付了,你个哈麻屁又来喊我付款,老子遇到你了。"); } } }
最新高级C#面试知识点
这个示例演示了一个订单(Order)实体、值对象(Money)、领域事件(OrderPaidEvent)和领域事件发布器(DomainEvents)的基本概念。DDD的核心思想是将业务领域的复杂性进行建模,使代码更加清晰、可维护和易于理解。在实际应用中,DDD可能涉及更复杂的业务规则、聚合和仓储等概念。
八、浅拷贝和深拷贝
所谓的对象拷贝(复制)就是为对象创建副本,得到相同的对象。
深拷贝:指的是拷贝一个对象时,**不仅仅把对象的引用进行拷贝,还把该对象引用的值也一起拷贝。这样进行深拷贝后的副本对象就和源对象互相独立**,其中任何一个的成员对象改动都不会对另外一个成员对象造成影响。
完全将对象中的所有字段(引用类型和值类型等)都复制到副本对象中,这些字段都会被重新创建并且复制,副本对象内的值并不会因为源对象数据的值的修改而跟着发生改变。**(也就是说深拷贝出来的副本对象中,对象里的数据如果是值类型,栈内容是其值本身;对于引用类型,其值是托管堆中保存的具体的值,而不是托管堆的内存地址。因此对拷贝出来的副本对象的修改不会反映到被拷贝的源对象上。深拷贝本质上就是软件设计模式里的原型模式。与C#对应的接口是ICloneable。)
浅拷贝: 仅仅**把对象的引用进行拷贝,但是拷贝对象和源对象还是引用同一份实体**。此时,其中一个的成员对象的改变都会影响到另一个的成员对象。
简单的复制栈的内容,对于值类型,栈内容是其值本身,对于引用类型,其值为托管堆的内存地址,对拷贝的对象的修改会反映到被拷贝的对象。(同样都是将对象中的所有字段都复制到副本对象中,副本对象里的数据如果是值类型,栈内容是其值本身,在源数据内修改值类型,副本的值类型不会发生改变,因为值类型本身在栈内有不同的地址。但是如果副本对象的数据是引用类型,由于浅拷贝只是拷贝引用类型值的引用,也就是堆的地址,所以副本对象的引用类型数据发生改变时,源对象中的引用类型数据也会跟着改变。)
C#实现浅拷贝:**调用MemberwiseClone方法**,创建一个新的对象,然后复制当前对象的非静态字段的新对象创建一个浅表副本。
C#深拷贝的有三种实现:
1. **反射**
2. **序列化**
3. **表达式树**
参考文章:
https://zhuanlan.zhihu.com/p/349094034
https://www.cnblogs.com/abeam/p/11919838.html
https://www.cnblogs.com/dotnet261010/p/12329220.html
https://www.cnblogs.com/SF8588/p/16152078.html
https://blog.csdn.net/ananlele_/article/details/108126267
九、什么是泛型
定义:是将不确定的类型预先定义下来的一种C#高级语法,泛型引入了类型参数的概念,类似于C++中的模板,泛型将类型参数的概念引入 .NET,这样就可设计具有以下特征的类和方法:在客户端代码声明并初始化这些类或方法之前,这些类或方法会延迟指定一个或多个类型。例如,通过使用泛型类型参数 `T`,可以编写其他客户端代码能够使用的单个类,而不会产生运行时转换或装箱操作的成本或风险。(类型参数化)
**泛型类和泛型方法兼具可重用性、类型安全性和效率,这是非泛型类和非泛型方法无法实现的**。
泛型更强调一种范式,时间、空间上其实都没省,提升了代码的复用率减少冗余量。
###### 泛型(Generic)的特性
使用泛型是一种增强程序功能的技术,具体表现在以下几个方面:
– 它有助于您最大限度地重用代码、保护类型的安全以及提高性能。
– 您可以创建泛型集合类。.NET 框架类库在 *System.Collections.Generic* 命名空间中包含了一些新的泛型集合类。您可以使用这些泛型集合类来替代 *System.Collections* 中的集合类。
– 您可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
– 您可以对泛型类进行约束以访问特定数据类型的方法。
– 关于泛型数据类型中使用的类型的信息可在运行时通过使用反射获取。
泛型类型约束:
where约束:泛型定义中的 `where` 子句指定对用作泛型类型、方法、委托或本地函数中类型参数的参数类型的约束。 约束可指定接口、基类或要求泛型类型为引用、值或非托管类型。 约束声明类型参数必须具有的功能,并且约束必须位于任何声明的基类或实现的接口之后。
`new` 约束指定泛型类或方法声明中的类型实参必须有公共的无参数构造函数。 若要使用 `new` 约束,则该类型不能为抽象类型。
参考文章:
https://learn.microsoft.com/zh-cn/dotnet/csharp/fundamentals/types/generics
https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/generics/generic-methods
https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/where-generic-type-constraint
https://learn.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/where-generic-type-constraint
https://www.cnblogs.com/dotnet261010/p/9034594.html
https://www.runoob.com/csharp/csharp-generic.html
https://zhuanlan.zhihu.com/p/262002365
十、GC(垃圾回收)
C#的垃圾回收机制(Garbage Collection,简称GC)是一种自动内存管理系统,用于在运行时跟踪和回收不再使用的对象,以释放内存资源并防止内存泄漏。C#的垃圾回收机制允许开发人员专注于应用程序的逻辑,而无需显式地管理内存。
以下是C#垃圾回收机制的关键概念和工作原理:
- 垃圾回收器(Garbage Collector):垃圾回收器是.NET运行时的一部分,负责监视和管理内存中的对象。它定期检测不再被引用的对象,并回收这些对象所占用的内存。
- 对象的生命周期:每个在C#中创建的对象都有一个生命周期。当对象不再被任何引用变量引用时,它变成垃圾对象,等待垃圾回收器的处理。
- 引用计数和可达性分析:C#垃圾回收器使用可达性分析(Reachability Analysis)来确定对象是否可达。这意味着它从根对象开始,追踪对象之间的引用链,找出哪些对象是可达的。不可达的对象被标记为垃圾。
- 代和世代回收:垃圾回收器将堆内存分成不同的代。新创建的对象放在第0代(Generation 0)中。当第0代满时,垃圾回收器会检查并回收不可达的对象。如果对象在第0代经历了多次垃圾回收而仍然存活,它将升级到第1代(Generation 1)或第2代(Generation 2),并且垃圾回收的频率会降低,以提高性能。
- 垃圾回收算法:C#垃圾回收器使用一种叫做标记-清除(Mark and Sweep)的算法。它首先标记所有可达的对象,然后清除不可达的对象,最终压缩内存中的对象,以便更好地管理内存碎片。
- 终结器(Finalizers):C#允许对象定义终结器,它们是特殊的方法,会在对象被销毁之前被调用。通常,终结器用于释放非托管资源,例如文件句柄。垃圾回收器在处理终结器时会进行额外的工作。
- 弱引用(Weak References):C#允许创建弱引用,这些引用不会阻止对象被垃圾回收。这对于需要跟踪对象的生命周期但不希望阻止其回收的情况很有用。
- 手动内存管理:虽然C#的垃圾回收机制自动处理大多数内存管理任务,但开发人员仍然可以通过
IDisposable
接口和using
语句来显式释放托管和非托管资源,以提高性能和资源的可预测性释放。
总的来说,C#的垃圾回收机制通过监视对象的可达性和管理内存回收,帮助开发人员避免内存泄漏和手动内存管理的复杂性。这使得开发更容易,同时确保应用程序的内存资源得以有效利用。
10.1 服务器GC和客户机GC区别
C#的垃圾回收(Garbage Collection,GC)在Windows服务器和普通桌面客户机上的基本工作原理是相同的,因为它们都是基于.NET运行时的。然而,在实际应用中,有一些区别需要考虑:
- 资源和性能要求:
- 服务器应用程序通常需要处理更高的负载和大量的并发请求,因此对于服务器端应用来说,垃圾回收的性能和资源利用是关键。服务器GC(Server Garbage Collection)在服务器上进行了优化,它使用多个线程和堆段(heap segments)来提高回收性能。
- 桌面应用程序通常面临较低的负载,因此对于桌面应用来说,性能要求通常较低。它们使用工作站GC(Workstation Garbage Collection),它的性能目标相对较低。
- GC模式:
- 服务器GC在多核服务器上默认启用,并支持并发GC。它试图最小化应用程序的停滞时间,以支持高并发负载。
- 桌面GC通常是基于工作站的,性能较低,适合一般的桌面应用程序,但不适用于高并发服务器应用。
- 垃圾回收的频率:
- 服务器GC通常会更频繁地触发,以确保在高负载条件下有效管理内存。这可以在某种程度上增加停滞时间,但有助于维护可用性。
- 桌面GC通常会更少地触发,以减少对桌面应用程序的干扰。
- 垃圾回收的配置:
- 开发人员可以通过配置来自定义垃圾回收行为,包括在服务器和桌面环境中。配置可以影响堆大小、并发度、回收触发频率等方面。
- 内存消耗:
- 服务器GC可能会使用更多的内存来优化性能,因为服务器通常有更多的可用内存。
- 桌面GC会更加节约内存,因为桌面应用通常面临有限的内存资源。
总之,虽然C#的垃圾回收在服务器和桌面环境中的基本工作原理相同,但在性能和配置上存在差异,以满足不同类型应用的需求。开发人员应根据应用程序的具体要求来选择合适的GC配置和策略。
十一、分布式锁
在C#中实现分布式锁通常需要使用外部的分布式锁服务或者分布式锁库,因为分布式锁需要在多个应用程序实例之间协调。一种常见的方式是使用ZooKeeper、etcd、Redis等分布式系统,或者使用专门的分布式锁库,如DistributedLock等。
以下是一个简单的C#示例,演示如何使用Redis实现分布式锁:
using StackExchange.Redis; using System; using System.Threading; public class DistributedLockExample { private static IDatabase redisDatabase; private static string lockKey = "myLock"; private static string lockValue = Guid.NewGuid().ToString(); private static TimeSpan lockTimeout = TimeSpan.FromSeconds(30); public static void Main(string[] args) { // 连接到Redis服务器 ConnectionMultiplexer redis = ConnectionMultiplexer.Connect("YourRedisConnectionString"); redisDatabase = redis.GetDatabase(); // 尝试获取分布式锁 if (AcquireLock()) { try { // 在这里执行需要加锁的操作 Console.WriteLine("Lock acquired! Performing some work..."); Thread.Sleep(TimeSpan.FromSeconds(10)); } finally { // 释放锁 ReleaseLock(); Console.WriteLine("Lock released."); } } else { Console.WriteLine("Failed to acquire lock. Another process is holding the lock."); } // 关闭Redis连接 redis.Close(); } private static bool AcquireLock() { return redisDatabase.StringSet(lockKey, lockValue, lockTimeout, When.NotExists); } private static void ReleaseLock() { var luaScript = LuaScript.Prepare("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"); luaScript.Execute(redisDatabase, new { lockKey }, new { lockValue }); } }
最新高级C#面试知识点
在上面的示例中,我们首先连接到Redis服务器,然后使用AcquireLock
方法尝试获取分布式锁。如果成功获取锁,我们执行需要加锁的操作,然后使用ReleaseLock
方法释放锁。如果无法获取锁,表示其他进程已经持有锁。
这只是一个简单示例,实际中你可能需要更多的错误处理和异常处理,以确保分布式锁的可靠性。你还可以根据具体的需求调整锁的超时时间、重试策略等参数。
十二、lock(锁)
在C#中,lock
是一种用于多线程编程的关键字,它用于实现互斥锁(Mutex),确保一段代码在同一时刻只能由一个线程执行,从而避免多线程竞争条件导致的数据不一致或并发问题。lock
关键字基于.NET的监视器(Monitor)类实现了互斥锁。
以下是关于lock
关键字的一些重要特点和用法:
- 使用方式:
lock
关键字通常与Monitor
类的Enter
和Exit
方法一起使用,用于创建一个代码块,这个代码块在同一时刻只能被一个线程执行。通常,你会锁定一个对象,以确保在不同线程之间访问共享资源时的互斥。
object lockObject = new object(); lock (lockObject) { // 互斥的代码块 jhrs.com 示例代码 }
- 锁定的对象:
lock
关键字需要提供一个对象来进行锁定。这个对象通常是一个引用类型变量,可以是任何对象,但通常用于锁定共享资源。不同线程锁定同一个对象,以确保互斥。 - 隐式释放锁:
lock
关键字不需要显式释放锁,它会在代码块执行完毕后自动释放锁。这确保了即使发生异常,锁也会被正确释放。 - 递归锁:C# 的
lock
关键字支持递归锁,这意味着同一线程可以多次获取同一个锁,而不会导致死锁。但要小心使用递归锁,确保在合适的情况下使用它。 - 性能注意事项:使用
lock
可能会引入性能开销,因为它会导致线程阻塞等待锁的释放。在某些情况下,可以考虑使用更轻量级的同步原语,如Monitor
、Mutex
、Semaphore
或ReaderWriterLockSlim
,以根据具体需求来提高性能。
lock
关键字是C#中管理多线程并发的一种简单而有效的方式,但需要小心使用,以避免潜在的死锁和性能问题。在编写多线程应用程序时,确保正确使用锁来保护共享资源是至关重要的。
【江湖人士】(jhrs.com)原创文章,作者:江小编,如若转载,请注明出处:https://jhrs.com/2023/47094.html
扫码加入电报群,让你获得国外网赚一手信息。
文章标题:15+最新高级C#面试知识点