C#基础编程 面试题汇总(三)

本章内容覆盖:面向过程与面向对象、类与对象、方法与重载、构造方法与 this、访问权限修饰符与封装、继承与方法重写、接口、多态与类型转换、static 与字符串、值类型与引用类型比较、ToString、DateTime、异常处理。

一、面向对象基础

Q:什么是面向过程编程(POP)?它有哪些特点?

考察点: 面向过程编程的概念与特点

参考答案:

  • 面向过程编程(Procedure Oriented Programming,POP)是一种以过程为中心的编程方式,它将问题分解成一系列的步骤,然后按照顺序执行这些步骤,以达到求解问题的目的。
  • 编写代码时,把要解决的问题看作是一个个要完成的任务,用函数一步一步实现,最后在主函数中将这些函数一个一个调用。
  • 面向过程编程更加注重效率和速度,适用于小型、简单的程序或者性能要求较高的场景。
  • 典型例子:按照面向过程的思想把大象装入冰箱------第一步打开冰箱,第二步把大象塞进去,第三步关上冰箱。

Q:什么是面向对象编程(OOP)?它有哪些特点?

考察点: 面向对象编程的概念与特点

参考答案:

  • 面向对象编程(Object Oriented Programming,OOP)强调把问题分解成对象,通过封装、继承和多态等机制来处理对象之间的关系。
  • 以对象为中心,将数据(字段)和行为(方法)封装到一个对象中;对象是代码的基本单元,程序通过对象之间的交互来完成任务。
  • 每个对象都可以独立地处理自己的数据(字段)和行为(方法)。
  • 面向对象编程更加注重代码的重用性、可维护性和可扩展性,适用于大型、复杂的软件系统开发。
  • 在面向对象的世界里,万事万物皆对象。例如把大象装入冰箱:可以把大象看作一个对象,冰箱看作一个对象,冰箱有开门、装物体、关门等行为。

Q:面向过程和面向对象有什么区别?

考察点: 两种编程思想的对比

参考答案:

  • 组织方式不同:面向过程以"过程(函数)"为中心,把问题拆成步骤后按顺序执行;面向对象以"对象"为中心,把数据和操作封装在对象中。
  • 关注点不同:面向过程更注重效率和速度;面向对象更注重代码的重用性、可维护性和可扩展性。
  • 适用场景不同:面向过程适用于小型、简单的程序或性能要求较高的场景;面向对象适用于大型、复杂的软件系统。
  • 发展历程:早期先有面向过程思想,随着软件规模的扩大、问题复杂性的提高,面向过程的弊端越来越明显,于是出现了面向对象思想,并成为目前主流的方式。
  • 代表语言:C语言是典型的面向过程语言;C#、Java是典型的面向对象语言。

Q:什么是 OOA、OOD、OOP?

考察点: 面向对象的分析、设计、编程

参考答案:

  • 面向过程和面向对象都贯穿于软件分析、设计和开发的各个阶段。
  • 对应面向对象分别称为:面向对象分析(OOA)、面向对象设计(OOD)和面向对象编程(OOP)。

Q:面向对象编程有哪三大特性?

考察点: 封装、继承、多态

参考答案:

  • 封装:将数据和操作数据的方法捆绑在一起,隐藏内部实现细节,保护对象的数据不被随意修改。
  • 继承:子类可以继承父类的属性和方法,实现代码复用和层次化设计。
  • 多态:调用同一个父类/接口中的方法,由于子类/实现类不同,可能会有不同的行为。
  • 面向对象编程正是通过封装、继承和多态等机制来处理对象之间的关系。

Q:什么是类?什么是对象?二者是什么关系?

考察点: 类与对象的概念

参考答案:

  • 类(class)可以看成一个模板或者图纸,是用于描述同一类型对象的一个抽象概念,类中定义了这一类对象所应具有的共同的字段、方法。
  • 对象(object)也叫实例(instance),是类的一个具体实例;"某个类的对象"和"某个类的实例"是同一个意思。
  • 关系:类相当于图纸/模具,对象是根据图纸/模具造出来的具体产品。例如月饼模具是类,用模具制作的一个个月饼就是对象。
  • 类和对象都是引用类型。

Q:设计一个自定义类通常需要哪三个要素?命名规范是什么?

考察点: 类名、字段、方法及命名规范

参考答案:

  • 类名:同类事物的名字,使用大驼峰命名法(所有单词第一个字母大写);类名的提取通常使用名词提炼法分析整个业务流程,得出的名词通常就是类名。
  • 字段:同类事物具备的特征,使用小驼峰命名法(第一个单词首字母小写,其它单词首字母大写);字段的本质是全局变量。
  • 方法:同类事物具备的行为,使用大驼峰命名法(所有单词第一个字母大写)。
  • 语法格式:
csharp 复制代码
[访问权限修饰符] class 类名
{
    [访问权限修饰符] 数据类型 字段名;
    [访问权限修饰符] 返回值类型 方法名([形参列表]) { }
}

Q:如何创建对象(类的实例化)?如何访问对象中的字段?

考察点: 类的实例化与字段访问

参考答案:

  • 创建对象的语法:类名 对象名 = new 类名();
csharp 复制代码
Person person = new Person();
  • 获取字段值:数据类型 变量名 = 对象名.字段名;
  • 设置字段值:对象名.字段名 = 值;
csharp 复制代码
person.name = "光头强";
person.age = 32;
Console.WriteLine(person.name);
  • 每次 new 都会创建一个独立的对象,不同对象的字段值互不影响(如示例中 person 和 person1 各自独立赋值、独立输出)。

Q:为什么说"对象是代码的基本单元"?

考察点: 以对象为中心的编程思想

参考答案:

  • 面向对象以对象为中心,将数据(字段)和行为(方法)封装到一个对象中。
  • 程序通过对象之间的交互来完成任务,而不是靠主函数依次调用一堆函数。
  • 例如五子棋游戏可以拆分为:玩家对象(提供落黑子、白子的方法)、棋盘对象(提供绘制棋盘的方法)、规则对象(决定谁先走、判断输赢等),各对象协作完成整个游戏。

Q:面向过程和面向对象是互斥的吗?二者如何配合?

考察点: 两种思想的宏观与微观关系

参考答案:

  • 不互斥,二者是互补的关系。
  • 宏观上:通过面向对象进行整体分析、设计、开发。
  • 微观上:方法中代码的一步一步执行,仍然是面向过程。

Q:面向对象思想是在什么背景下出现的?

考察点: 面向对象产生的背景

参考答案:

  • 早期先有面向过程思想。
  • 随着软件规模的扩大、问题复杂性的提高,面向过程的弊端越来越明显地显示出来。
  • 于是出现了面向对象思想,并成为目前主流的编程方式。

二、方法与重载

Q:什么是方法?方法的语法格式是怎样的?

考察点: 方法的定义

参考答案:

  • 方法就是一大段代码,给这段代码起一个名称,这个名称叫做方法名;每一个方法都封装了对应的功能,当需要使用一个功能时就可以调用方法,实现代码的复用性。
  • 语法格式:
csharp 复制代码
[访问权限修饰符] [static] 返回值类型 方法名([形参列表])
{
    //方法体,编写功能代码
}
  • 中括号 \[\] 中的内容可以没有。
  • 访问权限修饰符:决定是否有权限调用该方法。
  • 返回值类型:方法执行完成后返回的结果类型,如果不需要返回结果使用 void 关键字。
  • 方法名:自定义名称,使用大驼峰命名规范。
  • 参数:方法运行时需要的数据,可以有参数,也可以没有参数。

Q:什么是方法重载(Overload)?构成重载的要求有哪些?

考察点: 方法重载的概念与要求

参考答案:

  • 方法重载允许在一个类中定义多个同名方法,但这些方法各自具有不同的参数类型、参数个数或参数的顺序。
  • 满足以下任一条件即可构成重载:
    1. 参数的类型不同。
    2. 参数个数不相同。
    3. 参数顺序不相同。
  • 和返回值、访问权限修饰符无关。
  • 当调用这些方法时,编译器会根据传递的参数来确定应该使用哪个方法。

Q:仅靠返回值不同或访问权限修饰符不同能构成方法重载吗?

考察点: 重载与返回值/访问修饰符的关系

参考答案:

  • 不能。
  • 方法重载与返回值、访问权限修饰符无关:只有参数的类型、个数或顺序不同才能构成重载。
  • 如果两个方法只有返回值不同、参数列表完全相同,编译器无法区分,会导致编译错误。

Q:方法重载有什么优点?为什么说它"没有提高任何效率"?

考察点: 重载的意义

参考答案:

  • 方法重载的主要优点:可以简化代码,避免为不同的参数组合编写多个函数。
  • 一般功能相同或者相似的方法可以使用同一个方法名,方便整理、归纳、记忆和调用。
  • 注意:方法重载没有提高任何效率,也没有节约任何代码,它仅仅能够帮助我们更加方便地组织代码。

Q:编译器是如何确定调用哪个重载方法的?

考察点: 重载的调用机制

参考答案:

  • 当调用重载方法时,编译器会根据传递的参数(实参)来确定应该使用哪个方法。
  • 即编译器按照实参的类型、个数和顺序与各个重载的形参列表进行匹配。

Q:判断下面的代码是否构成方法重载,并说明原因。

csharp 复制代码
public static void Test(string str) { }
public static void Test(int i) { }
public static void Test(double d) { }

考察点: 重载的判断

参考答案:

  • 构成方法重载。
  • 三个方法方法名相同(Test),但参数类型不同(string、int、double),满足重载要求"参数的类型不同"。
  • 重载与返回值、访问权限修饰符无关。

Q:举例说明重载方法在调用时如何匹配。

csharp 复制代码
public static void Test(string str) { Console.WriteLine(str); }
public static void Test(int i) { Console.WriteLine(i); }
public static void Test(double d) { Console.WriteLine(d); }

Test("hello");
Test(100);
Test(3.14);

考察点: 重载方法的调用

参考答案:

  • Test("hello"):实参为字符串,匹配 Test(string str),输出 hello。
  • Test(100):实参为整数,匹配 Test(int i),输出 100。
  • Test(3.14):实参为小数,匹配 Test(double d),输出 3.14。
  • 编译器根据实参的类型选择对应的重载版本。

Q:类中能否定义两个同名、同参数列表但访问修饰符不同的方法?

考察点: 重载与访问修饰符无关

参考答案:

  • 不能。
  • 重载只看参数列表(类型、个数、顺序),与访问权限修饰符无关。
  • 两个方法如果同名且参数列表完全相同,即使访问修饰符不同,也不构成重载,属于重复定义,无法通过编译。

三、构造方法与 this 关键字

Q:什么是构造方法(Constructor)?它的作用是什么?

考察点: 构造方法的概念与作用

参考答案:

  • 构造方法(构造函数/构造器)是一种特殊方法,当要创建类的实例(即对象)时,将会调用其构造函数。
  • 作用:可以通过构造函数完成相关初始化操作,例如为对象的字段、属性赋值,打开文件、连接数据库等。
  • 构造方法通过 new 关键字调用。

Q:构造方法有哪些特点?

考察点: 构造方法的特点

参考答案:

  • 构造方法的名称必须与定义它的类名完全相同。
  • 构造方法不返回任何值,即没有返回类型(不能写 void)。
  • 构造方法可以有任意的参数,并且可以重载。

Q:如果类中没有定义任何构造方法,编译器会怎么处理?

考察点: 默认构造方法

参考答案:

  • 如果没有定义任何构造方法,则编译器会自动定义一个无参的构造方法。
  • 如果已经定义了构造方法,则编译器不会自动添加。
  • 因此,当类中只定义了有参构造方法时,new 类名()(无参形式)将无法使用。

Q:构造方法执行时没有为字段赋值,字段的值是什么?

考察点: 字段的默认值

参考答案:

  • 构造方法执行时,如果没有为字段赋值,字段会被赋予默认值。
  • 例如 int 类型默认为 0,bool 类型默认为 false,string 等引用类型默认为 null。

Q:无参构造方法中给字段赋值有什么问题?有参构造方法如何解决?

考察点: 无参与有参构造的对比

参考答案:

  • 在无参构造方法中给字段赋值,每次实例化出来的对象字段值都是一样的;但不可能相同类型的事物特点都是完全相同的。
csharp 复制代码
public Person()
{
    name = "zs";
    age = 18;
    address = "北京";
    sex = "男";
    phoneNum = "123456";
}
  • 有参构造方法通过参数接收调用者传入的值,可以为不同对象赋予不同的字段值。
csharp 复制代码
public Person(string n, int a, string ad, string s, string p)
{
    name = n;
    age = a;
    address = ad;
    sex = s;
    phoneNum = p;
}

Q:构造方法可以重载吗?请举例说明。

考察点: 构造方法重载

参考答案:

  • 可以。构造方法可以有任意的参数,并且可以重载。
csharp 复制代码
class Person
{
    public Person() { }

    public Person(string n, int a)
    {
        name = n;
        age = a;
    }

    public Person(string n, int a, string ad, string s, string p)
    {
        name = n; age = a; address = ad; sex = s; phoneNum = p;
    }
}
  • 编译器根据 new 时传入的参数选择调用对应的构造方法。

Q:构造方法可以用 private 修饰吗?private 构造方法有什么效果?

考察点: 私有构造方法

参考答案:

  • 可以。构造方法也可以添加访问权限修饰符(如 public、internal、private)。
  • 构造方法是通过 new 关键字调用的,而 private 成员只能在本类中生效,因此 private 构造方法使得外部代码无法直接通过 new 创建该类的对象。
  • 典型用途:禁止外部随意实例化(例如只允许类内部创建对象,或配合静态成员实现单例等场景)。

Q:this 关键字是什么?可以在哪些地方使用?

考察点: this 的概念与使用范围

参考答案:

  • this 关键字引用当前类的实例(当前对象)。
  • 它只能在实例构造函数、实例方法和实例属性访问器中使用。
  • this 的本质是当前类实例化的对象。
  • 主要用途:区分字段(全局变量)和参数(局部变量)、传递当前对象、构造函数链和索引器。

Q:什么是全局变量和局部变量?它们有哪些区别?

考察点: 全局变量与局部变量

参考答案:

  • 全局变量是在类的范围内、和方法同级声明的,所以它们是类的成员,可以在类的任何地方访问。
  • 局部变量是在方法/函数、构造函数或语句块内声明的,只在声明它们的块内有效;方法/函数、构造函数的参数也属于局部变量。
  • 区别:
    1. 作用域:全局变量的作用域是整个类;局部变量的作用域仅限于声明它的方法/函数、构造函数或语句块。
    2. 生命周期:全局变量的生命周期与类的生命周期相同;局部变量的生命周期与所在的方法/函数、构造函数或语句块的执行周期相同。
    3. 默认值:全局变量如果没有显式初始化,会自动初始化为相应类型的默认值(0、false、null 等);局部变量在使用前必须初始化,系统不会为其设置默认值,否则编译器会报错。
    4. 性能:由于局部变量不需要在堆上分配内存,因此它们通常比全局变量快。

Q:全局变量和局部变量同名时如何区分?this 在其中起什么作用?

考察点: 就近原则与 this 区分成员

参考答案:

  • 全局变量和局部变量允许同名,使用时就近原则:在方法中如果出现了和成员变量同名的局部变量,会优先使用方法内部的局部变量。
  • 使用 this 关键字可以区分成员变量和局部变量(this 本质为当前类实例化的对象)。
csharp 复制代码
//参数属于局部变量。全局变量和局部变量允许同名,使用时就近原则。
public Person(string name, int age, string address, string sex, string phoneNum)
{
    this.name = name;
    this.age = age;
    this.address = address;
    this.sex = sex;
    this.phoneNum = phoneNum;
}
  • 如果当前方法中成员变量和局部变量的名称不同,this 可以省略不写。

Q:this 如何调用本类的构造方法(构造函数链)?

考察点: this 调用构造方法

参考答案:

  • 通过 this(参数列表) 可以调用本类的其它构造方法,实现构造函数链,避免重复代码。
csharp 复制代码
class Person
{
    private string _name;

    public Person()
    {
    }

    public Person(string name) : this()
    {
        _name = name;
    }
}
  • 调用时先执行 this() 指向的构造方法,再执行当前构造方法的代码。

Q:this 如何调用本类的普通方法?可以省略吗?

考察点: this 调用普通方法

参考答案:

  • 通过 this.方法名() 调用当前对象的实例方法;调用方法时,this 可以省略。
csharp 复制代码
class Person
{
    public void PlanA() { }
    public void PlanB() { }

    public void Play()
    {
        PlanA();      //省略 this
        this.PlanB(); //显式使用 this
    }
}

Q:什么是索引器?this 在索引器中如何使用?

考察点: 索引器

参考答案:

  • 索引器允许像访问数组一样通过下标访问对象的成员,使用 thisindex 定义。
csharp 复制代码
public class Collection
{
    private int[] items = new int[10];

    public int this[int index]
    {
        get { return items[index]; }
        set { items[index] = value; }
    }
}
  • 使用方式:Collection c = new Collection(); c[0] = 1; int v = c[0];

四、封装与访问修饰符

Q:C# 中有哪些访问权限修饰符?各自的可见范围是什么?

考察点: 四种访问修饰符

参考答案:

  • public(公开的):适用于类及类的成员,在整个解决方案生效。
  • internal(内部的):适用于类及类的成员,在整个项目中生效;类的默认访问权限是 internal。
  • private(私有的):适用于类的成员,只能在本类中生效;字段、属性和方法的默认访问权限是 private。
  • protected(受保护的):适用于类的成员,只能在本类中生效,也可以在子类(包含子类的子类)及跨项目的子类中生效。

Q:类和类的成员各自的默认访问权限是什么?

考察点: 默认访问权限

参考答案:

  • 类的默认访问权限是 internal。
  • 字段、属性和方法的默认访问权限是 private。

Q:public 和 internal 有什么区别?

考察点: public vs internal

参考答案:

  • public 在整个解决方案中生效,跨项目也能访问。
  • internal 在整个项目中生效,不能跨项目访问。
  • 例如:同一个项目中 internal 类和 public 类都可以正常使用;但在不同项目中,只能访问 public 类。

Q:protected 的可见范围是什么?它和 private 有什么区别?

考察点: protected vs private

参考答案:

  • protected:只能在本类中生效,也可以在子类(包含子类的子类)及跨项目的子类中生效。
  • private:只能在本类中生效。
  • 区别:protected 除了本类外还允许子类访问(包括跨项目的子类);private 只能本类访问,子类也不可以。

Q:什么是命名空间(namespace)?它的作用是什么?

考察点: 命名空间

参考答案:

  • namespace(命名空间)用于解决类重名问题,可以看作"类的文件夹"。
  • 使用规则:如果代码和使用的类在同一个 namespace 中,则不需要 using。
  • 在不同 namespace 下调用类有两种方法:
    1. 使用"命名空间.类名"直接调用。
    2. 先 using 引用命名空间,再直接调用。

Q:什么是封装?封装的原则是什么?

考察点: 封装的概念与原则

参考答案:

  • 封装是一种将数据和操作数据的方法捆绑在一起,并隐藏内部实现细节的机制。
  • 主要目的:保护对象的数据不被随意修改,隐藏访问数据操作的细节,只通过定义好的方法进行访问和操作,提高代码的安全性、可维护性和灵活性。
  • 封装的原则:
    1. 隐藏内部状态:将对象的字段设置为私有(private),外部代码不能直接访问这些字段。
    2. 提供公共接口:为对象提供公共方法(public)来操作内部数据,外部代码通过这些公共方法间接访问和修改内部数据。

Q:封装有哪些好处?

考察点: 封装的好处

参考答案:

  • 数据保护:防止外部代码直接访问和修改对象的内部数据,从而保护数据的一致性和完整性。
  • 易于维护:当内部实现发生变化时,只需要修改封装的方法,不需要修改外部代码。

Q:什么是属性(Property)?get 和 set 各有什么作用?

考察点: 属性的概念

参考答案:

  • 属性是一种特殊的成员,用于封装类中的字段,提供一种灵活的机制来读取(get)和/或写入(set)私有字段的值。
  • get:读取属性值时执行,返回私有字段的值。
  • set:给属性赋值时执行,通过 value 关键字拿到外部传入的值。
csharp 复制代码
internal class Person
{
    private string _name;

    public string Name
    {
        get { return _name; }
        set { _name = value; }
    }
}

Q:如何在属性中实现对赋值的校验?

考察点: set 中的校验

参考答案:

  • 可以在 set 访问器中对 value 进行判断和加工后再赋值给字段。
csharp 复制代码
public int Age
{
    get { return _age; }
    set { _age = value > 0 ? value : 0; }
}
  • 上述代码表示:赋给 Age 的值大于 0 时才采用,否则赋值为 0,从而保护数据的合理性。

Q:什么是自动属性?它解决了什么问题?

考察点: 自动属性(语法糖)

参考答案:

  • 如果属性仅为赋值和获取,没有其它复杂操作,C# 允许使用自动实现的属性,不需要显式地定义字段。
csharp 复制代码
internal class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
  • 自动属性是一种语法糖:语法糖指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但更方便程序员使用;简而言之,语法糖让程序更加简洁,有更高的可读性。

Q:私有字段和公开属性的命名规范是什么?

考察点: 命名规范

参考答案:

  • 私有的字段通常以 _ 开头,其它遵循小驼峰命名法(如 _name、_age)。
  • 公开的属性通常遵循大驼峰命名法(如 Name、Age)。
  • 开发工具快捷键:选中字段名后按 Ctrl + R + E 可快速生成 get/set 方法。

Q:封装中为什么字段要私有、对外提供公共属性/方法?

考察点: 封装的实现

参考答案:

  • 字段设置为 private 后,外部代码不能直接访问,防止数据被随意修改,保证数据的一致性和完整性。
  • 对外提供 public 属性(get/set)或方法,外部只能通过这些公共接口间接访问内部数据。
  • 在 set 中还可以加入校验等逻辑(如 Age 不能为负数),进一步提高安全性。
  • 当内部实现变化时,只需修改封装内部,外部调用代码无需改动,便于维护。

Q:访问权限修饰符可以修饰类本身吗?哪些可以?

考察点: 修饰类的访问修饰符

参考答案:

  • 可以。类可以使用 public 和 internal 修饰(private 和 protected 只能修饰类的成员)。
  • 类的默认访问权限是 internal。
  • 同一个项目中 internal 类和 public 类都能使用;不同项目中只有 public 类能被访问。

五、继承与方法重写

Q:什么是继承?继承有什么作用?

考察点: 继承的概念与作用

参考答案:

  • 继承是面向对象编程的核心特性,它允许创建一个新类(称为子类或派生类),基于另一个已存在的类(称为基类或父类)。
  • 通过继承,子类可以继承基类的属性(字段)和方法,并且可以添加新的属性、方法或覆盖(override)基类的成员。
  • 作用:如果多个类中存在相同的方法、属性,可以考虑将相同的方法、属性抽取到父类中,再通过继承关系让子类对象直接调用父类中的属性、方法,进而避免反复编写相同逻辑的代码,实现代码复用和层次化设计。

Q:继承的语法格式是什么?子类可以使用父类的哪些成员?

考察点: 继承语法

参考答案:

  • 语法格式:class A : B(A 类继承 B 类)。
csharp 复制代码
internal class Dog : Animal
{
    public void Bark() { }
}
  • A 类可以直接使用 B 类中可以被继承的属性和方法。
  • 如果 C 类继承 B 类、B 类继承 A 类,则 C 类可以直接使用 B 类和 A 类中可以被继承的属性和方法。
  • 总结:子类可以直接使用父类以及父类的父类中可以被继承的属性和方法。

Q:C# 支持多继承吗?为什么?

考察点: 单继承

参考答案:

  • C# 中类的继承是单继承,一个类只能有一个直接父类。
  • 接口为多继承,接口可以在某些方面弥补单继承的不足。

Q:什么情况下适合设计继承关系?

考察点: 继承的设计原则

参考答案:

  • 从语法角度讲,只要有公共的就可以提取到父类中;但按照面向对象思想,应该把具有继承关系的两个类设计成父子类。
  • 例如:可以把人类当做男人的父类,但不可以把牛设计为男人的父类(二者没有继承关系)。
  • 父类中应根据职责,封装子类共有的属性和方法。

Q:什么是方法重写(Override)?什么时候需要重写?

考察点: 方法重写的概念

参考答案:

  • 方法重写是面向对象编程中的核心概念:当从父类中继承的方法不能满足子类的需求时,子类可以重写(替换)从基类继承来的方法。
  • 重写的前提:父类方法使用 virtual 或 abstract 修饰,子类使用 override 关键字重写。

Q:virtual 方法有什么特点?

考察点: 虚方法

参考答案:

  • 父类中的方法使用 virtual 修饰,子类可以重写,也可以不重写。
  • 用 override 修饰的已经实现的方法即是虚方法。
csharp 复制代码
internal class Animal
{
    public virtual void Play()
    {
        Console.WriteLine("Animal中的Play方法");
    }
}

class Bird : Animal
{
    public override void Play()
    {
        Console.WriteLine("Bird中的Play方法");
    }
}

Q:什么是抽象方法和抽象类?抽象类有哪些特点?

考察点: abstract 方法与抽象类

参考答案:

  • 抽象方法:类中的方法使用 abstract 修饰,方法必须没有实现(没有方法体)。
  • 抽象类:使用 abstract 修饰的类。
  • 特点:
    1. 一个类中有抽象方法,这个类必须为抽象类。
    2. 抽象类中可以有非抽象方法。
    3. 抽象类有构造函数,但是抽象类不能创建对象。
    4. 抽象方法不能使用 private 访问权限修饰符(子类需要重写该方法)。
    5. 子类继承抽象类后,必须把抽象类的所有抽象方法重写(除非子类也是一个抽象类)。
csharp 复制代码
internal abstract class Car
{
    public abstract void drive();
}

Q:方法重写(Override)和方法重载(Overload)有什么区别?

考察点: 重写与重载的对比

参考答案:

  • 位置不同:重载发生在同一个类中;重写发生在子类和父类之间。
  • 作用不同:重载是在一个类里面的一种行为,方法名可以相同(参数不同),提高可读性、可维护性;重写是父类方法无法满足子类的要求时,子类通过方法重写来满足要求。
  • 实现方式不同:重载依靠参数列表(类型、个数、顺序)区分;重写需要父类方法用 virtual/abstract 修饰、子类方法用 override 修饰。

Q:什么是里氏替换原则(LSP)?它与方法重写有什么关系?

考察点: 里氏替换原则

参考答案:

  • 在 C# 中,继承和方法重写(virtual/override)是实现多态性的技术手段,而里氏替换原则(LSP)则是约束这些手段的设计准则。
  • 关系:里氏替换原则是"宪法",规定了子类替换父类后程序不能出错;方法重写是"执行工具",必须在宪法框架下使用,否则会引发灾难。
  • LSP 不仅要求语法上的兼容(子类能赋给父类),更要求行为上的兼容,具体体现在三大约束:
    1. 前置条件不能强化:子类重写方法时,参数要求不能比父类更严格(例如父类接受 int,子类不能要求必须是正整数)。
    2. 后置条件不能弱化:子类重写方法后,返回的结果保证不能比父类更少(例如父类保证返回非空,子类不能返回 null)。
    3. 不变量必须保持:子类不能破坏父类设定的内部状态逻辑(例如父类保证 Width > 0,子类重写后不能允许 Width = -1)。

Q:什么是 base 关键字?base 是父类对象的引用吗?

考察点: base 的概念

参考答案:

  • base 关键字用于访问当前类的基类成员,包括基类的字段、属性、方法、构造函数等。
  • base 不是父类对象的引用,而是一个指向当前对象中基类部分的关键字。
  • 派生类对象中包含一个基类部分,这个基类部分并不是一个独立的对象,而是派生类对象的一部分;base 提供了一种从派生类中访问基类部分的方式。
  • 在派生类中,this 引用的是整个当前对象(包括派生类和基类部分),而 base 引用的是当前对象中的基类部分。
csharp 复制代码
class Bird : Animal
{
    public string _name = "鸟";

    public override void Play()
    {
        Console.WriteLine("Bird中的Play方法");
    }

    public void Test()
    {
        Console.WriteLine(_name);      //鸟
        Console.WriteLine(base._name); //动物(基类字段)

        base.Play();                   //Animal中的Play方法
    }
}

Q:创建子类对象时,父类和子类构造方法的执行顺序是什么?

考察点: 父子类构造方法执行顺序

参考答案:

  • 创建子类对象时,会先执行父类的构造方法,再执行子类的构造方法(先父后子)。
  • 因为子类对象中包含基类部分,基类部分需要先被初始化。
  • 子类构造方法还可以通过 base(参数) 指定调用父类的某个构造方法。

Q:this 和 base 有什么区别?

考察点: this vs base

参考答案:

  • this 用于引用当前类的实例(对象):
    • 可以通过 this 访问当前对象的成员,每个实例方法都有一个隐式的 this 参数,该参数指向当前实例。
    • this 访问的成员在本类中没有时才访问父类中继承的成员。
  • base 用于访问父类对象中的成员:
    • base 关键字并不是父类对象的引用,而是一种用于访问基类成员的语法。
    • 它允许在派生类中访问基类的构造函数、方法和属性等。
  • 简单总结:this 指向当前对象(含基类部分),base 指向当前对象中的基类部分。

Q:什么是 sealed 密封类?有什么作用?

考察点: sealed

参考答案:

  • 在 C# 中,一个类可以被声明为 sealed 密封类,这意味着它不能被继承。
  • 作用:限制类的扩展,防止其它类以该类为父类继续派生。

Q:举例说明如何通过继承实现代码复用。

考察点: 继承的应用

参考答案:

  • 需求:定义动物类,有姓名和年龄字段、吃和睡的行为;猫类、狗类拥有动物类的所有字段和方法,猫还有抓老鼠的行为,狗还有叫的行为。
  • 做法:把共有的字段、方法抽取到 Animal 父类中,Cat、Dog 继承 Animal。
csharp 复制代码
internal class Animal
{
    private string _name;
    private int _age;

    public string Name { get => _name; set => _name = value; }
    public int Age { get => _age; set => _age = value; }

    public void Eat() { Console.WriteLine("Animal中继承的吃方法"); }
    public void Sleep() { Console.WriteLine("Animal中继承的睡方法"); }
}

internal class Dog : Animal
{
    public void Bark() { Console.WriteLine("Dog中的叫方法"); }
}

internal class Cat : Animal
{
    public void Catch() { Console.WriteLine("Cat中的抓老鼠方法"); }
}
  • 使用时,Dog、Cat 对象可以直接使用父类的 Name、Age、Eat、Sleep,无需重复编写,只需补充自己独有的行为。

六、接口

Q:什么是接口(Interface)?接口存在的意义是什么?

考察点: 接口的概念

参考答案:

  • 接口是一种定义规范、而不是定义行为具体实现的引用类型。
  • 它用于定义一组方法、属性、事件或索引器的契约;接口本身不提供实现,而是由实现接口的类来提供具体实现。
  • 接口存在的意义就是制定规范。
  • 接口定义中只包含属性、方法等的声明,而没有实际的方法实现或字段;可以把一个接口看作是一组公共的属性和方法的规范。

Q:接口有哪些特点?

考察点: 接口的特点

参考答案:

  • 接口存在的意义就是制定规范。
  • 接口没有构造函数,不能创建对象。
  • 接口中属性、方法不允许添加访问权限修饰符,默认为 public。
  • 接口与接口之间可以多继承。
  • 类与接口之间可以多实现。
  • 实现类实现了接口后,必须将接口中所有抽象方法都实现。

Q:接口的名称有什么约定?为什么?

考察点: 接口命名规范

参考答案:

  • 接口使用 interface 关键字声明,它与类的声明类似,接口声明默认是 public 的。
  • 按照约定,接口名称以大写字母 I 开头(如 IMyInterface、ICar)。

Q:接口中成员的访问权限是什么?

考察点: 接口成员的默认访问权限

参考答案:

  • 接口中属性、方法不允许添加访问权限修饰符,默认为 public。
  • 接口中的属性是隐式 public。
  • 接口中的方法隐式为 public abstract(公开的抽象方法)。

Q:接口中可以定义字段吗?为什么?

考察点: 接口的内容

参考答案:

  • 不能。
  • 接口定义中只包含属性、方法等的声明,而没有实际的方法实现或字段。
  • 接口中只能声明方法、属性、事件或索引器的契约,具体实现由实现接口的类提供。

Q:接口可以创建对象吗?

考察点: 接口与对象

参考答案:

  • 不能。
  • 接口没有构造函数,因此不能创建对象。
  • 接口只能用于声明规范,具体对象要由实现接口的类来创建(例如 ICar car = new Benz(); 创建的是实现类 Benz 的对象)。

Q:一个类可以实现多个接口吗?语法格式是什么?

考察点: 多实现

参考答案:

  • 可以。一个类可以实现一个或多个接口。
  • 语法格式:class 类名 : 接口名, 接口名
csharp 复制代码
internal interface IMyInterface
{
    void Play();
}
internal interface IMyInterface1
{
    void Play1();
}

class MyClass : IMyInterface, IMyInterface1
{
    public void Play() { }
    public void Play1() { }
}

Q:接口与接口之间可以继承吗?可以多继承吗?

考察点: 接口的多继承

参考答案:

  • 可以。接口与接口之间可以多继承。
  • 语法格式:interface 接口名 : 接口名, 接口名
csharp 复制代码
internal interface IAInterface : IBInterface, ICInterface
{
    void TestA();
}
internal interface IBInterface { void TestB(); }
internal interface ICInterface { void TestC(); }

class MyClass : IAInterface
{
    public void TestA() { }
    public void TestB() { }
    public void TestC() { }
}
  • 由于类之间的继承是单继承,接口的多继承可以弥补单继承的不足。

Q:实现接口的类必须做什么?

考察点: 接口实现的要求

参考答案:

  • 实现类实现了接口后,必须将接口中所有抽象方法都实现。
  • 例如接口 IMyInterface 中声明了 void Play();,实现类 MyClass 必须提供 public void Play() 的实现。
csharp 复制代码
internal interface IMyInterface
{
    void Play();
}

class MyClass : IMyInterface
{
    public void Play()
    {
        //实现接口中的方法
    }
}

Q:为什么说接口可以弥补 C# 单继承的不足?

考察点: 接口的意义

参考答案:

  • C# 中类的继承是单继承的,一个类只能有一个直接父类。
  • 接口为多继承,一个类可以实现多个接口。
  • 因此当类需要具备多个来源的能力(规范)时,可以通过实现多个接口来实现,弥补单继承的不足。

Q:接口和抽象类有什么区别?

考察点: 接口 vs 抽象类

参考答案:

  • 成员内容:抽象类中可以有抽象方法(无方法体),也可以有非抽象方法(有方法体);接口中只包含属性、方法等的声明,没有实际的方法实现,也不能有字段。
  • 构造与实例化:抽象类有构造函数,但不能创建对象;接口没有构造函数,不能创建对象。
  • 访问权限:抽象方法不能使用 private 修饰(子类需要重写);接口中的成员不允许添加访问权限修饰符,默认为 public(方法隐式为 public abstract)。
  • 继承与实现:抽象类用于类的单继承体系(子类必须重写所有抽象方法,除非子类也是抽象类);接口与接口之间可以多继承,类与接口之间可以多实现。
  • 相同点:二者都不能创建对象,子类/实现类都必须实现其中的抽象成员。

Q:接口是如何定义和实现的?请写出完整示例。

考察点: 接口定义与实现

参考答案:

  • 定义接口:
csharp 复制代码
[public | internal] interface 接口名
{
    返回值类型 方法名([形参列表]);
}
  • 实现接口:
csharp 复制代码
internal interface IMyInterface
{
    void Play();
}

class MyClass : IMyInterface
{
    public void Play()
    {
        Console.WriteLine("实现了接口中的Play方法");
    }
}

七、多态与类型转换

Q:什么是多态?

考察点: 多态的概念

参考答案:

  • 多态是面向对象三大特性之一。
  • 调用同一个父类/接口中的方法,由于子类/实现类不同,可能会有不同的行为。
  • 例如同样是调用 drive() 方法,Benz、Bmw、Audi 三个实现类输出的内容不同。

Q:多态的使用原则(前提)是什么?

考察点: 多态的前提

参考答案:

  • 以继承(继承类)或实现(实现接口)为前提。
  • 多态可以增加代码的灵活度。
  • 多态是调用方法的技巧,不会影响到类/接口的内部设计。

Q:多态的实现方式有哪几种?

考察点: 多态的实现

参考答案:

  • 三种常见方式:
    1. 虚方法重写:父类方法用 virtual 修饰,子类用 override 重写。
    2. 抽象方法重写:父类为抽象类,抽象方法用 abstract 声明,子类用 override 重写。
    3. 接口实现:类实现接口中的方法。
  • 使用父类型/接口类型的引用指向子类/实现类对象,调用方法时表现出不同的行为。

Q:下面的代码输出什么?为什么?

csharp 复制代码
internal class Car
{
    public virtual void drive() { }
}

class Benz : Car
{
    public override void drive() { Console.WriteLine("Benz实现的drive方法"); }
}
class Bmw : Car
{
    public override void drive() { Console.WriteLine("Bmw实现的drive方法"); }
}
class Audi : Car
{
    public override void drive() { Console.WriteLine("Audi实现的drive方法"); }
}

static void Main(string[] args)
{
    Car car1 = new Benz();
    car1.drive();
    Car car2 = new Bmw();
    car2.drive();
    Car car3 = new Audi();
    car3.drive();
}

考察点: 多态的调用结果

参考答案:

  • 输出:

    Benz实现的drive方法
    Bmw实现的drive方法
    Audi实现的drive方法

  • 原因:用父类型 Car 的引用指向不同的子类对象,调用被重写的 drive() 方法时,实际执行的是各自子类重写后的方法,这就是多态。

Q:抽象类和接口分别如何实现多态?请对比示例。

考察点: 多态的两种实现(抽象类/接口)

参考答案:

  • 抽象类方式:
csharp 复制代码
internal abstract class Car
{
    public abstract void drive();
}

class Benz : Car
{
    public override void drive() { Console.WriteLine("Benz实现的drive方法"); }
}
  • 接口方式:
csharp 复制代码
internal interface ICar
{
    void drive();
}

class Benz : ICar
{
    public void drive() { Console.WriteLine("Benz实现的drive方法"); }
}
  • 使用时都用父类型/接口类型的引用指向具体对象:
csharp 复制代码
Car car1 = new Benz();   //抽象类方式
car1.drive();

ICar car2 = new Benz();  //接口方式
car2.drive();
  • 区别:抽象类方式用 override 重写抽象方法;接口方式由实现类直接实现接口方法(接口方法隐式为 public abstract)。

Q:使用多态创建的对象能调用子类独有的方法吗?为什么?

考察点: 多态的限制

参考答案:

  • 不能。
  • 使用多态时只能调用子类/实现类中继承、重写或实现的父类/接口的方法,不能调用子类/实现类中独有的方法。
  • 因为引用变量的类型是父类型/接口类型,编译时只能看到该类型声明的成员。
  • 转换类型后(把父类型引用转回子类型),就可以调用子类独有的方法了。

Q:多态创建对象后,如何把父类型引用转换为子类型引用?有哪几种方式?

考察点: 类型转换

参考答案:

  • 使用多态创建对象:父类型|接口类型 对象名1 = new 子类型|实现类型();
csharp 复制代码
Person person = new Zs();
  • 方式1:强制类型转换。
csharp 复制代码
Zs zs1 = (Zs)person;
  • 方式2:as 关键字。as 关键字用于将一个对象强制转换为指定的类型,转换失败时返回 null(不抛异常)。
csharp 复制代码
Zs zs2 = person as Zs;
  • 转换类型后,就可以调用子类独有的方法了。

Q:is 关键字的作用是什么?

考察点: is 类型检查

参考答案:

  • is 关键字主要作用是进行类型检查,判断一个对象是否属于某个类型,返回 bool 值。
  • 语法格式:bool 变量名 = 对象名 is 类型;
  • 使用 is 关键字可以增加代码的安全性,先判断再转换。
csharp 复制代码
Person person = new Zs();
if (person is Zs)
{
    Zs zs2 = person as Zs;
    Zs zs1 = (Zs)person;
}

Q:as 和 is 有什么区别?

考察点: as vs is

参考答案:

  • is 操作符主要用于检查一个表达式的类型是否与指定的类型兼容,返回 bool(true/false),不进行转换。
  • as 操作符主要用于将一个对象强制转换为指定的类型,转换成功返回该类型的对象。
  • 通常配合使用:先用 is 判断类型是否兼容,再用 as(或强转)进行转换,保证安全。

Q:为什么说多态能增加代码的灵活度?请举例说明。

考察点: 多态的灵活性

参考答案:

  • 因为调用的是同一个父类/接口的方法,但可以传入不同的子类/实现类对象,表现出不同的行为。
csharp 复制代码
internal abstract class Person
{
    public abstract void Play();
}

class Zs : Person { public override void Play() { Console.WriteLine("Zs 重写的Play方法"); } }
class Ls : Person { public override void Play() { Console.WriteLine("Ls 重写的Play方法"); } }
class Ww : Person { public override void Play() { Console.WriteLine("Ww 重写的Play方法"); } }

class TestClass
{
    public void Test(Person person)
    {
        person.Play();
    }
}
  • 调用时只需要一个 Test 方法,就能处理所有 Person 的子类对象,无需为每个子类单独写一个方法;以后新增子类也无需修改 TestClass。
csharp 复制代码
TestClass testClass = new TestClass();
testClass.Test(new Zs());
testClass.Test(new Ls());
testClass.Test(new Ww());

八、static 与字符串

Q:static 关键字可以修饰什么?静态成员有什么特点?

考察点: static 概述

参考答案:

  • static(静态)关键字是一个修饰符,它可以修饰类、方法、字段、属性。
  • 静态成员属于类所有,不属于类的任何实例;可以通过类名直接访问它们,无需创建类的实例。
  • 使用 static 修饰的静态成员,在类被加载时就会加载到内存中,直到程序执行结束,所以尽可能少用静态。

Q:静态字段和实例字段(非静态字段)有什么区别?

考察点: 静态字段 vs 实例字段

参考答案:

  • 静态字段:属于类;在类被加载时就完成相关处理(类加载只进行一次),静态字段只有一份,被该类的所有实例化对象共享;存储在静态存储区中;用"类名.字段名"调用。
  • 非静态字段(实例字段):属于实例化对象;实例化对象时完成相关处理,实例化一次对象就有一份;存储在堆空间中;用"对象名.字段名"调用。
csharp 复制代码
class Person
{
    public string name;        //非静态字段(实例字段)
    public static string addr; //静态字段
}

Person.addr = "北京";           //类名.字段名 调用静态字段

Person person = new Person();
person.name = "光头强";          //对象名.字段名 调用实例字段

Q:什么操作会触发类加载?类加载时静态字段会怎样?

考察点: 类加载

参考答案:

  • 触发类加载的三种情况:
    1. 通过"类名.静态成员"访问静态成员时触发。
    2. 实例化对象时触发。
    3. 使用反射时触发。
  • 只要进行了类加载,静态字段就会完成相关处理。
  • 实例化对象时,非静态字段(实例字段)完成相关处理。

Q:静态方法有哪些特点?静态方法可以被重写吗?

考察点: 静态方法

参考答案:

  • 静态方法:属于类;类加载时就完成相关处理(类加载只进行一次);存储在静态存储区中;通过"类名.方法名"调用。
  • 静态方法可以被继承,不允许被重写。
  • 非静态方法(实例方法):属于对象;实例化对象时完成相关处理(实例化一次处理一次);存储在堆空间中;通过"对象名.方法名"调用。
csharp 复制代码
class Person
{
    public void Play() { }        //非静态方法(实例方法)
    public static void Eat() { }  //静态方法
}

Person.Eat();        //类名.方法名 调用静态方法

Person person = new Person();
person.Play();       //对象名.方法名 调用实例方法

Q:静态方法和实例方法之间可以互相访问吗?有什么注意事项?

考察点: 静态方法的访问限制

参考答案:

  • 非静态方法(实例方法)中可以直接使用静态成员和非静态成员。
  • 静态方法中只能直接使用静态成员,不能直接使用非静态成员(实例成员属于对象,而静态方法不依赖对象存在)。
  • 因此 Main 方法(静态方法)中要使用实例成员,必须先 new 创建对象,再通过对象访问。

Q:什么是静态类?静态类有哪些特点?

考察点: 静态类

参考答案:

  • 静态类用于包含只能使用静态成员的特殊类。
  • 特点:
    1. 不能创建静态类的实例。
    2. 不能从静态类派生(因为它没有实例成员)。
    3. 静态类不能有实例构造器。
    4. 静态类不能有实例成员,只能有静态成员。
    5. 静态类在编译时初始化,所有静态成员都必须在静态构造函数中初始化。
  • 典型例子:Console 类就是一个静态类,它提供了许多与控制台交互的方法,比如 ReadLine、WriteLine 等。

Q:静态构造函数是什么?什么时候执行?

考察点: 静态构造函数

参考答案:

  • 静态构造函数用于初始化静态成员;静态类在编译时初始化,所有静态成员都必须在静态构造函数中初始化。
  • 静态类不能有实例构造器(不能定义实例构造函数)。
  • 静态构造函数在类加载时执行,且只执行一次(与静态成员"类加载只进行一次"的特点一致)。

Q:为什么要"尽可能少用静态"?

考察点: 静态的使用注意

参考答案:

  • 使用 static 修饰的静态成员,在类被加载时就会加载到内存中,直到程序执行结束才释放。
  • 静态成员在整个应用程序域中只有一个实例,长期占用内存。
  • 因此应尽可能少用静态,只在确实需要全局共享或通用功能时使用。

Q:静态成员有哪些典型用途?

考察点: 静态的用途

参考答案:

  • 静态成员提供了全局数据共享的功能,因为它们在整个应用程序域中只有一个实例,非常适合用于跨对象共享数据。
  • 静态成员常用于实现通用的功能,因为它们不需要实例化即可使用。
  • 静态类通常用于实现一组相关的功能而不需要实例化,比如 Console 类的 ReadLine、WriteLine 等方法。

Q:string 是什么类型?string 和 String 有什么关系?

考察点: string 类型

参考答案:

  • 字符串类型为引用数据类型。
  • 在 C# 中,string 关键字是 System.String 的别名,因此 String 和 string 是等效的。
  • 建议使用提供的别名 string。

Q:字符串在内存中是如何存储的?

考察点: 字符串存储

参考答案:

  • 直接赋值方式创建(string s1 = "hello";):会先判断这个内容有没有已经在驻留池中出现过;如果已经出现过,不会再在驻留池中存放一个相同的内容,该内容只会固定有一个引用,所以创建相同内容的 string 时,它们的引用都是相同的。
  • 动态拼接操作(string s2 = s1 + " World";):拼接操作会创建新字符串,新字符串在堆上;进行很多字符串拼接时,会产生大量的临时字符串。

Q:为什么说 string 是不可变的?频繁修改字符串有什么问题?

考察点: 字符串不可变性

参考答案:

  • String 字符串是不可改变的对象,字符串在创建以后就不能再进行修改;如果进行了修改,实际上是产生了一个新的字符串对象。
  • 如果频繁地使用字符串中的方法对字符串进行操作,会产生大量的字符串对象,增加系统的压力,造成系统资源的浪费。
  • 典型场景:在一个循环中将许多字符串连接在一起时,使用 string 拼接会产生大量临时字符串,性能较差。

Q:StringBuilder 有什么作用?它与 string 有什么区别?

考察点: StringBuilder vs string

参考答案:

  • 使用 StringBuilder 操作字符串不会产生新的字符串对象(在原有对象上进行修改)。
  • 使用 StringBuilder 对象时首先要引用命名空间 System.Text。
  • 区别:string 不可变,每次修改都产生新对象;StringBuilder 可变,修改不产生新对象。
  • 应用场景:当在一个循环中将许多字符串连接在一起时,使用 StringBuilder 类可以提升性能。
csharp 复制代码
StringBuilder stringBuilder = new StringBuilder("a");
stringBuilder.Append("b");
stringBuilder.Append("c");
Console.WriteLine(stringBuilder.ToString());

Q:StringBuilder 的常见方法有哪些?

考察点: StringBuilder 方法

参考答案:

  • Append(string value):字符串拼接。
  • Insert(int index, string value):向指定位置添加。
  • Remove(int startIndex, int length):删除,从哪个下标开始、删除几个。
  • Replace(string oldValue, string newValue):用新字符串替换旧字符串。
  • ToString():将 StringBuilder 类型变为 String 类型。
csharp 复制代码
StringBuilder stringBuilder = new StringBuilder("a");
stringBuilder.Append("b");
stringBuilder.Append("c");

stringBuilder.Insert(1, "x");
stringBuilder.Remove(1, 1);
stringBuilder.Replace("c", "ccc");
Console.WriteLine(stringBuilder.ToString());

Q:string 类型有哪些常见方法?分别有什么作用?

考察点: string 常见方法

参考答案:

  • 静态方法:
    • string.Concat(...):连接多个字符串(如 string.Concat("Hello", "World"))。
    • string.Join(分隔符, 数组):使用指定的分隔符连接字符串数组中的元素(如 string.Join("-", new\[\]{"1","2","3","4"}) 结果为 "1-2-3-4")。
    • string.IsNullOrEmpty(str):判断字符串是 null 或空字符串 "",是则返回 True。
    • string.IsNullOrWhiteSpace(str):判断字符串是 null 或仅包含空白字符,是则返回 True。
  • 实例方法:
    • Contains(value):判断当前字符串是否包含指定的子字符串。
    • IndexOf(value) / LastIndexOf(value):返回子字符串首次/最后一次出现的位置,没有则返回 -1。
    • StartsWith(value) / EndsWith(value):判断是否以指定的字符串开始/结束。
    • Replace(oldValue, newValue):替换所有指定的子字符串。
    • Split(分隔符):字符串分割,返回字符串数组。
    • Substring(startIndex, length):截取子字符串。
    • ToLower() / ToUpper():小写/大写转换。
    • Trim():去除字符串前后空格。
  • 另外:new string(char\[\] value) 构造方法可以把字符数组转换为字符串。

九、值类型与引用类型比较

Q:值类型之间如何比较?比较的是什么?

考察点: 值类型比较

参考答案:

  • 值类型之间的比较,比较的是实际存储的值。
csharp 复制代码
int num1 = 100;
int num2 = 100;
Console.WriteLine(num1 == num2); //True

bool b1 = true;
bool b2 = true;
Console.WriteLine(b1 == b2); //True
  • 只要实际存储的值相等,比较结果就是 True。

Q:引用类型用 == 比较的是什么?

考察点: 引用类型 == 比较

参考答案:

  • 引用类型用 == 比较的是两个对象在堆中的地址(引用是否相同)。
csharp 复制代码
Person person1 = new Person("光头强", 32);
Person person2 = new Person("光头强", 32);

Console.WriteLine(person1 == person2); //False
  • 两个对象虽然字段内容完全相同,但它们在堆中的地址不同,所以 == 结果为 False。

Q:ReferenceEquals() 方法是什么?它比较什么?

考察点: ReferenceEquals

参考答案:

  • ReferenceEquals(object objA, object objB) 是 Object 中的静态方法。
  • 静态方法可以被继承,不允许被重写。
  • ReferenceEquals() 比较的是两个对象在堆中的地址。
csharp 复制代码
Person person1 = new Person("光头强", 32);
Person person2 = new Person("光头强", 32);

Console.WriteLine(ReferenceEquals(person1, person2)); //False

Q:Equals() 方法是什么?默认比较什么?

考察点: Equals

参考答案:

  • Equals(object obj) 是 Object 中的实例方法。
  • 默认情况下,Equals() 比较的是两个对象在堆中的地址。
csharp 复制代码
Person person1 = new Person("光头强", 32);
Person person2 = new Person("光头强", 32);
bool a = person1.Equals(person2);

Console.WriteLine(a); //False
  • 因此两个分别 new 出来、内容相同的对象,默认 Equals 结果也是 False。

Q:如何让两个内容相同的对象通过 Equals 比较返回 True?

考察点: 重写 Equals

参考答案:

  • 需要重写 Object 中的 Equals() 方法,自定义比较规则(比较字段内容)。
csharp 复制代码
class Person
{
    string name;
    int age;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    public override bool Equals(object obj)
    {
        return obj is Person person &&
               name == person.name &&
               age == person.age;
    }
}

Person person1 = new Person("光头强", 32);
Person person2 = new Person("光头强", 32);
bool a = person1.Equals(person2);

Console.WriteLine(a); //True

Q:== 和 Equals() 有什么区别?

考察点: == vs Equals

参考答案:

  • 值类型:== 比较实际存储的值,内容相同即为 True。
  • 引用类型:== 默认比较两个对象在堆中的地址;Equals() 是 Object 中的实例方法,默认也比较堆中的地址,但可以通过重写 Equals() 改为按对象内容比较。
  • string 特殊:由于字符串驻留池(常量池)机制,相同内容的字符串地址相同,所以 ==、ReferenceEquals、Equals 都为 True。
  • 总结:== 的语义取决于类型;Equals 的语义取决于是否重写。

Q:为什么 string 类型的比较是特殊的?

考察点: string 的特殊比较

参考答案:

  • string 是特殊的引用数据类型,在常量区里会分配一块空间叫做 String 暂存池(常量池),字符串数据是存储在这个常量池中的;存储相同的数据,返回相同的地址。
  • 因此两个内容相同的字符串,它们的引用是相同的,三种比较结果都为 True:
csharp 复制代码
string str1 = "abc";
string str2 = "abc";

Console.WriteLine(str1 == str2);                //True
Console.WriteLine(ReferenceEquals(str1, str2)); //True
Console.WriteLine(str1.Equals(str2));           //True

Q:重写 Equals() 方法时常用什么写法?

考察点: Equals 重写技巧

参考答案:

  • 常用"类型模式匹配 + 字段比较"的写法:
csharp 复制代码
public override bool Equals(object obj)
{
    return obj is Person person &&
           name == person.name &&
           age == person.age;
}
  • 先用 obj is Person person 判断对象是否为 Person 类型并转换,再逐个比较字段值,全部相等才返回 True。
  • 课程案例中的登录功能就重写了 User 类的 Equals,比较 _username 和 _password 两个字段。

Q:输出一个引用类型对象时会发生什么?

考察点: ToString()

参考答案:

  • 输出引用类型对象时,编译器会默认调用 Object 中的 ToString() 方法。
  • 如果没有重写,默认输出类的完整名称(命名空间.类名)。
csharp 复制代码
Person person = new Person("光头强", 32);

//默认调用Object中继承的ToString()方法
Console.WriteLine(person); //Demo01.Person

Q:如何让 Console.WriteLine(person) 输出有意义的内容?

考察点: 重写 ToString()

参考答案:

  • 重写 Object 中的 ToString() 方法,返回自定义的字符串。
csharp 复制代码
class Person
{
    string name;
    int age;

    public Person(string name, int age)
    {
        this.name = name;
        this.age = age;
    }

    public override string ToString()
    {
        return $"name={name}, age={age}";
    }
}

Person person = new Person("光头强", 32);

//调用重写后的ToString()方法
Console.WriteLine(person); //name=光头强, age=32
  • 输出对象时就会自动调用重写后的 ToString() 方法。

Q:值类型和引用类型在内存中的存储有什么区别?

考察点: 值类型 vs 引用类型(存储)

参考答案:

  • 值类型:变量直接存储实际的数据值,比较时比较的就是这个值。
  • 引用类型:变量存储的是对象在堆中的引用(地址),对象数据本身存放在堆空间中。
  • 因此引用类型比较的是堆中的地址,两个内容相同但分别 new 出来的对象地址不同,比较结果为 False。
  • 补充:课程笔记中强调,实例字段(非静态字段)存储在堆空间中,静态字段存储在静态存储区中。

Q:两个字段值完全相同的 Person 对象,为什么 == 和 ReferenceEquals 都是 False,而重写 Equals 后可以是 True?

考察点: 三种比较方式综合

参考答案:

  • == 比较的是两个对象在堆中的地址,person1 和 person2 是分别 new 出来的两个对象,地址不同,结果为 False。
  • ReferenceEquals() 是 Object 的静态方法,同样比较堆中的地址,结果也是 False。
  • Equals() 默认也比较堆中的地址;但重写 Equals() 后,比较规则变成了"比较字段内容",字段值相同就返回 True。
  • 总结:默认三种方式都比较地址;只有重写 Equals() 后才能实现"按内容比较"。

十、DateTime

Q:DateTime 是什么?如何创建 DateTime 对象?

考察点: DateTime 的创建

参考答案:

  • DateTime 是 C# 中的日期时间类。
  • 两种常用构造方法:
    • public DateTime(int year, int month, int day):创建指定日期的 DateTime 对象。
    • public DateTime(int year, int month, int day, int hour, int minute, int second):创建指定日期与时间的 DateTime 对象。
csharp 复制代码
DateTime time = new DateTime(2018, 1, 20);                    //2018年1月20日 0时0分0秒
DateTime time2 = new DateTime(2019, 11, 30, 12, 01, 59);      //2019年11月30日 12:01:59

Q:DateTime.Now 是什么?它和 new DateTime(...) 有什么区别?

考察点: DateTime.Now

参考答案:

  • DateTime.Now 表示当前系统的日期与时间,即代码运行到这里的系统日期与时间。
  • new DateTime(年,月,日,时,分,秒) 是创建指定日期时间的对象。
csharp 复制代码
DateTime time = DateTime.Now; //当前系统日期与时间

Q:如何获取 DateTime 的年、月、日、时、分、秒?

考察点: DateTime 属性

参考答案:

  • 通过属性获取:
csharp 复制代码
DateTime dateTime = DateTime.Now;

int year = dateTime.Year;        //年
int month = dateTime.Month;      //月
int day = dateTime.Day;          //日
DayOfWeek dayOfWeek = dateTime.DayOfWeek; //星期几
string v = dayOfWeek.ToString();

int hour = dateTime.Hour;        //时
int minute = dateTime.Minute;    //分
int second = dateTime.Second;    //秒
  • 常用属性:Year、Month、Day、DayOfWeek、Hour、Minute、Second。

Q:DateTime 转换为字符串有哪些常用方法?

考察点: DateTime 转 string

参考答案:

csharp 复制代码
DateTime dateTime = new DateTime(2019, 11, 30, 12, 01, 59);

Console.WriteLine(dateTime.ToString());            //2019/11/30 12:01:59
Console.WriteLine(dateTime.ToLongDateString());    //2019年11月30日
Console.WriteLine(dateTime.ToShortDateString());   //2019/11/30
Console.WriteLine(dateTime.ToLongTimeString());    //12:01:59
Console.WriteLine(dateTime.ToShortTimeString());   //12:01
  • ToString():默认格式;ToLongDateString():长日期格式;ToShortDateString():短日期格式;ToLongTimeString():长时间格式;ToShortTimeString():短时间格式。

Q:DateTime.ToString("yyyy-MM-dd hh:mm:ss") 中的格式符分别表示什么?

考察点: 日期时间格式符

参考答案:

  • yyyy:四位年份(如 2019);yy:两位年份(如 19)。
  • MM:两位月份(0112);M:月份(112)。
  • dd:两位天(0131);d:天(131,具体看月份允许的最大天数)。
  • hh:12小时制小时(0112);h:12小时制(112)。
  • HH:24小时制小时(0023);H:24小时制(023)。
  • mm:两位分钟(0059);m:分钟(059)。
  • ss:两位秒(0059);s:秒(059)。
csharp 复制代码
DateTime dateTime = new DateTime(2019, 11, 30, 12, 01, 59);
string str = dateTime.ToString("yyyy-MM-dd hh:mm:ss"); //2019-11-30 12:01:59

Q:h 和 H 有什么区别?

考察点: 12小时制 vs 24小时制

参考答案:

  • h:12小时制,显示 1~12(hh 显示 01~12)。
  • H:24小时制,显示 0~23(HH 显示 00~23)。
  • 例如下午 3 点:h 显示 3 或 03,H 显示 15。

Q:yy 和 yyyy 有什么区别?

考察点: 年份格式符

参考答案:

  • yy:年份的两位显示,如 2020 年显示 20,2019 年显示 19。
  • yyyy:年份的四位显示,范围 0000~9999,如 2019 年显示 2019。

Q:M 和 MM、d 和 dd 有什么区别?

考察点: 月份/天格式符

参考答案:

  • M:月份,显示 1~12(不补零);MM:月份,显示 01~12(补零)。
  • d:天,1~31(具体看月份允许的最大天数,不补零);dd:与 d 一致,但显示为 01~31(补零)。
  • 例如 3 月 5 日:"M-d" 显示 "3-5","MM-dd" 显示 "03-05"。

十一、异常处理

Q:什么是异常(Exception)?不处理异常会怎样?

考察点: 异常的概念

参考答案:

  • 异常用于处理程序执行过程中出现的错误或异常情况,这些情况会干扰程序的正常流程。
  • 程序运行时出现了异常,如果没有捕获并处理异常,程序将被挂起(停止执行)。
  • 使用异常处理处理了异常后,程序遇到异常不会停止执行,程序可以继续执行。

Q:C# 中异常是如何分类的?Exception、SystemException、ApplicationException 分别是什么?

考察点: 异常分类

参考答案:

  • Exception:所有异常的基类。
  • SystemException:系统异常,预定义的公共语言运行库异常类的基类(如 DivideByZeroException 属于系统异常体系)。
  • ApplicationException:应用程序异常,用户自定义异常的基类。
  • 自定义异常通常继承 ApplicationException。

Q:try、catch、finally 三个块分别有什么作用?

考察点: 异常处理结构

参考答案:

  • try 块:包含为避免出现异常而被保护的代码。
  • catch 块:是异常处理程序,捕获异常并完成出现异常时的处理;catch 可以出现一个或多个。
  • finally 块:无论异常是否发生都需要执行的代码;finally 可以不使用。
csharp 复制代码
try //避免发生异常的代码
{
    int x = i / j;
}
catch //捕获异常,捕获到异常时才会执行,完成出现异常时的处理
{
    Console.WriteLine("程序出现异常");
}
finally //无论是否发生异常都会被调用
{
    Console.WriteLine("无论是否出现异常都执行");
}

Q:catch 块可以有几个?finally 块可以不写吗?

考察点: try-catch-finally 的组成

参考答案:

  • catch 块可以有一个或多个(多个 catch 可以分别捕获不同类型的异常)。
  • finally 块可以不使用(不是必须的)。
  • try 块是必须的,其中包含被保护的代码。

Q:一般 catch 子句和特定 catch 子句有什么区别?

考察点: catch 的两种形式

参考答案:

  • 一般 catch 子句(不带参数):能接受任何异常,但不能确定引发异常的类型。
csharp 复制代码
try
{
    int x = i / j;
}
catch
{
    Console.WriteLine("出现异常,不能确定异常的类型。");
}
  • 特定 catch 子句(带异常类型参数):捕获指定异常类或派生类的异常,可以拿到异常对象的信息。
csharp 复制代码
try
{
    int x = i / j;
}
catch (DivideByZeroException ex)
{
    Console.WriteLine(ex.StackTrace); //异常堆栈信息
    Console.WriteLine(ex.Message);    //异常信息
}

Q:catch (DivideByZeroException ex) 中 ex 的 Message 和 StackTrace 分别是什么?

考察点: 异常对象属性

参考答案:

  • ex.Message:异常信息,描述发生了什么错误。
  • ex.StackTrace:异常堆栈信息,记录异常发生的位置和调用链。
  • 特定 catch 子句捕获指定异常类(DivideByZeroException)或派生类的异常,可以通过异常对象获取这些信息。

Q:什么时候会抛出 DivideByZeroException?

考察点: 常见异常

参考答案:

  • 整数除以零时会抛出 DivideByZeroException。
csharp 复制代码
int i = 5;
int j = 0;
int x = i / j; //抛出 DivideByZeroException
  • 该异常属于系统预定义异常,可以用特定 catch 子句捕获。

Q:finally 块一定会执行吗?它有什么典型用途?

考察点: finally

参考答案:

  • 只要进入了带 finally 块的 try 语句,finally 始终会被执行(无论是否发生异常)。
  • 即使 try 中发生了异常,finally 中的代码也会执行。
  • 典型用途:放置"无论是否出现异常都要执行"的收尾代码(如释放资源、提示信息等)。

Q:如何处理"输入非数字"这类异常?请写出处理代码。

考察点: 异常处理的应用

参考答案:

  • 使用 try-catch 把可能出错的代码包起来,捕获到异常后提示用户重新输入,程序不停止。
csharp 复制代码
try
{
    int num = Convert.ToInt32(Console.ReadLine());
    //后续处理
}
catch
{
    Console.ForegroundColor = ConsoleColor.Red;
    Console.WriteLine("输入的操作有误,请重新输入~");
    Console.ResetColor();
}
  • 课程案例:菜单系统中用户输入菜单编号时输入了非数字,Convert.ToInt32 会抛出异常,通过 catch 捕获后提示重新输入,程序继续运行。

Q:如何自定义异常类?

考察点: 自定义异常

参考答案:

  • 自定义异常类继承 ApplicationException(应用程序异常,用户自定义异常的基类)。
  • 构造方法中通过 :base(message) 把异常信息传递给父类。
csharp 复制代码
class MyCustomException : ApplicationException
{
    public MyCustomException(string message) : base(message)
    {
    }
}

Q:如何抛出异常?请举例说明。

考察点: throw

参考答案:

  • 使用 throw new 异常类型(信息) 抛出新异常。
csharp 复制代码
public static void Test()
{
    Console.WriteLine("Test方法执行了");

    //抛出自定义的异常
    throw new MyCustomException("自定义的异常");
}
  • 抛出后,如果调用处没有捕获处理,程序会停止执行(被挂起)。

Q:异常处理的完整流程是怎样的?try-catch-finally 的执行顺序是什么?

考察点: 异常处理机制

参考答案:

  • 执行顺序:
    1. 进入 try 块执行被保护的代码。
    2. 如果 try 中出现异常,跳到匹配的 catch 块执行异常处理(catch 可以有多个,匹配第一个能捕获该异常类型的 catch)。
    3. 如果 try 中没有异常,catch 不执行。
    4. finally 块无论是否发生异常都会执行。
  • 处理完异常后程序不会停止执行,可以继续向下运行。
  • 小结:try 保护代码 → catch 捕获处理(可多个、可特定类型)→ finally 收尾(可选、始终执行)。
相关推荐
懿路向前29 分钟前
【HarmonyOS学习笔记】2026-08-27 | 端插件踩坑与设计取舍
笔记·学习·harmonyos
ouynagda1 小时前
Linux多线程:互斥锁与信号量学习笔记
linux·笔记·学习
半亩码田2 小时前
C#转Python第4.1篇:当 try-catch 遇上 try-except:异常处理的大不同
开发语言·python·c#
z落落3 小时前
C# Modbus-RTU 串口读写设备+CRC16+邮件告警
开发语言·c#
DS随心转小程序3 小时前
Gemini只能导出了一部分Word文档,很少的一部分,不是我这个窗口所有的对话内容?
开发语言·人工智能·c#·word·豆包·deepseek·ai导出鸭
格林威3 小时前
C# 相机图像配合频闪光源:实现高速稳定拍摄的几个方法
开发语言·网络·人工智能·数码相机·计算机视觉·c#·视觉检测
疯狂打码的少年3 小时前
【数据库技术】两级映像与数据独立性(逻辑/物理独立性)
数据库·笔记
CSDN_RTKLIB4 小时前
CAD绘图单位
c#
翻身的咸鱼ing4 小时前
ArkTS 学习笔记
笔记·学习