CodeGym /课程 /C# SELF /Generics集合的优势

Generics集合的优势

C# SELF
第 27 级 , 课程 1
可用

1. 简单历史背景

在C#的远古时代,玩集合只有两种乐趣:用数组,或者用那种“类型不深”的集合——比如ArrayList,啥类型都能往里塞。看起来很自由对吧?但你要是把字符串和整数混着加进去——那就乱套了:你没法安全地把元素拿出来,除非加一堆(还不一定靠谱的)类型检查。还有——各种莫名其妙的bug和类型转换(casting)舞蹈。

非generic集合的例子(ArrayList):


using System.Collections;

ArrayList stuff = new ArrayList();
stuff.Add(42);
stuff.Add("你好 C#");

int number = (int)stuff[0]; // OK
string text = (string)stuff[1]; // OK

int fail = (int)stuff[1]; // 崩了!InvalidCastException(运行时错误)

对,编译器不报错——只有运行的时候才炸!就像你打开冰箱,发现里面有个热锅——惊喜!

泛型的本质是啥?

泛型集合(Generics)就是为了让你能创建有类型保证的集合。它有三个核心好处:

  • 编译时类型安全。 编译器不会让你乱塞东西进集合。
  • 方便: 不用每次都手动类型转换。
  • 性能: 不用浪费时间做“装箱和拆箱”boxing/unboxing)。

2. Generics(泛型)到底是啥?

Generics——就是一种神奇的方式,让你能描述数据结构和方法,它们能和任何类型一起用,但依然是强类型的

想象一下有个万能箱子,可以装书也能装袜子,但一次只能装一种类型。如果箱子声明成“只装书”——谁也别想往里塞袜子。generic集合也是这样:你要是创建了int的集合,string就绝对进不去。

泛型类的声明例子


public class Box<T>
{
    public T Value { get; set; }
}

var boxOfInt = new Box<int> { Value = 42 };
var boxOfString = new Box<string> { Value = "你好 Generics!" };

T——就是“类型参数”,你得告诉箱子要装啥。C#会强制你明确指定类型。

.NET里的泛型集合

在.NET Framework里,几乎所有现代集合都有泛型版本。比如:

  • List<T>——动态元素列表,类型是T
  • Dictionary<TKey, TValue>——带键值对的字典。
  • Queue<T>Stack<T>——队列和栈。
  • 还有很多别的。

3. 工作原理:Generics的机制

类型参数

当你声明一个像List<int>这样的集合时,C#编译器会专门为int类型生成一个版本(specialization)。你要是声明List<string>,编译器又会为字符串生成一个版本,依此类推。

对你来说,这就像魔法一样:


List<int> numbers = new List<int>();
numbers.Add(1); // 只能加int

List<string> words = new List<string>();
words.Add("hello"); // 只能加string

你要是想加别的类型,编译器立马不乐意:


numbers.Add("fail"); // 编译时就报错!

类型安全:编译时vs运行时

这就意味着,上面那种错误根本不会等到程序运行才爆雷(run-time)。你的集合安全得很,编译器就像门口的地狱三头犬,谁都别想乱来。

底层原理

  • 对于引用类型(比如stringobject),再也不用多余的类型转换了。
  • 对于值类型(intdouble),“装箱/拆箱”(boxing/unboxing)的问题没了。
  • 在.NET里用generics不会导致代码膨胀——CLR会在JIT编译时优化。

4. Generics怎么提升性能

再来强调下Generics给代码带来的神奇属性:

类型安全(Type Safety):

这绝对是最重要的。有了Generics,编译器就像你的贴身保镖,绝不让“外人”混进你的集合。你可以100%放心,List<Product>里只会有Product对象,不会混进什么奇怪的字符串或数字。这直接消灭了一大堆以前只有运行时才会爆的bug(比如InvalidCastException)。你的代码更靠谱、更可预期。

性能(Performance):

就像前面说的,对于值类型(比如intdoubleDateTime),Generics让内存和CPU用得更高效。要是你的集合有几百万个元素或者经常改动,这点就特别关键。你不用再把橙子从箱子倒进袋子再倒回来,直接放进对的箱子就行。

代码复用(Code Reusability):

Generics让你写一次代码,能搞定各种类型,不用重复造轮子。比如你要写个交换两个变量的函数。没Generics你得写SwapInt(ref int a, ref int b)SwapString(ref string a, ref string b)SwapProduct(ref Product a, ref Product b),等等。有了Generics你只要写一个:Swap<T>(ref T a, ref T b)。集合也是一样:不用IntListStringListProductList,只要List<T>就够了。这样代码更简洁、好维护、易扩展。

5. 泛型方法和自定义generic类

泛型方法

Generics不只是集合!你可以写自己的泛型方法,啥类型都能用,让代码更通用。


//类型参数T写在方法名后面
public static void Swap<T>(ref T x, ref T y)
{
    T temp = x;
    x = y;
    y = temp;
}

// 用法:	
int a = 10, b = 20;
Swap(ref a, ref b); // 现在a == 20, b == 10

string one = "一", two = "二";
Swap(ref one, ref two); // 字符串也能用!

注意,C#编译器能根据你传的变量类型自动推断方法的类型参数。所以上面例子里,Swap方法不用额外指定类型。

自定义generic类

你甚至可以自己造“箱子”(generic类)。其实很简单:


public class Pair<TFirst, TSecond>
{
    public TFirst First { get; set; }
    public TSecond Second { get; set; }
}

// 用法:
var pair = new Pair<int, string> { First = 42, Second = "答案" };

6. 泛型集合的使用例子

咱们回到之前讲过的“任务清单”(To-Do List)。以前我们用数组存任务,或者直接打印出来。现在——用List<string>动态存。

例子:动态添加任务到列表


using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List<string> tasks = new List<string>();

        Console.WriteLine("请输入任务(留空回车结束):");
        string input;
        while (!string.IsNullOrWhiteSpace(input = Console.ReadLine()))
        {
            tasks.Add(input);
            Console.WriteLine("任务已添加!继续输入(留空回车结束):");
        }

        Console.WriteLine("\n你今天的任务清单:");
        foreach (string task in tasks)
        {
            Console.WriteLine("- " + task);
        }
    }
}

这里有啥好处?

  • 集合会随着你加任务自动扩容。
  • 你不可能误加非字符串进列表。
  • 可以很方便地遍历和输出内容。

7. 新手常见错误、注意点和建议

从非generic集合转到generic,有时候会有点懵。下面是新手常遇到的几种情况:

  • 想加错类型的元素(List<int> numbers = new List<int>(); numbers.Add("hi"); // 报错)。
  • 想在一个集合里混合多种类型——这时候你得用基类(比如List<object>)——但之后每次都得手动转回需要的类型。
  • 忘了有constraints这种东西,写得太“泛”,结果编译时遇到奇怪的错误。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION