C#面试题(中级篇),详细讲解,帮助你深刻理解,拒绝背话术!

拒绝背话术,帮助你深刻理解其原理,真正做到实战时游刃有余!适用于Unity游戏开发、C#语言相关面试等相关学习。

此为C#中级篇,不久将更新后续篇目。

初级篇:C#面试题(初级篇),详细讲解,帮助你深刻理解,拒绝背话术!-CSDN博客

1.构造函数为什么不能是虚函数?

对象初始化顺序角度:在创建对象时,首先要分配内存,然后调用基类的构造函数,接着调用派生类的构造函数,这是一个自顶向下的过程。虚函数是基于对象的运行时类型来确定具体调用哪个实现的,其调用依赖于对象的虚表(一个存储虚函数地址的表格)。而在构造函数执行期间,对象还处于初始化阶段,虚表还未完全构建好。如果构造函数是虚函数,就无法准确地根据对象的运行时类型来调用正确的构造函数,因为此时对象的运行时类型还在构建过程中,可能会导致不可预测的行为。

明确的初始化顺序:构造函数的调用顺序是明确规定的,即先调用基类构造函数,再调用派生类构造函数。这种固定的顺序有助于保证对象的正确初始化。如果构造函数可以是虚函数,就会打破这种明确的顺序,使得对象的初始化过程变得复杂和难以理解,增加了代码的维护难度。

2.什么是匿名⽅法?还有Lambda表达式?

 详解:C#面试常考随笔7:什么是匿名⽅法?还有Lambda表达式?-CSDN博客

匿名方法本质上是一种没有显式名称的方法,它可以作为参数传递给需要委托类型方法,常用于事件处理、回调函数等场景,能够让代码更加简洁和紧凑。

优点
  • 代码简洁:避免了为每个简单的委托实现单独定义一个命名方法,减少了代码量,使代码更加紧凑。
  • 内联定义:可以在需要委托的地方直接定义逻辑,提高了代码的可读性和可维护性。
缺点
  • 复用性差:由于匿名方法没有名称,不能在其他地方复用,只能在定义的地方使用。
  • 调试不便:在调试时,由于没有明确的方法名称,可能会增加调试的难度。

与 Lambda 表达式的关系

在 C# 3.0 及以后的版本中,引入了 Lambda 表达式,它是一种更简洁的创建委托实例的方式,通常可以替代匿名方法。Lambda 表达式在语法上更加简洁,因此在实际开发中使用更为广泛。

Lambda 表达式参数列表规则

  • 无参数:如果 Lambda 表达式不需要参数,可以使用空的参数列表,例如:() => Console.WriteLine("无参数的Lambda表达式");
  • 单个参数:当只有一个参数时,可以省略参数列表的括号,例如:num => num * 2;
  • 多个参数:多个参数需要用逗号分隔,并放在括号内,例如:(a, b) => a + b;

 3.using关键字有哪些用法?

(1)using 指令:引入命名空间

(2)using static 指令:引入静态成员

通过using static + 类型,可以指定无需指定类型名称即可访问其静态成员的类型。使用该指令后,在代码中可以直接使用该类型的静态成员,而不必通过类型名来调用。例如:

using static System.Math;
class Program
{
    static void Main()
    {
        double result = Sqrt(16); // 直接使用System.Math的静态方法Sqrt
        Console.WriteLine(result);
    }
}

(3)using 别名:为类型创建别名

当同一个 C# 文件引用了两个不同的命名空间,且这两个命名空间中都包含一个相同名字的类型时,为了避免混淆,可以使用using + 别名 = 包括详细命名空间信息的具体类型的方式为类型创建别名。例如:

using aClass = NameSpace1.MyClass;
using bClass = NameSpace2.MyClass;
namespace NameSpace1
{
    public class MyClass
    {
        public override string ToString()
        {
            return "You are in NameSpace1.MyClass";
        }
    }
}
namespace NameSpace2
{
    class MyClass
    {
        public override string ToString()
        {
            return "You are in NameSpace2.MyClass";
        }
    }
}
namespace testUsing
{
    class Class1
    {
        static void Main()
        {
            aClass my1 = new aClass();
            Console.WriteLine(my1);
            bClass my2 = new bClass();
            Console.WriteLine(my2);
        }
    }
}

(4) using 语句:自动释放资源

using语句用于定义一个范围,在范围结束时自动调用对象的Dispose方法来释放资源。通常用于处理实现了IDisposable接口的对象,比如文件流、数据库连接等。语法形式为:

using (var resource = new ResourceType())
{
    // 使用resource对象的代码
}
// 离开这个代码块后,resource的Dispose方法会被自动调用,释放相关资源

例如,使用文件流读取文件内容时:

using (System.IO.StreamReader reader = new System.IO.StreamReader("test.txt"))
{
    string line;
    while ((line = reader.ReadLine()) != null)
    {
        Console.WriteLine(line);
    }
}
// 读取完毕后,StreamReader对象的Dispose方法会自动被调用,释放文件资源

这样可以确保即使在代码块中发生异常,相关资源也能被及时释放,避免资源泄漏。

 扩展:

C#面试常考随笔8:using关键字有哪些用法?-CSDN博客

如何使用C#的using语句释放资源?什么是IDisposable接口?与垃圾回收有什么关系?-CSDN博客

 什么情况下,C#需要手动进行资源分配和释放?什么又是非托管资源?-CSDN博客

 4.什么是闭包?

最简单的例子:

Lambda可以访问Lambda表达式块外部的变量,叫闭包。

定义

闭包是指有权访问另一个函数作用域中的变量的函数。即使该函数已经执行完毕,其作用域内的变量也不会被销毁,而是会被闭包所捕获并保留,供闭包函数后续使用。

 详解:C#面试常考随笔8:using关键字有哪些用法?-CSDN博客

 5.Dictionary<K, V>、Hashtable的内部实现原理是什么?

Dictionary<K, V>

  • 底层数据结构:使用哈希表(Hash Table),由一个数组和链表(或在.NET Core 2.1 及之后版本中,当链表长度达到一定阈值时转换为红黑树)组成。数组中的每个元素称为一个桶(Bucket),用于存储键值对。
  • 工作原理:添加键值对时,先计算键(Key)的哈希码,通过哈希码对数组长度取模,得到对应的桶索引。若该桶为空,直接将键值对存入;若不为空(即发生哈希冲突),则以链表(或红黑树)的形式将新的键值对添加到该桶的链表(或红黑树)中。查找元素时,先计算键的哈希码定位到桶,然后在桶内的链表(或红黑树)中通过键的比较(调用Equals方法)找到对应的值。

Hashtable

  • 底层数据结构:同样基于哈希表实现,由数组和链表构成。数组中的每个元素是一个桶,用于存储键值对。
  • 工作原理:添加键值对时,计算键的哈希码,对数组长度取模确定桶索引。若桶为空,直接存入键值对;若桶已有元素(哈希冲突),则将新键值对添加到该桶的链表中。查找元素时,先计算键的哈希码定位桶,再在桶内链表中通过键的比较(调用Equals方法)获取对应的值。与Dictionary<K, V>不同的是,Hashtable是线程安全的,因为其方法实现中添加了synchronized关键字(在多线程环境下可确保线程同步,但性能相对Dictionary<K, V>较低),并且Hashtable的键和值都是object类型,存储和获取时可能需要类型转换,存在装箱和拆箱操作。

查找效率

  • 理想情况:二者在理想情况下查找效率都较高。它们内部都基于哈希表结构,通过计算键的哈希码来定位存储位置。在没有哈希冲突(即不同键计算出的哈希码对应到哈希表中的不同位置)时,查找操作可以在接近常数时间(O (1))内完成,即能快速定位到目标键值对。
  • 存在哈希冲突时
    • Hashtable:当发生哈希冲突,同一桶中存在多个键值对(以链表形式存储)时,查找时需要在链表中顺序比较键。如果哈希冲突严重,链表较长,查找效率会降低,时间复杂度可能退化为 O (n),n 为链表长度。不过,由于其内部实现对哈希函数等进行了优化,在一般情况下能较好地分散哈希值,减少冲突。
    • Dictionary<K, V> :在.NET Core 2.1 之前,处理哈希冲突与Hashtable类似,通过链表存储冲突的键值对,冲突严重时查找效率受链表长度影响。在.NET Core 2.1 及之后版本中,当链表长度达到一定阈值(通常为 8 )时,会将链表转换为红黑树。红黑树是一种自平衡的二叉搜索树,查找操作的时间复杂度为 O (log n),相比于长链表的 O (n),在冲突较多时能提供更稳定和高效的查找性能。

 6.双检锁实现⼀个单例模式?

双检锁(Double-Checked Locking)概念

双检锁是一种用于实现线程安全的单例模式的优化技术。在多线程环境下,为了确保一个类只有一个实例,并且在需要时才进行实例化,同时避免不必要的同步开销,就可以使用双检锁机制。

其核心思想是在实例化对象之前进行两次检查:第一次检查实例是否已经创建,如果没有创建再进入同步块;进入同步块后,再进行第二次检查,确保在多线程环境下只有一个线程能创建实例。这样可以减少同步操作的次数,提高性能。

(比如两个对象都到 Lock处,前一个创建实例后,下一个对象Lock后,就需要判断是否被创建过实例)

using System;

// 定义单例类
public sealed class Singleton
{
    // 私有静态字段,用于存储单例实例
    private static Singleton _instance;

    // 用于线程同步的锁对象
    private static readonly object _lock = new object();

    // 私有构造函数,防止外部实例化
    private Singleton()
    {
        Console.WriteLine("Singleton instance is created.");
    }

    // 公共静态属性,用于获取单例实例
    public static Singleton Instance
    {
        get
        {
            // 第一次检查:判断实例是否已经创建
            if (_instance == null)
            {
                // 进入同步块
                lock (_lock)
                {
                    // 第二次检查:确保在同步块内只有一个线程能创建实例
                    if (_instance == null)
                    {
                        _instance = new Singleton();
                    }
                }
            }
            return _instance;
        }
    }
}

class Program
{
    static void Main()
    {
        // 获取单例实例
        Singleton singleton1 = Singleton.Instance;
        Singleton singleton2 = Singleton.Instance;

        // 验证两个实例是否相同
        Console.WriteLine(object.ReferenceEquals(singleton1, singleton2)); // 输出: True
    }
}

7.关于设计模式

分为:创建型、结构型、行为型模式

常用的有:

创建型模式:

  • 单例模式:确保类只有一个实例,并提供全局访问点。如游戏中的游戏管理器、音效管理器等常采用单例模式,保证在游戏任何地方都能方便访问和操作。

  • 工厂模式:用于创建对象,将对象的创建和使用分离。在游戏中创建不同类型的敌人、道具等可使用工厂模式,根据配置或游戏逻辑创建相应对象。

行为型模式:

  • 观察者模式:定义对象间的一对多依赖关系,当一个对象状态改变时,其依赖者会收到通知并自动更新。游戏中的任务系统、UI 界面更新等常采用此模式,如任务完成时通知相关 UI 更新任务状态。(类似事件系统)

  • 状态模式:允许对象在内部状态改变时改变行为,看起来像修改了类。游戏中角色的不同状态(如奔跑、跳跃、攻击等)可用状态模式实现,每个状态有不同行为逻辑。(类型状态机)

结构型模式:

  • 代理模式:为其他对象提供代理以控制对它的访问。在网络游戏中,可能用代理模式来处理远程对象访问,本地代理对象代替远程对象进行操作,减少网络通信开销。

  • 装饰器模式:动态地给对象添加额外职责。游戏中给角色添加装备或增益效果可使用装饰器模式,不修改角色原有代码基础上,动态添加新属性和行为。

  • 适配器模式:将一个类的接口转换成客户希望的另一个接口。在 Unity 中使用不同外部插件或 SDK 时,可能需要用适配器模式使它们接口与游戏项目兼容。

补充:C#面试常考随笔12:游戏开发中常用的设计模式【C#面试题(中级篇)补充】-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dr.勿忘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值