MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的集合类型与泛型编程

2022-12-105.3k 阅读

C# 中的集合类型

在 C# 编程中,集合类型是非常重要的组成部分。它们为存储和管理一组相关的数据提供了便捷的方式。C# 提供了丰富的集合类型,每种类型都有其特定的用途和特点。

数组(Array)

数组是 C# 中最基本的集合类型之一。它是一种固定大小的数据结构,用于存储相同类型的元素。数组的元素可以通过索引来访问,索引从 0 开始。

以下是创建和使用数组的示例代码:

// 创建一个整数数组
int[] numbers = new int[5];
// 赋值
numbers[0] = 10;
numbers[1] = 20;
// 访问数组元素
int firstNumber = numbers[0];

// 另一种创建并初始化数组的方式
string[] names = { "Alice", "Bob", "Charlie" };

数组的优点在于其访问效率高,通过索引可以直接定位到特定元素。然而,数组的大小在创建后就固定了,不便于动态增加或减少元素。

列表(List)

List<T> 是一个泛型集合类型,它提供了可动态调整大小的数组功能。T 表示列表中元素的类型。

示例代码如下:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 创建一个整数列表
        List<int> numberList = new List<int>();
        // 添加元素
        numberList.Add(1);
        numberList.Add(2);
        // 插入元素到指定位置
        numberList.Insert(1, 10);

        // 通过索引访问元素
        int firstElement = numberList[0];

        // 删除元素
        numberList.Remove(2);

        // 遍历列表
        foreach (int num in numberList)
        {
            Console.WriteLine(num);
        }
    }
}

List<T> 的优点在于其灵活性,可以动态添加、删除和插入元素。它在需要频繁修改集合大小的场景中非常实用。

字典(Dictionary<TKey, TValue>)

Dictionary<TKey, TValue> 是一种键值对集合类型。每个元素由一个唯一的键和与之关联的值组成。

示例代码如下:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 创建一个字典,键为字符串,值为整数
        Dictionary<string, int> ages = new Dictionary<string, int>();
        // 添加键值对
        ages.Add("Alice", 25);
        ages.Add("Bob", 30);

        // 通过键访问值
        int aliceAge = ages["Alice"];

        // 修改值
        ages["Bob"] = 31;

        // 检查键是否存在
        bool hasCharlie = ages.ContainsKey("Charlie");

        // 遍历字典
        foreach (KeyValuePair<string, int> pair in ages)
        {
            Console.WriteLine($"Name: {pair.Key}, Age: {pair.Value}");
        }
    }
}

字典的优点在于其查找效率高,通过键可以快速定位到对应的值。但需要注意的是,键必须是唯一的。

哈希集(HashSet)

HashSet<T> 是一种集合类型,它不允许包含重复的元素。它基于哈希表实现,具有快速的查找和插入性能。

示例代码如下:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 创建一个哈希集
        HashSet<int> numbers = new HashSet<int>();
        // 添加元素
        numbers.Add(1);
        numbers.Add(2);
        // 尝试添加重复元素
        numbers.Add(1);

        // 检查元素是否存在
        bool hasThree = numbers.Contains(3);

        // 遍历哈希集
        foreach (int num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}

哈希集适用于需要确保元素唯一性的场景,例如去重操作。

队列(Queue)

Queue<T> 是一种先进先出(FIFO)的集合类型。元素从队列的尾部插入,从头部移除。

示例代码如下:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 创建一个队列
        Queue<string> messages = new Queue<string>();
        // 入队操作
        messages.Enqueue("Message 1");
        messages.Enqueue("Message 2");

        // 出队操作
        string firstMessage = messages.Dequeue();

        // 查看队首元素但不移除
        string peekMessage = messages.Peek();

        // 遍历队列
        foreach (string msg in messages)
        {
            Console.WriteLine(msg);
        }
    }
}

队列常用于需要按顺序处理元素的场景,如任务队列等。

栈(Stack)

Stack<T> 是一种后进先出(LIFO)的集合类型。元素从栈顶插入和移除。

示例代码如下:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 创建一个栈
        Stack<int> numbers = new Stack<int>();
        // 入栈操作
        numbers.Push(1);
        numbers.Push(2);

        // 出栈操作
        int topNumber = numbers.Pop();

        // 查看栈顶元素但不移除
        int peekNumber = numbers.Peek();

        // 遍历栈
        foreach (int num in numbers)
        {
            Console.WriteLine(num);
        }
    }
}

栈常用于需要按照逆序处理元素的场景,如表达式求值等。

泛型编程

泛型编程是 C# 中的一项强大特性,它允许我们编写可以处理不同类型数据的通用代码。通过泛型,我们可以提高代码的复用性、类型安全性和性能。

泛型类型参数

在泛型编程中,我们使用类型参数来表示不同的数据类型。类型参数通常用大写字母表示,如 TUKV 等。

以下是一个简单的泛型类示例:

public class Box<T>
{
    private T value;

    public void SetValue(T newValue)
    {
        value = newValue;
    }

    public T GetValue()
    {
        return value;
    }
}

在上述代码中,Box<T> 是一个泛型类,T 是类型参数。我们可以使用不同的类型来实例化 Box<T> 类。

示例如下:

class Program
{
    static void Main()
    {
        // 创建一个存储整数的 Box
        Box<int> intBox = new Box<int>();
        intBox.SetValue(10);
        int result = intBox.GetValue();

        // 创建一个存储字符串的 Box
        Box<string> stringBox = new Box<string>();
        stringBox.SetValue("Hello");
        string strResult = stringBox.GetValue();
    }
}

通过这种方式,我们只需要编写一次 Box<T> 类,就可以处理不同类型的数据,大大提高了代码的复用性。

泛型方法

除了泛型类,我们还可以定义泛型方法。泛型方法允许我们在方法级别使用类型参数。

以下是一个交换两个值的泛型方法示例:

public static class Utility
{
    public static void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
}

在上述代码中,Swap<T> 是一个泛型方法,T 是类型参数。ref 关键字用于表示参数是按引用传递的。

示例如下:

class Program
{
    static void Main()
    {
        int num1 = 5;
        int num2 = 10;
        Utility.Swap(ref num1, ref num2);

        string str1 = "Hello";
        string str2 = "World";
        Utility.Swap(ref str1, ref str2);
    }
}

泛型方法的优点在于它可以在不创建泛型类的情况下,为不同类型的数据提供通用的操作。

泛型约束

泛型约束用于限制类型参数的类型。通过泛型约束,我们可以确保类型参数具有某些特定的行为或特征。

常见的泛型约束有以下几种:

引用类型约束(where T : class)

该约束表示类型参数 T 必须是引用类型。

示例如下:

public class ReferenceTypeProcessor<T> where T : class
{
    public void Process(T obj)
    {
        // 可以对引用类型进行操作,如调用方法
        if (obj != null)
        {
            // 假设 T 类型有一个 ToString 方法
            string result = obj.ToString();
            Console.WriteLine(result);
        }
    }
}

值类型约束(where T : struct)

该约束表示类型参数 T 必须是值类型。

示例如下:

public class ValueTypeProcessor<T> where T : struct
{
    public void Process(T value)
    {
        // 可以对值类型进行操作
        Console.WriteLine($"Value: {value}");
    }
}

无参数构造函数约束(where T : new())

该约束表示类型参数 T 必须具有一个无参数的构造函数。

示例如下:

public class ConstructorProcessor<T> where T : new()
{
    public T CreateInstance()
    {
        return new T();
    }
}

继承约束(where T : BaseClass)

该约束表示类型参数 T 必须是指定基类或其子类。

示例如下:

public class Animal { }
public class Dog : Animal { }

public class AnimalProcessor<T> where T : Animal
{
    public void Process(T animal)
    {
        // 可以对 Animal 类型及其子类进行操作
        Console.WriteLine($"Processing {animal.GetType().Name}");
    }
}

接口约束(where T : IInterface)

该约束表示类型参数 T 必须实现指定的接口。

示例如下:

public interface IPrintable
{
    void Print();
}

public class Printer<T> where T : IPrintable
{
    public void PrintAll(T[] items)
    {
        foreach (T item in items)
        {
            item.Print();
        }
    }
}

泛型集合与性能

使用泛型集合(如 List<T>Dictionary<TKey, TValue> 等)在性能上通常比使用非泛型集合更好。这是因为泛型集合避免了装箱和拆箱操作。

装箱是将值类型转换为引用类型的过程,而拆箱则是将引用类型转换回值类型的过程。在非泛型集合中,由于所有元素都被存储为 object 类型(引用类型),当存储值类型时会发生装箱操作,当取出值类型时会发生拆箱操作,这会带来额外的性能开销。

例如,使用非泛型的 ArrayList 存储整数时:

using System;
using System.Collections;

class Program
{
    static void Main()
    {
        ArrayList list = new ArrayList();
        list.Add(10); // 装箱操作
        int value = (int)list[0]; // 拆箱操作
    }
}

而使用泛型的 List<int> 则不会发生装箱和拆箱操作:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<int> list = new List<int>();
        list.Add(10);
        int value = list[0];
    }
}

因此,在编写代码时,优先选择泛型集合可以提高程序的性能。

泛型与类型擦除

在 C# 中,泛型是在编译时实现的,而不是在运行时。这意味着在运行时,泛型类型的实际类型信息已经被擦除。

例如,对于 List<int>List<string>,在运行时它们的底层实现是相同的,只是在编译时根据类型参数进行了不同的处理。

这种机制使得泛型在保证类型安全性的同时,不会增加太多运行时的开销。然而,这也带来了一些限制,比如在运行时无法直接获取泛型类型参数的实际类型信息(除非使用反射)。

泛型的实际应用场景

  1. 数据结构与算法:如前面提到的各种泛型集合类型,它们是实现常用数据结构(如列表、字典、栈、队列等)的基础。在算法实现中,泛型可以使算法适用于不同类型的数据,提高算法的通用性。
  2. 框架与库开发:在开发框架和库时,泛型可以提供高度可复用的组件。例如,ASP.NET Core 框架中的许多组件都使用了泛型,以支持不同类型的应用场景。
  3. 代码复用:通过泛型类和泛型方法,我们可以编写一次代码,然后在不同的类型上复用,减少代码冗余。

集合类型与泛型的结合使用

在实际编程中,集合类型和泛型通常会结合使用,以发挥最大的优势。

例如,我们可以创建一个泛型字典,其中键和值的类型都可以根据实际需求进行指定:

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 创建一个泛型字典,键为字符串,值为自定义类型
        Dictionary<string, MyClass> myDict = new Dictionary<string, MyClass>();
        MyClass obj1 = new MyClass { Name = "Object 1" };
        myDict.Add("key1", obj1);

        // 遍历字典
        foreach (KeyValuePair<string, MyClass> pair in myDict)
        {
            Console.WriteLine($"Key: {pair.Key}, Value: {pair.Value.Name}");
        }
    }
}

public class MyClass
{
    public string Name { get; set; }
}

在这个例子中,我们使用泛型字典 Dictionary<string, MyClass> 来存储键值对,其中值的类型是自定义的 MyClass。这种结合方式使得我们可以灵活地存储和管理不同类型的数据。

又如,我们可以创建一个泛型列表,用于存储不同类型的元素,并且可以对列表中的元素进行通用的操作。

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 创建一个泛型列表
        List<MyBaseClass> list = new List<MyBaseClass>();
        MyDerivedClass1 obj1 = new MyDerivedClass1 { Name = "Derived 1" };
        MyDerivedClass2 obj2 = new MyDerivedClass2 { Value = 10 };
        list.Add(obj1);
        list.Add(obj2);

        // 遍历列表并调用公共方法
        foreach (MyBaseClass obj in list)
        {
            obj.PrintInfo();
        }
    }
}

public class MyBaseClass
{
    public virtual void PrintInfo()
    {
        Console.WriteLine("Base class info");
    }
}

public class MyDerivedClass1 : MyBaseClass
{
    public string Name { get; set; }
    public override void PrintInfo()
    {
        Console.WriteLine($"Derived 1: Name = {Name}");
    }
}

public class MyDerivedClass2 : MyBaseClass
{
    public int Value { get; set; }
    public override void PrintInfo()
    {
        Console.WriteLine($"Derived 2: Value = {Value}");
    }
}

在这个例子中,我们创建了一个 List<MyBaseClass>,它可以存储 MyBaseClass 及其子类的对象。通过这种方式,我们可以利用多态性对列表中的不同类型对象进行统一的操作。

总结集合类型与泛型编程的要点

  1. 集合类型的选择:根据实际需求选择合适的集合类型。如果需要固定大小且高效的索引访问,可选择数组;如果需要动态调整大小,可选择 List<T>;如果需要通过键查找值,可选择 Dictionary<TKey, TValue>;如果需要确保元素唯一性,可选择 HashSet<T>;如果需要先进先出的顺序,可选择 Queue<T>;如果需要后进先出的顺序,可选择 Stack<T>
  2. 泛型的优势:泛型提供了代码复用、类型安全和性能优化的功能。通过使用泛型类、泛型方法和泛型约束,可以编写更通用、更健壮的代码。
  3. 结合使用:将集合类型与泛型结合使用,可以进一步提高代码的灵活性和可维护性。在实际编程中,充分利用这两者的特性,能够更好地解决各种数据存储和处理的问题。

通过深入理解 C# 中的集合类型与泛型编程,开发者可以编写出更高效、更灵活、更易于维护的代码,从而提升整个项目的质量和开发效率。无论是小型应用程序还是大型企业级项目,这些知识都是至关重要的。希望本文所介绍的内容能够帮助读者在 C# 编程中更好地运用集合类型和泛型编程技术。