C# 面试常见问题及答案

发布:2024-09-09 12:50 阅读:16 点赞:0

1. 什么是 C#?最新的版本是什么?

C# 是一种计算机编程语言,由微软在2000年开发,旨在提供一种现代化的通用编程语言,用于开发针对各种平台(包括Windows、Web和移动设备)的各种软件。C# 是构建 Microsoft .NET 软件应用程序的主要语言,可以用来开发几乎任何类型的软件。C# 提供了快速的应用程序开发,并且是一种现代的、面向对象的、简洁的、多用途的、性能导向的语言。截至目前(2024年9月),C#的最新版本是C# 12。

2. C# 中的对象是什么?

C# 是一种面向对象的编程语言。类是 C# 的基础。类是一个定义数据结构以及数据如何存储、管理和传输的模板。类包含字段、属性、方法等成员。类是概念上的,而对象是实际存在的。对象是通过类实例化创建的。例如,汽车是一个对象,可以创建一个名为 Car 的类来描述汽车的特性,如型号、类型、颜色和尺寸。Honda Civic 就是 Car 类的一个实例。

3. 什么是托管代码或非托管代码?

托管代码是指使用 .NET 框架及其支持的编程语言(如 C# 或 VB.NET)编写的代码。这类代码直接由公共语言运行时(CLR)执行,并由运行时管理其生命周期,包括对象的创建、内存分配和对象的释放。非托管代码则是指那些不在 .NET 框架控制之外编写的代码,例如用 C 或 C++ 编写的代码。程序员直接管理非托管代码的对象创建、执行和释放。.NET 框架提供了机制使非托管代码可以在托管代码中使用,反之亦然。

4. C# 中的装箱和拆箱是什么?

装箱是从值类型转换到引用类型的过程,是隐式转换。拆箱是从引用类型转换回值类型的过程。例如:

// 装箱
int anum = 123;
Object obj = anum; // 装箱
Console.WriteLine(anum);
Console.WriteLine(obj);

// 拆箱
Object obj2 = 123;
int anum2 = (int)obj2; // 拆箱
Console.WriteLine(anum2);
Console.WriteLine(obj2);

5. C# 中的结构体(struct)和类(class)有什么区别?

结构体和类都是用户定义的数据类型,但它们有一些关键的区别: 当然,以下是结构体和类的比较表格:

特性 结构体 (Struct) 类 (Class)
类型 值类型 (Value Type) 引用类型 (Reference Type)
基类 继承自 System.ValueType 继承自 System.Object
数据量 通常用于少量数据 通常用于大量数据
继承 不能从其他类型继承 可以从其他类继承
抽象 不能是抽象的 可以是抽象的
构造函数 不允许创建任何默认构造函数;创建对象时不需要使用 new 关键字(但可以使用 new 调用构造函数) 可以创建默认构造函数
对象创建 可以直接创建,不一定需要 new 关键字 需要使用 new 关键字来创建对象

6. C# 中接口(Interface)和抽象类(Abstract Class)的区别是什么?

以下是 C# 中接口和抽象类的一些常见区别:

以下是抽象类与接口的比较表格:

特性 抽象类 (Abstract Class) 接口 (Interface)
继承 子类最多只能继承一个抽象类 类可以实现任意数量的接口
方法 可以包含非抽象方法(即具体方法) 所有方法都必须是抽象的
变量 可以声明或使用任何变量 不可以声明变量
访问修饰符 数据成员和方法默认是私有的,除非显式声明为公开 默认都是公开的,无法更改
抽象方法声明 需要使用 abstract 关键字来声明抽象方法 不需要使用 abstract 关键字
多重继承 不能用于多重继承 可以用于多重继承
构造函数 可以使用构造函数 没有构造函数

7. C# 中的枚举(Enum)是什么?

枚举(Enum)是一种具有相关命名常量集合的值类型,通常称为枚举列表。enum 关键字用于声明枚举。它是一种用户定义的基本数据类型。

枚举类型可以是整数类型(如 float, int, byte, double 等)。但如果使用除了 int 以外的类型,它必须被显式转换。

枚举用于在 .NET 框架中创建数值常量。所有的枚举成员都是枚举类型的一部分,因此每个枚举类型都必须有一个数值。

枚举元素的底层默认类型是 int。默认情况下,第一个枚举成员的值为 0,之后每个枚举成员的值递增 1。

enum Dow {Sat, Sun, Mon, Tue, Wed, Thu, Fri};

关于枚举的一些要点:

  • 枚举是 C# 中的枚举数据类型。
  • 枚举不是为最终用户设计的,而是为开发者设计的。
  • 枚举是强类型的常量。它们是强类型的,即,即使它们的成员底层值相同,一个类型的枚举也不能隐式地赋值给另一个类型的枚举。
  • 枚举使得代码更加可读和易于理解。
  • 枚举值是固定的。枚举可以作为字符串显示并作为整数处理。
  • 默认类型是 int,允许的类型有 byte, sbyte, short, ushort, uint, long, ulong。
  • 每个枚举类型自动派生自 System.Enum,因此我们可以对枚举使用 System.Enum 方法。
  • 枚举是在栈上而不是堆上创建的值类型。

8. C# 中的 “continue” 和 “break” 语句有什么区别?

使用break语句,你可以跳出循环,而使用continue语句,你可以跳过一次迭代并恢复循环的执行。

Break 语句示例

using System;

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);
            }
        }
    }
}

输出:

The number is 0
The number is 1
The number is 2
The number is 3

Continue 语句示例

using System;

namespace continue_example {
    class Cntnu_Stmt {
        public static void Main(string[] args) {
            for (int i = 0; i <= 5; i++) {
                if (i == 4) {
                    continue;
                }
                Console.WriteLine("The number is " + i);
            }
        }
    }
}

输出:

The number is 0
The number is 1
The number is 2
The number is 3
The number is 5

9. C# 中的常量(const)和只读(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);
    }
}

10. C# 中的 ref 和 out 关键字有什么区别?

  • ref 关键字按引用传递参数。因此,在方法中对此参数所做的任何更改都会反映在返回到调用方法时的那个变量上。

  • out 关键字也按引用传递参数。这与 ref 关键字非常相似。

两者之间的主要区别在于,使用 ref 关键字传递参数时,必须在调用方法前初始化变量;而使用 out 关键字时,变量在方法返回前必须被赋值,但是不需要预先初始化。

11. “this” 能否在一个静态方法中使用?

不能在静态方法中使用 'this',因为 'this' 关键字返回包含它的类的当前实例的引用。静态方法(或任何静态成员)不属于特定实例。它们无需创建类的实例即可存在,并且是通过类名而非实例调用的,所以我们不能在静态方法体内使用 this 关键字。但在扩展方法的情况下,可以使用方法的参数。

12. C# 中的属性是什么?

在 C# 中,属性是类的一个成员,它提供了一种读取、写入或计算私有字段值的方法。它提供了一个公共接口来访问和修改存储在类中的数据,同时允许类控制数据的访问和修改方式。

属性使用 get 和 set 访问器声明,这些访问器定义了获取或设置属性值的行为。get 访问器检索属性值,set 访问器设置属性值。属性可以根据它是只读、只写还是读写来拥有其中一个或两个访问器。

例如,考虑一个包含私有字段 name 的 Person 类。然后,可以创建一个名为 Name 的属性来访问此字段:

class Person
{
    private string name;

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

这个属性允许从类外部访问 name 字段,但仅通过属性方法。这提供了一层封装并控制了数据的访问和修改方式。

13. C# 中的扩展方法是什么?

在 C# 中,扩展方法是一种静态方法,用于在不修改原始类型或不创建新派生类型的情况下扩展现有类型的功能。扩展方法允许开发人员向未在这些类型中最初定义的现有类型添加方法,如类、结构、接口、枚举等。

扩展方法在静态类中声明,并被定义为静态方法,具有一个特殊的第一个参数,称为“this”参数。该“this”参数指定了要扩展的类型,并允许像实例方法一样调用扩展方法。

例如,以下扩展方法扩展了 string 类型,提供了一个将字符串首字母大写的方法:

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 方法不是在 string 类中定义的,而是由 StringExtensions 类提供的扩展方法。

14. C# 中的 Dispose 和 Finalize 有什么区别?

在 C# 中,Dispose 和 Finalize 方法都用于释放资源,但它们的目的和行为不同。

Dispose 方法用于释放不受 .NET 运行时自动管理的非托管资源,例如文件句柄或数据库连接。通常在实现了 IDisposable 接口的类中实现 Dispose 方法。

客户代码显式调用 Dispose 方法来释放不再需要的资源。也可以使用 using 语句隐式调用 Dispose 方法,以确保当对象超出作用域时调用 Dispose 方法。

另一方面,Finalize 方法用于在对象即将被垃圾回收时执行清理操作。通常在覆盖 Object.Finalize 方法的类中实现。

垃圾收集器会调用 Finalize 方法来释放未被 Dispose 方法显式释放的非托管资源。

这两个方法的主要区别在于 Dispose 方法是确定性的并且可以由客户代码显式调用;而 Finalize 方法是非确定性的,并且由垃圾收集器在不确定的时间点调用。

实现 Dispose 方法的对象还应实现 Finalize 方法,作为一种后备机制,以防客户代码未调用 Dispose 方法。

总结来说,Dispose 方法用于确定性地释放非托管资源,而 Finalize 方法则作为对象被垃圾回收时释放非托管资源的后备机制。

15. C# 中的 String 和 StringBuilder 有什么区别?

StringBuilder 和 string 都用于处理字符串值,但它们在实例创建和性能方面有很多不同之处。

  • String

    • 字符串是一个不可变对象。一旦创建,就不能修改。当我们需要改变字符串时,旧的字符串值会被丢弃,并在内存中创建一个新的实例来保存新的字符串值。
    • 性能方面,由于每次修改都会创建新的实例,所以字符串操作相对较慢。
    • 字符串属于 System 命名空间。
  • StringBuilder

    • StringBuilder 是一个可变对象,意味着一旦创建,就可以对其进行任何操作,如插入、替换或追加值,而无需每次都创建新的实例。
    • 性能方面,StringBuilder 非常快,因为它使用同一个实例来执行任何操作,如在现有字符串中插入值。
    • StringBuilder 属于 System.Text 命名空间。

16. C# 中委托(Delegate)的用途是什么?

委托是一种或多个函数指针的抽象形式(就像在 C++ 中存在的那样)。.NET 框架以委托的形式实现了函数指针的概念。有了委托,你可以像处理数据一样处理函数。委托允许函数作为参数传递,作为值从函数返回,并存储在数组中。委托具有以下特征:

  • 委托是从 System.MulticastDelegate 类派生的。
  • 它们具有签名和返回类型。添加到委托的函数必须与此签名兼容。
  • 委托可以指向静态方法或实例方法。
  • 一旦创建了委托对象,它可以在运行时动态地调用其所指向的方法。
  • 委托可以同步或异步地调用方法。
  • 委托包含几个有用的字段。第一个字段持有对象的引用,第二个字段持有方法指针。当调用委托时,实例方法会在所包含的引用上调用。然而,如果对象引用为 null,则运行时会理解为该方法是一个静态方法。此外,调用委托语法上与调用常规函数相同。

为什么我们需要委托?

历史上,Windows API 经常用 C 风格的函数指针来创建回调函数。通过使用回调,程序员能够配置一个函数以便向应用程序中的另一个函数报告。因此,使用回调的目标是处理按钮点击、菜单选择和鼠标移动等活动。但是,这种传统方法的问题在于回调函数不具备类型安全性。在 .NET 框架中,仍然可以使用更有效的方式通过委托来实现回调。然而,委托保持了三个重要的信息:

  • 方法的参数。
  • 它调用的方法的地址。
  • 方法的返回类型。

委托适用于你想将方法传递给其他方法的情况。你可能习惯于将数据作为参数传递给方法,而将方法作为参数传递的想法可能会显得有些奇怪。然而,在某些情况下,你有一个方法去做某事,例如调用另一个方法。在编译时你不知道这个第二个方法是什么。这个信息只有在运行时才可用。因此,委托是克服此类复杂情况的工具。

17. C# 中的密封类(sealed class)是什么?

密封类用于限制面向对象编程中的继承特性。一旦一个类被定义为密封类,该类就不能被继承。

在 C# 中,sealed 修饰符定义一个类为密封类。在 Visual Basic .NET 中,Not Inheritable 关键字服务于密封类的目的。如果从一个密封类派生一个类,编译器会抛出错误。

如果你注意到的话,结构体(structs)是密封的。你不能从结构体派生一个类。

下面的类定义在 C# 中定义了一个密封类:

// 密封类
sealed class SealedClass
{
}

18. C# 中的部分类(partial class)是什么?为什么我们需要部分类?

部分类用于将类的定义拆分成两个或更多的类,在同一个源代码文件或多个源文件中。你可以在多个文件中创建一个类定义,这将在运行时编译成一个类。此外,当你创建这个类的一个实例时,你可以使用同一个对象访问来自所有源文件的所有方法。

部分类可以在相同的命名空间中创建。然而,在不同的命名空间中创建部分类是不可能的。因此,请在你想要绑定在同一命名空间中的同一个类名上使用“partial”关键字。让我们看一个例子:

// 文件 1
partial class MyClass
{
    public void Method1()
    {
        Console.WriteLine("Method 1");
    }
}

// 文件 2
partial class MyClass
{
    public void Method2()
    {
        Console.WriteLine("Method 2");
    }
}

19. C# 中装箱(boxing)和拆箱(unboxing)有什么区别?

装箱和拆箱都是用于类型转换的,但它们有一些区别:

  • 装箱(Boxing)

    • 装箱是将值类型的数据类型转换为对象或由该值类型实现的任何接口数据类型的过程。例如,当 CLR 将一个值类型装箱时,意味着当 CLR 将一个值类型转换为对象类型时,它会将该值包装在一个 System.Object 中,并将其存储在应用程序域的堆区域。

    示例:

    int x = 10;
    object obj = x; // 装箱
  • 拆箱(Unboxing)

    • 拆箱是从对象或任何实现的接口类型中提取值类型的过程。装箱可能是隐式的,但拆箱必须通过代码显式完成。

    示例:

    object obj = 10;
    int x = (int)obj; // 拆箱

装箱和拆箱的概念支持 C# 统一的类型系统观点,即任何类型的值都可以作为对象对待。

20. C# 中的 IEnumerable<> 是什么?

IEnumerable 是 System.Collections 命名空间中所有非泛型集合(如 ArrayList, HashTable 等)的父接口,这些集合是可以枚举的。这个接口的泛型版本是 IEnumerable,它是 System.Collections.Generic 命名空间中所有泛型集合类(如 List<> 等)的父接口。

在 System.Collections.Generic.IEnumerable 中只有一个方法,即 GetEnumerator() 方法,它返回一个 IEnumerator。IEnumerator 提供了通过暴露 Current 属性以及 MoveNext 和 Reset 方法来遍历集合的能力。如果没有这个父接口,我们就不能使用 foreach 循环进行迭代,也不能在我们的 LINQ 查询中使用那个类对象。

21. C# 中的晚期绑定(Late Binding)和早期绑定(Early Binding)有什么区别?

早期绑定和晚期绑定概念属于 C# 中的多态性。多态性是面向对象编程的一个特性,它允许语言使用同一个名称在不同的形式下。例如,一个名为 Add 的方法可以用来加整数、双精度浮点数和十进制数。

实现多态性有两种不同的类型:

  • 编译时多态性,也称为早期绑定或重载。
  • 运行时多态性,也称为晚期绑定或重写。

编译时多态性或早期绑定

在编译时多态性或早期绑定中,我们使用具有相同名称但参数类型不同或参数数量不同的多个方法。正因为如此,我们可以使用相同的名称在同一个类中执行不同的任务,这也被称为方法重载。

以下是实现这一过程的一个示例:

public class Calculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }

    public double Add(double a, double b)
    {
        return a + b;
    }
}

// 使用
Calculator calc = new Calculator();
int sumInt = calc.Add(23); // 5
double sumDouble = calc.Add(2.53.5); // 6.0

运行时多态性或晚期绑定

运行时多态性也称为晚期绑定。在运行时多态性或晚期绑定中,我们可以使用具有相同签名的相同方法名称,这意味着相同的参数类型或数量,但这不在同一个类中,因为在编译时编译器不允许这样做。因此,我们可以在派生类实例化子类或派生类对象时在运行时绑定这些方法。这就是为什么我们称其为晚期绑定。我们必须在基类中声明虚方法,并在驱动类或派生类中用 override 关键字重写这些方法。

示例如下:

public class Animal
{
    public virtual void Sound()
    {
        Console.WriteLine("Some generic sound");
    }
}

public class Dog : Animal
{
    public override void Sound()
    {
        Console.WriteLine("Woof!");
    }
}

// 使用
Animal animal = new Dog();
animal.Sound(); // 输出 "Woof!"

22. IEnumerable 和 IQueryable 之间的区别是什么?

在讨论差异之前,让我们先了解 IEnumerable 和 IQueryable 分别是什么。

IEnumerable

这是 System.Collections 命名空间中所有可以枚举的非泛型集合(如 ArrayList, HashTable 等)的父接口。这个接口的泛型版本是 IEnumerable,它是 System.Collections.Generic 命名空间中所有泛型集合类(如 List<> 等)的父接口。

IQueryable

根据 MSDN 的说法,IQueryable 接口旨在由查询提供者实现。因此,它应该只由同时也实现了 IQueryable 的提供者实现。如果提供者没有同时实现 IQueryable,那么标准查询运算符就不能在这个提供者的数据源上使用。

IQueryable 接口继承了 IEnumerable 接口,以便如果它表示一个查询,那么该查询的结果可以被枚举。枚举会导致与 IQueryable 对象相关联的表达树被执行。执行“表达树”的定义是特定于查询提供者的。例如,它可以涉及将表达树翻译成适合底层数据源的适当查询语言。不返回可枚举结果的查询在调用 Execute 方法时被执行。

23. 如果继承的接口有冲突的方法名称会发生什么?

如果我们实现多个接口并在同一类中有冲突的方法名称,我们不需要定义所有这些方法。换句话说,如果我们在同一个类中有冲突的方法,我们不能独立地在同一类中实现它们的主体,因为它们具有相同的名称和相同的签名。因此,我们必须在方法名称前使用接口名称来消除这种方法冲突。让我们看一个例子:

interface testInterface1
{
    void Show();
}

interface testInterface2
{
    void Show();
}

class Abc : testInterface1testInterface2
{
    void testInterface1.Show()
    {
        Console.WriteLine("For testInterface1 !!");
    }

    void testInterface2.Show()
    {
        Console.WriteLine("For testInterface2 !!");
    }
}

现在看看如何在类中使用这些接口:

class Program
{
    static void Main(string[] args)
    {
        testInterface1 obj1 = new Abc();
        testInterface2 obj2 = new Abc();
        obj1.Show(); // 输出 "For testInterface1 !!"
        obj2.Show(); // 输出 "For testInterface2 !!"
        Console.ReadLine();
    }
}

输出将是:

For testInterface1 !!
For testInterface2 !!

24. C# 中的数组是什么?

在 C# 中,数组索引从零开始。这意味着数组的第一个元素位于第 0 个位置。因此,数组中最后一个元素的位置将是元素总数减 1。如果一个数组有十个元素,那么第十个元素实际上位于第 9 个位置。

在 C# 中,数组可以声明为固定长度或动态。

  • 固定长度数组可以存储预定义数量的项。
  • 动态数组没有预定义的大小。相反,随着向数组添加新项,动态数组的大小会增加。

你可以声明一个固定长度的数组或动态数组。甚至可以在定义之后将动态数组变为静态。

让我们看一下 C# 中数组的简单声明。以下代码片段定义了一个最简单的整数类型的动态数组,没有固定的大小:

int[] intArray;

从上面的代码片段可以看出,数组的声明以数组类型开始,后跟方括号([])和数组的名称。

以下代码片段声明了一个只能存储五个项目的数组,从索引 0 到 4:

int[] intArray = new int[5];

以下代码片段声明了一个可以从索引 0 到 99 存储 100 个项目数组:

int[] intArray = new int[100];

25. C# 中的构造函数链接(Constructor Chaining)是什么?

构造函数链接是一种将两个或多个类以继承关系连接起来的方式。在构造函数链接中,每个子类构造函数都会通过 base 关键字隐式映射到父类构造函数,因此当你创建子类的实例时,它会调用父类的构造函数。没有构造函数链接,继承是不可能的。

26. C# 中 Array.CopyTo() 和 Array.Clone() 之间的区别是什么?

Array.Clone() 方法创建数组的浅拷贝。浅拷贝数组只会复制数组的元素本身,无论这些元素是引用类型还是值类型,但它不会复制这些引用所指向的对象。新数组中的引用指向的是与原始数组中的引用相同的对象。

CopyTo() 是 Array 类的一个静态方法,它将数组的一部分复制到另一个数组。CopyTo 方法将一个数组的所有元素复制到另一个一维数组中。例如,列表九中列出的代码将一个整数数组的内容复制到各种对象类型中。

27. C# 中是否可以执行多个 Catch 块?

可以使用多个 catch 块与一个 try 语句一起使用。这是因为每个 catch 块可以捕获不同的异常。下面的代码示例展示了如何使用单个 try 语句实现多个 catch 语句:

using System;

class MyClient {
    public static void Main() {
        int x = 0;
        int div = 0;
        try {
            div = 100 / x;
            Console.WriteLine("Not executed line");
        } catch (DivideByZeroException de) {
            Console.WriteLine("DivideByZeroException");
        } catch (Exception ee) {
            Console.WriteLine("Exception");
        } finally {
            Console.WriteLine("Finally Block");
        }
        Console.WriteLine("Result is {0}", div);
    }
}

在这个例子中,如果发生除零异常(DivideByZeroException),则会执行第一个 catch 块;如果发生的异常不是除零异常,但仍然是某种异常,则会执行第二个 catch 块。finally 块无论是否有异常发生都会被执行。

28. 单例设计模式(Singleton Design Pattern)是什么,如何在 C# 中实现它?

单例设计模式是一种创建型设计模式,它确保一个类只有一个实例,并且提供一个全局访问该实例的点。此外,此模式控制对象的创建,将可以创建的实例数量限制为单个实例,该实例在整个应用程序中共享。

在典型的单例实现中,单例类具有一个私有构造函数以防止直接实例化,并且具有一个静态方法来返回单例类的唯一实例。当第一次调用静态方法时,它会创建一个新的类实例并将其存储在私有的静态变量中。随后对该静态方法的调用将返回相同的实例。

例如,考虑一个用于管理数据库连接的单例类 DatabaseConnection,该类可以这样实现:

public class DatabaseConnection
{
    private static DatabaseConnection instance;
    private DatabaseConnection() { }

    public static DatabaseConnection GetInstance()
    {
        if (instance == null)
            instance = new DatabaseConnection();
        return instance;
    }

    // 数据库连接方法
    public void Connect() { /* ... */ }
    public void Disconnect() { /* ... */ }
}

在此实现中,GetInstance 方法用于创建或检索类的单一实例。构造函数是私有的,因此不能从类外部直接实例化该类。

单例广泛用于应用程序中来管理那些创建代价高昂或数量有限的资源,如数据库连接、网络套接字、日志系统等。

需要注意的是,单例类可能会引入诸如全局状态和单元测试困难等问题,因此应谨慎使用,并且仅在必要时使用。

29. 抛出异常(Throw Exception)和抛出子句(Throw Clause)之间的区别是什么?

基本的区别在于 throw 异常会覆盖堆栈跟踪,使得很难找到最初抛出异常的代码行号。

throw 基本上保留了堆栈信息,并在抛出异常时添加到堆栈信息中。

让我们来看看这意味着什么,以便更好地理解两者之间的区别。我使用一个控制台应用程序来轻松测试并查看两者在功能上的不同之处。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TestingThrowExceptions {
    class Program {
        public void ExceptionMethod() {
            throw new Exception("Original Exception occurred in ExceptionMethod");
        }
        static void Main(string[] args) {
            Program p = new Program();
            try {
                p.ExceptionMethod();
            } catch (Exception ex) {
                throw ex;
            }
        }
    }
}

现在按 F5 键运行代码并查看发生了什么。它返回一个异常并查看堆栈跟踪。

30. C# 中的索引器(Indexers)是什么?

C# 引入了一种称为索引器的新概念,用于将对象作为数组处理。索引器通常被称为 C# 中的智能数组。然而,它们并不是面向对象编程的基本组成部分。

定义索引器允许你创建充当虚拟数组的类。然后,该类的实例可以使用 [] 数组访问操作符进行访问。

创建索引器:

 <return type> this[argument list] {
    get {
        // 你的获取块代码
    }
    set {
        // 你的设置块代码
    }
}

在上面的代码中,

 

它可以是 private, public, protected 或 internal。

 

它可以是任何有效的 C# 类型。

31. C# 中的多播委托(Multicast Delegate)是什么?

委托是 .NET 中的基础类型之一。委托是一个类,用于在运行时创建和调用委托。

C# 中的委托允许开发者像处理对象一样处理方法,并从他们的代码中调用这些方法。

多播委托(Multicast Delegate)是一个可以存储多个方法引用的委托。当调用多播委托时,它会依次调用所有附加到它的方法。如果其中一个方法抛出了异常,多播委托将继续调用其他方法,除非显式停止。这种行为使得多播委托非常适合事件处理。

多播委托示例实现:

using System;

delegate void MDelegate();

class DM {
    static public void Display() {
        Console.WriteLine("Meerut");
    }

    static public void Print() {
        Console.WriteLine("Roorkee");
    }
}

class MTest {
    public static void Main() {
        MDelegate m1 = new MDelegate(DM.Display);
        MDelegate m2 = new MDelegate(DM.Print);
        
        // 创建一个多播委托
        MDelegate m3 = m1 + m2;
        m3();

        // 多播委托也可以这样创建
        MDelegate m4 = (MDelegate)(DM.Display + DM.Print);
        m4();
    }
}

这段代码将会输出:

Meerut
Roorkee
Meerut
Roorkee

32. C# 中的相等运算符(==)和 Equals() 方法之间有什么区别?

== 运算符和 Equals() 方法都可以比较两种值类型或引用类型的数据项。== 运算符是比较运算符,而 Equals() 方法通常用来比较字符串的内容。== 运算符比较的是引用身份,而 Equals() 方法比较的是内容。

以下示例中,我们将一个字符串变量赋值给另一个变量。字符串是一种引用类型。因此,在下面的例子中,一个字符串变量被赋值给另一个字符串变量,这两个变量在堆中引用的是同一个对象,并且它们的内容相同。因此,对于 == 运算符和 Equals() 方法,你会得到 True 的输出。

using System;

namespace ComparisonExample {
    class Program {
        static void Main(string[] args) {
            string name = "sandeep";
            string myName = name;
            Console.WriteLine("== operator result is {0}", name == myName);
            Console.WriteLine("Equals method result is {0}", name.Equals(myName));
            Console.ReadKey();
        }
    }
}

33. C# 中的 is 和 as 操作符之间有什么区别?

is 操作符

在 C# 语言中,我们使用 is 操作符来检查对象的类型。如果两个对象是同一类型,它返回 true;否则返回 false

让我们通过 C# 代码来理解这一点。首先,我们声明两个类 Speaker 和 Author

class Speaker {
    public string Name { getset; }
}

class Author {
    public string Name { getset; }
}

现在,让我们创建一个 Speaker 类型的对象:

var speaker = new Speaker { Name = "Gaurav Kumar Arora" };

现在,让我们检查这个对象是否是 Speaker 类型:

var isTrue = speaker is Speaker;

在前面的代码中,我们正在检查匹配的类型。所以,是的,我们的 speaker 是 Speaker 类型的对象。

Console.WriteLine("speaker is of Speaker type: {0}", isTrue);

结果是 true

但是,这里我们得到 false

var author = new Author { Name = "Gaurav Kumar Arora" };
var isTrue = speaker is Author;
Console.WriteLine("speaker is of Author type: {0}", isTrue);

因为我们的 speaker 不是 Author 类型的对象。

as 操作符

as 操作符的行为类似于 is 操作符。唯一的区别是,如果两个对象兼容于那种类型,它返回那个对象;否则返回 null

让我们通过 C# 代码来理解这一点。

public static string GetAuthorName(dynamic obj) {
    Author authorObj = obj as Author;
    return (authorObj != null) ? authorObj.Name : string.Empty;
}

我们有一个方法,它接受一个动态对象,如果该对象是 Author 类型,则返回该对象的名字属性。

这里,我们声明了两个对象:

var speaker = new Speaker { Name = "Gaurav Kumar Arora" };
var author = new Author { Name = "Gaurav Kumar Arora" };

以下代码返回 Name 属性:

var authorName = GetAuthorName(author);
Console.WriteLine("Author name is: {0}", authorName);

它返回空字符串:

authorName = GetAuthorName(speaker);
Console.WriteLine("Author name is: {0}", authorName);

34. 如何在 C# 中使用 Nullable< > 类型?

Nullable 类型是一种包含定义的数据类型或 null 值的数据类型。

这个 Nullable 类型的概念不兼容于 var

任何数据类型都可以通过 ? 操作符的帮助声明为 Nullable 类型。

例如,以下代码声明了一个可以为 null 的 int 类型 i

int? i = null;

正如前面部分所讨论的,var 与 Nullable 类型不兼容。因此,如果你这样声明,你会得到错误:

var? i = null;

35. 方法重载有哪些不同的方式?

方法重载是一种实现编译时多态性的方法,其中我们可以使用相同名称但不同签名的方法。例如,以下代码示例有一个名为 volume 的方法,它基于参数的数量、类型和返回值具有三个不同的签名。

示例:

using System;

namespace HelloWorld {
    class Overloading {
        public static void Main() {
            Console.WriteLine(Volume(10));
            Console.WriteLine(Volume(2.5F8));
            Console.WriteLine(Volume(100L7515));
            Console.ReadLine();
        }

        static int Volume(int x) {
            return (x * x * x);
        }

        static double Volume(float r, int h) {
            return (3.14 * r * r * h);
        }

        static long Volume(long l, int b, int h) {
            return (l * b * h);
        }
    }
}

注意

如果我们有一个带有两个对象参数的方法以及一个带有两个整数参数的方法,并且当我们调用该方法时传递了整数值,那么它将调用带有整数参数的方法而不是带有对象类型参数的方法。

36. 什么是对象池(Object Pooling)?

对象池是在 .NET 中的一种技术,它允许对象保持在内存池中以便能够重复使用而不必重新创建它们。本文解释了在 .NET 中对象池是什么以及如何在 C# 中实现对象池。

这是什么意思?

对象池是一个包含准备好使用的对象的容器。每当有新的对象请求时,池管理器将接收请求并通过从池中分配一个对象来服务该请求。

它是如何工作的?

我们将使用工厂模式来达到这一目的。我们将拥有一个工厂方法,该方法将负责对象的创建。每当有新的对象请求时,工厂方法将检查对象池(我们使用 Queue 对象)。如果在允许的限制内有任何对象可用,它将返回该对象(值对象)。否则,将创建一个新的对象并返回给您。

37. C# 中的泛型(Generics)是什么?

泛型允许您延迟指定类或方法中编程元素的数据类型,直到程序中实际使用它们。换句话说,泛型允许您编写可以处理任何数据类型的类或方法。

您编写类或方法的规范,其中使用数据类型的替代参数。当编译器遇到类的构造函数或方法的函数调用时,它将生成处理特定数据类型的代码。

ASP.NET

泛型类和方法结合了可重用性、类型安全性和效率,这是它们的非泛型对应物无法做到的。泛型最常用于集合及其操作方法。.NET Framework 2.0 版本的类库提供了一个新的命名空间 System.Collections.Generic,其中包含几个新的基于泛型的集合类。建议所有针对 .NET Framework 2.0 及更高版本的应用程序都使用新的泛型集合类,而不是旧的非泛型对应类,如 ArrayList。

泛型的特点

泛型是一种以以下方式丰富您的程序的技术:

首先,它帮助您最大化代码重用、类型安全性和性能。 您可以创建泛型集合类。.NET Framework 类库在 System.Collections.Generic 命名空间中包含了几个新的泛型集合类。您可以使用这些泛型集合类代替 System.Collections 命名空间中的集合类。 您可以创建自己的泛型接口、类、方法、事件和委托。 您可以创建受约束的泛型类,以启用对特定数据类型的方法访问。 您可以使用反射在运行时获取泛型数据类型中使用类型的信息。

38. 访问修饰符的作用是什么?

访问修饰符是用于指定类型成员或类型的声明可访问性的关键字。

访问修饰符是用于指定类型成员或类型的可访问范围的关键字。例如,公共类可以被整个世界访问,而内部类可能只在程序集中可见。

为什么要使用访问修饰符?

访问修饰符是面向对象编程的重要组成部分。访问修饰符用于实现 OOP 的封装。此外,访问修饰符允许您定义谁有权访问某些特性。

在 C# 中,有六种不同的访问修饰符:

修饰符 描述
public 公共成员没有访问限制。
private 访问限于类定义内部。如果没有明确指定,这是默认的访问修饰符类型。
protected 访问限于类定义内部及从该类继承的任何类。
internal 访问限于当前项目程序集中的类。
protected internal 访问限于当前程序集和从包含类派生的类型。当前项目中的所有成员和派生类都可以访问这些变量。
private protected 访问限于当前程序集中的包含类或从包含类派生的类型。

39. C# 中的虚方法(Virtual Method)是什么?

虚方法是在派生类中可以被重新定义的方法。虚方法在基类中有一个实现,并且可以从派生类中继承。当一个方法的基本功能相同,但在派生类中有时需要更多的功能时,就会使用虚方法。基类中创建了一个可以在派生类中重写的虚方法。我们在基类中使用 virtual 关键字创建虚方法,并在派生类中使用 override 关键字重写该方法。

当在基类中声明一个虚方法时,可以在基类中定义该方法,并且派生类可以选择性地重写该方法。重写的方法也提供了方法的多种形式。因此,这也是多态的一个例子。

当基类中的虚方法和派生类中的方法具有相同的定义时,不需要在派生类中重写它。但是,当基类中的虚方法和派生类中的方法有不同的定义时,则有必要在派生类中重写它。

当调用虚方法时,会检查对象的运行时类型以查找重写的成员。首先调用最派生类中的重写成员,如果没有派生类重写了成员,则可能是原始成员。

默认情况下,方法是非虚的。因此,我们不能重写非虚方法。 我们不能将 virtual 修饰符与 static、abstract、private 或 override 修饰符一起使用。

40. C# 中的数组(Array)和 ArrayList 之间的区别是什么?

在这里有一份两者之间的区别列表:

  • Array 是固定大小的,一旦创建后其大小不能改变。如果您想要更改数组的大小,您需要创建一个新的数组并将原始数组的内容复制到新数组中。
  • ArrayList 是动态大小的,它可以根据需要自动调整大小。ArrayList 类位于 System.Collections 命名空间下,并使用一个后台数组来存储其元素。当添加元素时,如果当前数组已满,ArrayList 将创建一个更大的数组并将现有元素复制到新数组中。
  • Array 支持泛型,这意味着您可以在数组中存储特定类型的对象,并且编译器会在编译时检查类型一致性。
  • ArrayList 在 C# 早期版本中不支持泛型。因此,如果您在 ArrayList 中存储不同类型的数据,您需要在运行时进行类型转换。不过,从 .NET Framework 4 开始,System.Collections.ObjectModel.Collection 提供了泛型集合的功能,通常比 ArrayList 更高效和类型安全。
  • Array 使用索引访问元素,索引是从零开始的整数。
  • ArrayList 同样使用索引访问元素,但提供了更多的集合操作方法,如 Add、Remove、Insert 等。

请注意,尽管 ArrayList 曾经是一个常用的集合类型,但由于它的非泛型性质和较低的性能,现在更推荐使用泛型集合,如 List

41. C# 中的值类型(Value Types)和引用类型(Reference Types)是什么?

在 C# 中,数据类型可以分为两大类:值类型和引用类型。值类型变量直接包含它们的对象(或数据)。如果我们把一个值类型变量复制到另一个变量,我们会为第二个变量复制一份该对象。这两个变量将独立地操作它们各自的值。值类型数据类型存储在栈上,而引用数据类型则存储在堆上。

在 C# 中,基本数据类型如 intcharbool 和 long 都是值类型。另外,类和集合属于引用类型。

42. C# 中的序列化(Serialization)是什么?

序列化在 C# 中是指将一个对象转换成字节流,以便将该对象存储在内存、数据库或文件中。其主要目的是为了保存对象的状态,以便在需要的时候能够重新创建它。反向过程称为反序列化(Deserialization)。

序列化主要有三种类型:

  • 二进制序列化(Binary Serialization):将对象数据保存为二进制格式。
  • SOAP 序列化(SOAP Serialization):将对象数据保存为二进制格式;主要用于网络相关通信。
  • XML 序列化(XMLSerialization):将对象数据保存为 XML 文件。

43. 如何在 C# 中使用“using”语句?

在 C# 中使用 using 关键字有两种方式:一种是作为指令(Directive),另一种是作为语句(Statement)。

using 指令(Directive)

通常,我们使用 using 关键字在代码隐藏文件和类文件中添加命名空间。这使得当前页面上的所有类、接口和抽象类及其方法和属性都可以使用。添加命名空间可以通过以下两种方式进行:

  • 在文件顶部使用 using 关键字导入命名空间。
  • 在类定义中使用 internal 或 public 关键字声明命名空间。

using 语句(Statement)

这是在 C# 中使用 using 关键字的另一种方式。它在提高垃圾回收的性能方面起着关键作用。

using 语句通常用于确保实现了 IDisposable 接口的对象能够在使用完毕后正确地释放资源。

44. C# 中的锯齿数组(Jagged Array)是什么?

锯齿数组是一个元素为数组的数组。锯齿数组的元素可以有不同的维度和大小。锯齿数组有时被称为“数组的数组”。

在 C# 中引入了一种特殊的数组类型。锯齿数组是一个数组中的数组,其中每个数组索引的长度可以不同。

示例

int[][] jagArray = new int[5][];

在上面的声明中,行的大小是固定的。但是列没有指定,因为它们可以变化。

声明并初始化一个锯齿数组:

int[][] jaggedArray = new int[5][];
jaggedArray[0] = new int[3];
jaggedArray[1] = new int[5];
jaggedArray[2] = new int[2];
jaggedArray[3] = new int[8];
jaggedArray[4] = new int[10];

// 初始化数组
jaggedArray[0] = new int[] { 357 };
jaggedArray[1] = new int[] { 10246 };
jaggedArray[2] = new int[] { 16 };
jaggedArray[3] = new int[] { 10246456778 };
jaggedArray[4] = new int[] { 102463454678778 };

45. .NET 中的多线程(Multithreading)是什么?

多线程允许程序并发地运行多个线程。本文解释了在 .NET 中多线程是如何工作的。本文涵盖了从线程创建、竞态条件、死锁、监视器、互斥量、同步、信号量等方面的所有多线程领域。

线程的实际用途不仅仅在于单个顺序执行的线程,而是利用单个程序中的多个线程。同时运行多个线程并执行各种任务称为多线程。一个线程被认为是一个轻量级的过程,因为它在一个程序的上下文中运行,并利用分配给该程序的资源。

单线程进程只包含一个线程,而多线程进程则包含一个以上的线程用于执行。

46. C# 中的匿名类型(Anonymous Types)是什么?

匿名类型允许我们在不定义它们的情况下创建新类型。这是一种在单个对象中定义只读属性的方法,而无需显式地定义每种类型。在这里,类型由编译器生成,并且仅在当前代码块中可访问。属性的类型也由编译器推断。

我们可以使用“new”关键字和对象初始化器来创建匿名类型。

示例

var anonymousData = new
{
    ForeName = "Jignesh",
    SurName = "Trivedi"
};
Console.WriteLine("First Name : " + anonymousData.ForeName);

匿名类型与 LINQ 示例

匿名类型还与 LINQ 查询表达式的“Select”子句一起使用,以返回一组属性的子集。

示例

如果任何对象集合具有诸如 FirstName、LastName、DOB 等属性,并且您只想在查询数据后获取 FirstName 和 LastName:

class MyData {
    public string FirstName { getset; }
    public string LastName { getset; }
    public DateTime DOB { getset; }
    public string MiddleName { getset; }
}

static void Main(string[] args) {
    // 创建虚拟数据填充集合
    List data = new List();
    data.Add(new MyData { FirstName = "Jignesh", LastName = "Trivedi", MiddleName = "G", DOB = new DateTime(19901230) });
    // 更多数据添加...

    var anonymousData = from pl in data
                        select new { pl.FirstName, pl.LastName };

    foreach (var m in anonymousData) {
        Console.WriteLine("Name : " + m.FirstName + " " + m.LastName);
    }
}

47. C# 中的哈希表(Hashtable)是什么?

哈希表是一个存储(键,值)对的集合。在这里,键用来查找存储位置,是不可变的,并且在哈希表中不能有重复条目。.NET Framework 提供了一个哈希表类,它包含了实现哈希表所需的所有功能,无需额外开发。哈希表是一种通用的字典集合。集合内的每个项都是一个 DictionaryEntry 对象,具有两个属性:一个键对象和一个值对象。这些被称为键/值。当项被添加到哈希表时,自动生成一个哈希码。此代码对开发者是隐藏的。通过键对象标识来访问表中的值。由于集合中的项根据隐藏的哈希码排序,因此应认为项是随机排列的。

哈希表集合

基础类库提供了一个定义在 System.Collections 命名空间下的哈希表类,因此您不必自己编写哈希表。它每次都会处理您添加的哈希的每个键,然后使用哈希码快速查找元素。哈希表的容量是指哈希表可以容纳的元素数量。随着元素被添加到哈希表中,容量会根据需要通过重新分配自动增加。这是一个较老的 .NET Framework 类型。

声明哈希表

哈希表类通常在 System.Collections 命名空间中找到。因此,为了执行任何示例,我们必须在源代码中添加 using System.Collections;。哈希表的声明如下:

Hashtable HT = new Hashtable();

48. C# 中的 LINQ 是什么?

LINQ 代表 Language Integrated Query(语言集成查询)。LINQ 是一种提供类似于 SQL 查询语法能力的数据查询方法论。

LINQ 强大之处在于它可以查询任何数据源。数据源可以是对象集合、数据库或 XML 文件。我们可以轻松地从实现了 IEnumerable 接口的任何对象中检索数据。

LINQ 的优点

  • LINQ 提供了一种基于对象的、语言集成的方式来查询数据,无论这些数据来自何处。因此,通过 LINQ,我们可以查询数据库、XML 和集合。
  • 编译时语法检查。
  • 它允许您像使用 SQL 查询数据库一样使用应用程序的本语言(如 VB 或 C#)来查询集合,如数组、可枚举类等。

49. C#.Net 中的文件处理(File Handling)是什么?

System.IO 命名空间提供了四个类,使您能够操纵单独的文件并与机器目录结构交互。Directory 和 File 直接扩展 System.Object 并使用各种静态方法支持文件的创建、复制、移动和删除。然而,它们只包含静态方法且从不实例化。FileInfo 和 DirectoryInfo 类型是从抽象类 FileSystemInfo 类型派生的。它们通常用于获取文件或目录的完整详细信息,因为它们的成员倾向于返回强类型的对象。它们实现了与 Directory 和 File 大致相同的公共方法,但它们是状态化的,这些类的成员不是静态的。

50. C# 中的反射(Reflection)是什么?

反射是在运行时发现类型的过程,用于检查元数据、CIL 代码、后期绑定和自动生成代码。在运行时,使用反射,我们可以访问与设计时 ilasm 实用工具显示相同的“类型”信息。反射类似于逆向工程,即我们可以解构现有的 .exe 或 .dll 组件以探索定义的重要内容信息,包括方法、字段、事件和属性。

使用 System.Reflection 命名空间,您可以动态发现给定类型支持的接口集。

反射通常用于转储加载的组件列表及其引用以检查方法、属性等。反射也被外部反汇编工具如 Reflector、Fxcop 和 NUnit 使用,因为 .NET 工具不需要解析源代码,类似于 C++。

元数据调查

下面的程序通过创建一个控制台应用程序演示了反射的过程。此程序将显示 mscorlib.dll 组件中任何类型的字段、方法、属性和接口的详细信息。在此之前,必须导入“System.Reflection”。

在这里,我们在程序类中定义了几个静态方法来枚举指定类型中的字段、方法和接口。静态方法接受一个“System.Type”参数并返回 void。

static void FieldInvestigation(Type t) {
    Console.WriteLine("*********Fields*********");
    FieldInfo[] fld = t.GetFields();
    foreach (FieldInfo f in fld) {
        Console.WriteLine("-->{0}", f.Name);
    }
}

static void MethodInvestigation(Type t) {
    Console.WriteLine("*********Methods*********");
    MethodInfo[] mth = t.GetMethods();
    foreach (MethodInfo m in mth) {
        Console.WriteLine("-->{0}", m.Name);
    }
}