20 道(中级)C# 面试题
嗨,C# 开发者们!准备好迎接新的挑战了吗?
我们正在解决 20 个中级 C# 面试问题,以提升您的技能并增强您的解决问题的能力。
你认为你能处理好吗?
让我们开始吧!
如何在 C# 中创建属性?
回答
在 C# 中,属性是类的成员,它提供灵活的机制来读取、写入或计算私有字段的值。可以在属性定义中使用get
和访问器来创建属性。set
属性可以具有get
访问器、set
访问器或两者,这取决于您希望属性是只读、只写还是读写。
get
下面是 C# 中同时具有和访问器的属性示例set
:
public class Person
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
}
在 C# 3.0 及更高版本中,您可以使用自动实现的属性,这样您就可以创建属性,而无需明确定义支持字段:
public class Person
{
public string Name { get; set; }
}
C# 中“params”关键字的用途是什么?
回答
C# 中的关键字params
允许方法接受指定类型的可变数量的参数。使用该params
关键字,你可以将值数组或以逗号分隔的单独值传递给方法。在方法内部,params
参数将被视为数组。
以下是使用关键字的示例params
:
public static class Utility
{
public static int Sum(params int[] values)
{
int sum = 0;
foreach (int value in values)
{
sum += value;
}
return sum;
}
}
您可以Sum
使用任意数量的参数调用该方法,如下所示:
int result1 = Utility.Sum(1, 2, 3);
int result2 = Utility.Sum(1, 2, 3, 4, 5);
C#中静态类的特点是什么?
回答
在 C# 中,静态类是无法实例化或继承的类。它只能包含静态成员,不能包含实例成员(例如实例属性、方法或字段)。静态类的主要特征如下:
- 它是用
static
关键字声明的。 - 它不能有构造函数(静态构造函数除外)。
- 它不能被实例化或继承。
- 静态类的所有成员也必须是静态的。
- 可以使用类名直接访问它,而无需创建实例。
静态类的常见用途是用于实用程序函数和扩展方法。以下是 C# 中静态类的一个示例:
public static class MathUtility
{
public static int Add(int x, int y)
{
return x + y;
}
public static int Subtract(int x, int y)
{
return x - y;
}
}
要使用这个静态类,只需像这样调用它的方法:
int sum = MathUtility.Add(1, 2);
int difference = MathUtility.Subtract(3, 1);
如何在 C# 中在两个表单之间传递数据?
回答
在 C# 中,有几种方法可以在 Windows 窗体应用程序中的两个窗体之间传递数据。一种常见的方法是使用公共属性或方法来公开需要传递的数据。以下是示例:
- 创建第二个具有公共属性的表单,该表单将保存要传递的数据。
public partial class SecondForm : Form
{
public string Data { get; set; }
public SecondForm()
{
InitializeComponent();
}
}
- 在第一个表单中,实例化第二个表单并
Data
使用您想要传递的数据设置属性。
public partial class FirstForm : Form
{
public FirstForm()
{
InitializeComponent();
}
private void OpenSecondFormButton_Click(object sender, EventArgs e)
{
SecondForm secondForm = new SecondForm();
secondForm.Data = "Data to be passed";
secondForm.Show();
}
}
在这个例子中,当用户单击时OpenSecondFormButton
,第二个表单打开,并且Data
第二个表单的属性包含从第一个表单传递的数据。
C# 中的类和对象有什么区别?
回答
在 C# 中,类是创建对象的蓝图。它定义了该类的对象将具有的结构、属性和方法。类是引用类型,它们可以从其他类继承以扩展或覆盖功能。
另一方面,对象是类的实例。它由类定义创建,并在实例化时占用内存。对象是类的实例,它们包含类中定义的数据(字段)和行为(方法)。
例如,考虑一个Car
类:
public class Car
{
public string Model { get; set; }
public int Year { get; set; }
public void Drive()
{
Console.WriteLine("The car is driving.");
}
}
在这个例子中,Car
是类,它定义了汽车对象的结构和行为。要创建该类的对象(实例),可以使用new
关键字,如下所示:
Car car1 = new Car();
car1.Model = "Toyota";
car1.Year = 2020;
Car car2 = new Car();
car2.Model = "Honda";
car2.Year = 2021;
在这个例子中,car1
和car2
是类的对象(实例)Car
,它们有各自的属性和方法集。
C# 中的委托是什么?
回答
C# 中的委托是一种定义方法签名的类型,可以保存对具有匹配签名的方法的引用。委托类似于 C++ 中的函数指针,但它们是类型安全的。它们通常用于实现事件和回调。
委托可用于将方法作为参数传递、将其赋值给类属性或将其存储在集合中。定义委托后,即可使用它来创建指向具有相同签名的方法的委托实例。
以下是 C# 中委托的一个示例:
public delegate void MyDelegate(string message);
public class MyClass
{
public static void DisplayMessage(string message)
{
Console.WriteLine("Message: " + message);
}
}
public class Program
{
public static void Main()
{
// Create a delegate instance
MyDelegate myDelegate = MyClass.DisplayMessage;
// Invoke the method through the delegate
myDelegate("Hello, World!");
}
}
在此示例中,是一个委托,它定义采用参数并返回 的MyDelegate
方法的方法签名。该方法创建一个委托实例,将方法分配给它,并通过委托调用该方法。string
void
Main
MyClass.DisplayMessage
如何在 C# 中实现多态性?
回答
多态性是面向对象编程的基本原则之一。它允许将不同类的对象视为同一超类的对象。在 C# 中,多态性有两种类型:编译时(方法重载)和运行时(方法覆盖和接口)。
方法重写和接口是实现运行时多态的常用方法。以下是一个使用方法重写的示例:
// Base class
public class Animal
{
public virtual void Speak()
{
Console.WriteLine("The animal speaks.");
}
}
// Derived class
public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("The dog barks.");
}
}
public class Program
{
public static void Main()
{
Animal myAnimal = new Dog();
myAnimal.Speak(); // Output: The dog barks.
}
}
在这个例子中,我们定义了一个基类Animal
和一个派生类Dog
。类Speak
中的方法Animal
被标记为virtual
,允许其行为被派生类覆盖。
类用自己的实现Dog
重写了方法。当我们创建一个对象并将其赋值给一个变量时,类中被重写的方法会在运行时被调用,从而表现出多态性。Speak
Dog
Animal
Dog
要通过接口实现多态性,可以定义一个接口,然后让类实现接口方法。例如:
public interface ISpeak
{
void Speak();
}
public class Dog : ISpeak
{
public void Speak()
{
Console.WriteLine("The dog barks.");
}
}
public class Cat : ISpeak
{
public void Speak()
{
Console.WriteLine("The cat meows.");
}
}
public class Program
{
public static void Main()
{
ISpeak myAnimal = new Dog();
myAnimal.Speak(); // Output: The dog barks.
myAnimal = new Cat();
myAnimal.Speak(); // Output: The cat meows.
}
}
在这个例子中,我们定义了一个ISpeak
带有Speak
方法的接口。Dog
和Cat
类实现了这个接口,并提供了它们自己的Speak
方法实现。我们可以创建这些类的对象并将其赋值给一个ISpeak
变量,然后在运行时调用该方法的相应实现Speak
,从而演示了多态性。
C# 中的结构是什么?
回答
C# 中的 struct(“structure”的缩写)是一种值类型,可以包含字段、属性、方法和其他成员,就像类一样。
但是,与类(引用类型)不同,结构体是值类型,存储在堆栈中。结构体不支持继承(除了从 System.ValueType 隐式继承),并且不能用作其他类或结构的基。
结构体非常适合表示小型轻量级的对象,这些对象占用的内存较少,并且不需要垃圾回收机制。它们通常用于表示简单的数据结构,例如二维空间中的点、日期、时间间隔和颜色。
以下是 C# 中结构体的一个例子:
public struct Point
{
public int X { get; set; }
public int Y { get; set; }
public Point(int x, int y)
{
X = x;
Y = y;
}
public double DistanceToOrigin()
{
return Math.Sqrt(X * X + Y * Y);
}
}
public class Program
{
public static void Main()
{
Point p = new Point(3, 4);
Console.WriteLine("Distance to origin: " + p.DistanceToOrigin()); // Output: Distance to origin: 5
}
}
在这个例子中,我们定义了一个Point
结构体,它有属性X
和Y
,一个构造函数 和一个方法DistanceToOrigin
。我们可以Point
像创建类实例一样创建实例并使用它们的方法。
异常处理中'throw'和'throw ex'有什么区别?
回答
throw
在 C# 中,处理异常时,了解和之间的区别非常重要throw ex
:
throw
:当您捕获异常并希望重新抛出原始异常时,可以使用该throw
语句而不指定任何异常对象。这样可以保留原始异常的堆栈跟踪,并允许上游异常处理程序访问完整的堆栈跟踪。throw ex
:当你捕获异常并想要抛出一个新的或修改过的异常对象时,可以使用throw ex
语句,其中是异常的实例。这会替换原始异常的堆栈跟踪,上游异常处理程序将只能看到从调用ex
点开始的堆栈跟踪。throw ex
以下是一个例子:
public class Example
{
public void Method1()
{
try
{
// Code that might raise an exception
}
catch (Exception ex)
{
// Logging or other exception handling code
// Rethrow the original exception
throw;
}
}
public void Method2()
{
try
{
// Code that might raise an exception
}
catch (Exception ex)
{
// Logging or other exception handling code
// Rethrow a modified exception
throw new ApplicationException("An error occurred in Method2.", ex);
}
}
}
在 中Method1
,我们捕获一个异常并使用 重新抛出它throw
。上游异常处理程序可以访问异常的原始堆栈跟踪。在 中Method2
,我们捕获一个异常并ApplicationException
使用 抛出一个新的异常,并将原始异常作为其内部异常。上游异常处理程序将从被调用的throw ex
位置开始查看堆栈跟踪。throw ex
什么是可空类型以及如何在 C# 中使用它?
回答
C# 中的可空类型是指可以具有“null”值的值类型。默认情况下,值类型不能具有“null”值,因为它们具有默认值(例如,int 为 0,bool 为 false 等)。要创建可空类型,可以使用内置结构Nullable<T>
或语法糖?
修饰符。
当您需要指示某个值的缺失或使用可能包含缺失值的数据源(如数据库)时,可空类型很有用。
创建可空类型:
Nullable<int> nullableInt = null;
int? anotherNullableInt = null;
可以使用 属性检查可空类型是否具有值HasValue
,并使用 属性检索该值Value
。或者,也可以使用GetValueOrDefault
方法来获取值(如果存在)或默认值:
int? x = null;
if (x.HasValue)
{
Console.WriteLine("x has a value of: " + x.Value);
}
else
{
Console.WriteLine("x is null"); // This will be printed
}
int y = x.GetValueOrDefault(-1); // y = -1, because x is null
C# 还支持可空值类型转换和空合并??
,当可空类型为“null”时,使用运算符提供默认值:
int? a = null;
int b = a ?? 10; // b = 10, because a is null
描述 C# 中 LINQ 的概念。
回答
语言集成查询 ( LINQ ) 是 C# 3.0(.NET Framework 3.5)中引入的一项功能,它提供了一组查询运算符和统一的查询语法,用于查询、过滤和转换来自各种类型的数据源(如集合、XML、数据库甚至自定义源)的数据。
LINQ 允许您直接在 C# 中编写类型安全、强大且富有表现力的查询,并享受编译器、静态类型和 IntelliSense 的全面支持。
LINQ 提供两种类型的查询编写语法:
- 查询语法:这是一种更类似于 SQL 的声明式语法,具有高度的抽象性和可读性。查询语法需要
from
关键字(检索数据)、select
关键字(指定输出)以及可选关键字(例如where
、group
或orderby
)。
var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evenNumbersQuery = from n in numbers
where n % 2 == 0
orderby n
select n;
foreach (var n in evenNumbersQuery)
{
Console.WriteLine(n); // 2, 4, 6
}
- 方法语法:基于
System.Linq.Enumerable
类的 LINQ 扩展方法,并使用 lambda 表达式来操作数据。方法语法提供了更实用的方法,但如果查询比较复杂,则可读性可能会较差。
var evenNumbersMethod = numbers.Where(n => n % 2 == 0).OrderBy(n => n);
foreach (var n in evenNumbersMethod)
{
Console.WriteLine(n); // 2, 4, 6
}
LINQ 支持多种提供程序,如 LINQ to Objects(用于内存集合)、LINQ to XML(用于 XML 文档)和 LINQ to Entities(用于实体框架),使开发人员能够使用类似的查询语法处理不同的数据源。
C# 中的 lambda 表达式是什么?
回答
C# 中的 Lambda 表达式是一个匿名函数,用于创建内联委托、表达式树以及 LINQ 查询或其他函数式编程构造的表达式。Lambda 表达式依赖于=>
运算符(称为 Lambda 运算符)来分隔输入参数和表达式或语句块。
Lambda 表达式可用于表达式和语句 lambda:
- Lambda 表达式:它由一个输入参数列表、一个 Lambda 运算符和一个表达式组成。Lambda 表达式会自动返回表达式的结果,无需显式
return
语句。
Func<int, int, int> sum = (a, b) => a + b;
int result = sum(1, 2); // result = 3
- 语句 Lambda:类似于表达式 Lambda,但包含一个用括号括起来的语句块,
{}
而不是单个表达式。语句 Lambda 允许包含多个语句,并且return
如果必须返回值,则需要显式声明。
Func<int, int, int> sum = (a, b) => {
int result = a + b;
return result;
};
int result = sum(1, 2); // result = 3
Lambda 表达式是一种简洁而有效的表示委托或表达式的方式,通常与 LINQ 查询、事件处理程序或需要函数作为参数的高阶函数一起使用。
描述一下 C# 中的 async 和 await 关键字。它们如何一起使用?
回答
在 C# 中,关键字async
和[await](https://www.bytehide.com/blog/await-csharp/ "Mastering Await in C#: From Zero to Hero")
用来编写异步代码,这些代码可以并发运行而不会阻塞主执行线程。这使得应用程序更加高效、响应更快,尤其是在 I/O 密集型操作(例如,调用 Web 服务、读取文件或使用数据库)或 CPU 密集型操作(可并行化)的情况下。
- async:此
async
关键字添加到方法中,表示该方法为异步方法。异步方法通常返回一个Task
返回值,例如返回 void 或Task<TResult>
返回 TResult 类型值的方法。异步方法可以包含一个或多个await
表达式。
public async Task<string> DownloadContentAsync(string url)
{
using (HttpClient client = new HttpClient())
{
var content = await client.GetStringAsync(url);
return content;
}
}
- await:该
await
关键字用于暂停异步方法的执行,直到等待的任务完成。它异步等待任务完成并返回结果(如果有),而不会阻塞主执行线程。该await
关键字只能在异步方法内部使用。
public async Task ProcessDataAsync()
{
var content = await DownloadContentAsync("https://example.com");
Console.WriteLine(content);
}
一起使用async
和await
时,请确保通过在方法体中使用关键字 和 await 标记每个调用方法,将异步行为沿调用堆栈向上传播async
。这种“全程异步”的方法可确保在整个应用程序中充分利用异步编程的优势。
什么是任务并行库 (TPL) 以及它与 C# 有何关系?
回答
任务并行库 (TPL) 是 .NET Framework 4.0 中引入的一组 API,用于简化并行、并发和异步编程。它是System.Threading.Tasks
命名空间的一部分,与传统的低级线程结构(例如 ThreadPool 或 Thread 类)相比,它提供了一个更高级别、更抽象的模型来处理多线程、异步和并行。
TPL的主要组件包括Task
、Task<TResult>
和Parallel
类,它们的作用如下:
- Task:表示不返回值的异步操作。可以使用
await
关键字等待任务,也可以使用ContinueWith
方法来将延续任务附加到任务上。
Task.Run(() => {
Console.WriteLine("Task-based operation running on a different thread");
});
- Task:表示返回 TResult 类型值的异步操作。它派生自该类
Task
,并具有一个Result
用于保存任务完成后结果的属性。
Task<int> task = Task.Run<int>(() => {
return 42;
});
int result = task.Result; // waits for the task to complete and retrieves the result
- Parallel:提供并行版本的循环
for
,foreach
以及Invoke
并发执行多个操作的方法。该类Parallel
可以自动利用多个 CPU 核心或线程,让您轻松并行化 CPU 密集型操作。
Parallel.For(0, 10, i => {
Console.WriteLine($"Parallel loop iteration: {i}");
});
TPL 允许开发人员使用 C# 编写高效且可扩展的并行、并发和异步代码,而无需直接处理低级线程和同步原语。
你能解释一下 C# 中的协变和逆变吗?
回答
协变和逆变是与 C# 中处理泛型、委托或数组时的类型关系相关的术语。它们描述了在分配变量、参数或返回值时如何使用派生类型代替基类型(反之亦然)。
- 协变(从派生程度较低的类型到派生程度较高的类型):协变允许使用派生程度较高的类型,而不是原始的泛型类型参数。在 C# 中,数组、具有泛型参数的接口以及具有匹配返回类型的委托都支持协变。
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // Covariant assignment
- 逆变(从派生程度较高的类型到派生程度较低的类型):逆变允许使用派生程度较低的类型来代替原始泛型类型参数。在 C# 中,对于带有
in
关键字标记的泛型参数的接口以及带有匹配输入参数的委托,逆变均受支持。
IComparer<object> objectComparer = Comparer<object>.Default;
IComparer<string> stringComparer = objectComparer; // Contravariant assignment
// Using contravariant delegate
Action<object> setObject = obj => Console.WriteLine(obj);
Action<string> setString = setObject;
协变和逆变可允许在不同类型层次结构之间进行分配和转换,而无需显式类型转换,从而有助于提高代码的灵活性和可重用性。
描述 C# 中早期绑定和晚期绑定之间的区别。
回答
早期绑定和晚期绑定是 C# 中的两种机制,与编译器和运行时在程序执行期间如何解析类型、方法或属性有关。
- 早期绑定:早期绑定也称为静态绑定或编译时绑定,是指在编译时解析类型、方法或属性的过程。通过早期绑定,编译器会验证被调用的成员是否存在,以及这些成员的可访问性、参数和返回值是否正确调用。早期绑定具有诸多优势,例如更高的性能(由于减少了运行时开销)和类型安全性,因为编译器可以在编译期间捕获并报告错误。例如:
var myString = "Hello, World!";
int stringLength = myString.Length; // Early binding
- 后期绑定:后期绑定也称为动态绑定或运行时绑定,是指在运行时解析类型、方法或属性的过程。使用后期绑定时,成员的实际绑定仅在代码执行时发生,如果成员不存在或无法访问,则可能导致运行时错误。
例子:
dynamic someObject = GetSomeObject();
someObject.DoSomething(); // Late binding
当编译时无法确定确切的类型或成员,或者使用 COM 对象、反射或动态类型时,通常使用后期绑定。后期绑定具有某些优势,例如,允许更大的灵活性、可扩展性,以及能够处理编译时未知的类型。
然而,它也有一些缺点,例如增加运行时开销(由于额外的类型检查、方法查找或动态调度)和损失类型安全性(由于可能存在运行时错误)。
C# 中的全局程序集缓存 (GAC) 是什么?
回答
全局程序集缓存 (GAC) 是一个集中式存储库或缓存,用于在计算机上存储和共享 .NET 程序集 (DLL)。GAC 是 .NET Framework 的一部分,用于避免 DLL 冲突并支持并行执行同一程序集的不同版本。
存储在 GAC 中的程序集必须具有强名称,该名称由程序集名称、版本号、区域性信息(如果适用)以及公钥令牌(由开发人员的私钥生成)组成。此强名称允许 GAC 唯一地标识和管理每个程序集及其版本。
要在 GAC 中安装程序集,您可以使用gacutil
实用程序,也可以使用 Windows 资源管理器将程序集拖放到 GAC 文件夹中。
gacutil -i MyAssembly.dll
作为开发人员,通常无需显式引用 GAC 中的程序集,因为 .NET 运行时会在搜索其他位置之前自动在 GAC 中查找它们。但是,了解 GAC 并理解它如何影响程序集共享、版本控制和部署方案至关重要。
解释“var”关键字在 C# 中的工作原理,并给出其使用的实际示例。
回答
在 C# 中,var
当不需要或不想显式指定类型时,可以使用关键字 隐式键入局部变量。使用var
关键字时,C# 编译器会根据初始化期间赋给变量的值推断变量的类型。该变量在编译时仍然是强类型的,就像任何其他类型变量一样。
需要注意的是,该var
关键字只能与局部变量一起使用,并且变量必须在声明期间用一个值初始化(否则,编译器无法推断类型)。
使用var
关键字可以带来诸如提高可读性和易于重构等好处,特别是在处理复杂或详细的类型(例如泛型或匿名类型)时。
例子:
var number = 42; // int
var message = "Hello, World!"; // string
var collection = new List<string>(); // List<string>
var anonymousType = new { Name = "John", Age = 30 }; // Anonymous type
在上面的例子中,编译器根据变量的赋值推断其类型。使用var
这里可以使代码更加简洁易读。
然而,在可读性和可维护性之间取得适当的平衡非常重要。如果使用var
可能会降低代码的可读性或可理解性,最好使用显式类型。
什么是线程安全集合,您能给出一个 C# 中的例子吗?
回答
线程安全集合是一种数据结构,旨在被多个线程并发安全且正确地访问或修改。通常,大多数内置的 .NET 集合类(例如List<T>
、Dictionary<TKey, TValue>
等)都不是线程安全的,这意味着并发访问或修改可能会导致意外结果或数据损坏。
为了提供线程安全的集合,.NET Framework 提供了System.Collections.Concurrent
命名空间,其中包括几个线程安全的集合类,例如:
ConcurrentBag<T>
Add
:允许重复并具有快速和操作的无序项目集合Take
。ConcurrentQueue<T>
Enqueue
:支持并发和操作的先进先出(FIFO)队列TryDequeue
。ConcurrentStack<T>
Push
:支持并发和操作的后进先出 (LIFO) 堆栈TryPop
。ConcurrentDictionary<TKey, TValue>
:支持线程安全的Add
、Remove
、 等字典操作的字典。
例子:
ConcurrentQueue<int> concurrentQueue = new ConcurrentQueue<int>();
Parallel.For(0, 1000, i => {
concurrentQueue.Enqueue(i);
});
int itemCount = concurrentQueue.Count; // itemCount = 1000
在上面的例子中,ConcurrentQueue<int>
实例安全地处理并发Enqueue
操作,而不需要手动锁定或同步。
请记住,使用线程安全集合可能会带来一些性能开销,因此您应该在特定用例中仔细评估线程安全性和性能之间的权衡。
解释如何使用 try-catch-finally 块在 C# 中处理异常。
回答
C# 中的异常处理是一种处理程序执行期间可能发生的意外或异常情况的机制。它有助于维护程序的正常流程,并确保即使遇到异常也能正确释放资源。
try
在 C# 中,异常处理主要使用、catch
和块完成finally
:
- try:该
try
代码块包含可能引发异常的代码。你将可能引发异常的代码封装在此代码块中。 - catch:该
catch
代码块用于处理try
代码块中抛出的特定异常。您可以catch
为不同的异常类型创建多个代码块。第一个匹配catch
并能够处理该异常类型的代码块将被执行。 - finally:
finally
无论是否引发异常,此块都会执行代码。此块是可选的,通常用于资源清理(例如,关闭文件或数据库连接)。
例子:
try
{
int result = divide(10, 0);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Caught DivideByZeroException: {ex.Message}");
}
catch (Exception ex)
{
Console.WriteLine($"Caught a generic exception: {ex.Message}");
}
finally
{
Console.WriteLine("This will execute, no matter what.");
}
在上面的例子中,try
代码块尝试执行一个抛出 的除法运算DivideByZeroException
。catch
代码块处理DivideByZeroException
被执行,异常消息被写入控制台。无论异常是什么,finally
代码块都会执行,确保执行所有必要的清理工作。
好了——20 道中级 C# 问题!别忘了,我们的系列文章涵盖了所有技能水平的 C# 面试问答。
关注我们的内容,随时掌握最新资讯,挑战更多 C# 挑战。大家,继续编程!
鏂囩珷鏉ユ簮锛�https://dev.to/bytehide/20-intermediate-level-c-interview-questions-4ff0