(基于C# 编程技术要点详解(一)文章范围)

面试题来了,这篇文章将帮助你梳理C#面试中常问的问题,以及相关技术点。

对于 相关技术点: ,参照 文章面试必备:C# 编程技术要点详解(一)、轻松应对技术拷问(细到每个版本,技术产生原因、好处、示例) ,复习相关知识点。

对于 XX总结: ,自己理解,然后用自己的话总结出来,这样印象更深刻。结合自己的理解和体会,一定能让面试官眼前一亮。

1. 问题:C#中readonly和const有什么区别?

答案:

const和readonly都是用来创建不可变的值,但它们之间有重要区别:

  1. const是编译时常量,在编译时就确定值,存储在程序集的元数据中;
  2. readonly是运行时常量,可以在声明时或构造函数中赋值;
  3. const只能在声明时初始化,readonly可以在声明时或构造函数中初始化;
  4. const默认是静态的,而readonly可以是实例成员也可以是静态成员;
  5. const只能用于基本数据类型、枚举或null,readonly可以用于任何引用类型。

相关技术点: C# 1.0 基础特性

毒舌总结: 记住这个区别,别再把const当成readonly的简化写法了,不然面试官会觉得你连入门都还没入门。

2. 问题:C#中的值类型和引用类型有什么区别?string是哪种类型?

答案:

值类型和引用类型的主要区别:

  1. 值类型(如int, struct)直接存储数据,分配在栈上;
  2. 引用类型(如class, string)存储对数据的引用,分配在堆上;
  3. 值类型赋值时会复制整个对象,引用类型赋值时只复制引用;
  4. 值类型不会产生垃圾回收压力,引用类型会产生GC压力。

string是引用类型,但它具有值类型的语义特征:当修改string变量时,会创建新的string对象,而不是修改原对象。

相关技术点: C# 1.0 基础特性

鸡汤总结: 理解值类型和引用类型就像理解人与人之间的关系,有些是直接的联系,有些是通过媒介的间接联系,而string就像一个善变的朋友,表面看起来一样,内心却已改变。

3. 问题:什么是泛型?它解决了什么问题?

答案:

泛型是C# 2.0引入的特性,允许在定义类、接口、委托或方法时不指定具体的数据类型,而在使用时再指定类型。

泛型解决了以下问题:

  1. 类型安全:编译时类型检查,避免运行时类型转换错误;
  2. 性能提升:避免装箱和拆箱操作;
  3. 代码复用:一个泛型定义可以用于多种类型;
  4. 消除强制类型转换:不再需要显式的类型转换。

示例:

// 旧方式
ArrayList list = new ArrayList();
list.Add(1);
int value = (int)list[0]; // 需要强制转换,运行时可能出错,除非开发者知道list中只包含int类型

// 新方式
List<int> list = new List<int>();
list.Add(1);
int value = list[0]; // 类型安全,无需强制转换

相关技术点: C# 2.0 泛型

幽默总结: 泛型就像一个"通用模板",就像你去理发店办了张万能卡,不管你是长发、短发还是卷发,都能用它来剪,而且保证不会把你剪成隔壁老王。

4. 问题:C# 3.0中LINQ是什么?它有什么优势?

答案:

LINQ(Language Integrated Query)是C# 3.0引入的语言集成查询功能,允许开发者使用类似SQL的语法查询各种数据源。

LINQ的优势:

  1. 统一查询语法:可以用相同的语法查询内存集合、数据库、XML等;
  2. 编译时检查:查询语法在编译时检查,避免运行时错误;
  3. 智能提示:在IDE中提供智能感知支持;
  4. 类型安全:返回的结果是强类型的;
  5. 延迟执行:查询在枚举时才执行,提高性能。

示例:

var query = from p in people
            where p.Age > 18
            orderby p.Name
            select p;

// 或使用方法语法
var query = people.Where(p => p.Age > 18).OrderBy(p => p.Name);

相关技术点: C# 3.0 LINQ相关特性

毒舌总结: LINQ让你可以用写SQL的方式写C#代码,但别忘了你还是在写C#,不是在偷懒写存储过程。

5. 问题:C#中扩展方法是什么?如何使用?

答案:

扩展方法是C# 3.0引入的特性,允许为现有类型添加新方法而无需修改原始类型或创建新派生类型。

扩展方法的特点:

  1. 必须定义在静态类中;
  2. 方法本身必须是静态的;
  3. 第一个参数使用this关键字修饰,指定要扩展的类型;
  4. 在客户端代码中使用时,就像实例方法一样调用。

示例:

public static class StringExtensions
{
    public static string ToTitleCase(this string str)
    {
        if (string.IsNullOrEmpty(str))
            return str;
        
        return char.ToUpper(str[0]) + str.Substring(1).ToLower();
    }
}

// 使用
string name = "zhang san";
Console.WriteLine(name.ToTitleCase()); // 输出: Zhang san

相关技术点: C# 3.0 扩展方法

鸡汤总结: 扩展方法就像给人添加技能点,不需要改变人的本质,就能让他拥有新的能力,这就是程序员的"开挂人生"。

6. 问题:async/await关键字解决了什么问题?它们是如何工作的?

答案:

async/await是C# 5.0引入的异步编程模型,解决了传统异步编程复杂、容易出错的问题。

解决的问题:

  1. 简化异步代码编写和维护;
  2. 避免阻塞UI线程,提高应用响应性;
  3. 提高服务器应用程序的可伸缩性;
  4. 减少开发人员编写异步代码时的认知负担。

工作原理:

  1. 当方法标记为async时,编译器会生成状态机;
  2. 遇到await关键字时,如果等待的任务未完成,方法会立即返回;
  3. 当任务完成后,方法从await点继续执行。

示例:

public async Task<string> GetDataAsync()
{
    await Task.Delay(2000); // 模拟耗时操作
    return "数据获取完成";
}

public async void Button_Click()
{
    string result = await GetDataAsync(); // 不会阻塞UI线程
    Console.WriteLine(result);
}

相关技术点: C# 5.0 异步编程支持

幽默总结: async/await就像"分身术",你可以让一个任务去慢慢执行,而你的主线程可以继续处理其他事情,等任务完成了再回来汇报结果。

7. 问题:C# 6.0中的空条件运算符(?.)有什么作用?

答案:

空条件运算符(?.)是C# 6.0引入的特性,用于安全地访问可能为null的对象成员,避免NullReferenceException异常。

作用:

  1. 简化空引用检查代码;
  2. 减少NullReferenceException异常的发生;
  3. 提高代码的可读性和安全性;
  4. 减少样板代码的数量。

示例:

// 旧方式
if (person != null && person.Address != null)
{
    string city = person.Address.City;
}

// 新方式
string city = person?.Address?.City;

// 更多示例
int length = person?.Name?.Length ?? 0;
int hobbyCount = person?.Hobbies?.Count ?? 0;
someDelegate?.Invoke(); // 安全调用委托

相关技术点: C# 6.0 空条件运算符

毒舌总结: 有了?.运算符,你再也不用写那些啰嗦的null检查代码了,不过别指望它能解决你代码设计上的所有问题。

8. 问题:C# 7.0中的元组(Tuples)有什么优势?

答案:

C# 7.0中的元组提供了一种简洁的方式来从方法返回多个值,并支持命名元素。

优势:

  1. 简化多值返回的语法;
  2. 提供更好的命名支持,提高代码可读性;
  3. 减少创建简单数据传输类的需求;
  4. 支持解构,使元组使用更加灵活。

示例:

// 定义和使用命名元组
public (string Name, int Age) GetPersonInfo()
{
    return ("张三", 25);
}

// 使用解构
var (name, age) = GetPersonInfo();

// 丢弃不需要的值
var (firstName, _) = GetPersonInfo(); // 只关心名字,不关心年龄

相关技术点: C# 7.0 元组

鸡汤总结: 元组就像一个临时的工具箱,虽然简单但非常实用,让你在不需要复杂结构的时候也能轻松携带多个工具。

9. 问题:C# 7.0中的模式匹配是什么?如何使用?

答案:

模式匹配是C# 7.0引入的特性,允许在is表达式和switch语句中检查对象的类型并提取值。

使用方式:

  1. is表达式模式匹配:在类型检查的同时声明变量;
  2. switch模式匹配:在switch语句中使用模式进行更复杂的匹配。

示例:

// is表达式模式匹配
public void ProcessObject(object obj)
{
    if (obj is string s)
    {
        Console.WriteLine($"字符串长度: {s.Length}");
    }
    else if (obj is int i && i > 0)
    {
        Console.WriteLine($"正整数: {i}");
    }
}

// switch模式匹配
public string GetDiscount(object customer)
{
    switch (customer)
    {
        case PremiumCustomer p when p.Years > 5:
            return "20%";
        case PremiumCustomer p:
            return "15%";
        case BasicCustomer b when b.Age > 65:
            return "10%";
        default:
            return "5%";
    }
}

相关技术点: C# 7.0 模式匹配

幽默总结: 模式匹配就像"人脸识别系统",不仅能认出你是谁,还能顺便提取你的个人信息,比保安还厉害。

10. 问题:C# 4.0中的协变和逆变是什么?

答案:

协变和逆变是C# 4.0中对泛型接口和委托的增强,允许在特定情况下进行更灵活的类型转换。

  1. 协变(Covariance):允许派生类型替换基类型,用out关键字标记;
  2. 逆变(Contravariance):允许基类型替换派生类型,用in关键字标记。

示例:

// 协变 - 允许派生类型替换基类型
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // C# 4.0 之前不允许

// 逆变 - 允许基类型替换派生类型
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // C# 4.0 之前不允许

相关技术点: C# 4.0 泛型协变和逆变

毒舌总结: 协变和逆变听起来高大上,但实际使用中大部分程序员一辈子可能都用不到几次,面试官问这个更像是在炫耀自己的理论水平。

11. 问题:C# 6.0中的nameof表达式有什么作用?

答案:

nameof表达式是C# 6.0引入的特性,用于获取变量、类型或成员的名称作为字符串,提供编译时安全保障。

作用:

  1. 提供编译时安全的标识符名称获取;
  2. 减少因重命名导致的字符串不匹配错误;
  3. 提高代码重构的安全性;
  4. 简化参数验证等场景的代码。

示例:

public void SetName(string name)
{
    if (name == null)
        throw new ArgumentNullException(nameof(name)); // 编译时安全
}

public class Person : INotifyPropertyChanged
{
    private string _name;
    
    public string Name
    {
        get => _name;
        set
        {
            _name = value;
            OnPropertyChanged(nameof(Name)); // 重构安全
        }
    }
}

相关技术点: C# 6.0 nameof表达式

鸡汤总结: nameof就像代码的"身份证",不管变量名怎么改,它都能准确地告诉你原来的"名字",让代码重构变得安全无忧。

12. 问题:C# 3.0中的Lambda表达式与匿名方法有什么区别?

答案:

Lambda表达式是C# 3.0中对匿名方法的简化语法,提供了更简洁的函数定义方式。

区别:

  1. 语法简洁性:Lambda表达式语法更简洁;
  2. 类型推断:Lambda表达式支持更好的类型推断;
  3. 表达式体:Lambda表达式可以直接使用表达式作为函数体;
  4. 应用场景:Lambda表达式更适用于LINQ查询。

示例:

// 匿名方法(C# 2.0)
button.Click += delegate(object sender, EventArgs e) { 
    MessageBox.Show("按钮被点击"); 
};

// Lambda表达式(C# 3.0)
button.Click += (sender, e) => MessageBox.Show("按钮被点击");

// 用于LINQ
var evenNumbers = numbers.Where(n => n % 2 == 0);

相关技术点: C# 3.0 Lambda表达式

幽默总结: Lambda表达式就像"缩骨功",把原来需要大段代码才能表达的逻辑压缩成一行,让代码看起来更加"苗条"。

13. 问题:C# 7.0中的out变量有什么优势?

答案:

out变量是C# 7.0引入的特性,允许在方法调用时直接声明out参数变量,使代码更加简洁。

优势:

  1. 简化out参数的使用;
  2. 减少变量声明的冗余代码;
  3. 提高代码的可读性;
  4. 支持隐式类型推断。

示例:

// 旧方式
int result;
if (int.TryParse("123", out result))
{
    Console.WriteLine($"解析成功: {result}");
}

// 新方式
if (int.TryParse("123", out int result))
{
    Console.WriteLine($"解析成功: {result}");
}

// 使用 var 关键字
if (int.TryParse("123", out var result))
{
    Console.WriteLine($"解析成功: {result}");
}

相关技术点: C# 7.0 Out变量

毒舌总结: out变量让你少写一行代码,虽然看似微不足道,但在大量使用时能显著减少代码量,至少让你的手少敲几次键盘。

14. 问题:C# 7.2中的in参数修饰符解决了什么问题?

答案:

in参数修饰符是C# 7.2引入的特性,用于以只读引用的方式传递参数,既保证安全性又提高性能。

解决的问题:

  1. 提高大型结构体传递的性能;
  2. 保证参数不会被方法修改;
  3. 减少不必要的数据复制。

示例:

public struct LargeStruct
{
    public int Value1, Value2, Value3, Value4, Value5;
    
    public void ProcessData(in LargeStruct other)
    {
        // 可以读取 other 的值
        int result = this.Value1 + other.Value1;
        
        // 不能修改 other
        // other.Value1 = 10; // 编译错误
    }
}

相关技术点: C# 7.2 in参数修饰符

鸡汤总结: in参数就像"保险箱",你可以查看里面的内容但不能修改,既保证了安全性又避免了复制的开销,一举两得。

15. 问题:C# 6.0中的表达式体成员是什么?适用于哪些场景?

答案:

表达式体成员是C# 6.0引入的特性,提供了一种更简洁的语法来定义只包含单个表达式的成员。

适用场景:

  1. 简单的只读属性;
  2. 只包含单个表达式的方法;
  3. 只读索引器;
  4. 运算符重载等。

示例:

public class Person
{
    private string firstName;
    private string lastName;
    
    // 表达式体只读属性
    public string FullName => $"{firstName} {lastName}";
    
    // 表达式体方法
    public string GetInfo() => $"姓名: {FullName}";
}

public class MathHelper
{
    // 只读属性的表达式体
    public double Pi => Math.PI;
    public double E => Math.E;
}

相关技术点: C# 6.0 表达式体成员

幽默总结: 表达式体成员就像"一句话介绍自己",简单明了,不需要长篇大论,适合那些不想多说的简单方法和属性。

如果轻松搞定上面15题,恭喜你,算入门过关。
作为开发者,务必多实践多写多练多领悟。

更多推荐