C#面试题及详细答案120道(11-20)-- 面向对象编程(OOP)

前后端面试题》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux... 。

前后端面试题-专栏总目录

文章目录

  • 一、本文面试题目录
        1. 简述面向对象的三大特性(封装、继承、多态)
        1. 什么是接口(interface)?接口与抽象类(abstract class)的区别
        1. 什么是密封类(sealed class)?使用场景是什么?
        1. 方法重载(Overload)和方法重写(Override)的区别
        1. 什么是虚方法(virtual method)?如何实现?
        1. 什么是隐藏方法(new关键字)?与重写的区别
        1. 什么是构造函数?静态构造函数的特点
        1. 什么是析构函数?与IDisposable接口的区别
        1. 什么是索引器(Indexer)?如何实现?
        1. 什么是部分类(partial class)?使用场景
  • 二、120道C#面试题目录列表

一、本文面试题目录

11. 简述面向对象的三大特性(封装、继承、多态)

面向对象编程(OOP)的三大核心特性是封装、继承和多态,它们共同构成了面向对象设计的基础:

1. 封装(Encapsulation)

  • 原理:将数据(字段)和操作数据的方法捆绑在一起,隐藏内部实现细节,仅通过公共接口暴露必要功能。
  • 作用:提高安全性(防止数据被随意修改)、简化使用(只需关注接口而非实现)。
  • 示例
csharp 复制代码
public class Person
{
    // 私有字段(封装数据)
    private string _name;
    
    // 公共属性(提供访问接口)
    public string Name
    {
        get => _name;
        set 
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("姓名不能为空");
            _name = value;
        }
    }
}

2. 继承(Inheritance)

  • 原理:允许一个类(子类)继承另一个类(父类)的属性和方法,实现代码复用和扩展。
  • 作用:减少代码重复,建立类之间的层次关系。
  • 示例
csharp 复制代码
// 父类
public class Animal
{
    public void Eat() => Console.WriteLine("吃东西");
}

// 子类继承父类
public class Dog : Animal
{
    // 扩展父类功能
    public void Bark() => Console.WriteLine("汪汪叫");
}

// 使用
var dog = new Dog();
dog.Eat();  // 继承的方法
dog.Bark(); // 自己的方法

3. 多态(Polymorphism)

  • 原理:同一操作作用于不同对象可产生不同结果,允许使用基类引用指向派生类对象。
  • 作用:提高代码灵活性和扩展性,支持"开闭原则"。
  • 示例
csharp 复制代码
public class Shape
{
    public virtual void Draw() => Console.WriteLine("绘制形状");
}

public class Circle : Shape
{
    public override void Draw() => Console.WriteLine("绘制圆形");
}

public class Square : Shape
{
    public override void Draw() => Console.WriteLine("绘制正方形");
}

// 使用多态
Shape[] shapes = { new Shape(), new Circle(), new Square() };
foreach (var shape in shapes)
{
    shape.Draw(); // 根据实际类型执行不同实现
}

12. 什么是接口(interface)?接口与抽象类(abstract class)的区别

接口(interface) 是一种契约,定义了类或结构必须实现的成员(方法、属性、事件等),但不包含实现细节。

语法示例

csharp 复制代码
public interface IVehicle
{
    void Start();
    void Stop();
    int Speed { get; set; }
}

// 实现接口
public class Car : IVehicle
{
    public int Speed { get; set; }
    
    public void Start() => Console.WriteLine("汽车启动");
    public void Stop() => Console.WriteLine("汽车停止");
}

接口与抽象类的区别

特性 接口 抽象类
成员实现 只能定义成员,不能有实现 可以有抽象成员(无实现)和具体成员(有实现)
继承限制 一个类可以实现多个接口 一个类只能继承一个抽象类(单继承)
构造函数 不能有构造函数 可以有构造函数
访问修饰符 成员默认为public(不能显式指定其他修饰符) 成员可以有各种访问修饰符
字段 不能包含字段(可包含自动属性) 可以包含字段
多态用途 定义跨层次结构的功能契约 用于同层次结构的代码复用
版本兼容 新增成员不影响现有实现(C# 8.0+支持默认实现) 新增抽象成员会破坏现有继承类

适用场景

  • 接口 :定义不相关类的共同行为(如IDisposable)、实现多继承效果、制定API契约。
  • 抽象类:在相关类之间共享代码、定义基础行为并允许派生类扩展。

13. 什么是密封类(sealed class)?使用场景是什么?

密封类(sealed class) 是不允许被继承的类,使用sealed关键字修饰。

语法示例

csharp 复制代码
public sealed class MathHelper
{
    public static int Add(int a, int b) => a + b;
}

// 错误:无法继承密封类
// public class AdvancedMath : MathHelper { }

密封方法(sealed method)

密封类中的方法默认是密封的,也可以在非密封类中用sealed修饰重写的方法,防止进一步重写:

csharp 复制代码
public class BaseClass
{
    public virtual void Method() { }
}

public class DerivedClass : BaseClass
{
    // 密封此方法,防止子类重写
    public sealed override void Method() { }
}

public class GrandchildClass : DerivedClass
{
    // 错误:无法重写密封方法
    // public override void Method() { }
}

使用场景

  1. 安全保护:防止核心类被恶意继承和修改(如加密算法类)。
  2. 性能优化:JIT编译器可对密封类进行优化,调用方法时无需检查虚方法表。
  3. 设计完整性:当类的设计不适合扩展时(如工具类、数据传输对象)。
  4. 防止破坏封装:避免子类修改基类的关键行为导致逻辑错误。

注意:密封类不能同时是抽象类(抽象类必须被继承才有意义)。

14. 方法重载(Overload)和方法重写(Override)的区别

方法重载(Overload)方法重写(Override) 是实现多态的两种方式,但机制完全不同:

特性 方法重载 方法重写
定义 同一类中,方法名相同,参数列表不同 父子类中,方法名、参数、返回值完全相同
关键字 不需要特殊关键字 需要virtual(基类)和override(派生类)
目的 提供相同功能的不同实现方式 子类重新定义父类的方法实现
绑定时机 编译时绑定(静态多态) 运行时绑定(动态多态)
返回值 可以不同(但不能仅靠返回值区分) 必须相同(或协变返回类型)
访问修饰符 可以不同 不能比基类方法更严格

示例代码

csharp 复制代码
// 方法重载示例
public class Calculator
{
    // 重载1:两个int参数
    public int Add(int a, int b) => a + b;
    
    // 重载2:三个int参数
    public int Add(int a, int b, int c) => a + b + c;
    
    // 重载3:两个double参数
    public double Add(double a, double b) => a + b;
}

// 方法重写示例
public class Animal
{
    // 基类虚方法
    public virtual void MakeSound() => Console.WriteLine("动物发出声音");
}

public class Cat : Animal
{
    // 重写基类方法
    public override void MakeSound() => Console.WriteLine("猫喵喵叫");
}

// 使用
Animal animal = new Cat();
animal.MakeSound(); // 输出"猫喵喵叫"(运行时多态)

使用场景

  • 重载 :同一操作有不同参数需求(如Console.WriteLine支持多种参数类型)。
  • 重写:子类需要提供与父类不同的实现(如不同动物的叫声不同)。

15. 什么是虚方法(virtual method)?如何实现?

虚方法(virtual method) 是在基类中声明的、允许派生类重写(override)其实现的方法,是实现多态的基础。

实现步骤

  1. 在基类中用virtual关键字声明方法
  2. 在派生类中用override关键字重写方法
  3. 通过基类引用调用方法时,会根据实际对象类型执行相应实现

示例代码

csharp 复制代码
// 基类定义虚方法
public class Document
{
    public string Title { get; set; }
    
    // 虚方法:提供默认实现
    public virtual void Print()
    {
        Console.WriteLine($"打印文档: {Title}");
    }
}

// 派生类重写虚方法
public class PdfDocument : Document
{
    // 重写基类方法
    public override void Print()
    {
        // 可以调用基类实现
        base.Print();
        // 增加PDF特有的打印逻辑
        Console.WriteLine("使用PDF打印机打印");
    }
}

public class WordDocument : Document
{
    // 重写基类方法
    public override void Print()
    {
        Console.WriteLine($"打印Word文档: {Title}(包含格式)");
    }
}

// 使用虚方法实现多态
public static void Main()
{
    Document[] documents = {
        new Document { Title = "普通文档" },
        new PdfDocument { Title = "报告.pdf" },
        new WordDocument { Title = "合同.docx" }
    };
    
    foreach (var doc in documents)
    {
        doc.Print(); // 根据实际类型调用不同实现
    }
}

输出结果

复制代码
打印文档: 普通文档
打印文档: 报告.pdf
使用PDF打印机打印
打印Word文档: 合同.docx(包含格式)

注意事项

  • 虚方法必须有实现(哪怕是空实现)
  • 静态方法、密封方法、私有方法不能声明为虚方法
  • 派生类可以选择是否重写虚方法(不重写则使用基类实现)

16. 什么是隐藏方法(new关键字)?与重写的区别

隐藏方法 是指在派生类中用new关键字声明与基类同名的方法,从而隐藏基类的方法实现,而非重写。

语法示例

csharp 复制代码
public class BaseClass
{
    public void ShowMessage()
    {
        Console.WriteLine("这是基类方法");
    }
}

public class DerivedClass : BaseClass
{
    // 使用new关键字隐藏基类方法
    public new void ShowMessage()
    {
        Console.WriteLine("这是派生类方法");
    }
}

隐藏与重写的区别

特性 隐藏方法(new) 重写方法(override)
机制 创建新的方法,隐藏基类方法 替换基类虚方法的实现
关键字 使用new 需要基类virtual和派生类override
多态行为 不支持多态,调用取决于引用类型 支持多态,调用取决于实际对象类型
基类方法要求 基类方法可以是任意方法 基类方法必须是virtualabstractoverride

行为对比示例

csharp 复制代码
public static void Main()
{
    // 隐藏方法的行为
    BaseClass baseObj = new DerivedClass();
    DerivedClass derivedObj = new DerivedClass();
    
    baseObj.ShowMessage();    // 输出"这是基类方法"(取决于引用类型)
    derivedObj.ShowMessage(); // 输出"这是派生类方法"(取决于引用类型)
    
    // 重写方法的行为(对比)
    BaseClass baseVirtual = new DerivedVirtual();
    baseVirtual.VirtualMethod(); // 输出"派生类重写的方法"(取决于实际对象类型)
}

// 重写对比示例
public class BaseVirtual
{
    public virtual void VirtualMethod() => Console.WriteLine("基类虚方法");
}

public class DerivedVirtual : BaseVirtual
{
    public override void VirtualMethod() => Console.WriteLine("派生类重写的方法");
}

使用建议

  • 隐藏方法会破坏多态性,一般不推荐使用
  • 必须使用时应显式添加new关键字(避免编译器警告)
  • 优先使用重写(override)实现多态行为

17. 什么是构造函数?静态构造函数的特点

构造函数是一种特殊的方法,用于在创建对象时初始化对象,与类同名且无返回值。

实例构造函数示例

csharp 复制代码
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    
    // 无参构造函数
    public Person()
    {
        Name = "未知";
        Age = 0;
    }
    
    // 带参构造函数
    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

// 使用
var person1 = new Person(); // 调用无参构造函数
var person2 = new Person("张三", 30); // 调用带参构造函数

静态构造函数是用于初始化类的静态成员的特殊构造函数,具有以下特点:

  1. 声明方式 :使用static关键字,无参数,无访问修饰符
  2. 调用时机:第一次访问类(创建实例或访问静态成员)时自动调用,仅调用一次
  3. 用途:初始化静态字段、注册事件、加载资源等

静态构造函数示例

csharp 复制代码
public class Logger
{
    // 静态字段
    public static string LogFilePath;
    
    // 静态构造函数
    static Logger()
    {
        Console.WriteLine("静态构造函数调用");
        LogFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log.txt");
        // 可以执行初始化文件、注册事件等操作
    }
    
    // 实例构造函数
    public Logger()
    {
        Console.WriteLine("实例构造函数调用");
    }
}

// 使用
public static void Main()
{
    // 第一次访问类,触发静态构造函数
    Logger.LogFilePath = "test.log";
    
    // 创建实例,静态构造函数不会再次调用
    var logger1 = new Logger();
    var logger2 = new Logger();
}

输出结果

复制代码
静态构造函数调用
实例构造函数调用
实例构造函数调用

注意事项

  • 静态构造函数不能被直接调用
  • 若未定义静态构造函数,编译器会自动生成默认的
  • 静态构造函数中发生的异常会导致类型初始化失败,该类型将无法使用

18. 什么是析构函数?与IDisposable接口的区别

析构函数(Destructor)是用于在对象被垃圾回收前释放非托管资源的特殊方法。

语法与特点

  • 名称与类名相同,前缀为~
  • 无参数、无返回值、无访问修饰符
  • 不能显式调用,由垃圾回收器自动调用
  • 仅适用于引用类型(类)

析构函数示例

csharp 复制代码
public class FileHandler
{
    private FileStream _stream;
    
    public FileHandler(string filePath)
    {
        _stream = new FileStream(filePath, FileMode.Open);
    }
    
    // 析构函数
    ~FileHandler()
    {
        // 释放非托管资源
        _stream?.Dispose();
        Console.WriteLine("析构函数执行,释放资源");
    }
}

IDisposable接口 用于显式释放托管和非托管资源,定义了Dispose()方法供手动调用。

IDisposable实现示例

csharp 复制代码
public class ResourceHandler : IDisposable
{
    private bool _disposed = false;
    private FileStream _fileStream; // 托管资源
    private IntPtr _unmanagedResource; // 非托管资源
    
    public void Dispose()
    {
        Dispose(true);
        // 告诉GC不需要调用析构函数
        GC.SuppressFinalize(this);
    }
    
    // 核心释放逻辑
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;
        
        // 释放托管资源
        if (disposing)
        {
            _fileStream?.Dispose();
        }
        
        // 释放非托管资源
        if (_unmanagedResource != IntPtr.Zero)
        {
            // 调用非托管释放函数
            CloseHandle(_unmanagedResource);
            _unmanagedResource = IntPtr.Zero;
        }
        
        _disposed = true;
    }
    
    // 析构函数:仅释放非托管资源
    ~ResourceHandler()
    {
        Dispose(false);
    }
    
    // 外部非托管函数
    [DllImport("kernel32.dll")]
    private static extern bool CloseHandle(IntPtr hObject);
}

析构函数与IDisposable的区别

特性 析构函数 IDisposable接口
调用方式 由GC自动调用,不可控 手动调用或通过using语句自动调用
释放内容 仅适合释放非托管资源 可释放托管和非托管资源
执行时机 不确定(取决于GC) 确定(调用时立即执行)
性能影响 会延长对象生命周期(进入终结队列) 可及时释放资源,提高性能
使用场景 作为释放非托管资源的后备机制 主要的资源释放方式

最佳实践

  • 实现IDisposable接口进行主动资源释放
  • 使用using语句确保Dispose()被调用
  • 析构函数仅作为最后防线,释放未被手动释放的非托管资源

19. 什么是索引器(Indexer)?如何实现?

索引器(Indexer) 允许类或结构像数组一样通过索引访问,使对象可以模拟数组的行为。

实现语法

csharp 复制代码
public 返回类型 this[参数列表]
{
    get { /* 获取逻辑 */ }
    set { /* 设置逻辑 */ }
}

示例:自定义集合类

csharp 复制代码
public class StringCollection
{
    private List<string> _items = new List<string>();
    
    // 索引器实现
    public string this[int index]
    {
        get
        {
            // 验证索引有效性
            if (index < 0 || index >= _items.Count)
                throw new IndexOutOfRangeException("索引超出范围");
            return _items[index];
        }
        set
        {
            // 支持动态扩展
            if (index >= _items.Count)
            {
                _items.AddRange(Enumerable.Repeat("", index - _items.Count + 1));
            }
            _items[index] = value;
        }
    }
    
    // 还可以实现其他索引类型(如字符串索引)
    public int this[string item]
    {
        get => _items.IndexOf(item);
    }
    
    public int Count => _items.Count;
}

// 使用索引器
public static void Main()
{
    var collection = new StringCollection();
    
    // 像数组一样赋值
    collection[0] = "第一个元素";
    collection[1] = "第二个元素";
    
    // 像数组一样访问
    Console.WriteLine(collection[0]); // 输出"第一个元素"
    
    // 使用字符串索引
    Console.WriteLine(collection["第二个元素"]); // 输出 1
}

索引器的特点

  1. 可以重载(定义多个不同参数的索引器)
  2. 支持任意类型的索引参数(不局限于int)
  3. 可以像属性一样使用访问修饰符和验证逻辑
  4. 接口中也可以定义索引器(无实现)

与数组的区别

  • 索引器可以使用非整数索引(如字符串、枚举)
  • 索引器的访问器可以有复杂逻辑(验证、计算等)
  • 索引器没有Length属性,需自行实现

常见应用

  • 自定义集合类(如List<T>内部实现了索引器)
  • 字典类(如Dictionary<TKey, TValue>的键索引)
  • 包装复杂数据结构,提供简化访问方式

20. 什么是部分类(partial class)?使用场景

部分类(partial class) 允许将一个类的定义拆分到多个源文件中,编译时会合并为一个完整的类。

语法示例

文件1:Person.cs

csharp 复制代码
public partial class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    
    public void Introduce()
    {
        Console.WriteLine($"我叫{Name},今年{Age}岁");
    }
}

文件2:Person_Additional.cs

csharp 复制代码
public partial class Person
{
    public string Address { get; set; }
    
    public void Move(string newAddress)
    {
        Address = newAddress;
        Console.WriteLine($"搬到了{newAddress}");
    }
}

编译后,这两个文件会被视为一个完整的Person类:

csharp 复制代码
// 等效于合并后的类
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
    
    public void Introduce() { /* 实现 */ }
    public void Move(string newAddress) { /* 实现 */ }
}

部分方法(partial method)

部分类中可以定义部分方法,在一个文件中声明,在另一个文件中实现:

csharp 复制代码
// 声明
partial class DataProcessor
{
    // 部分方法:只有签名,没有实现
    partial void OnDataProcessed(string data);
}

// 实现(可选)
partial class DataProcessor
{
    partial void OnDataProcessed(string data)
    {
        Console.WriteLine($"数据处理完成: {data}");
    }
}

使用场景

  1. 大型项目协作:多人同时编辑同一个类的不同部分。
  2. 代码生成:工具生成的代码与手动编写的代码分离(如ORM实体类、Windows Forms设计器代码)。
  3. 功能拆分:按功能模块拆分类的定义(如数据访问、业务逻辑、事件处理)。
  4. 条件编译:不同平台或配置的代码放在不同文件中。

注意事项

  • 所有部分类必须使用partial关键字
  • 所有部分类必须在同一命名空间
  • 所有部分类必须有相同的访问修饰符
  • 若部分类是抽象的或密封的,整个类就是抽象的或密封的
  • 部分方法必须是private,且返回类型为void

二、120道C#面试题目录列表

文章序号 C#面试题120道
1 C#面试题及详细答案120道(01-10)
2 C#面试题及详细答案120道(11-20)
3 C#面试题及详细答案120道(21-30)
4 C#面试题及详细答案120道(31-40)
5 C#面试题及详细答案120道(41-50)
6 C#面试题及详细答案120道(51-60)
7 C#面试题及详细答案120道(61-75)
8 C#面试题及详细答案120道(76-85)
9 C#面试题及详细答案120道(86-95)
10 C#面试题及详细答案120道(96-105)
11 C#面试题及详细答案120道(106-115)
12 C#面试题及详细答案120道(116-120)
相关推荐
还是大剑师兰特1 天前
解决nginx错误:http://localhost:7000正常,http://localhost:7000/map 报错404
nginx·大剑师
还是大剑师兰特4 天前
地图的分类
大剑师·地图分类
还是大剑师兰特15 天前
ruoyi‑vue3 添加【车辆管理】菜单组完整方案
大剑师·若依系统
还是大剑师兰特19 天前
若依(ruoyi)前后台系统从零到一部署配置(坑点带解析)
大剑师
还是大剑师兰特19 天前
ideaIC‑2024.1.7(社区版zip绿色解压版)完整使用教程
大剑师·idealc
还是大剑师兰特21 天前
MySQL8.0 Windows完整安装教程
windows·mysql·大剑师
还是大剑师兰特2 个月前
VSCode 插件冲突导致崩溃、卡死、假死 全套根治方案
vscode·大剑师
还是大剑师兰特2 个月前
ThinkPad T490 Windows 自动修复失败完整解决步骤
windows·大剑师·t490
还是大剑师兰特2 个月前
二年级奥数(12) : 巧填符号+竖式数字谜
大剑师·二年级奥数
还是大剑师兰特2 个月前
二年级奥数(9):鸡兔同笼+ 归一问题
大剑师·二年级奥数