C# 是最流行的编程语言之一,也是 .NET 开发的首选语言。因此,如果您是一名 .NET 开发人员,正在参加 .NET 面试,您将被问到有关 C# 编程的问题。以下是针对初学者和专业 C# 开发人员的 50 个最佳 C# 面试问题和答案。
1. 什么是 C#?C# 的最新版本是什么?
C# 是一种计算机编程语言。微软于 2000 年开发了 C#,旨在提供一种现代通用编程语言,只需一种编程语言即可开发针对各种平台(包括 Windows、Web 和 Mobile)的各种软件。如今,C# 是世界上最流行的编程语言之一。数百万软件开发人员使用 C# 来构建各种软件。
C# 是构建 Microsoft .NET 软件应用程序的主要语言。开发人员可以使用 C# 构建几乎所有类型的软件,包括 Windows UI 应用程序、控制台应用程序、后端服务、云 API、Web 服务、控件和库、无服务器应用程序、Web 应用程序、原生 iOS 和 Android 应用程序、AI 和机器学习软件以及区块链应用程序。
C# 借助 Visual Studio IDE 提供快速应用程序开发。C# 是一种现代、面向对象、简单、多功能且注重性能的编程语言。C# 是基于几种编程语言的最佳功能和用例开发的,包括 C++、Java、Pascal 和 SmallTalk。
C# 语法与 C++ 类似。.NET,C# 库与 Java 类似。C# 支持现代面向对象编程语言特性,包括抽象、封装、多态和继承。C# 是一种强类型语言。.NET 中的大多数类型都继承自 Object 类。
C# 支持类和对象的概念。类具有字段、属性、事件和方法等成员。
C# 功能多样且现代,支持现代编程需求。自诞生以来,C# 语言经历了各种升级。C# 的最新版本是 C# 12。
2. C# 中的对象是什么?
C# 语言是一种面向对象的编程语言。类是 C# 的基础。类是一个模板,它定义数据结构以及如何存储、管理和传输数据。类具有字段、属性、方法和其他成员。
类是概念,而对象是真实存在的。对象是使用类实例创建的。类定义对象的类型。对象将真实值存储在计算机内存中。
任何具有某些特征或可以执行某些工作的现实世界实体都称为对象。这种对象也称为实例,即编程语言中实体的副本。对象是类的实例。
例如,我们需要创建一个处理汽车的程序。我们需要为汽车创建实体。我们称之为类,汽车。汽车有四个属性,即型号、类型、颜色和尺寸。
为了在编程中表示汽车,我们可以创建一个具有四个属性的类 Car,即型号、类型、颜色和大小。这些被称为类的成员。类有几种类型的成员、构造函数、字段、属性、方法、委托和事件。类成员可以是私有的、受保护的或公共的。此外,由于这些属性可以在类外部访问,因此它们可以是公共的。
对象是类的实例。类可以根据需要拥有任意数量的实例。例如,本田思域是汽车的实例。在实际编程中,本田思域是一个对象。因此,本田思域是汽车类的实例。本田思域的型号、类型、颜色和尺寸属性分别为 Civic、Honda、Red 和 4。宝马 330、丰田卡罗拉、福特 350、本田 CR4、本田雅阁和本田 Pilot 是汽车对象的更多示例。
3.什么是托管代码或非托管代码?
托管代码
托管代码是使用 .NET 框架及其支持的编程语言(如 C# 或 VB.NET)开发的代码。托管代码由公共语言运行时(CLR 或 Runtime)直接执行,Runtime 管理其生命周期,包括对象创建、内存分配和对象处置。任何用 .NET 框架编写的语言都是托管代码”。
非托管代码
在 .NET 框架之外开发的代码称为非托管代码。
不在 CLR 控制下运行的应用程序被称为非托管应用程序。例如,C 或 C++ 或 Visual Basic 等语言都是非托管的。
程序员直接管理非托管代码的对象创建、执行和处置。因此,如果程序员编写了糟糕的代码,可能会导致内存泄漏和不必要的资源分配。
.NET Framework 提供了一种机制,使非托管代码可以在托管代码中使用,反之亦然。这个过程是在包装类的帮助下完成的。
4. C# 中的装箱和拆箱是什么?
装箱和拆箱均用于类型转换。
从值类型转换为引用类型称为装箱。装箱是一种隐式转换。以下是 C# 中装箱的一个示例。
// 装箱
int anum = 123;
Object obj = anum;
Console.WriteLine(anum);
Console.WriteLine(obj);
从引用类型转换为值类型称为拆箱。以下是 C# 中拆箱的示例。
// 拆箱
Object obj2 = 123;
int anum2 = (int)obj;
Console.WriteLine(anum2);
Console.WriteLine(obj);
5. C# 中的结构和类有什么区别?
类和结构都是用户定义的数据类型,但是具有一些主要区别:
结构
- struct 是 C# 中的值类型,继承自 System.Value Type。
- 结构通常用于较少量的数据。
- 结构不能从其他类型继承。
- 结构不能是抽象的。
- 不需要用新关键字创建对象。
- 没有权限创建任何默认构造函数。
类
- 该类是C#中的引用类型,它继承自System.Object类型。
- 类通常用于大量数据。
- 类可以从其他类继承。
- 类可以是抽象类型。
- 我们可以创建一个默认构造函数。
6. C# 中的接口和抽象类有什么区别?
以下是 C# 中接口和抽象类之间的一些常见区别。
- 一个类可以实现任意数量的接口,但一个子类最多只能使用一个抽象类。
- 抽象类可以有非抽象方法(具体方法),而在接口的情况下,所有方法都必须是抽象的。
- 抽象类可以声明或使用任何变量,而接口不能这样做。
- 在抽象类中,所有数据成员或函数默认都是私有的,而在接口中,所有数据成员或函数都是公共的;我们无法手动更改它们。
- 在抽象类中,我们需要使用 abstract 关键字来声明抽象方法;在接口中,我们不需要这样做。
- 抽象类不能用于多重继承,而接口可以用于多重继承。
- 抽象类使用构造函数,而接口中没有任何构造函数。
7. C# 中的枚举是什么?
枚举是一种具有一组相关命名常量的值类型,通常称为枚举器列表。枚举关键字用于声明枚举。它是一种用户定义的原始数据类型。
枚举类型可以是整数(float、int、byte、double 等)。但如果在 int 之外使用它,则必须进行强制转换。
枚举用于在 .NET 框架中创建数字常量。枚举的所有成员都是枚举类型。因此,每个枚举类型都必须有一个数字值。
枚举元素的底层默认类型为 int。默认情况下,第一个枚举器的值为 0,后续每个枚举器的值均增加 1。
enum Dow {Sat, Sun, Mon, Tue, Wed, Thu, Fri};
关于枚举的一些要点,
- 枚举是 C# 中的枚举数据类型。
- 枚举不是为最终用户准备的。它们是为开发人员准备的。
- 枚举是强类型常量。它们是强类型的,即一种类型的枚举不能隐式分配给另一种类型的枚举,即使它们成员的底层值相同。
- 枚举(enums)使您的代码更具可读性和可理解性。
- 枚举值是固定的。枚举可以显示为字符串并作为整数处理。
- 默认类型为int,认可的类型有byte、sbyte、short、ushort、uint、long、ulong。
- 每个枚举类型都自动从 System.Enum 派生,因此,我们可以在枚举上使用 System.Enum 方法。
- 枚举是在堆栈而不是堆上创建的值类型。
8. C# 中的“continue”和“break”语句有什么区别?
使用 break 语句,您可以“跳出循环”,而使用 continue 语句,您可以“跳过一次迭代”并恢复循环执行。
例如,Break 语句
using System;
using System.Collections;
using System.Linq;
using System.Text;
namespace break_example {
Class brk_stmt {
public static void main(String[] args) {
for (int i = 0; i <= 5; i++) {
if (i == 4) {
break;
}
Console.WriteLine("The number is " + i);
Console.ReadLine();
}
}
}
}
输出
数量为0;
数量为1;
数量是2;
数量是3;
例如,Continue 语句
using System;
using System.Collections;
using System.Linq;
using System.Text;
namespace continue_example {
Class cntnu_stmt {
public static void main(String[] {
for (int i = 0; i <= 5; i++) {
if (i == 4) {
continue;
}
Console.WriteLine("The number is "+ i);
Console.ReadLine();
}
}
}
}
输出
数量为1;
数量是2;
数量是3;
数量是5;
9. C#中constant和readonly有什么区别?
Const 就是“常量”,即在编译时其值保持不变的变量。因此,必须为其赋值。默认情况下,const 是静态的,我们不能在整个程序中更改 const 变量的值。
Readonly 是一个关键字,我们可以在运行时更改其值或在运行时分配它,但只能通过非静态构造函数。
例子
我们有一个测试类,其中有两个变量,一个是只读的,另一个是常量。
class Test {
readonly int read = 10;
const int cons = 10;
public Test() {
read = 100;
cons = 100;
}
public void Check() {
Console.WriteLine("Read only : {0}", read);
Console.WriteLine("const : {0}", cons);
}
}
在这里,我试图改变构造函数中两个变量的值,但是当我尝试改变常量时,它会给出一个错误,让我在运行时调用的块中改变它们的值。
最后,从类中删除该行代码并调用此 Check() 函数,如下面的代码片段所示:
class Program {
static void Main(string[] args) {
Test obj = new Test();
obj.Check();
Console.ReadLine();
}
}
class Test {
readonly int read = 10;
const int cons = 10;
public Test() {
read = 100;
}
public void Check() {
Console.WriteLine("Read only : {0}", read);
Console.WriteLine("const : {0}", cons);
}
}
输出
10. ref 和 out 关键字有什么区别?
ref 关键字通过引用传递参数。因此,当控制权返回到调用方法时,对方法中此参数所做的任何更改都将反映在该变量中。
out 关键字通过引用传递参数。这与 ref 关键字非常相似。
ref | out |
---|---|
参数或自变量必须首先初始化,然后才能传递给ref。 | 在将参数或参数传递给out之前,不强制初始化参数或参数, |
不需要分配或初始化参数的值(在转到调用方法之前由ref传递)。 | 被调用的方法需要在返回到调用方法之前分配或初始化参数的值(传递给out)。 |
当还需要调用的方法时,通过ref传递参数值是有用的 | 声明一个参数为 out 方法在需要从函数或方法返回多个值时很有用。 |
在调用方法中使用参数值之前,不必强制初始化它。 | 在调用方法中,参数值必须在使用前进行初始化。 |
使用 ref 时,数据可以双向传递。 | 使用 out 时,数据只能单向传递(从被调用的方法传递到调用方法)。 |
11. 静态方法中可以使用“this”吗?
我们不能在静态方法中使用“this”,因为关键字“this”返回对包含它的类的当前实例的引用。静态方法(或任何静态成员)不属于特定实例。它们存在而不创建类的实例,并且使用类的名称调用,而不是通过实例调用,因此我们不能在静态方法的主体中使用 this 关键字。但是,在扩展方法的情况下,我们可以使用方法的参数。
让我们看一下“this”关键字。
C# 中的“this”关键字是一种特殊类型的引用变量,它在每个构造函数和非静态方法中隐式定义为定义它的类型类的第一个参数。
12. C# 中的属性是什么?
在 C# 中,属性是类的成员,它提供读取、写入或计算私有字段值的方法。它公开一个公共接口来访问和修改存储在类中的数据,同时允许类保持对如何访问和操作这些数据的控制。
属性使用 get 和 set 访问器进行声明,它们定义获取或设置属性值的行为。get 访问器检索属性的值,而 set 访问器设置属性的值。属性可以有一个或两个访问器,具体取决于它是只读、只写还是读写。
例如,假设 Person 类有一个私有字段 name。然后,可以创建一个属性 Name 来提供对该字段的访问,如下所示。
class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
此属性允许从类外部访问名称字段,但只能通过属性方法访问。这提供了一定程度的封装,并控制了数据的访问和修改方式。
C# 中的属性是面向对象编程的重要组成部分,广泛应用于应用程序中,以提供一种干净、安全的方式来访问和修改类数据。
13. C# 中的扩展方法是什么?
在 C# 中,扩展方法是一种静态方法,用于扩展现有类型的功能,而无需修改原始类型或创建新的派生类型。扩展方法允许开发人员向现有类型(例如类、结构、接口、枚举等)添加最初未在这些类型中定义的方法。
扩展方法在静态类中声明,并定义为静态方法,其第一个参数称为“this”参数。“this”参数指定要扩展的类型,并允许调用扩展方法,就像它是该类型的实例方法一样。
例如,考虑以下扩展方法,它通过提供将字符串首字母大写的方法来扩展字符串类型:
public static class StringExtensions
{
public static string CapitalizeFirstLetter(this string str)
{
if (string.IsNullOrEmpty(str))
return str;
return char.ToUpper(str[0]) + str.Substring(1);
}
}
使用此扩展方法,可以对任何字符串对象调用 CapitalizeFirstLetter 方法,如下所示:
string s = "hello world";
string capitalized = s.CapitalizeFirstLetter(); // "Hello world"
请注意,CapitalizeFirstLetter 方法不是在字符串类中定义的,而是由 StringExtensions 类提供的扩展方法。
扩展方法是 C# 中的一个强大功能,允许开发人员轻松地向现有类型添加新功能,并广泛用于应用程序中以简化代码并提高代码的可读性。
14. C# 中的 Dispose 和 Finalize 有什么区别?
在 C# 中,Dispose 和 Finalize 方法都用于释放资源,但它们的用途和行为不同。
Dispose 方法释放 .NET 运行时不自动管理的非托管资源,例如文件句柄或数据库连接。它通常在实现 IDisposable 接口的类中实现,该接口定义 Dispose 方法。
当不再需要资源时,客户端代码会显式调用 Dispose 方法来释放这些资源。可以使用语句隐式调用该方法,以确保在对象超出范围时调用 Dispose 方法。
另一方面,Finalize 方法用于在对象被垃圾回收之前对其进行清理操作。因此,它通常在重写 Object.Finalize 方法的类中实现。
垃圾收集器调用 Finalize 方法(该方法自动管理 .NET 对象的内存)来释放尚未通过 Dispose 方法明确释放的非托管资源。
这两种方法的主要区别在于 Dispose 方法是确定性的,可以由客户端代码显式调用。相反,Finalize 方法是非确定性的,由垃圾收集器在不确定的时间调用。
值得注意的是,实现 Dispose 方法的对象也应该实现 Finalize 方法,作为客户端代码不调用 Dispose 方法的备用机制。
综上所述,Dispose 方法用于确定性地释放非托管资源。相反,Finalize 方法用作对象被垃圾回收时释放非托管资源的后备机制。
15. C# 中的 String 和 StringBuilder 有什么区别?
StringBuilder 和 string 都是用于字符串值,但是两者在实例创建和性能方面存在很多区别。
(1) String
字符串是不可变的对象。不可变是指我们在代码中创建字符串对象,因此我们无法在任何操作中修改或更改该对象,例如插入新值或用字符串对象中的现有值替换或附加任何值。当我们必须执行某些操作来更改字符串时,它将简单地处理字符串对象的旧值,并在内存中创建一个新实例来保存字符串对象中的新值,例如:
备注
- 它是一个保存字符串值的不可变对象。
- 从性能角度来看,字符串很慢,因为它会创建一个新实例来覆盖或更改先前的值。
- 字符串属于系统命名空间。
(2) StringBuilder
System.Text.StringBuilder 是一个可变对象,用于保存字符串值;可变意味着我们一旦创建了 System.Text.Stringbuilder 对象。我们可以使用此对象执行任何操作,例如使用插入函数在现有字符串中插入值,以及替换或附加值,而无需每次都创建 System.Text.StringBuilder 的新实例,因此它使用的是前一个对象。这样,它比 System.String 运行得更快。让我们看一个例子来了解 System.Text.StringBuilder。
备注
- StringBuilder 是一个可变对象。
- 性能方面,StringBuilder 非常快,因为它将使用 StringBuilder 对象的相同实例来执行任何操作,例如在现有字符串中插入值。
- StringBuilder 属于 System.Text 命名空间。
16. C# 中的委托有什么用途?
委托是一个或多个函数指针的抽象(存在于 C++ 中;关于这一点的解释超出了本文的范围)。.NET 以委托的形式实现了函数指针的概念。使用委托,您可以将函数视为数据。委托允许将函数作为参数传递、从函数作为值返回以及存储在数组中。委托具有以下特点:
- 委托是从 System.MulticastDelegate 类派生的。
- 它们具有签名和返回类型。添加到委托的函数必须与此签名兼容。
- 委托可以指向静态方法或实例方法。
- 一旦创建了委托对象,它可以在运行时动态调用它指向的方法。
- 委托可以同步或异步调用方法。
委托包含几个有用的字段。第一个字段保存对对象的引用,第二个字段保存方法指针。调用委托时,将在所包含的引用上调用实例方法。但是,如果对象引用为空,则运行时会将其理解为该方法是静态方法。此外,从语法上讲,调用委托与调用常规函数相同。因此,委托非常适合实现回调。
为什么我们需要代表?
从历史上看,Windows API 经常使用 C 样式的函数指针来创建回调函数。使用回调,程序员能够配置一个函数以向应用程序中的另一个函数报告。因此,使用回调的目的是处理按钮单击、菜单选择和鼠标移动活动。但这种传统方法的问题在于回调函数不是类型安全的。在 .NET 框架中,仍然可以使用委托以更有效的方式实现回调。但是,委托维护三个重要信息:
- 方法的参数。
- 它调用的方法的地址。
- 方法的返回类型。
委托是针对您想要将方法传递给其他方法的情况的解决方案。您已经习惯将数据作为参数传递给方法,因此将方法作为参数而不是数据传递的想法可能听起来很奇怪。但是,在某些情况下,您有一个方法可以执行某些操作,例如调用其他方法。您在编译时不知道第二个方法是什么。该信息仅在运行时可用。因此,委托是解决此类复杂情况的设备。
17. C# 中的密封类是什么?
密封类用于限制面向对象编程的继承特性,一旦某个类被定义为密封类,则该类将无法被继承。
在 C# 中,sealed 修饰符将类定义为密封的。在 Visual Basic .NET 中,Not Inheritable 关键字用于密封类。如果类派生自密封类,则编译器会抛出错误。
如果你曾经注意到,结构是密封的。你不能从结构派生出类。
以下类定义在 C# 中定义了一个密封类:
// 密封类
sealed class SealedClass
{
}
18. C# 中的(partial )类是什么?为什么我们需要(partial )类?
(partial )类仅用于将类的定义拆分为同一源代码文件或多个源文件中的两个或多个类。您可以在多个文件中创建一个类定义,这些文件将在运行时编译为一个类。此外,当您创建此类的实例时,您可以使用同一对象访问所有源文件中的所有方法。
可以在同一个命名空间中创建(partial )类。但是,无法在不同的命名空间中创建部分类。因此,请使用“partial”关键字将所有要绑定的类名与同一命名空间中的类名相同。让我们看一个例子:
19. C# 中的装箱和拆箱有什么区别?
装箱与拆箱都用于类型转换,但它们有一些区别:
(1)装箱
装箱是将值类型数据类型转换为对象或此值类型实现的任何接口数据类型。例如,当 CLR 装箱时,值意味着当 CLR 将值类型转换为对象类型时,它会将值包装在 System.Object 内部并将其存储在应用程序域中的堆区域中。
(2)拆箱
拆箱也是从对象或任何实现的接口类型中提取值类型的过程。装箱可以隐式完成,但拆箱必须通过代码明确完成。
装箱和拆箱的概念是 C# 类型系统统一视图的基础,其中任何类型的值都可以被视为对象。
20. C# 中的 IEnumerable<> 是什么?
IEnumerable 是 System.Collections 命名空间中所有可枚举的非泛型集合(如 ArrayList、HastTable 等)的父接口。此接口的泛型版本是 IEnumerable,它是 System.Collections.Generic 命名空间中所有泛型集合类(如 List<> 等)的父接口。
在 System.Collections.Generic.IEnumerable 中,只有一个方法,即 GetEnumerator(),该方法返回一个 IEnumerator。如果我们没有此接口作为父接口,IEnumerator 可以通过公开 Current 属性和 Move Next 和 Reset 方法,来迭代整个集合,因此我们不能通过 foreach 循环进行迭代,也不能在我们的 LINQ 查询中使用该类对象。