20道(中级)C#面试题
如何在 C# 中创建属性?
回答
在 C# 中,属性是类的成员,提供灵活的机制来读取、写入或计算私有字段的值。可以使用属性定义中的get
和访问器来创建属性。set
属性可以有一个get
访问器、一个set
访问器或两者,具体取决于您希望该属性是只读、只写还是读写。
下面是C# 中同时具有get
和访问器的属性示例: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# 中的结构(“结构”的缩写)是一种值类型,可以包含字段、属性、方法和其他成员(如类)。
但是,与引用类型的类不同,结构是值类型并存储在堆栈中。结构不支持继承(从 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”有什么区别?
回答
在 C# 中,处理异常时,了解throw
和之间的区别很重要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 等)。要创建可为 null 的类型,您可以使用内置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 值类型转换和null 合并,当可为 null 类型为“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
}
- 方法语法:这是基于类的 LINQ 扩展方法
System.Linq.Enumerable
,并使用 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 运算符和单个表达式组成。表达式 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 和 wait 关键字。它们如何一起使用?
回答
在 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;
}
}
- wait:该
await
关键字用于暂停异步方法的执行,直到等待的任务完成。它异步等待任务完成并返回结果(如果有),而不会阻塞主执行线程。该await
关键字只能在异步方法内使用。
public async Task ProcessDataAsync()
{
var content = await DownloadContentAsync("https://example.com");
Console.WriteLine(content);
}
当一起使用async
和时,请确保通过使用关键字标记每个调用方法并在方法体中等待异步方法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
- 逆变(从派生程度较高的类型到派生程度较低的类型):逆变允许您使用派生程度较低的类型而不是原始泛型类型参数。
in
在 C# 中,具有用关键字标记的泛型参数的接口和具有匹配输入参数的委托 支持逆变。
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>
:先进先出(FIFO)队列,支持并发Enqueue
和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#中的异常处理是一种处理程序执行过程中可能发生的意外或异常情况的机制。它有助于维持程序的正常流程,并确保即使遇到异常也能正确释放资源。
在 C# 中,异常处理主要使用try
、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# 面试问题和答案。
- 点赞
- 收藏
- 关注作者
评论(0)