C#进阶学习(二)泛型及泛型约束的认识
目录
一、什么是泛型
二、泛型有什么作用
三、有哪些泛型体现和怎么实现泛型
1、泛型类声明
2、泛型接口声明
3、3-1泛型类中的泛型方法
3-2、普通类中的泛型方法
4. 多泛型占位符的复杂示例
泛型类 Tuple 的声明与使用
四、关于泛型类和泛型接口的重要性说明
1. 泛型类的作用
尝试在普通类中用泛型方法实现
关键区别:泛型类 vs 普通类中的泛型方法
2. 泛型接口的作用
五、泛型约束
5-1 什么是泛型约束
5-2 为什么需要泛型约束
5-3 泛型约束有哪些
5-4各种泛型类型的使用
1. 值类型约束(where T : struct)
2. 引用类型约束(where T : class)
3. 无参构造函数约束(where T : new())
4. 基类约束(where T : BaseClass)
5. 接口约束(where T : IComparable )
6. 类型参数约束(where T : U)
7. 多约束组合
5-5总结
一、什么是泛型
泛型(Generics) 是 C# 中的一种编程机制,允许在定义类、方法、接口或委托时使用类型参数(Type Parameters),而不是具体的类型。实际使用时,再指定具体的类型。例如,List<T>
中的 T
是类型参数,使用时可指定为 int
、string
等。
-
核心思想:延迟类型的指定,直到代码被使用。
-
本质:通过参数化类型实现代码的通用性,同时保证类型安全。
泛型实现了类型参数化,达到代码重用的目的
通过类型参数化来实现同一份代码上操作多种数据类型泛型相当于类型占位符
定义类或方法时使用代替符代表变量类型
当真正使用类或者方法时,再指定具体的类型
例如我们上一篇文章中提到的基本数据结构stack 以及queue其底层实际都是object数组,使用的时候存在装箱拆箱的消耗,所以请见如下示例代码。直接在使用的时候就确定了,该数据容器装载什么样的数据
非泛型数据结构:
// 非泛型 Stack
Stack stack = new Stack();
stack.Push(100); // int 装箱为 object
stack.Push("hello"); // string 转为 object
int num = (int)stack.Pop(); // 需要显式拆箱和强制类型转换
string str = (string)stack.Pop();
泛型数据结构:
Stack<int> intStack = new Stack<int>();
intStack.Push(100); // 直接存储 int,无需装箱
int num = intStack.Pop(); // 直接获取 int,无需拆箱
Stack<string> stringStack = new Stack<string>();
stringStack.Push("hello"); // 仅允许存储 string
string str = stringStack.Pop();
这样使用起来,就更加的方便和代码可读。
二、泛型有什么作用
不同类型的对象的相同处理逻辑可以选择泛型
使用泛型可以一定程度上避免装箱和拆箱
-
类型安全
编译器在编译时检查类型,避免运行时类型错误(如ArrayList
存储不同类型时的强制转换错误)。前面我们说过,ArrayList什么都可以装,但是当你使用的时候,你不一定记得自己装过些什么,随便取出来使用,就容易发生类型转换错误,导致编译失败。 -
性能优化
避免值类型的装箱(Boxing)和拆箱(Unboxing)。例如,List<int>
直接操作值类型,而ArrayList
需要将int
装箱为object
。 -
代码复用
一份泛型代码可处理多种类型,减少重复代码。例如,Sort<T>
方法可为int
、double
、string
等排序。 -
可读性与可维护性
代码逻辑与类型解耦,增强扩展性。
三、有哪些泛型体现和怎么实现泛型
泛型类和泛型接口
基本语法:
class 类名<泛型占位字母>
interface 接口名<泛型占位字母>泛型方法
基本语法:函数名<泛型占位字母>(参数列表)泛型占位字母可以有多个,用逗号隔开
1、泛型类声明
// 单个泛型占位符
public class GenericClass<T>
{
public T Value { get; set; }
}
// 多个泛型占位符
public class Pair<T1, T2>
{
public T1 First { get; set; }
public T2 Second { get; set; }
}
使用说明:
单个泛型字符使用:
// 声明一个存储 int 的泛型类实例
GenericClass<int> intContainer = new GenericClass<int>();
intContainer.Value = 100;
Console.WriteLine(intContainer.Value); // 输出: 100
// 声明一个存储 string 的泛型类实例
GenericClass<string> stringContainer = new GenericClass<string>();
stringContainer.Value = "Hello";
Console.WriteLine(stringContainer.Value); // 输出: Hello
多个泛型字符示例:
// 存储 int 和 string 的键值对
Pair<int, string> idNamePair = new Pair<int, string>
{
First = 1,
Second = "Alice"
};
Console.WriteLine($"{idNamePair.First}: {idNamePair.Second}"); // 输出: 1: Alice
// 存储两个不同类型的值
Pair<double, bool> priceStatusPair = new Pair<double, bool>
{
First = 99.99,
Second = true
};
Console.WriteLine($"Price: {priceStatusPair.First}, Status: {priceStatusPair.Second}");
// 输出: Price: 99.99, Status: True
2、泛型接口声明
// 单个泛型占位符
public interface IGenericInterface<T>
{
void Process(T item);
}
// 多个泛型占位符
public interface IKeyValueStore<TKey, TValue>
{
void Add(TKey key, TValue value);
TValue Get(TKey key);
}
使用说明:
// 实现泛型接口
public class DictionaryStore<TKey, TValue> : IKeyValueStore<TKey, TValue>
{
private Dictionary<TKey, TValue> _dictionary = new Dictionary<TKey, TValue>();
public void Add(TKey key, TValue value)
{
_dictionary[key] = value;
}
public TValue Get(TKey key)
{
return _dictionary[key];
}
}
// 使用
var nameStore = new DictionaryStore<int, string>();
nameStore.Add(1, "Bob");
string name = nameStore.Get(1); // 返回 "Bob"
这里列举了一个字典的使用,能看懂就看懂,不明白我们后面学习了字典再回来看
3、3-1泛型类中的泛型方法
public class Processor<T>
{
// 泛型方法:方法自身定义泛型参数 U(与类的 T 不同)
public void Process<U>(T item1, U item2)
{
Console.WriteLine($"Class Type: {typeof(T)}, Method Type: {typeof(U)}");
Console.WriteLine($"Item1: {item1}, Item2: {item2}");
}
}
// 使用
var processor = new Processor<int>();
processor.Process<string>(10, "Hello");
// 输出:
// Class Type: System.Int32, Method Type: System.String
// Item1: 10, Item2: Hello
3-2、普通类中的泛型方法
public class Utility
{
// 泛型方法:交换两个变量的值
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
// 多泛型占位符方法:合并两个不同类型的值
public static Tuple<T1, T2> Merge<T1, T2>(T1 item1, T2 item2)
{
return new Tuple<T1, T2>(item1, item2);
}
}
// 使用
int x = 10, y = 20;
Utility.Swap(ref x, ref y);
Console.WriteLine($"x: {x}, y: {y}"); // 输出: x: 20, y: 10
var merged = Utility.Merge(42, "Answer");
Console.WriteLine(merged.Item1 + ": " + merged.Item2); // 输出: 42: Answer
对于这里元组的解释:
元组是 C# 中的一种轻量级数据结构,可以临时存储多个不同类型的值,类似于“组合打包”。
一些简单的语法:
传统元组(System.Tuple):
// 使用 System.Tuple(属性是只读的)
Tuple<int, string> tuple = Tuple.Create(42, "Answer");
Console.WriteLine(tuple.Item1); // 输出 42
Console.WriteLine(tuple.Item2); // 输出 Answer
新元组语法(C# 7.0+):
// 使用 ValueTuple(属性可写)
(int Number, string Text) tuple = (42, "Answer");
Console.WriteLine(tuple.Number); // 输出 42
Console.WriteLine(tuple.Text); // 输出 Answer
在调用泛型方法时,如果未显式指定泛型参数类型,编译器会根据传入的参数类型自动推断泛型类型。
4. 多泛型占位符的复杂示例
泛型类 Tuple<T1, T2, T3>
的声明与使用
// 定义三元组(三个泛型占位符)
public class Tuple<T1, T2, T3>
{
public T1 Item1 { get; set; }
public T2 Item2 { get; set; }
public T3 Item3 { get; set; }
public Tuple(T1 item1, T2 item2, T3 item3)
{
Item1 = item1;
Item2 = item2;
Item3 = item3;
}
}
// 使用
var personData = new Tuple<string, int, bool>("Alice", 30, true);
Console.WriteLine($"Name: {personData.Item1}, Age: {personData.Item2}, IsActive: {personData.Item3}");
// 输出: Name: Alice, Age: 30, IsActive: True
四、关于泛型类和泛型接口的重要性说明
泛型不仅仅是让函数的返回类型泛型化,泛型类和泛型接口的核心意义在于类型逻辑的复用和设计模式的扩展性。
1. 泛型类的作用
泛型类可以将整个类的逻辑与类型解耦,避免为不同类型重复编写几乎相同的代码。
示例对比:
-
非泛型类:如果要实现一个
IntStack
和一个StringStack
,需要写两套几乎相同的代码:
public class IntStack
{
private int[] _items;
public void Push(int item) { /* ... */ }
public int Pop() { /* ... */ }
}
public class StringStack
{
private string[] _items;
public void Push(string item) { /* ... */ }
public string Pop() { /* ... */ }
}
- 泛型类:只需编写一次
Stack<T>
,即可支持所有类型:
public class Stack<T>
{
private T[] _items;
public void Push(T item) { /* ... */ }
public T Pop() { /* ... */ }
}
那有的读者就要犟了,我为什么必须实现你的泛型类,我直接在普通类中实现泛型方法不好吗(哼哼)。 请接着往下阅读:
尝试在普通类中用泛型方法实现
public class Stack
{
private object[] _items; // 只能用 object 存储所有类型
// 泛型方法
public void Push<T>(T item)
{
// 需要将 T 转为 object 存储
_items[count++] = item;
}
public T Pop<T>()
{
// 需要强制类型转换(可能引发 InvalidCastException)
return (T)_items[--count];
}
}
来,你来用用看:
Stack stack = new Stack();
stack.Push<int>(10);
stack.Push<string>("Hello");
int num = stack.Pop<int>(); // 正常
string str = stack.Pop<string>(); // 正常
// 但如果类型不匹配:
double error = stack.Pop<double>(); // 运行时抛出 InvalidCastException
你自己看看这好用不?
可能出现的问题:
-
类型不安全:
编译器无法保证Push
和Pop
的泛型类型一致。例如:
stack.Push<int>(10);
string str = stack.Pop<string>(); // 编译通过,但运行时崩溃!
完全依赖开发者自觉,容易出错。
-
性能问题:
值类型(如int
)需要装箱(存储为object
)和拆箱(转回T
),消耗资源。 -
代码逻辑混乱:
栈中可能混合存储多种类型(如同时存int
和string
),违背栈的常规设计逻辑。
于是乎,我们尽量还是申明泛型类:
public class Stack<T>
{
private T[] _items; // 直接存储 T 类型
public void Push(T item)
{
_items[count++] = item;
}
public T Pop()
{
return _items[--count];
}
}
使用示例:
Stack<int> intStack = new Stack<int>();
intStack.Push(10);
int num = intStack.Pop(); // 安全且高效
Stack<string> stringStack = new Stack<string>();
stringStack.Push("Hello");
string str = stringStack.Pop(); // 安全且高效
小结:
关键区别:泛型类 vs 普通类中的泛型方法
特性 | 泛型类 | 普通类中的泛型方法 |
---|---|---|
类型一致性 | 整个类的实例绑定到同一类型(如 Stack<int> ) | 方法每次调用可以指定不同类型 |
存储逻辑 | 内部数据(如数组)类型明确(T[] ) | 内部数据需用 object ,失去类型安全 |
适用场景 | 数据结构(如集合)、类型逻辑紧密绑定的场景 | 独立的功能方法(如工具方法 Swap<T> ) |
泛型类用于解决类型逻辑与数据结构绑定的问题(如
Stack<T>
、List<T>
),确保类型安全和性能。泛型方法用于解决独立功能的类型通用性问题(如
Swap<T>
、Max<T>
),不依赖类的类型参数。
2. 泛型接口的作用
泛型接口允许定义通用的操作契约,适用于多种类型,同时保持类型安全。
示例:
// 泛型接口:定义数据存储的通用操作
public interface IRepository<T>
{
void Add(T entity);
T GetById(int id);
}
// 实现泛型接口的类
public class UserRepository : IRepository<User>
{
public void Add(User user) { /* 存储用户 */ }
public User GetById(int id) { /* 查询用户 */ }
}
public class ProductRepository : IRepository<Product>
{
public void Add(Product product) { /* 存储商品 */ }
public Product GetById(int id) { /* 查询商品 */ }
}
优势:
统一契约:所有实现
IRepository<T>
的类必须支持Add
和GetById
方法。类型明确:
UserRepository
只能处理User
类型,ProductRepository
只能处理Product
类型,避免类型混乱。
五、泛型约束
5-1 什么是泛型约束
泛型约束(Generic Constraints)是 C# 中用于限制泛型类型参数的范围的机制。通过 where
关键字,可以指定泛型参数必须满足的条件(如必须是值类型、必须实现某个接口、必须有默认构造函数等)。
-
核心目的:在泛型代码中安全地使用类型参数,确保它具备某些特性或能力。
-
本质:通过约束,告诉编译器泛型类型参数的“能力边界”,从而允许在代码中调用特定的方法或属性。
5-2 为什么需要泛型约束
没有约束的泛型类型参数默认是 object
类型,只能调用 object
的成员(如 ToString()
)。但在实际开发中,我们通常需要更精确的操作,例如:
-
调用接口方法:例如要求泛型类型必须实现
IComparable
,才能比较大小。 -
创建实例:确保泛型类型有默认构造函数(
new T()
)。 -
类型安全:限制类型参数为值类型或引用类型,避免意外操作。
-
复用基类功能:要求泛型类型必须继承自某个基类,以调用基类方法。
错误示例:
public T Max<T>(T a, T b)
{
return a > b ? a : b; // 编译错误:运算符 ">" 不能应用于 T 类型
}
若没有约束,编译器不知道 T
是否支持 >
操作符。通过约束 T : IComparable<T>
,即可解决此问题。
5-3 泛型约束有哪些
C# 支持以下泛型约束类型:
约束类型 | 语法 | 说明 |
---|---|---|
值类型约束 | where T : struct | T 必须是值类型(如 int 、struct )。 |
引用类型约束 | where T : class | T 必须是引用类型(如类、接口、数组)。 |
无参构造函数约束 | where T : new() | T 必须有一个无参的公共构造函数。 |
基类约束 | where T : BaseClass | T 必须继承自 BaseClass 。 |
接口约束 | where T : IInterface | T 必须实现接口 IInterface 。 |
类型参数约束 | where T : U | T 必须派生自另一个类型参数 U 。 |
注意:
-
多个约束可以组合使用,用逗号分隔。
-
struct
和class
约束不能同时使用。 -
new()
约束必须放在最后(若存在)。
5-4各种泛型类型的使用
1. 值类型约束(where T : struct
)
限制 T
为值类型(如 int
、自定义结构体):
public class ValueContainer<T> where T : struct
{
public T Value { get; set; }
}
// 使用
var container = new ValueContainer<int> { Value = 42 };
// var errorContainer = new ValueContainer<string>(); // 编译错误:string 是引用类型
2. 引用类型约束(where T : class
)
限制 T
为引用类型:
public class ReferenceContainer<T> where T : class
{
public T Data { get; set; }
}
// 使用
var container = new ReferenceContainer<string> { Data = "Hello" };
// var errorContainer = new ReferenceContainer<int>(); // 编译错误:int 是值类型
3. 无参构造函数约束(where T : new()
)
确保可以调用 new T()
创建实例:
public T CreateInstance<T>() where T : new()
{
return new T();
}
// 使用
var obj = CreateInstance<MyClass>(); // MyClass 必须有公共无参构造函数
4. 基类约束(where T : BaseClass
)
限制 T
必须继承自某个基类:
public class Animal { }
public class Dog : Animal { }
public class Zoo<T> where T : Animal
{
public void Feed(T animal) { /* 调用基类方法 */ }
}
// 使用
var dogZoo = new Zoo<Dog>(); // Dog 是 Animal 的子类
// var errorZoo = new Zoo<string>(); // 编译错误
5. 接口约束(where T : IComparable<T>
)
要求 T
实现某个接口:
public T Max<T>(T a, T b) where T : IComparable<T>
{
return a.CompareTo(b) > 0 ? a : b;
}
// 使用
int max = Max(10, 20); // 正确:int 实现了 IComparable<int>
6. 类型参数约束(where T : U
)
关联两个泛型参数,确保 T
派生自 U
:
public class DerivedProcessor<T, U> where T : U
{
public void Process(T item) { /* U 类型的方法可用 */ }
}
// 使用
DerivedProcessor<Dog, Animal> processor = new DerivedProcessor<Dog, Animal>();
7. 多约束组合
public class Complex<T> where T : class, IComparable, new()
{
// T 必须是引用类型、实现 IComparable 接口、且有默认构造函数
}
一般也不会这样子用,太绕了
5-5总结
泛型约束是 C# 泛型编程中保证类型安全和代码灵活性的核心机制。通过约束,可以:
-
明确类型能力:确保泛型参数具备必要的特性(如可比较、可实例化)。
-
避免运行时错误:编译器在编译时检查约束,减少类型转换异常。
-
增强代码复用性:一份泛型代码可安全地适配多种类型,无需强制类型转换。