.NET常见面试题,我VS大佬

##1.值类型与引用类型的区别
我的回答:
内存分配不同:
值类型分配在栈空间,引用类型分配在堆上
值传递
网上大佬的回答:
引用类型值传递的时候,传的其实只是内存地址而已。
值类型值传递,会进逐一字段复制。

##2.什么是装箱、拆箱
我的回答:
boxing值类型转换为引用类型,装箱过程是先分配一块空间给引用类型,值类型的值赋值给引用类型的值,最后形成指针+值得结构
unboxing引用类型转换为值类型,需要用类型强制转换
由于有内存空间的重新分配,可能影响程序性能
网上大佬的回答:
装箱:装箱操作时将值类型隐式地转换成引用类型。装箱一个数值会为其分配一个对象实例,并把该数值复制到新对象中。
拆箱:拆箱操作是指显式地把引用类型转换成值类型
装箱对象存的是地址引用,指向的是堆上的值,这个值的类型和变量一样,也是值类型,值就是从变量Copy 过来的一个副本值而已。
要在运行时成功拆箱值类型,被拆箱的项必须是对一个对象的引用,该对象是先前通过装箱该值类型的实例创建的。
频繁的装箱和拆箱比较耗费CUP资源,降低代码的执行效率和用户体验度。
改进方案:使用泛型类,泛型能很好的解决由装箱和拆箱带来的效率问题。
##3.多线程下C#如何保证线程安全?
我的回答:
1.用线程安全集合,非托管集合。.netframework提供了很多线程安全集合类型。
2.用多线程编程模型,比如Monitor,lock,cpu级别的线程对象Metux,SpinLock SpinWait,配合线程池来操作,还有AutoResetEvent线程切换操作,一些数值类型的计算用联锁Internal.Lock,锁的颗粒度可以用信号量控制,推荐使用Slim级别的对象,还有指令重排优化,内存屏障。
在多线程操作时要注意:提前做好内存资源分配,程序设计上避免死锁逻辑。
网上大佬的回答:
不安全线程的代码:类的静态变量、类的静态属性、单例对象的静态变量、单例对象的静态属性、多线程共享的局部变量
应用错误且无法恢复的,通常异常为:索引超出了数组界限

public class MessageService : BaseService
{
    private static Dictionary<string, Timer> _timerDict = new Dictionary<string, Timer>();
    public async void SendMessageAsync(string msgId, MessageInputDto2 input)
    {
        var timer = new Timer(60 * 1000) { AutoReset = true };
        _timerDict[msgId] = timer;     // 问题代码
        timer.Elapsed += (sender, eventArgs) =>
        {
            try
            {
                /* 具体业务代码 */
                timer.Stop();
                timer.Close();
                _timerDict.Remove(msgId);
            }
            catch(Exception exp)
            {
                // 异常处理代码
            }
        }
    }
}

解决方法,一般是加锁
注意:如果加lock 可能出现瓶颈,要进行流程梳理,是否要更换实现方案

lock(_timerDict)
{
    _timerDict[msgId] = timer;     // 问题代码
}
timer.Elapsed += (sender, eventArgs) =>
{
    try
    {
        /* 具体业务代码 */
        timer.Stop();
        timer.Close();
        lock(_timerDict)
        {
            _timerDict.Remove(msgId);
        }
    }
    catch(Exception exp)
    {
        // 异常处理代码
    }
}

2、陷入死循环,导致服务器CPU 100%卡顿问题:
有个常见业务,获取一串没有使用过的随机数或随机字符串,比如用户身份Token,比如抽奖等等
下面是常见的获取不重复的随机数代码,
在_rnd.Next 没有加锁,其内部方法InternalSample会导致返回结果都是0,从而导致while陷入死循环:


public class CodeService
{
    private static Random _rnd = new Random(Guid.NewGuid().GetHashCode());
    public static GetCode()
    {
        var ret = "";
        var redis = IocHelper.GetSingleInstance<IRedis>();
        // 获取一个未使用过的序号
        do
        {
            ret = _rnd.Next(10000).ToString();  // 问题代码
        }while(!redis.Add(ret, "", TimeSpan.FromSeconds(3600)));
        return ret;
    }
}

解决方法,双重校验:加锁,并判断循环次数:


public class CodeService
{
    private static Random _rnd = new Random(Guid.NewGuid().GetHashCode());
    public static GetCode()
    {
        var ret = "";
        var redis = IocHelper.GetSingleInstance<IRedis>();
        var maxLoop = 10;
        // 获取一个未使用过的序号
        do
        {
            lock(_rnd)
            {
                ret = _rnd.Next(10000).ToString();
            }
        }while(!redis.Add(ret, "", TimeSpan.FromSeconds(3600)) && (maxLoop--) > 0);
        if(maxLoop <= 0)
        {
            throw new Exception("10次循环,未找到可用数据:" + ret);
        }
        return ret;
    }
}

##4.内存溢出与内存泄露
我的回答:
内存资源分配是在程序启动时为程序指令栈内存,为数据对象分配堆内存(托管堆和非托管堆),程序运行时回管理运行时内存以及垃圾回收机制。
当某些对象的指针指向空的内存空间,就会出现内存泄漏的问题,程序异常退出,有些对象没有合理的释放和回收,还可能是共享内存空间数据丢失造成内存泄漏。
内存溢出是程序的运行过程中,某些大的资源或者程序集合没有合理的分配空间,超出物理设备的可用内存空间以及虚拟内存空间,程序无法正常运行的现象。
网上大佬的回答:
1.内存溢出
系统不能再给你的请求分配所需要的空间了,比如你申请了30M,系统剩余内存只有20M了。这就叫内存溢出。
比如一个办公室空间有限只有5个工位,领导安排6个人来这屋,还有一个人怎么办?只能找领导安排其它地方了。还比如在栈的操作中如果栈已经满了,当我们再对栈进行入栈操作就会造成上溢。
2.内存泄露
内存泄露是申请了内存空间的变量一直在占用,无法释放。比如申请了一块内存空间,没有回收一直占用,直到最后内存溢出。
比如在C#中使用非托管代码,如果不使用析构函数回收就会造成内存泄露。如果不是特殊情况,所以建议尽量不要使用非托管资源来编写代码。还比如在代码中使用了静态变量也容易导致内存泄露

##5.讲讲.NET的GC原理
我的回答:
.net垃圾回收机制是运行时对托管内存的资源回收,对于非托管内存不进行回收,需要手动释放,或者通过实现IDispose接口来自动回收。
内存分配的算法是最近最少使用算法LUR,每个对象的调用次数以及最近一次调用的时间戳来区分,每次回收的时候,回收时根据时间戳排序找到最大的时间并且次数最少的一项,将对象释放(某些对象可以标记为非托管对象 比如unsafe 或者指针类型),然后将当前被调用的所有指令集合的调用次数+1并更新时间戳。
编写规范的程序指令有助于CLR进行合理的垃圾回收。
网上大佬的回答:
引用对象的生命周期
托管堆中存放引用类型对象,因此GC的内存管理的目标主要都是引用类型对象
对象创建及生命周期
一个对象的生命周期简单概括就是:创建>使用>释放,在.NET中一个对象的生命周期:
new创建对象并分配内存----->对象初始化----->对象操作、使用----->资源清理(非托管资源)----->GC垃圾回收

那其中重要的一个环节,就是对象的创建,大部分的对象创建都是开始于关键字new。为什么说是大部分呢,因为有个别引用类型是由专门IL指令的,比如string有ldstr指令,0基数组好像也有一个专门指令。
引用对象都是分配在托管堆上的, 先来看看托管堆的基本结构,如下图,托管堆中的对象是顺序存放的,托管堆维护着一个指针NextObjPtr,它指向下一个对象在堆中的分配位置。
创建一个新对象的主要流程:

模拟下面User对象的创建过程:

public class User
{
    public int Age { get; set; }
    public string Name { get; set; }

    public string _Name = "123" + "abc";
    public List<string> _Names;
}

对象大小估算,共计40个字节:

属性Age值类型Int,4字节;

属性Name,引用类型,初始为NULL,4个字节,指向空地址;

字段_Name初始赋值了,由前面的文章(NET专题面试题:string与字符串操作含解析(精))可知,代码会被编译器优化为_Name=”123abc”。一个字符两个字节,字符串占用2×6+8(附加成员:4字节TypeHandle地址,4字节同步索引块)=20字节,总共内存大小=字符串对象20字节+_Name指向字符串的内存地址4字节=24字节;

引用类型字段List _Names初始默认为NULL,4个字节;

User对象的初始附加成员(4字节TypeHandle地址,4字节同步索引块)8个字节;

内存申请:申请44个字节的内存块,从指针NextObjPtr开始验证,空间是否足够,若不够则触发垃圾回收。

内存分配:从指针NextObjPtr处开始划分44个字节内存块。

对象初始化:首先初始化对象附加成员,再调用User对象的构造函数,对成员初始化,值类型默认初始为0,引用类型默认初始化为NULL;

托管堆指针后移:指针NextObjPtr后移44个字节。

返回内存地址:返回对象的内存地址给引用变量。

##6.async/await相关问题
我的回答:
.netframework4.5以及高级版本的.netcore都支持async await异步编程模型
它不是真正意义上的多线程,只是一种语法糖,让多个指令可以切换执行,类似协程的方式,它不会阻塞主线程,而是简化了多线程切换的复杂编程方式,其实是一种非阻塞的同步方式。
网上大佬的回答:
TAP模型
下表显示了异步编程提高响应能力的典型区域。 列出的 .NET 和 Windows 运行时 API 包含支持异步编程的方法。
应用程序区域 包含异步方法的 .NET 类型 包含异步方法的 Windows 运行时类型
Web 访问 HttpClient Windows.Web.Http.HttpClient SyndicationClient
使用文件 JsonSerializer StorageFile
StreamReader
StreamWriter
XmlReader
XmlWriter
使用图像 MediaCapture
BitmapEncoder
BitmapDecoder
WCF 编程 同步和异步操作

由于所有与用户界面相关的活动通常共享一个线程,因此,异步对访问 UI 线程的应用程序来说尤为重要。 如果任何进程在同步应用程序中受阻,则所有进程都将受阻。 你的应用程序停止响应,因此,你可能在其等待过程中认为它已经失败。

private async void button1_Click(object sender, EventArgs e)
{
    var t = Task.Run(() => {
        Thread.Sleep(5000);
        return "Hello I am TimeConsumingMethod";
    });
    textBox1.Text = await t;//不阻塞UI
}

对于异步编程而言,该基于异步的方法优于几乎每个用例中的现有方法。 具体而言,此方法比 BackgroundWorker 类更适用于 I/O 绑定操作,因为此代码更简单且无需防止争用条件。 结合 Task.Run 方法使用时,异步编程比 BackgroundWorker 更适用于 CPU 绑定操作,因为异步编程将运行代码的协调细节与 Task.Run 传输至线程池的工作区分开来。
如果使用 async 修饰符将某种方法指定为异步方法,即启用以下两种功能。

标记的异步方法可以使用 await 来指定暂停点。 await 运算符通知编译器异步方法:在等待的异步过程完成后才能继续通过该点。 同时,控制返回至异步方法的调用方。
异步方法在 await 表达式执行时暂停并不构成方法退出,只会导致 finally 代码块不运行。
标记的异步方法本身可以通过调用它的方法等待。
异步方法通常包含 await 运算符的一个或多个实例,但缺少 await 表达式也不会导致生成编译器错误。 如果异步方法未使用 await 运算符标记暂停点,则该方法会作为同步方法执行,即使有 async 修饰符,也不例外。 编译器将为此类方法发布一个警告。

扩展:Windows Communication Foundation (WCF)异步编程
WCF 使应用程序能够进行通信,无论它们是在同一台计算机上、在 Internet 中还是在不同的应用程序平台上。
WCF 服务和客户端可以在两个不同的应用程序级别参与异步操作调用,这会为 WCF 应用程序在最大程度提高针对交互性而平衡的吞吐量方面提供更好的灵活性。
##7.事件和委托的异同
我的回答:
委托是一种类型,可以声明一个具有方法签名的类型的函数,它适合于函数式编程,开发中常用linq表达式就是一种匿名的委托调用它的参数类型可以自定义,也可以自定义返回值类型,有一种特殊的委托叫多播委托,支持调用一次同时触发多个委托实例。允许通过new的方式实例化一个委托。
事件是一种特殊的委托,它的返回值必须是event类型,方法签名包含事件源object,以及要传递的事件消息,事件可以通过注册和卸载的方式来实例化一个事件+=或者-=,事件也可以有返回结果,虽然不常用,因为事件的本质是触发和通知,不需要返回给调用者任何数据,而且注册多个事件可以被覆盖的,只有最后一个注册的事件才会触发,这是需要注意的。
网上大佬的回答:
C# 中的委托(Delegate)类似于 C 或 Css++ 中函数的指针(我补充:C++的函数指针只可以指向静态函数,C#中委托不但保存了此函数入口指针的引用,还保存了调用此函数类的实例引用,面向对象、类型安全、可靠的受控对象)。委托(Delegate) 是存有对某个方法的引用的一种引用类型变量。引用可在运行时被改变。它本质上也是一个类。它定义了方法的类型,使得可以将方法当作另一个方法的参数来进行传递,这种将方法动态地赋给参数的做法。
作用:可以把方法当参数传递,可以避免在程序中大量使用 If-Else(Switch)语句,同时使得程序具有更好的可扩展性。C#2.0之后出现了匿名函数和lambda表达式也是Delegate演化而来。
事件由对象引发,通过我们提供的代码来处理。一个事件我们必须订阅(Subscribe)他们,订阅一个事件的含义就是提供代码,在这个事件发生时执行这些代码,这些代码称为事件处理程序。
事件是在委托类型变量前加上event关键字,其本质是用来对委托类型的变量进行封装,类似于类的属性对字段的封装。
作用:事件的使用一般通过发布者和订阅者来进行。发布者会在某一条件下触发某事件,订阅者可以通过订阅该事件,来对该事件的触发做出反应。在设计模式中的订阅者模式是最佳实践。

//以观烧水的察者模式来举例说明
    // 热水器
    public class Heater
    {
        private int temperature;
        public delegate void BoilHandler(int param); //声明委托
        public event BoilHandler BoilEvent; //声明事件
                                            // 烧水
        public void BoilWater()
        {
            for (int i = 0; i <= 100; i++)
            {
                temperature = i;
                if (temperature > 95)
                {
                    if (BoilEvent != null)
                    { //如果有对象注册
                        BoilEvent(temperature); //调用所有注册对象的方法
                    }
                }
            }
        }
    }
    // 警报器
    public class Alarm
    {
        public void MakeAlert(int param)
        {
            Console.WriteLine("Alarm:嘀嘀嘀,水已经 {0} 度了:", param);
        }
    }
    // 显示器
    public class Display
    {
        public static void ShowMsg(int param)
        { //静态方法
            Console.WriteLine("Display:水快烧开了,当前温度:{0}度。", param);
        }
    }
    //调用
       static void Main(string[] args)
        {
            Heater heater = new Heater();
            Alarm alarm = new Alarm();
            heater.BoilEvent += alarm.MakeAlert; //注册方法
            heater.BoilEvent += (new Alarm()).MakeAlert; //给匿名对象注册方法
            heater.BoilEvent += Display.ShowMsg; //注册静态方法
            heater.BoilWater(); //烧水,会自动调用注册过对象的方法
        }

委托和事件的区别
1、事件是委托的封装,可以理解为一种特殊的委托。
2、事件里面其实就两个方法(即add_event()和remove_event())和一个私有的委托变量,这两个方法里面分别是对这个私有的委托变量进行的合并和移除,当调用事件的+=时其实是调用的事件里面的add_event()方法,同样-=调用的是remove_event()方法。
3、在注册和注销事件上,委托可以使用=和+=来将函数注册到委托的变量上,使用-=来将函数注销。而事件则有着更严格的限制,事件只能使用+=来将函数注册到其上,使用-=来将函数注销。

##8.依赖注入相关问题:依赖注入生命周期
我的回答:
依赖注入容器会分配一块空间给组件,这个时候所有组件都没有实例化,当某个程序集被加载了,就会根据程序集的路径路由到指定的程序及,并且便利程序集内的对象进行实例化。(没深入研究过,只能说出这些)
网上大佬的回答:
ServiceDescriptor构造函数里面的ServiceLifetime枚举分类
1、Transient(瞬时):即用即建,用后即弃。就是每次获取这个服务的实例时都要创建一个这个服务的实例。
2、Scoped(作用域):这种类型的服务实例保存在当前依赖注入容器(IServiceProvider)上。在同作用域,服务每个请求只创建一次。
3、Singleton(单例):服务请求时只创建实例化一次,其后相同请求都沿用这个服务。

我的回答好像不在点上

##9.ASP.NET Core 中的服务生命周期
我的回答:
.netcore的依赖注入有Single,Scope,Transient,因为每个服务都有一个跟对象空间也就是程序运行的生命周期,随着程序启动和关闭而变化。Single是单例它直接继承Root生命周期,并且只实例化一次,Scope是上下文周期,每个服务请求开始生成一个实例,在服务运行过程中不会重新分配对象,Transient是每次请求都会实例化一个新的实例,每次调用服务都会重新实例化一个对象
网上大佬的回答:
ServiceDescriptor构造函数里面的ServiceLifetime枚举分类
1、Transient(瞬时):即用即建,用后即弃。就是每次获取这个服务的实例时都要创建一个这个服务的实例。
2、Scoped(作用域):这种类型的服务实例保存在当前依赖注入容器(IServiceProvider)上。在同作用域,服务每个请求只创建一次。
3、Singleton(单例):服务请求时只创建实例化一次,其后相同请求都沿用这个服务。

##10.ASP.NET Core中间件
我的回答:
Logger日志工厂
Cross跨域配置
JWT授权
Configuration配置文件加载
WebHost托管服务
ServiceProvider
MVC框架
StaticResource静态资源
网上大佬的回答:
什么是中间件:请求处理管道由一系列中间件组件组成。每个组件在 HttpContext 上执行操作,调用管道中的下一个中间件或终止请求。
下图是中间件的管道模型图,有点类似过滤器。

中间件可以用来做什么?在我们的应用程序当中和业务关系不大的一些需要在管道中做的事情可以使用,比如身份验证,Session存储,日志记录,定时任务等。.NET自带很多中间件,比如身份认证中间件UseAuthorization,Session中间件等。并且中间件可以自定义。
如何自定义中间件:当系统自带的中间件不能满足我们需求时,我们可以自定义中间件来实现功能,比如自己开发的定时任务等。注册中间件的方式可以使用use和run,use可以快速注册中间件,而run是终端中间件,是中间件管道末尾,当注册该中间件后,后面的中间件将不再执行。下面介绍三种自定义中间件的方式。
自定义匿名中间件
自定义匿名中间件在Program.cs文件中就可以实现,可以通过use和run来自定义匿名中间件。案例如下:

app.Use(async (context, next) =>
{
    Console.WriteLine("测试匿名中间件");
    await next();
});
//如果用app.run将在这里结束。

如何重新排列现有中间件,或根据场景需要注入新的自定义中间件

此外我们可以得出一些结论,如下:

关于自定义中间件的方法:

1、中间件的方法名必须叫 Invoke 或者 InvokeAsync ,且为 public ,非 static 。

2、Invoke 或者 InvokeAsync 方法第一个参数必须是 HttpContext 类型。

3、Invoke 或者 InvokeAsync 方法的返回值类型必须是 Task 。

4、Invoke 或者 InvokeAsync 方法可以有多个参数,除 HttpContext 外其它参数会尝试从依赖注入容器中获取。

5、Invoke 或者 InvokeAsync 方法不能有重载。

关于自定义中间件的构造函数:

1、构造函数必须包含 RequestDelegate 参数,该参数传入的是下一个中间件。

2、构造函数参数中的 RequestDelegate 参数不是必须放在第一个,可以是任意位置。

3、构造函数可以有多个参数,参数会优先从给定的参数列表中查找,其次会从依赖注入容器中获取,获取失败会尝试获取默认值,都失败则会抛出异常。

4、构造函数可以有多个,届时会根据构造函数参数列表和给定的参数列表选择匹配度最高的一个。

补充:中间件的调用顺序

在某些场景下,中间件的排序有所不同。 例如,高速缓存和压缩排序是特定于场景的,存在多个有效的排序。 例如:

app.UseResponseCaching();
app.UseResponseCompression();

上面代码以通过缓存压缩的响应来减少 CPU 使用量,但你可能最终会使用不同的压缩算法(如 Gzip 或 Brotli)来缓存资源的多个表示形式。

以下排序结合了静态文件以允许缓存压缩的静态文件:

app.UseResponseCaching();
app.UseResponseCompression();
app.UseStaticFiles();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
}
else
{
    app.UseExceptionHandler("/Error");//异常/错误处理
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();//HTTPS 重定向中间件
app.UseStaticFiles();//静态文件中间件 (UseStaticFiles) 返回静态文件,并简化进一步请求处理。
// app.UseCookiePolicy();//Cookie 策略中间件 (UseCookiePolicy) 使应用符合欧盟一般数据保护条例 (GDPR) 规定。

app.UseRouting();//用于路由请求的路由中间件 (UseRouting)。
// app.UseRequestLocalization();//UseRequestLocalization 必须在可能检查请求区域性的任何中间件(例如 app.UseMvcWithDefaultRoute())之前出现。
// app.UseCors();//当前必须在 UseResponseCaching 之前出现。

//UseCors、UseAuthentication 和 UseAuthorization 必须按显示的顺序出现。
app.UseAuthentication();//身份验证中间件 (UseAuthentication) 尝试对用户进行身份验证,然后才会允许用户访问安全资源。
app.UseAuthorization();//用于授权用户访问安全资源的授权中间件 (UseAuthorization)。
// app.UseSession();//会话中间件 (UseSession) 建立和维护会话状态。 如果应用使用会话状态,请在 Cookie 策略中间件之后和 MVC 中间件之前调用会话中间件。
// app.UseResponseCompression();
// app.UseResponseCaching();

app.MapRazorPages();//用于将 Razor Pages 终结点添加到请求管道的终结点路由中间件(带有 MapRazorPages 的 UseEndpoints)。
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

结束语

好了,MSDN博大精深,它的文档真的详细,如果都能熟悉到位,这些小问题都不是问题了,看来还是自己太菜了~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值