在C++基础上理解CSharp-6

在C++基础上理解CSharp-6 1本篇学习目标理解泛型的核心思想掌握 C# 泛型的基本语法彻底搞懂 C# 泛型与 C 模板的本质区别消除认知误区掌握泛型类、泛型方法的定义与使用熟练运用 6 种泛型约束理解每种约束的作用和适用场景掌握ListT、DictionaryTKey, TValue两大核心泛型集合的常用操作了解QueueT、StackT、HashSetT等常用泛型集合的用法所有知识点均与 C 模板、STL 容器进行深度对比2泛型概述泛型Generics是一种允许我们在定义类、方法、接口时不指定具体类型而是在使用时再指定类型的技术。泛型的核心价值是类型安全 代码复用。1为什么需要泛型在没有泛型的时代如果我们想实现一个通用的集合只能使用object类型public class ArrayList { private object[] _items; public void Add(object item) { ... } public object this[int index] { get { ... } } }这种方式有两个严重问题类型不安全可以向集合中添加任何类型的对象取出时需要强制类型转换容易出现运行时异常性能损耗值类型存入时需要装箱取出时需要拆箱带来显著的性能开销泛型完美解决了这两个问题public class ListT { private T[] _items; public void Add(T item) { ... } public T this[int index] { get { ... } } }使用泛型后Listint只能存储int类型Liststring只能存储string类型编译时就能保证类型安全也不需要装箱拆箱。2C#泛型与C模板的本质区别这是 C 开发者最容易误解的知识点两者虽然语法相似但底层实现机制完全不同对比维度C# 泛型C 模板实例化时机运行时由 CLR 完成编译时由编译器完成代码生成方式所有引用类型共享同一份代码值类型各自生成独立代码每个类型都生成独立的代码模板展开类型检查编译时对泛型约束进行检查运行时保证类型安全编译时对每个具体类型进行检查错误信息晦涩约束系统有明确的where约束语法约束类型的能力C20 之前只有隐式约束C20 引入 Concepts泛型参数只能是类型可以是类型、整数、指针等特化不支持模板特化支持完全特化和偏特化核心结论C 模板本质是代码生成器编译时为每个类型生成一份独立的代码C# 泛型本质是运行时类型系统由 CLR 在运行时处理更轻量、更类型安全3泛型类泛型类是最常用的泛型形式在类名后使用尖括号包裹类型参数。1定义一个泛型类// 定义一个泛型栈类 public class MyStackT { private T[] _items; private int _count; public MyStack(int capacity 10) { _items new T[capacity]; _count 0; } public void Push(T item) { if (_count _items.Length) { Array.Resize(ref _items, _items.Length * 2); } _items[_count] item; } public T Pop() { if (_count 0) { throw new InvalidOperationException(栈为空); } T item _items[--_count]; _items[_count] default(T); // 清空引用帮助GC回收 return item; } public T Peek() { if (_count 0) { throw new InvalidOperationException(栈为空); } return _items[_count - 1]; } public int Count _count; }2使用泛型类// 使用int类型的栈 MyStackint intStack new MyStackint(); intStack.Push(1); intStack.Push(2); intStack.Push(3); Console.WriteLine(intStack.Pop()); // 输出 3 Console.WriteLine(intStack.Peek()); // 输出 2 // 使用string类型的栈 MyStackstring stringStack new MyStackstring(); stringStack.Push(Hello); stringStack.Push(World); Console.WriteLine(stringStack.Pop()); // 输出 World3多个类型参数泛型类可以有多个类型参数用逗号分隔// 定义一个泛型键值对类 public class MyPairTKey, TValue { public TKey Key { get; set; } public TValue Value { get; set; } public MyPair(TKey key, TValue value) { Key key; Value value; } public override string ToString() { return $[{Key}, {Value}]; } } // 使用 MyPairint, string pair new MyPairint, string(1, 张三); Console.WriteLine(pair); // 输出 [1, 张三]4泛型方法方法也可以是泛型的即使所在的类不是泛型类。1定义泛型方法public static class ArrayUtils { // 泛型方法交换数组中两个元素的位置 public static void SwapT(T[] array, int index1, int index2) { T temp array[index1]; array[index1] array[index2]; array[index2] temp; } // 泛型方法查找数组中元素的索引 public static int IndexOfT(T[] array, T item) { for (int i 0; i array.Length; i) { if (EqualityComparerT.Default.Equals(array[i], item)) { return i; } } return -1; } }2调用泛型方法int[] numbers { 1, 2, 3, 4, 5 }; ArrayUtils.Swap(numbers, 0, 4); // 现在数组变成 {5, 2, 3, 4, 1} int index ArrayUtils.IndexOf(numbers, 3); Console.WriteLine(index); // 输出 2类型推断调用泛型方法时编译器通常可以根据参数自动推断出类型参数不需要显式指定ArrayUtils.Swapint(numbers, 0, 4); // 显式指定类型 ArrayUtils.Swap(numbers, 0, 4); // 编译器自动推断类型推荐写法5泛型约束默认情况下泛型类型参数T可以是任何类型这意味着我们只能对T执行object类拥有的操作如ToString()、Equals()等。如果我们需要对T执行更多操作就需要使用泛型约束来限制类型参数的范围。C# 使用where关键字指定约束。1六种泛型约束约束作用对应 C 概念where T : structT 必须是值类型类似std::is_arithmetic_vTwhere T : classT 必须是引用类型类似std::is_class_vTwhere T : new()T 必须有公共的无参数构造函数类似std::is_default_constructible_vTwhere T : 基类名T 必须继承自指定的基类类似std::is_base_of_vBase, Twhere T : 接口名T 必须实现指定的接口类似 Concepts 中的接口约束where T : UT 必须继承自另一个类型参数 U偏特化的一种形式2约束使用示例值类型约束public class NumberCalculatorT where T : struct { public T Add(T a, T b) { // 注意即使有struct约束也不能直接使用运算符 // 需要借助泛型数学或动态方法C# 11引入了INumberT接口解决此问题 return (T)((dynamic)a (dynamic)b); } }引用类型约束public class ReferenceTypeContainerT where T : class { private T _item; public void SetItem(T item) { _item item; } public bool IsNull() { return _item null; // 有class约束才能用null比较 } }构造函数约束public class FactoryT where T : new() { public T CreateInstance() { return new T(); // 有new()约束才能new T() } }基类约束public class AnimalContainerT where T : Animal { private ListT _animals new ListT(); public void Add(T animal) { _animals.Add(animal); } public void MakeAllSound() { foreach (T animal in _animals) { animal.MakeSound(); // 有基类约束才能调用基类方法 } } }接口约束public class ComparableUtilsT where T : IComparableT { public static T Max(T a, T b) { return a.CompareTo(b) 0 ? a : b; // 有接口约束才能调用CompareTo } public static T Min(T a, T b) { return a.CompareTo(b) 0 ? a : b; } }3组合约束可以将多个约束组合在一起使用public class MyContainerT where T : class, IDisposable, new() { // T必须是引用类型实现IDisposable接口且有公共无参构造函数 }注意多个约束有顺序要求主约束class/struct/ 基类必须放在最前面接口约束放在中间new()构造函数约束必须放在最后6总结本来想将常用的泛型集合的但是想了想还是单开一个专题最后讲吧。在这篇博客中我们系统学习了 C# 的泛型系统和常用泛型集合。对于 C 开发者来说核心是理解C# 泛型是运行时类型系统不是编译时代码展开泛型约束是保证类型安全的关键比 C 早期的隐式约束更清晰泛型集合替代了早期的非泛型集合既类型安全又高性能