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)
相关推荐
还是大剑师兰特5 小时前
Node.js面试题及详细答案120题(16-30) -- 核心模块篇
node.js·大剑师·nodejs面试题
还是大剑师兰特10 小时前
浏览器面试题及详细答案 88道(23-33)
大剑师·浏览器面试题
还是大剑师兰特19 小时前
浏览器面试题及详细答案 88道(12-22)
大剑师·浏览器面试题
还是大剑师兰特3 天前
Python面试题及详细答案150道(41-55) -- 面向对象编程篇
python·大剑师·python面试题
还是大剑师兰特4 天前
Redis面试题及详细答案100道(01-15) --- 基础认知篇
redis·大剑师·redis面试
还是大剑师兰特6 天前
MySQL面试题及详细答案 155道(061-080)
大剑师·mysql面试题
还是大剑师兰特9 天前
Javascript面试题及详细答案150道之(046-060)
javascript·大剑师·js面试题
还是大剑师兰特9 天前
Javascript面试题及详细答案150道之(031-045)
大剑师·javascript面试题
还是大剑师兰特10 天前
MySQL面试题及详细答案 155道(021-040)
大剑师·mysql面试题