1.抽象类和接口有什么区别?
**抽象类是特殊的类,不能被直接实例化。它们可以包含抽象方法和普通方法,以及属性和事件等成员。抽象类的主要目的是作为其他类的基类(只能被继承),提供部分实现的功能和一些共享的属性或方法
**接口是一种引用类型,也不能被实例化。它只包含方法的声明,不包含任何实现。接口的成员(包括方法、属性、事件等)都是公开的。接口的主要目的是定义一组行为,这些行为可以被任何实现该接口的类所共享。
2.说一下构造函数?
**父类构造函数先执行:当创建一个子类的对象时,首先会调用父类的构造函数。如果父类没有定义任何构造函数(即没有显式声明任何构造函数),C#编译器会自动提供一个默认
的无参构造函数。如果父类有一个或多个带参数的构造函数,并且没有默认构造函数,那么在创建子类对象时,必须在子类的构造函数中通过: base(...)语法显式地调用父类的构造函数。
**子类构造函数后执行:在父类构造函数执行完毕后,接着会执行子类的构造函数。子类构造函数中可以使用base关键字来访问父类的成员,包括父类的构造函数。
**父类构造函数不会被继承,父类的静态属性和私有属性也不会被继承
3.讲一下面向对象?三大特性?
**面向对象是一种编码的思想,就OOP思想,编写代码的过程中,对于类的使用可以是编写代码更加边界,让代码结构更清晰。
**面向对象有三大特性:封装,继承,多态
1.封装就是隐藏对象的内部实现细节,只提供公开的接口进行交互
2.继承就是一个类可以继承另一个类的属性,并且可以添加自己的属性,当然,私有的和静态的属性不能被继承
@3.多态就是同一个接口,不同的实现方式
拓展问题,如何实现多态呢?
**通过接口实现:基类定义的接口,然后在派生类里面有其他实现
**通过虚方法实现:基类用virtual关键字声明方法,派生类使用override关键字重写该方法
**用抽象类和抽象方法实现:使用abstract关键字定义抽象类和抽象方法,必须在派生类中提供具体的实现
4.讲一下抽象方法(abstract)和虚方法(virtual)?
**虚方法是C#中用于实现运行时多态的机制。基类中标记为virtual的方法可以在派生类中通过override关键字重写。
**抽象方法是定义在抽象类中的方法,不包含实现代码。抽象方法要求派生类必须重写这个方法。
**在这两种情况下,方法都可以在派生类中重写。不同点在于:
如果基类方法标记为虚拟,则不需要在派生类中显式地使用override关键字。
如果基类方法是抽象的,则派生类必须使用override关键字来重写该方法,否则基类将保持抽象状态。(如果派生类没有实现所有基类的抽象方法,那么这个派生类也必须声明为抽象类)
**在实际应用中,选择虚方法还是抽象方法取决于需求:
如果你想要有一个默认的实现,并且允许子类以不同的方式实现它,那么使用虚方法。
如果你想要强制子类提供一个实现,那么使用抽象方法。
抽象方法不能使用static和virtual修饰符,且只能定义在抽象类中
抽象方法也不能是private(因为派生类需要访问它来实现)
虚方法可以有方法体,抽象方法不能有方法体
密封方法(sealed)可以阻止虚方法在派生类中被进一步重写
抽象类可以包含非抽象成员(字段、属性、方法等)
5.说一下拆箱和装箱?
**拆箱:引用类型向值类型转换的过程,拆箱过程会获取已装箱的值类型的值
在拆箱过程中,会检查对象引用是否确实指向一个装箱的值类型实例。
如果是,该值就从堆上复制回栈上的值类型变量中。如果引用的是null或者不是装箱的值类型,那么会抛出一个异常。
**装箱:值类型转换为引用类型,装箱过程会创建一个新的对象,将值类型的数据复制到该对象中
当值类型被装箱时,它会在托管堆上分配一个新的对象实例,并将该值复制到新分配的对象中。然后,这个对象的引用会被返回。装箱操作通常发生在以下情况:
当值类型作为参数传递给只接受引用类型参数的方法时。
当值类型被赋值给一个引用类型的变量时。
装箱和拆箱涉及到内存分配和值类型的转换,对性能有影响,所以在开发过程中应当注意避免不必要的装箱和拆箱的操作。
6.说一下事务的特性?
**原子性(Atomicity):原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部完成,要么全部不完成。
如果任何一个操作失败,整个事务就会回滚到事务开始前的状态,就像这个事务从来没有执行过一样。
**一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态转变到另一个一致性状态。一致性与业务规则有关,
比如转账操作,无论事务是否成功,参与转账的两个账户的总金额应该保持不变。
**隔离性(Isolation):多个事务并发执行时,一个事务的操作不应影响其他事务。隔离性通过锁和其他机制来实现,
以防止并发操作导致数据不一致。
**持久性(Durability):一旦事务完成(无论成功还是失败),其对数据库所做的更改就是永久的。即使系统崩溃或发生故障,
重新启动后数据库还能恢复到事务成功结束时的状态。
7.说一下锁?
**lock关键字:这是C#中最简单和最常用的锁机制。lock关键字用于获取对象的互斥锁,确保同一时间只有一个线程能够执行特定的代码块。它用于实现线程同步和互斥
**Monitor类:提供了一种更灵活的同步机制,通过Monitor.Enter和Monitor.Exit方法来获取和释放对象的锁。除了基本的互斥锁外,Monitor类还提供了等待和通知的功能,可以实现更复杂的线程同步方案
**Mutex类:这是一种操作系统级别的内核对象,用于进程间的同步。在C#中,Mutex类封装了操作系统提供的互斥体,可以用于实现跨进程的线程同步
**Semaphore类:也是一种操作系统级别的同步原语,用于控制同时访问共享资源的线程数量。Semaphore类允许指定一个计数器,表示可访问共享资源的线程数量,适用于一些限流的场景
**AutoResetEvent和ManualResetEvent类:这些是基于事件的同步原语,用于线程间的信号通知和同步。它们允许一个或多个线程等待另一个线程发送信号后继续执行
//lock 锁代码示例,lock关键字自动管理锁得获取和释放
object lockObject = new object();
lock (lockObject)
{
// 访问共享资源的代码
};
//Monitor锁代码示例
object _locker = new object();
//获取锁加上超时时间,避免死锁方法之一
TimeSpan timeout = TimeSpan.FromSeconds(5);
var isLock = Monitor.TryEnter(_locker, timeout);
if (isLock)
{
try
{
//上锁代码
}
finally
{
Monitor.Exit(_locker);
}
}
else
{
//获取锁超时
}
//Mutex锁代码示例
//创建Mutex实例
Mutex m = new Mutex();
//加锁
m.WaitOne();
//访问共享资源
//解锁
m.ReleaseMutex();
//Semaphore 使用示例
Semaphore semaphore = new Semaphore(3, 3);//允许最大访问数
//访问共享资源前,等待Semaphore
semaphore.WaitOne();
//执行完之后,释放Semaphore
semaphore.Release();
**Semaphore和Mutex有什么区别?
Semaphore和Mutex在并发编程中都是用于同步的机制,但它们在用途和操作上存在明显区别:
用途:Semaphore用于控制对多个同类资源的访问,允许多个线程同时访问不同资源;而Mutex主要用于实现线程间的互斥,确保同一时刻只有一个线程能访问共享资源或临界区,保护单个资源。
操作:Semaphore可以由一个线程获取并由另一个线程释放,灵活性更高;而Mutex通常由一个线程获取并最终释放,其他线程必须等待直到锁被释放。
计数器:Semaphore的计数器可以是任意非负整数,表示可用资源的数量;Mutex的计数器只有0和1两个值,类似于特殊的二元Semaphore。
总的来说,Semaphore适用于需要控制多个资源访问的场景,而Mutex适用于保护单个资源的场景
各种锁:
-
互斥锁(Mutex):互斥锁是一种最常见的同步原语,用于控制对共享资源的访问。当一个线程已经获得了互斥锁,其他线程必须等待,直到锁被释放才能继续执行。互斥锁适用于长时间持有锁的情况,可以有效避免CPU资源的浪费,但其系统开销较高
-
自旋锁(SpinLock):自旋锁通过重复执行一些简单的指令来尝试获取锁,直到锁可用。这种锁适用于锁定时间非常短的场景,因为它不会使线程挂起,而是通过忙等待来尝试获取锁。自旋锁适合于锁定时间非常短且CPU资源充足的情况,因为它不会消耗线程挂起和恢复的开销
-
读写锁(ReaderWriterLock):读写锁允许多个线程同时读取共享资源,但只允许单个线程写入共享资源。这种锁适用于读操作远多于写操作的场景,可以提高并发性能
-
信号量(Semaphore):信号量允许多个线程同时访问同一个资源,但需要指定最大并发数。信号量适用于需要限制并发数量的场景
-
临界区(CriticalSection):临界区是一种轻量级的同步机制,用于保护一小段代码或数据。临界区适用于保护代码区域,防止多个线程同时执行
-
混合锁(Monitor):混合锁通常通过
lock
关键字实现,是Monitor
的语法糖。Monitor
可以设置超时时间,避免无限制的等待,并且有Pulse
和PulseAll
方法用于唤醒等待的线程
8.说一下数据库锁?
数据库锁主要包括以下几种类型,以及它们的作用如下:
**共享锁(Shared Lock):也称为读锁,允许多个事务同时读取同一资源。
当一个事务持有共享锁时,其他事务可以继续获取该资源的共享锁,但不能获取排他锁。
主要用于支持并发的读取操作,确保读取数据时不会被其他事务修改,从而避免重复读的问题。
**排他锁(Exclusive Lock):也称为写锁,确保资源在某一时刻只被一个事务独占访问。
当一个事务持有排他锁时,其他事务既不能获取该资源的共享锁,也不能获取排他锁。
主要用于数据的更新和删除操作,确保在修改数据时不会被其他事务读取或修改,从而避免脏数据和脏读的问题。
**意向锁(Intent Lock):是一种表明事务意图获取某种类型锁的锁。在获取行级锁或表级锁之前,事务可以先获取意向锁,以提高并发性能。
意向锁分为意向共享锁(IS)和意向排他锁(IX),分别表示事务想要获取的是共享锁还是排他锁。
**记录锁(Record Lock):锁定的是表中的某一行记录。当事务对某条记录加上记录锁时,其他事务不能对这条记录进行更新或删除操作,直到锁被释放。
**表锁:锁定的是整个表。当一个事务对表加上表锁时,其他事务必须等待该锁释放后才能对该表进行访问。
表锁的开销较小,但并发性能较低。
9.怎么启一个线程,有哪几种方式?
C# 支持多线程编程,这意味着你可以在单个进程中同时执行多个线程。多线程编程可以充分利用多核处理器,提高应用程序的响应性和性能
**Thread 类:这是最直接的方法,通过 System.Threading.Thread 类来创建线程。你可以通过继承 Thread 类,并重写 Run 方法,或者直接实例化 Thread 对象并传递一个 ThreadStart 或 ParameterizedThreadStart 委托给其构造函数。
// 使用 ThreadStart 委托
Thread thread = new Thread(new ThreadStart(ThreadFunction));
thread.Start();
// 使用 ParameterizedThreadStart 委托
Thread threadWithParam = new Thread(new ParameterizedThreadStart(ThreadFunctionWithParam));
threadWithParam.Start("Hello from Thread!");
**ThreadPool:线程池是一个由系统管理的线程集合,它允许你排队执行任务,而不需要显式地创建和管理线程。ThreadPool 类提供了在后台线程上异步执行操作的方法,如 QueueUserWorkItem。
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadPoolFunction));
**Task类:从C# 4.0开始,引入了基于任务的异步模式(TAP),它使用System.Threading.Tasks.Task类来表示异步操作。Task类提供了更高级的抽象,允许更简洁的代码和更好的异常处理。
Task task = Task.Run(() => TaskFunction());
Task task = Task.Factory.StartNew(() => TaskFunction());
task.Wait(); // 等待任务完成
static void TaskFunction()
{
// 异步执行的代码
}
**异步编程模型 (Async/Await):从C# 5.0开始,你可以使用 async 和 await 关键字来编写异步代码,使异步操作看起来更像同步代码,且更容易编写和理解。用于简化异步编程模型。这种方式不会直接创建新线程,而是在现有线程上进行异步操作,使得线程可以在等待I/O操作等耗时任务时不会被阻塞,提高了线程的利用率。
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
await AsyncFunction();
}
static async Task AsyncFunction()
{
// 异步执行的代码
await Task.Run(() => { /* 异步操作 */ });
}
}
10.C#中List集合是线程安全的吗,线程安全的集合有哪些?
在C#中,List<T> 集合类本身不是线程安全的。这意味着在多线程环境中,如果你不对其进行同步访问,就可能会出现数据不一致的问题,如数据竞争、死锁等
为了确保线程安全,你需要采取额外的措施,如使用锁(lock)语句或其他同步机制(如信号量、互斥体等)来确保对List<T>的并发访问是同步的。
线程安全的集合类包括以下几种:
**ConcurrentBag**:这是一个无序的集合,用于存储唯一的元素。它提供了线程安全的添加和移除操作。
**ConcurrentDictionary<TKey, TValue>**:这是一个线程安全的键值对集合,允许你进行并发的添加、移除和查找操作。
**ConcurrentQueue**:这是一个线程安全的先进先出(FIFO)队列。
**ConcurrentStack**:这是一个线程安全的后进先出(LIFO)栈。
**BlockingCollection**:这是一个线程安全的集合,它支持添加和移除操作,并提供阻塞和超时选项。
ReaderWriterLockSlim:这是一个同步原语,它可以用来保护对集合的读取和写入操作。虽然它本身不是一个集合,但可以与各种集合结合使用来提供线程安全的访问。
使用这些线程安全的集合类,你可以在多线程环境中更安全地操作集合,而无需担心数据一致性问题。这些集合内部使用各种同步机制(如锁、信号量等)来确保并发访问的安全性。
11.C# 说一下多线程?
**. 线程基础
Thread 类:这是C#中表示线程的基础类。你可以通过继承 Thread 类并重写 Run 方法来创建线程,或者直接实例化 Thread 对象,并传递一个 ThreadStart 或 ParameterizedThreadStart 委托给其构造函数来启动线程。
线程状态:每个线程都有多种可能的状态,包括 Unstarted、Running、WaitSleepJoin、Stopped、Aborted、Suspended(已过时)和 Background。
线程优先级:线程具有优先级,这决定了线程调度器如何在线程之间分配处理器时间。你可以使用 ThreadPriority 枚举来设置线程的优先级。
**. 线程同步
锁(lock):使用 lock 语句来确保一段代码在同一时间只被一个线程访问。这可以防止多个线程同时访问共享资源时发生的数据竞争和不一致。
信号量(Semaphore):信号量用于限制对共享资源的并发访问数量。
互斥体(Mutex):互斥体是一种同步原语,用于保护共享资源免受多个线程的并发访问。
监视器(Monitor):Monitor 类提供了一种机制,用于同步对共享资源的访问。你可以使用 Monitor.Enter 和 Monitor.Exit方法来确保资源在任何时候都只被一个线程访问。
**. 线程池
ThreadPool:线程池是一个由系统管理的线程集合,用于执行异步操作。线程池可以显著减少线程的创建和销毁开销,提高应用程序的性能。你可以使用 ThreadPool 类来排队执行任务,而无需显式地创建和管理线程。
**. 异步编程模型(Async/Await)
异步方法(Async Methods):C# 5.0 引入了 async 和 await 关键字,它们使异步编程更加简单和直观。你可以使用这些关键字来编写异步方法,这些方法可以在不阻塞调用线程的情况下执行耗时操作。
Task 类:Task 类表示一个异步操作。你可以使用 Task.Run 方法来在后台线程上执行操作,并使用 await 关键字来等待操作完成。
**. 并行编程(Parallel)
Parallel 类:Parallel 类提供了一系列静态方法,用于简化并行执行循环和数据转换的任务。
例如,Parallel.For 和 Parallel.ForEach 方法可以用于并行执行循环,而 Parallel.Invoke 方法可以用于并行执行多个委托。
注意事项
线程安全:在多线程环境中,需要确保对共享资源的访问是线程安全的。这通常涉及到使用同步机制来避免数据竞争和不一致。
死锁和活锁:不恰当的同步可能导致死锁(两个或更多线程无限期地等待对方释放资源)或活锁(两个或更多线程反复尝试获取资源但总是失败)。
性能考虑:虽然多线程可以提高性能,但过多的线程可能导致过多的上下文切换和资源争用,从而降低性能。因此,需要仔细考虑线程的数量和如何最有效地使用它们。
12.值类型和引用类型
值类型直接存储其数据,值类型的数据在分配内存时,是在栈(stack)上进行的。值类型的数据在声明的同时必须初始化,因为它们直接包含了数据。常见的值类型有基本数据类型(int, float, double等),枚举(enum),结构(struct)等。
引用类型存储的是数据的引用,引用类型的数据在分配内存时,是在堆(heap)上进行的。
引用类型的数据在声明时可以不必初始化,因为它只是保存了一个引用地址,该地址指向实际的数据。
当没有任何变量引用这个地址时,这部分数据就变成了垃圾,可以被垃圾回收器回收。
常见的引用类型有类(class),接口(interface),数组(array),委托(delegate),字符串(string)等。
13.C#泛型
在定义的时候可以使用占位符不指定具体参数类型。然后具体使用,实例化的时候
再去指定想要实例化的类型,这里的类型可以是类,接口,事件,委托等类型
优点:
代码可重用
性能好,不需要装箱和拆箱
类型安全(协变和逆变)
14.C# 协变和逆变
(out T)使用协变的目的,是为了限制这个泛型类型参数不能作为方法的入参类型使用。
使得泛型子类对象引用可以指向泛型基类的对象引用。保证了类型安全
(in T)使用逆变的目的,是为了限制这个泛型类型参数不能作为方法的返回值类型使用。
使得泛型基类对象引用也可以安全的指向泛型子类的对象引用。保证了类型安全
15.依赖注入
依赖注入(Dependency Injection, DI)是一种软件设计模式,主要是为了解决代码之间的耦合问题。
在项目启动的时候,startup类里面。ConfigureServices来注册依赖注入的服务,通过调用 services.Add... 方法来注册所需的服务
DIP:依赖注入倒置原则:依赖于抽象,不依赖预聚体,高层模块不依赖于低层模块,它们依赖于抽象。
依赖注入的基本思想是将一个对象所依赖的资源(即依赖项)注入到该对象中,而不是让对象自己去创建这些依赖项。在C#中,依赖注入通常通过以下方式实现:
构造函数注入:通过类的构造函数将依赖传递给类的实例,确保在对象创建时所有的依赖项都已准备好。
属性注入:通过属性将依赖注入到类的实例中,这种方式较为灵活,但在某些情况下可能导致类的实例在没有正确依赖的情况下被使用。
方法注入:通过方法将依赖注入到类的实例中。
16.怎么定位项目中的哪些sql需要优化
我们项目中大部分使用的是SQLSugar的方法去跟数据据交互的,我们用该ORM提供的日志功能,来查看生成的sql和执行时间
或者我们还可以在写完SQL后,把sql放在sqlserver里面去执行一下,查看执行计划,里面也可以看到该sql的性能如何。
17.mysql 索引有哪几种
MySQL索引主要有以下几种类型:
普通索引:最基本的索引,没有任何限制,允许在定义索引的列中插入重复值和空值。
唯一索引:索引列的值必须唯一,但允许有空值。如果是组合索引,则列值的组合必须唯一。
主键索引:一种特殊的唯一索引,不允许有空值,一个表只能有一个主键索引。
全文索引:用于查找文本中的关键字,只能在CHAR、VARCHAR或TEXT类型的列上创建。
空间索引:对空间数据类型的字段建立的索引,主要用于地理空间数据类型。
组合索引:在多个字段上创建的索引,只有在查询条件中使用了创建索引时的第一个字段,索引才会被使用
18.抽象类和接口,抽象方法和虚方法
**抽象类:抽象类可以包含抽象方法和非抽象方法。
抽象类不能被实例化,只能被继承。
子类继承抽象类后,必须实现抽象类中的所有抽象方法,除非它们自己也是抽象类。
**接口:接口只能包含抽象方法。
接口不能包含非抽象实现的方法。
类可以实现多个接口,(前提是抽象类)实现接口的部分或全部方法。
**抽象方法:抽象方法必须在抽象类中,且没有具体的实现,必须在子类中实现
**虚方法:虚方法可以有实现代码,子类可以选择不重写虚方法,从而继承父类的实现。
子类可以选择重写虚方法,从而覆盖父类的实现。
19.跨类调用方法有什么方式
通过实例化对象调用:如果方法是实例方法,需要先创建类的实例,然后通过这个实例来调用方法。
通过静态方式调用:如果方法是静态的,可以直接通过类名来调用,无需创建类的实例。
通过接口调用:如果类实现了某个接口,可以通过接口类型来调用实现的方法。
通过继承调用:如果子类继承了父类,子类可以直接调用父类中非私有的方法。
20.项目中比较有挑战的问题
Redis 的消息队列可以通过以下几种方式实现:
List 类型:使用 LPUSH/RPUSH 命令将元素推入队列,使用 LPOP/RPOP 命令从队列中弹出元素。
Pub/Sub 模式:发布/订阅模式,使用 PUBLISH 发布消息,SUBSCRIBE 订阅频道。
Streams 类型:类似于 Kafka 的 message broker,可以使用 XADD 添加消息,XREAD 读取消息。
List 适合简单的队列操作,Pub/Sub 适合消息广播,Streams 提供了更高级的队列特性,比如消息的序列化和分组消费。
List 的缺点是不支持消息订阅,Pub/Sub 的缺点是消息一旦发布,订阅者不能重复消费,Streams 提供了acknowledging机制,但命令较为复杂。
21.订阅发布,发布的消息没接收者,这时候消息怎么处理。
当没有订阅者时,Redis默认消息会丢失,如果想要消息不丢失的话,可以将消息存到Redis缓存中,然后等有订阅者了,再去取出消息,再发布消息。
22.Redis的消息队列实现的方式以及特点
LPUSH/LPOP:这是基于Redis列表(List)结构实现的简单消息队列。LPUSH用于从列表左侧插入元素,LPOP则从列表左侧弹出元素。这种方式适用于简单的生产者-消费者模型,但不支持复杂的消息确认机制或持久化保证。
发布订阅(Pub/Sub):支持一个或多个发布者向多个订阅者发送消息。消息传递是一对多的关系,适用于实时通知和事件处理场景。但Pub/Sub不提供消息持久化,且如果订阅者离线,将错过发布的消息。
Stream:Redis 5.0引入的一种高级消息队列实现。它支持消息的持久化、消费者组、消息确认等高级特性。Stream允许一个或多个生产者向队列发送消息,同时支持多个消费者组以竞争或共享的方式消费消息。这种方式更接近于专业的消息队列系统,适用于需要高可靠性和复杂消息处理的场景。
23.Redis持久化消息的具体流程主要包括两种方式:RDB和AOF。
RDB(Redis DataBase):通过定时或手动触发,将内存中的数据生成快照并保存到磁盘上。快照生成时,Redis会利用操作系统的COW(Copy On Write)机制,创建一个子进程来遍历内存数据并序列化到磁盘,期间主进程可以继续处理客户端请求。这种方式适合大规模数据恢复,但可能会丢失最后一次快照后的数据。
AOF(Append Only File):以日志的形式记录每个写操作,并将这些操作追加到AOF文件中。Redis提供了多种写磁盘的策略,如Always(每条指令都同步写入磁盘)、EverySec(每秒同步一次)和No(由操作系统决定何时写入磁盘)。AOF文件会定期重写以压缩文件大小,提高恢复效率。这种方式数据丢失少,但恢复速度相对较慢。
24.String和StringBuilder区别
String:是一个不可变的字符序列,一旦创建,就不能再被修改。这意味着每次对字符串的修改(如拼接、替换等)都会生成一个新的字符串对象。这种不可变性使得String在处理常量或只读数据时非常有用,因为它提供了较高的安全性和易用性。然而,对于需要频繁修改字符串的场景,String的性能会受到影响,因为它需要不断地创建新的对象来存储修改后的结果
StringBuilder:是一个可变的字符序列,设计用于在单线程环境中频繁地修改字符串。它通过提供一个内部字符数组来实现字符串的拼接和修改,而不需要像String那样创建新的对象。这使得StringBuilder在性能上优于String,尤其是在需要进行大量字符串拼接或修改的操作时。然而,需要注意的是,StringBuilder不是线程安全的,如果在多线程环境中使用,可能会遇到并发问题
StringBuffer与StringBuilder类似,也是一个可变的字符序列,但它是为了多线程环境设计的。与StringBuilder相比,StringBuffer的方法是同步的,保证了线程安全。这意味着在多线程环境中对StringBuffer的操作是安全的,但这也带来了额外的性能开销。因此,如果不需要线程安全,使用StringBuilder通常会更高效
25.面向对象三大特性
封装:是一种信息隐藏技术,将对象的状态(数据)和行为(方法)打包在一起,隐藏对象的内部实现细节,只提供公开的接口让其他对象与之交互。
继承:是子类继承父类的特性(包括数据和方法),从而可以扩展或修改父类的行为。
多态:是指一个对象可以有多种形态,在运行时,可以通过指向子类的父类指针,调用子类重写的方法。
26.说一下线程和进程
定义不同:
进程:是程序运行的一个实例,包括程序计数器、寄存器和变量的当前状态。
线程:是进程中的一个执行单元,也被称为轻量级进程。
系统资源分配不同:
进程:拥有独立的内存空间和系统资源。
线程:与同属一个进程的其他线程共享内存和系统资源。
调度和切换开销不同:
进程:由于拥有独立的内存空间,切换开销大,但相对更稳定。
线程:由于共享内存,切换开销小,但需要处理同步和互斥问题。
并发性不同:
进程:操作系统中有多进程并发执行。
线程:在同一进程中可以有多线程并发执行
27.如何解决多线程高并发问题
在C#中解决多线程高并发问题,通常涉及到以下几个方面:
使用lock关键字或Monitor类来同步访问共享资源。
使用Interlocked类处理原子操作。
使用ConcurrentQueue, ConcurrentBag, ConcurrentDictionary等线程安全集合。
使用Semaphore, Mutex, ReaderWriterLockSlim等同步机制。
避免死锁,通过正确的锁顺序和锁层次结构。
使用异步编程(async/await)来避免线程阻塞。
使用队列和工作线程池来管理并发任务。
对于数据库等资源,使用锁机制或乐观并发控制。
28.说一下异步
异步编程主要是通过async和await关键字来实现的。async关键字用于声明异步方法,而await用于挂起方法的执行,直到等待的异步操作完成。这样可以在不阻塞调用线程的情况下执行操作
29.const和readonly的区别
静态常量是指编译器在编译时候会对常量进行解析,并将常量的值替换成初始化的那个值。而动态常量的值则是在运行的那一刻才获得的,编译器编译期间将其标示为只读常量,而不用常量的值代替,这样动态常量不必在声明的时候就初始化,而可以延迟到构造函数中初始化。
1.const默认是静态常量,readonly默认是动态常量,如果需要设置成静态,需要显示声明
修饰引用类型时不同,const只能修饰基元类型,枚举类型或者字符串类型,readonly可以是任何类型。
2.const修饰的常量在声明的时候必须初始化;readonly修饰的常量则可以延迟到构造函数初始化
3.const修饰的常量在编译期间就被解析,即常量值被替换成初始化的值;readonly修饰的常量则延迟到运行的时候
4.const常量既可以声明在类中也可以在函数体内,但是static readonly常量只能声明在类中
30.多线程的优缺点
多线程主要用于提高应用程序的性能和响应性,通过同时执行多个任务来优化资源的利用。
优点:
1.提高性能:通过同时执行多个任务,多线程可以显著提高应用程序的性能,特别是在处理CPU密集型或I/O密集型任务时
2.改善用户体验:多线程允许应用程序在执行长时间任务时保持响应性,例如在等待数据库响应或文件读写操作时,用户界面仍然可以保持活跃
3.资源优化:多线程可以有效的利用多核处理器,每个核心可以同时运行一个线程,从而提高整体处理能力
4.异步编程支持:.NET支持基于任务的异步模式(TAP),这使得编写异步代码更加简单和直观,进而以利用多线程的优势
缺点:
1.复杂性增加:多线程编程比单线程变成更加复杂,需要管理线程的生命周期,同步和通信等问题,如果不当的管理,还有可能导致线程的静态条件,死锁等其他并发问题
2.调试困难:多线程程序的调试比单线程程序更难,因为并发错误(如竞态条件)可能在任何时刻发生,且难以重现
3.资源消耗:创建和管理线程本身需要消耗资源(如内存),过多的线程可能会导致资源耗尽,尤其是在资源受限的环境中(如移动设备)
4.设计挑战:设计良好的多线程应用程序需要深思熟虑的架构和设计,以确保线程间的正确同步和数据一致性问题
5.上下文切换开销:频繁的线程切换会产生一定的开销,尤其是在高负载或大量线程竞争CPU资源的情况下
如何避免线程造成的问题:
-
使用高级抽象:尽量使用
Task
、async
和await
等高级抽象来简化异步编程和任务并行处理。 -
避免共享状态:尽可能减少线程间的共享状态,使用消息传递或其他机制来减少对共享资源的依赖。
-
使用锁和同步机制:当需要时,使用如
lock
、Monitor
、Semaphore
、Mutex
等同步机制来保护数据的一致性。 -
异常处理:确保在多线程环境中妥善处理异常,避免因一个线程的异常导致整个应用程序崩溃。
-
性能监控:使用工具(如.NET自带的性能分析工具)监控和分析多线程应用程序的性能,及时发现并解决瓶颈或问题
31.多线程锁的应用场景
1.共享资源访问:多个线程需要访问同一个资源时,为了避免数据被多个线程同时修改导致数据错乱,锁就确保一次只有一个线程可以访问到该资源
2.读写锁:读多写少的场景,使用读写锁更常见,比简单的互斥锁更加有效,读锁允许多个线程同时读取资源,而写锁则确保在写入时不会有其他线程读取活写入
3.集合操作:当多个线程需要修改同一个集合时,为了避免线程安全问题,加入锁。当在添加或删除集合元素之前获取锁,执行完操作后再释放锁
4.状态管理:在需要确保对象状态一致性的场景中,例如在实现单例模式时,可以通过锁来保证实例的唯一性和线程安全
5.缓存系统:在实现缓存系统时,多个线程可能会尝试读取或写入缓存数据,通过使用锁或其他并发数据结构,可以确保缓存数据的一致性和线程安全
6.定时任务和事件处理:在处理定时任务或事件驱动的场景时,例如在Web应用程序中处理HTTP请求,如果多个请求需要修改同一资源,可以使用锁来防止数据竞争
32.Class和Struct的区别?
Class为引用类型,可以被实例化,存储实际的引用;Struct为值类型,值类型自身存储数据数据。
33.C#中类的修饰符和类成员的修饰符有哪些?
C#中可以修饰类的修饰符为:public、internal、sealed、abstract
C#中不能修饰类的修饰符为:private和protected
C#中成员的修饰符为:public、internal、protected、private
public:完全公开,没有访问限制。
internal:可以应用于当前应用程序以及类库。
protected:在当前类和子类中可以使用。
private:只有在当前类中可以使用。
sealed:密封类,不能被其他类型继承。
abstract:抽象类,不能创建实例。
34.面向对象和面向过程的区别?
面向对象:把问题分解成多个对象,强调的是解决问题行为标准(方式)
面向过程:分析问题并解决问题的步骤,强调的势解决问题的步骤。
35.什么是IOC?
控制反转,是一种思想(设计模式)而不是一种技术实现。
控制:拥有创建对象的权利。
反转:把控制权交给IOC容器。
优点: 1、对象之间的耦合度或依赖度降低。
2、资源容易管理
传统模式下:在一个类中需要new关键字去实例化另一个类。
IOC设计模式:不需要new去创建对象,直接从IOC容器中获取。
36.什么是OOP?
面向对象编程
提取共有的属性和方法,形成一个父类。
37.什么是AOP?
面向切面的编程,在不改变原有的业务逻辑情况下,横切逻辑代码,解耦合代码,避免横切代码重复。
切:指的是横向逻辑,就是保证原有的业务逻辑代码不变,操作横切逻辑代码,所以就是面相横切编程。
面:横切代码影响的是很多方法,每个方法如同一个点,很多点就形成面。
应用场景:事务控制,权限效验、日志信息等。
38..NetCore 和 .Net Standard 和 .Net Framework 有什么区别?
.Net Core在.Net Core3.1之后统称为.Net(.Net8),开源跨平台实现(windows/Linux/macOS),专注于现代应用(云服务,微服务),采用模块化设计和独立部署;
完全兼容.NET Standard(可引用Standard库),并提供更广泛的API(如操作系统交互),适合跨平台和云原生场景
.Net FrameWork是最早的微软.Net的实现,主要用于windows平台开发(WPF和Asp.Net),没有跨平台的特性,也不开源,并且依赖系统组件;最高支持.NET Standard 2.0,适合传统Windows应用开发,但无法直接升级到.NET Core
.Net Standard是一种规范而并非实现,定义跨.NET平台的统一API标准,确保不同实现(如Framework/Core/Xamarin)间的代码共享
39.项目中鉴权这块都有什么方式?
1.基于Cookie的鉴权:
实现原理
**用户登录后,服务端生成加密的Cookie(包含用户标识),客户端后续请求自动携带该Cookie
**服务端通过HttpContext
验证Cookie有效性,例如:
// 设置Cookie
httpContext.Response.Cookies.Append("AuthCookie", encryptedData, new CookieOptions {
Expires = DateTime.Now.AddMinutes(30)
});
// 验证Cookie
var cookieValue = httpContext.Request.Cookies["AuthCookie"];
**适用于传统Web应用(如MVC),依赖浏览器自动管理Cookie
✅ 简单易用,无需前端手动处理
❌ 跨域限制,且需防范CSRF攻击(需配合AntiForgeryToken)
2.基于Session的鉴权
实现原理
**服务端存储用户会话数据(如内存、Redis),通过Session ID关联客户端
示例代码:
// 启用Session
services.AddSession(options => {
options.IdleTimeout = TimeSpan.FromMinutes(30);
});
// 存储Session
HttpContext.Session.SetString("UserId", "123");
// 读取Session
var userId = HttpContext.Session.GetString("UserId");
**适用于需要服务端状态管理的场景(如购物车)
注意事项
**分布式环境下需配置共享Session存储(如Redis)
**性能低于无状态方案(如Token)。
3.基于OAuth2/OpenID Connect的第三方鉴权
适用场景
**集成第三方登录(如微信、Google、GitHub)
**通过Microsoft.AspNetCore.Authentication.OAuth
包实现,例如:
services.AddAuthentication()
.AddOAuth("GitHub", options => {
options.ClientId = "your_client_id";
options.ClientSecret = "your_client_secret";
options.CallbackPath = "/signin-github";
});
**用户授权后,第三方平台返回授权码,服务端换取AccessToken
优势
✅ 无需自行管理用户凭证,降低安全风险
✅ 支持SSO(单点登录)
4.基于角色的鉴权(RBAC)
实现方式
**结合ASP.NET Core Identity,通过[Authorize(Roles = "Admin")]
标记控制器或Action
**数据库需包含角色表(Role)、用户角色关联表(UserRole)
示例:
// 配置策略
services.AddAuthorization(options => {
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
});
// 应用策略
[Authorize(Policy = "AdminOnly")]
public IActionResult AdminDashboard() { ... }
扩展性
**可结合声明(Claims)实现更细粒度的权限控制
5.其他方案
API密钥鉴权
**客户端请求时携带固定API Key,服务端验证其合法性
**适用于机器对机器(M2M)通信,如内部服务调用
自定义鉴权过滤器
**通过IAuthorizationFilter
或ActionFilterAttribute
实现逻辑,例如:
public class CustomAuthFilter : IAuthorizationFilter {
public void OnAuthorization(AuthorizationFilterContext context) {
if (!CheckPermission(context)) {
context.Result = new ForbidResult();
}
}
}
**灵活但需自行处理安全细节
40.服务怎么使用的?
服务注册
1.定义接口与实现类
public interface IMyService {
string GetData();
}
public class MyService : IMyService {
public string GetData() => "Hello from service!";
}
2.在DI容器中注册服务,在Program.cs
或Startup.cs
中配置服务生存期
builder.Services.AddScoped<IMyService, MyService>(); // 作用域生命周期
// 或
builder.Services.AddSingleton<IMyService, MyService>(); // 单例模式
// 或
builder.Services.AddTransient<IMyService, MyService>(); // 每次请求新实例
服务使用
1.构造函数注入,在控制器、中间件或其他服务中通过构造函数自动激活:
public class MyController : Controller {
private readonly IMyService _service;
public MyController(IMyService service) {
_service = service; // 由DI容器自动实例化
}
public IActionResult Index() {
var data = _service.GetData();
return View(data);
}
}
2.方法注入(需要手动调用),通过[FromServices]
特性在方法参数中注入:
public IActionResult GetData([FromServices] IMyService service) {
return Ok(service.GetData());
}
3.手动解析(不推荐),通过IServiceProvider
手动获取服务实例:
var service = HttpContext.RequestServices.GetService<IMyService>();
41.项目部署怎么部署的?
本地部署(IIS)
1.发布准备
**在Vs中生成解决方案,确保编译无误
**右键项目选择发布
,选择发布目标为文件夹
或IIS
,配置输出路径
2.IIS配置
**在IIS中创建新网站,设置物理路径为发布文件夹
**安装.NET Core Hosting Bundle(若为.NET Core项目)
**配置应用程序池为无托管代码
(.NET Core)或对应.NET版本
3.访问测试
通过浏览器或Postman访问配置的URL(如http://localhost:端口号
)验证API是否正常运行
跨平台部署(独立部署)
1.发布为独立应用
**使用CLI命令生成跨平台可执行文件:
dotnet publish -c Release -r linux-x64 --self-contained true
**输出文件包含运行时,可直接在目标系统运行
2.部署到Linux
**将发布文件夹拷贝至Linux服务器,通过chmod +x
赋予执行权限
**使用nohup
或Systemd托管服务:
nohup ./YourApp &
容器化部署(Docker)
1.Dockerfile配置
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY ./publish ./
ENTRYPOINT ["dotnet", "YourApp.dll"]
2.构建镜像并运行
docker build -t yourapp .
docker run -d -p 8080:80 yourapp
42.在C#项目中,自己写过滤器的步骤,和过滤器用于什么场景?
1.创建过滤器类
继承对应接口或Attribute
基类,例如动作过滤器:
public class CustomActionFilter : IActionFilter
{
public void OnActionExecuting(ActionExecutingContext context)
{
// 方法执行前逻辑(如参数校验)
}
public void OnActionExecuted(ActionExecutedContext context)
{
// 方法执行后逻辑(如日志记录)
}
}
2.注册过滤器
全局注册(所有控制器生效):
services.AddControllers(options =>
{
options.Filters.Add<CustomActionFilter>();
});
局部注册(通过特性标记):
[CustomActionFilter]
public class HomeController : Controller { }
如果项目中是Async/await,要写异步过滤器:实现IAsyncActionFilter
等异步接口:
public class AsyncFilter : IAsyncActionFilter
{
public async Task OnActionExecutionAsync(
ActionExecutingContext context,
ActionExecutionDelegate next)
{
await next(); // 执行后续逻辑
}
}
核心应用场景
-
安全控制
- 授权验证(
IAuthorizationFilter
) - 防SQL注入参数过滤
- 授权验证(
-
性能优化
- 缓存响应结果(
IResultFilter
) - 接口限流(
OnActionExecuting
中计数)
- 缓存响应结果(
-
日志与监控
- 记录请求参数和响应时间
- 异常统一处理(
IExceptionFilter
)
-
数据预处理
- 请求参数自动解密/格式化
- 响应数据统一包装(如添加标准JSON结构)
43.sqlsugar有什么特性,sqlsugar底层代码是怎么实现的,或者说sqlsugar的底层是什么?
核心特性
-
多数据库支持
- 原生支持SQL Server、MySQL、Oracle等主流关系型数据库,通过统一API简化跨数据库开发
- 支持动态切换数据库类型,仅需修改连接配置
-
链式查询
- 提供类似LINQ的链式调用语法,支持条件过滤、字段选择等操作,代码可读性高
db.Queryable<User>().Where(u => u.Age > 18).Select(u => new { u.Name }).ToList();
-
高性能优化
- 内置缓存机制(如SQL模板缓存)减少重复解析开销
- 批量操作(BulkInsert/Update)显著提升大数据量处理效率
-
代码生成与映射
- 支持根据数据库表结构自动生成实体类
- 通过
[SugarTable]
和[SugarColumn]
特性实现灵活的对象-关系映射
-
事务管理
- 提供显式事务控制接口,支持跨多操作的事务一致性
db.Ado.BeginTran(); try { db.Insertable(entity).ExecuteCommand(); db.CommitTran(); } catch { db.RollbackTran(); }
底层实现原理
-
核心模块架构
- Core模块:通过反射动态解析实体类属性,生成SQL语句和参数化查询
- Ado模块:基于ADO.NET封装底层数据库连接(SqlConnection/SqlCommand)
- Queryable模块:将链式调用转换为表达式树,最终生成优化后的SQL
-
SQL生成机制
- 使用
SqlBuilder
动态构建SQL,智能处理分页、排序等复杂语法 - 表达式树解析器自动转换Lambda表达式为WHERE/ORDER BY子句
- 使用
-
缓存设计
- 二级缓存结构:
- 一级缓存:SQL模板缓存(避免重复解析)
- 二级缓存:查询结果缓存(需显式启用)
- 二级缓存结构:
-
扩展性设计
- 支持EAV模型(实体-属性-值)实现动态表结构管理
- 通过
AOP
拦截器实现SQL日志、性能监控等扩展功能