C#面试题及详细答案120道(21-30)-- 集合与泛型
《前后端面试题
》专栏集合了前后端各个知识模块的面试题,包括html,javascript,css,vue,react,java,Openlayers,leaflet,cesium,mapboxGL,threejs,nodejs,mangoDB,SQL,Linux… 。
文章目录
- 一、本文面试题目录
- 21. 简述C#中常用的集合类型及其特点(List、Dictionary、HashSet等)
- 22. Array和ArrayList的区别
- 23. List<T>和ArrayList的区别
- 24. Dictionary<TKey, TValue>的实现原理是什么?如何解决哈希冲突?
- 25. 什么是泛型?泛型的优点是什么?
- 26. 泛型约束有哪些类型?如何使用?
- 27. 什么是协变和逆变?在泛型中如何实现?
- 28. IEnumerable和IEnumerator的区别
- 29. ICollection和IList接口的区别
- 30. 什么是 HashSet<T>?与 List<T> 相比有什么优势?
- 二、120道C#面试题目录列表
一、本文面试题目录
21. 简述C#中常用的集合类型及其特点(List、Dictionary、HashSet等)
C#提供了多种集合类型,适用于不同场景,以下是常用集合及其特点:
-
List
- 特点:动态大小的泛型列表,基于数组实现
- 优势:随机访问效率高(O(1)),支持索引操作
- 劣势:插入/删除中间元素效率低(O(n))
- 适用场景:需要频繁访问元素、顺序存储数据
var list = new List<int> { 1, 2, 3 }; list.Add(4); int value = list[2]; // 随机访问
-
Dictionary<TKey, TValue>
- 特点:键值对集合,基于哈希表实现
- 优势:按键查找效率高(O(1)),支持快速插入
- 劣势:无序存储,键不能重复
- 适用场景:需要通过唯一键快速查找数据
var dict = new Dictionary<string, int>(); dict["one"] = 1; int num = dict["one"]; // 快速查找
-
HashSet
- 特点:不重复元素的集合,基于哈希表实现
- 优势:检查元素是否存在效率高(O(1)),自动去重
- 劣势:无序,不能通过索引访问
- 适用场景:需要去重、集合运算(交集、并集)
var set = new HashSet<int> { 1, 2, 3 }; bool exists = set.Contains(2); // 快速检查存在性
-
Queue
- 特点:先进先出(FIFO)的队列
- 优势:入队/出队操作高效(O(1))
- 适用场景:消息队列、任务调度
var queue = new Queue<string>(); queue.Enqueue("first"); string item = queue.Dequeue(); // 获取并移除第一个元素
-
Stack
- 特点:后进先出(LIFO)的栈
- 优势:入栈/出栈操作高效(O(1))
- 适用场景:表达式计算、回溯算法
var stack = new Stack<int>(); stack.Push(1); int top = stack.Pop(); // 获取并移除顶部元素
-
LinkedList
- 特点:双向链表实现
- 优势:插入/删除中间元素效率高(O(1),已知节点时)
- 劣势:随机访问效率低(O(n))
- 适用场景:频繁插入删除且不需要随机访问
-
SortedDictionary<TKey, TValue>
- 特点:按键排序的键值对集合,基于红黑树实现
- 优势:自动排序,范围查询高效
- 劣势:插入/查找效率略低于Dictionary(O(log n))
22. Array和ArrayList的区别
Array(数组)和ArrayList都是用于存储多个元素的集合,但存在以下关键区别:
特性 | Array | ArrayList |
---|---|---|
类型安全 | 强类型,只能存储声明类型的元素 | 弱类型,存储object类型,可混合不同类型 |
大小 | 固定大小,创建后不能改变 | 动态大小,自动扩容 |
泛型支持 | 非泛型(C# 2.0后有泛型数组) | 非泛型,始终存储object |
性能 | 更高,无需装箱拆箱(值类型) | 较低,值类型需要装箱拆箱 |
方法支持 | 基本方法(Length等) | 丰富的方法(Add、Remove、Sort等) |
索引器 | 支持 | 支持 |
示例代码:
// Array示例
int[] numbers = new int[3]; // 固定大小
numbers[0] = 1;
numbers[1] = 2;
// numbers[3] = 3; // 错误:超出数组范围// ArrayList示例
ArrayList arrayList = new ArrayList(); // 动态大小
arrayList.Add(1); // 装箱
arrayList.Add("hello"); // 可以添加不同类型
arrayList.Add(3.14);
int first = (int)arrayList[0]; // 拆箱
使用建议:
- 需要固定大小、强类型集合时用Array
- 需要动态大小且元素类型多样时用ArrayList(但建议优先用List)
- 避免在高性能场景使用ArrayList(因装箱拆箱开销)
23. List和ArrayList的区别
List和ArrayList都是动态大小的集合,但List是泛型实现,两者区别如下:
特性 | List | ArrayList |
---|---|---|
类型安全 | 强类型,只允许T类型元素 | 弱类型,允许任何类型(存储为object) |
装箱拆箱 | 不需要(值类型也无需转换) | 需要(值类型存储和读取时) |
性能 | 更高(避免类型转换开销) | 较低(装箱拆箱和类型检查) |
编译时检查 | 有,错误在编译时发现 | 无,错误可能在运行时才发现 |
命名空间 | System.Collections.Generic | System.Collections |
兼容性 | .NET 2.0+(泛型引入后) | .NET 1.0+ |
示例代码对比:
// List<T>示例(强类型)
List<int> intList = new List<int>();
intList.Add(10);
intList.Add(20);
// intList.Add("30"); // 编译错误:类型不匹配
int sum = intList[0] + intList[1]; // 无需类型转换// ArrayList示例(弱类型)
ArrayList arrayList = new ArrayList();
arrayList.Add(10); // 装箱
arrayList.Add("20"); // 允许不同类型
// 运行时错误(string不能转换为int)
// int sum = (int)arrayList[0] + (int)arrayList[1];
性能测试示例:
// 性能对比:添加100万整数
Stopwatch sw = new Stopwatch();// List<int>
sw.Start();
var list = new List<int>();
for (int i = 0; i < 1000000; i++)list.Add(i);
sw.Stop();
Console.WriteLine($"List耗时: {sw.ElapsedMilliseconds}ms");// ArrayList
sw.Restart();
var arrayList = new ArrayList();
for (int i = 0; i < 1000000; i++)arrayList.Add(i); // 产生装箱
sw.Stop();
Console.WriteLine($"ArrayList耗时: {sw.ElapsedMilliseconds}ms");
输出通常为:
List耗时: 15ms
ArrayList耗时: 45ms
结论:除非需要兼容旧代码或存储多种类型元素,否则应优先使用List。
24. Dictionary<TKey, TValue>的实现原理是什么?如何解决哈希冲突?
Dictionary<TKey, TValue> 是基于哈希表实现的键值对集合,其核心原理和哈希冲突解决机制如下:
实现原理:
- 内部结构:包含一个存储键值对的数组(
Entry[]
),每个元素是一个结构体,包含Key、Value、哈希码和下一个元素索引 - 哈希计算:对键进行哈希计算,得到哈希码
- 索引计算:通过哈希码和数组长度计算存储位置(索引)
- 存储数据:将键值对存储到计算出的索引位置
基本操作流程:
添加元素:
Key → 计算哈希码 → 计算索引 → 存储到数组对应位置查找元素:
Key → 计算哈希码 → 计算索引 → 从对应位置查找匹配的Key
哈希冲突:不同的Key可能计算出相同的索引,这种情况称为哈希冲突。Dictionary采用链地址法解决冲突:
- 每个数组位置(桶)可以形成一个链表
- 发生冲突时,新元素会被添加到对应桶的链表末尾
- 查找时,先定位到桶,再遍历链表查找匹配的Key
示意图:
数组索引: 0 1 2 3↓ ↓ ↓ ↓
元素: [Entry] → [Entry] [Entry]→ [Entry]
代码示例:
var dict = new Dictionary<string, int>();
dict.Add("apple", 1);
dict.Add("banana", 2);
dict.Add("cherry", 3);// 查找元素(内部经历哈希计算和可能的链表遍历)
if (dict.TryGetValue("banana", out int value))
{Console.WriteLine($"找到值: {value}");
}
冲突处理的影响:
- 少量冲突对性能影响不大
- 大量冲突会使查找退化为O(n)复杂度(类似链表)
- Dictionary会在负载因子(元素数/桶数)超过阈值(默认0.72)时自动扩容,减少冲突
关键优化:
- 扩容时创建更大的数组(通常是原大小的2倍)
- 重新计算所有元素的哈希和索引,分散存储
- 优质的哈希函数可减少冲突,提高性能
25. 什么是泛型?泛型的优点是什么?
泛型是允许在定义类、接口、方法时使用未指定的类型(类型参数),在使用时再指定具体类型的技术。
基本语法:
// 泛型类
public class GenericList<T>
{// 泛型方法public void Add(T item) { }// 泛型属性public T this[int index] { get; set; }
}
使用示例:
// 使用时指定具体类型
var intList = new GenericList<int>();
intList.Add(10);
int value = intList[0];var stringList = new GenericList<string>();
stringList.Add("hello");
泛型的主要优点:
-
类型安全
- 编译时检查类型,避免运行时类型转换错误
- 示例:
List<int>
只能添加int类型,编译时阻止添加string
-
消除装箱拆箱
- 值类型无需转换为object,提高性能
- 对比:
ArrayList
添加int会装箱,List<int>
不会
-
代码复用
- 一套代码支持多种数据类型,减少重复代码
- 示例:
List<T>
可用于int、string等任何类型
-
更好的性能
- 避免类型转换的性能开销
- 对于值类型集合,性能提升尤为明显
-
清晰的代码意图
- 明确指定集合或方法支持的类型
- 提高代码可读性和可维护性
泛型方法示例:
// 泛型方法:交换两个值
public static void Swap<T>(ref T a, ref T b)
{T temp = a;a = b;b = temp;
}// 使用
int x = 1, y = 2;
Swap(ref x, ref y); // 交换intstring s1 = "hello", s2 = "world";
Swap(ref s1, ref s2); // 交换string
总结:泛型是C#中非常重要的特性,通过类型参数化实现了类型安全、代码复用和性能优化,广泛应用于集合类、算法实现等场景。
26. 泛型约束有哪些类型?如何使用?
泛型约束用于限制类型参数可以接受的类型,确保类型参数具有特定的功能,语法为where 类型参数 : 约束
。
C#支持以下6种泛型约束:
-
值类型约束(struct)
- 限制类型参数必须是值类型(int、struct等)
public class NumericProcessor<T> where T : struct {// T必须是值类型 }
-
引用类型约束(class)
- 限制类型参数必须是引用类型(class、interface等)
public class ReferenceHandler<T> where T : class {// T必须是引用类型 }
-
无参数构造函数约束(new())
- 限制类型参数必须有公共无参数构造函数
- 必须放在约束列表的最后
public class ObjectCreator<T> where T : new() {public T Create() => new T(); // 可以安全地创建实例 }
-
基类约束
- 限制类型参数必须是指定基类或其派生类
public class AnimalHandler<T> where T : Animal {public void Feed(T animal){animal.Eat(); // 可以调用基类方法} }
-
接口约束
- 限制类型参数必须实现指定接口
public class SortableCollection<T> where T : IComparable<T> {public void Sort(T[] items){Array.Sort(items); // 依赖IComparable接口} }
-
另一个类型参数约束
- 限制一个类型参数必须是另一个类型参数的派生类
public class DerivedConstraint<T, U> where T : U {// T必须是U的派生类型 }
多重约束示例:
// 多重约束:T必须是引用类型、实现IDisposable、有默认构造函数
public class ResourceManager<T> where T : class, IDisposable, new()
{public T GetResource(){return new T(); // 因new()约束}public void ReleaseResource(T resource){resource.Dispose(); // 因IDisposable约束}
}
约束的作用:
- 使泛型代码可以安全地调用类型参数的方法和属性
- 缩小类型参数范围,提高类型安全性
- 编译器可以提供更好的类型检查和IntelliSense支持
注意:如果没有约束,泛型代码只能使用object
类的成员。
27. 什么是协变和逆变?在泛型中如何实现?
协变(Covariance) 和逆变(Contravariance) 是描述泛型类型参数在继承关系中的转换行为的概念,允许派生类型的泛型与基类型的泛型之间进行转换。
基本概念:
- 协变:允许从
Generic<Derived>
转换为Generic<Base>
(与继承方向相同) - 逆变:允许从
Generic<Base>
转换为Generic<Derived>
(与继承方向相反)
实现方式:
在泛型接口或委托中,使用out
关键字声明协变类型参数,使用in
关键字声明逆变类型参数。
协变示例(out关键字):
// 协变接口(out关键字)
public interface ICovariant<out T>
{T GetItem(); // T只能作为返回值
}public class CovariantImplementation<T> : ICovariant<T>
{public T GetItem() => default(T);
}// 继承关系
public class Animal { }
public class Dog : Animal { }// 使用协变
ICovariant<Dog> dogCovariant = new CovariantImplementation<Dog>();
ICovariant<Animal> animalCovariant = dogCovariant; // 允许转换(协变)
逆变示例(in关键字):
// 逆变接口(in关键字)
public interface IContravariant<in T>
{void SetItem(T item); // T只能作为参数
}public class ContravariantImplementation<T> : IContravariant<T>
{public void SetItem(T item) { }
}// 使用逆变
IContravariant<Animal> animalContravariant = new ContravariantImplementation<Animal>();
IContravariant<Dog> dogContravariant = animalContravariant; // 允许转换(逆变)
内置协变和逆变接口:
- 协变:
IEnumerable<out T>
、IQueryable<out T>
- 逆变:
IComparer<in T>
、IEqualityComparer<in T>
使用示例:
// 协变:IEnumerable<out T>
IEnumerable<Dog> dogs = new List<Dog>();
IEnumerable<Animal> animals = dogs; // 协变转换// 逆变:Action<in T>委托
Action<Animal> feedAnimal = (a) => { };
Action<Dog> feedDog = feedAnimal; // 逆变转换
限制条件:
- 协变(out):类型参数只能作为返回值或只读属性
- 逆变(in):类型参数只能作为方法参数
- 仅适用于接口和委托,类和结构不支持
- 值类型不参与协变和逆变(如
List<int>
不能转换为IEnumerable<object>
)
作用:提高泛型类型的灵活性,使泛型代码更好地支持多态。
28. IEnumerable和IEnumerator的区别
IEnumerable
和IEnumerator
是C#中用于支持迭代(遍历)集合的核心接口,两者分工不同但协同工作。
IEnumerable接口:
- 表示"可枚举的"集合,提供获取枚举器的方法
- 定义了一个方法:
GetEnumerator()
,返回IEnumerator
- 实现此接口的类可以使用
foreach
循环遍历
IEnumerator接口:
- 表示"枚举器",负责实际遍历集合的元素
- 定义了三个成员:
Current
:获取当前元素MoveNext()
:移动到下一个元素,返回是否成功Reset()
:重置枚举器到初始位置
工作流程:
foreach
循环调用集合的IEnumerable.GetEnumerator()
获取枚举器- 调用
IEnumerator.MoveNext()
移动到第一个元素 - 通过
IEnumerator.Current
访问当前元素 - 重复步骤2-3直到
MoveNext()
返回false - 自动释放枚举器(如果实现了
IDisposable
)
示例代码:
// 实现IEnumerable的集合
public class MyCollection : IEnumerable
{private int[] items = { 1, 2, 3, 4 };// 实现IEnumerable:返回枚举器public IEnumerator GetEnumerator(){return new MyEnumerator(items);}
}// 实现IEnumerator的枚举器
public class MyEnumerator : IEnumerator
{private int[] items;private int position = -1;public MyEnumerator(int[] items){this.items = items;}// 获取当前元素public object Current{get{if (position < 0 || position >= items.Length)throw new InvalidOperationException();return items[position];}}// 移动到下一个元素public bool MoveNext(){position++;return position < items.Length;}// 重置枚举器public void Reset(){position = -1;}
}// 使用示例
var collection = new MyCollection();
foreach (int item in collection) // 使用IEnumerable
{Console.WriteLine(item);
}
泛型版本:
IEnumerable<T>
:泛型可枚举接口,返回IEnumerator<T>
IEnumerator<T>
:泛型枚举器接口,Current
属性返回T类型- 泛型版本避免了装箱拆箱,更类型安全
区别总结:
IEnumerable
:定义集合"可被枚举"的能力,提供获取枚举器的入口IEnumerator
:实现实际的枚举逻辑,控制遍历过程- 关系:
IEnumerable
依赖IEnumerator
完成遍历
29. ICollection和IList接口的区别
ICollection
和IList
都是System.Collections命名空间中的核心接口,用于定义集合的功能,两者的继承关系和功能不同:
继承关系:
IList
继承自ICollection
和IEnumerable
ICollection
继承自IEnumerable
功能区别:
接口 | 核心功能 | 主要成员 | 适用场景 |
---|---|---|---|
ICollection | 定义基本的集合操作(计数、复制、同步) | Count、CopyTo()、IsSynchronized、SyncRoot | 只需要基本集合功能的场景 |
IList | 扩展ICollection,增加索引访问和列表特有操作 | 索引器、Add()、Remove()、Insert()、Contains()、IndexOf() | 需要通过索引访问、支持插入删除的有序集合 |
示例代码:
// ICollection示例
public void ProcessCollection(ICollection collection)
{Console.WriteLine($"集合有{collection.Count}个元素");// 复制到数组object[] array = new object[collection.Count];collection.CopyTo(array, 0);
}// IList示例
public void ProcessList(IList list)
{// IList继承了ICollection的功能Console.WriteLine($"列表有{list.Count}个元素");// IList特有的功能list.Add("new item"); // 添加元素list.Insert(0, "first"); // 插入元素int index = list.IndexOf("first"); // 查找索引object item = list[0]; // 索引访问list.RemoveAt(0); // 删除元素
}
泛型版本:
ICollection<T>
:泛型版本,增加了Add(T)
、Remove(T)
等强类型方法IList<T>
:泛型版本,提供强类型的索引器和列表操作
泛型IList示例:
public void ProcessGenericList(IList<string> list)
{list.Add("hello"); // 强类型添加,编译时检查string first = list[0]; // 无需类型转换bool contains = list.Contains("world");
}
使用建议:
- 当需要传递一个支持计数和复制的集合时,使用
ICollection
- 当需要索引访问或列表操作(插入、删除)时,使用
IList
- 优先使用泛型版本(
ICollection<T>
、IList<T>
)以获得类型安全和更好的性能
30. 什么是 HashSet?与 List 相比有什么优势?
HashSet<T>
是基于哈希表实现的泛型集合,用于存储不重复的元素,属于System.Collections.Generic
命名空间。
基本特性:
- 元素唯一(自动去重)
- 无序存储(不保证元素顺序)
- 基于哈希表,查找效率高
- 实现了
ICollection<T>
接口
基本用法:
var set = new HashSet<string>();// 添加元素(自动去重)
set.Add("apple");
set.Add("banana");
bool added = set.Add("apple"); // 返回false(已存在)// 检查元素是否存在
bool contains = set.Contains("banana");// 移除元素
set.Remove("banana");// 集合运算
var set1 = new HashSet<int> { 1, 2, 3 };
var set2 = new HashSet<int> { 3, 4, 5 };// 交集
set1.IntersectWith(set2); // set1现在包含{3}// 并集
set1.UnionWith(set2); // 包含{1,2,3,4,5}// 差集
set1.ExceptWith(set2); // 包含{1,2}
与List的优势对比:
-
元素唯一性
HashSet<T>
自动确保元素不重复,无需手动检查List<T>
需要手动调用Contains()
检查后再添加
-
查找性能
HashSet<T>.Contains()
:O(1)时间复杂度List<T>.Contains()
:O(n)时间复杂度- 大数据量时,HashSet优势明显
-
集合运算支持
HashSet<T>
内置交集、并集、差集等集合运算方法List<T>
需要手动实现这些功能
性能对比示例:
var list = new List<int>();
var hashSet = new HashSet<int>();// 填充数据
for (int i = 0; i < 10000; i++)
{list.Add(i);hashSet.Add(i);
}// 测试查找性能
Stopwatch sw = new Stopwatch();// List查找
sw.Start();
bool listContains = list.Contains(9999);
sw.Stop();
Console.WriteLine($"List查找耗时: {sw.ElapsedTicks}");// HashSet查找
sw.Restart();
bool setContains = hashSet.Contains(9999);
sw.Stop();
Console.WriteLine($"HashSet查找耗时: {sw.ElapsedTicks}");
典型输出:
List查找耗时: 1250
HashSet查找耗时: 10
适用场景:
- 需要存储唯一元素的场景
- 频繁检查元素是否存在的操作
- 需要进行集合运算(交集、并集等)
- 不关心元素顺序的情况
局限性:
- 不保证元素顺序,不能通过索引访问
- 元素必须正确实现
GetHashCode()
和Equals()
方法
HashSet<T>
和List<T>
都是常用的泛型集合,但各有侧重:当需要快速查找和去重时选择HashSet<T>
,当需要维护顺序和索引访问时选择List<T>
。
二、120道C#面试题目录列表
文章序号 | C#面试题120道 |
---|---|
1 | C#面试题及详细答案120道(01-10) |
2 | C#面试题及详细答案120道(11-20) |
3 | C#面试题及详细答案120道(21-30) |
4 | C#面试题及详细答案120道(31-40) |
5 | C#面试题及详细答案120道(41-50) |
6 | C#面试题及详细答案120道(51-60) |
7 | C#面试题及详细答案120道(61-75) |
8 | C#面试题及详细答案120道(76-85) |
9 | C#面试题及详细答案120道(86-95) |
10 | C#面试题及详细答案120道(96-105) |
11 | C#面试题及详细答案120道(106-115) |
12 | C#面试题及详细答案120道(116-120) |