C# 中泛型方法的定义、使用和注意事项
一、什么是泛型方法
泛型方法,简单说就是:
方法在定义时不固定具体类型,等调用时再指定或推断具体类型。
普通方法通常写死类型:
static void PrintInt(int value)
{
Console.WriteLine(value);
}
static void PrintString(string value)
{
Console.WriteLine(value);
}
这两个方法逻辑一样,都是打印一个值,只是参数类型不同。
如果每种类型都写一个方法,代码会越来越重复。
使用泛型方法后,可以写成一个:
static void Print<T>(T value)
{
Console.WriteLine(value);
}
调用:
Print<int>(100);
Print<string>("你好");
Print<double>(3.14);
也可以让编译器自动推断类型:
Print(100);
Print("你好");
Print(3.14);
这里的 T 是类型参数,可以理解为“类型占位符”。
一句话:
泛型方法就是一个可以适配多种类型的方法模板。
二、为什么需要泛型方法
泛型方法主要有三个好处:
- 减少重复代码
- 保持类型安全
- 提高代码复用性
1. 减少重复代码
没有泛型时,交换两个 int:
static void SwapInt(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
交换两个 string:
static void SwapString(ref string a, ref string b)
{
string temp = a;
a = b;
b = temp;
}
逻辑完全一样,只是类型不同。
用泛型方法:
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
现在既可以交换 int:
int x = 10;
int y = 20;
Swap(ref x, ref y);
也可以交换 string:
string first = "A";
string second = "B";
Swap(ref first, ref second);
2. 保持类型安全
如果用 object 写通用方法:
static object GetValue(object value)
{
return value;
}
使用时需要强制转换:
int number = (int)GetValue(100);
如果转换错了,运行时才会报错。
用泛型方法:
static T GetValue<T>(T value)
{
return value;
}
使用:
int number = GetValue(100);
string text = GetValue("hello");
不需要强制转换,类型也更明确。
3. 提高代码复用性
比如写一个查找方法:
static T Find<T>(List<T> list, Func<T, bool> condition)
{
foreach (T item in list)
{
if (condition(item))
{
return item;
}
}
return default(T);
}
它既可以查找学生:
Student student = Find(students, s => s.Id == 1);
也可以查找商品:
Product product = Find(products, p => p.Price > 100);
同一个方法可以服务不同类型。
三、泛型方法的基本语法
泛型方法的基本格式:
访问修饰符 返回值类型 方法名<T>(参数列表)
{
// 方法体
}
例如:
static void Print<T>(T value)
{
Console.WriteLine(value);
}
如果有返回值:
static T Echo<T>(T value)
{
return value;
}
如果有多个类型参数:
static TResult ConvertValue<TInput, TResult>(TInput input, Func<TInput, TResult> converter)
{
return converter(input);
}
重点看这个位置:
方法名<T>
<T> 写在方法名后面,表示这是一个泛型方法。
四、T 是什么意思
T 通常表示 Type,也就是“类型”。
它不是固定关键字,只是常见命名习惯。
下面这些都可以:
static void Print<T>(T value)
{
}
static void Print<TValue>(TValue value)
{
}
static void Print<TItem>(TItem item)
{
}
不过不推荐随便写成:
static void Print<ABC>(ABC value)
{
}
虽然能运行,但不利于阅读。
常见类型参数命名:
| 名称 | 常见含义 |
|---|---|
T |
通用类型 |
TInput |
输入类型 |
TOutput |
输出类型 |
TResult |
返回结果类型 |
TItem |
元素类型 |
TKey |
键类型 |
TValue |
值类型 |
教学时可以先统一使用 T,等学生熟悉后再讲更具体的命名。
五、最简单的泛型方法:打印任意类型
using System;
class Program
{
static void Main()
{
Print(100);
Print("你好");
Print(3.14);
Print(true);
}
static void Print<T>(T value)
{
Console.WriteLine(value);
}
}
输出:
100
你好
3.14
True
这里的 Print<T> 可以接收任意类型:
Print(100)时,T是intPrint("你好")时,T是stringPrint(3.14)时,T是doublePrint(true)时,T是bool
六、显式指定类型和自动推断类型
调用泛型方法有两种方式。
1. 显式指定类型
Print<int>(100);
Print<string>("你好");
这里我们明确告诉编译器:
T 是 int
T 是 string
2. 自动推断类型
Print(100);
Print("你好");
编译器会根据传入参数自动判断:
100是int,所以T是int"你好"是string,所以T是string
实际开发中,能自动推断时通常直接省略类型参数。
七、什么时候不能自动推断类型
有些泛型方法不能自动推断类型。
例如:
static T CreateDefault<T>()
{
return default(T);
}
调用时:
var value = CreateDefault();
这会报错。
原因是:
方法没有参数,编译器无法从参数中推断 T 是什么类型。
正确写法:
int number = CreateDefault<int>();
string text = CreateDefault<string>();
bool flag = CreateDefault<bool>();
所以要记住:
泛型方法的类型推断主要依靠参数,不会单纯根据返回值来推断。
例如:
int number = CreateDefault(); // 仍然不行
虽然左边是 int,但编译器通常不会只根据接收变量来推断泛型方法的 T。
八、有返回值的泛型方法
泛型方法可以返回 T 类型。
static T GetFirst<T>(List<T> list)
{
if (list.Count > 0)
{
return list[0];
}
return default(T);
}
使用:
List<int> numbers = new List<int> { 10, 20, 30 };
int firstNumber = GetFirst(numbers);
List<string> names = new List<string> { "小明", "小红" };
string firstName = GetFirst(names);
这里:
GetFirst(numbers)中的T是intGetFirst(names)中的T是string
泛型方法的返回值类型会跟着 T 变化。
九、多个类型参数的泛型方法
一个泛型方法可以有多个类型参数。
static TResult Map<TInput, TResult>(TInput input, Func<TInput, TResult> converter)
{
return converter(input);
}
使用:
int length = Map("hello", text => text.Length);
string text = Map(100, number => "数字是:" + number);
分析:
Map("hello", text => text.Length)
这里:
TInput是stringTResult是int
Map(100, number => "数字是:" + number)
这里:
TInput是intTResult是string
多个类型参数适合处理“输入类型和输出类型不同”的场景。
十、泛型方法和普通方法的区别
普通方法:
static int Add(int a, int b)
{
return a + b;
}
这个方法只能处理 int。
泛型方法:
static T Echo<T>(T value)
{
return value;
}
这个方法可以处理多种类型。
对比:
| 对比项 | 普通方法 | 泛型方法 |
|---|---|---|
| 类型是否固定 | 固定 | 调用时确定 |
| 是否能复用不同类型 | 通常不能 | 可以 |
| 是否有类型参数 | 没有 | 有 |
| 常见写法 | Method(int value) |
Method<T>(T value) |
泛型方法不是替代普通方法。
如果方法只针对明确类型,普通方法更清楚。
如果方法逻辑相同,但类型可能不同,泛型方法更合适。
十一、泛型方法和泛型类的区别
泛型类:
class Box<T>
{
public T Value { get; set; }
}
使用:
Box<int> box = new Box<int>();
泛型方法:
static void Print<T>(T value)
{
Console.WriteLine(value);
}
使用:
Print<int>(100);
区别:
| 对比项 | 泛型类 | 泛型方法 |
|---|---|---|
| 类型参数写在哪里 | 类名后面 | 方法名后面 |
| 作用范围 | 整个类内部 | 当前方法内部 |
| 使用时机 | 一个类整体依赖某种类型 | 只有某个方法需要通用类型 |
示例:
class Tool
{
public void Print<T>(T value)
{
Console.WriteLine(value);
}
}
这里 Tool 不是泛型类,但里面有一个泛型方法。
再看:
class Box<T>
{
public T Value { get; set; }
public void PrintValue()
{
Console.WriteLine(Value);
}
}
这里 Box<T> 是泛型类,类里面所有成员都可以使用 T。
十二、泛型类中的泛型方法
泛型类里面也可以定义泛型方法。
class Box<T>
{
public T Value { get; set; }
public void Print<TMessage>(TMessage message)
{
Console.WriteLine(message);
Console.WriteLine(Value);
}
}
使用:
Box<int> box = new Box<int>();
box.Value = 100;
box.Print<string>("当前值是:");
这里有两个类型参数:
- 类上的
T是int - 方法上的
TMessage是string
注意:
类的类型参数和方法的类型参数可以不同,各自有自己的作用范围。
不推荐在方法里再写同名 T:
class Box<T>
{
// 不推荐:方法的 T 会遮住类的 T,容易混乱
public void Test<T>(T value)
{
}
}
更推荐:
class Box<T>
{
public void Test<TValue>(TValue value)
{
}
}
十三、泛型方法的约束
泛型很灵活,但也有一个限制:
编译器不知道 T 到底是什么类型,所以不能随便调用 T 的成员。
错误示例:
static void PrintName<T>(T item)
{
Console.WriteLine(item.Name); // 错误
}
为什么?
因为 T 可能是 int、string、DateTime,它们不一定有 Name 属性。
这时可以使用泛型约束。
泛型约束使用 where:
static void 方法名<T>(T value) where T : 约束
{
}
十四、where T : class
static void PrintReference<T>(T value) where T : class
{
if (value == null)
{
Console.WriteLine("这是 null");
}
else
{
Console.WriteLine(value);
}
}
这里:
where T : class
表示:
T 必须是引用类型。
可以调用:
PrintReference<string>("hello");
PrintReference<object>(new object());
不能调用:
// 错误:int 是值类型
PrintReference<int>(100);
十五、where T : struct
static void PrintValueType<T>(T value) where T : struct
{
Console.WriteLine(value);
}
这里:
where T : struct
表示:
T 必须是非可空值类型。
可以调用:
PrintValueType<int>(100);
PrintValueType<double>(3.14);
PrintValueType<DateTime>(DateTime.Now);
不能调用:
// 错误:string 是引用类型
PrintValueType<string>("hello");
十六、where T : new()
如果泛型方法内部想创建 T 的对象,需要 new() 约束。
错误写法:
static T Create<T>()
{
return new T(); // 错误
}
正确写法:
static T Create<T>() where T : new()
{
return new T();
}
使用:
Student student = Create<Student>();
这里要求 Student 必须有 public 无参数构造方法。
示例:
class Student
{
public string Name { get; set; }
}
这个类可以使用,因为没有显式写构造方法时,C# 会提供默认无参数构造方法。
如果写成:
class Student
{
public string Name { get; set; }
public Student(string name)
{
Name = name;
}
}
就不能满足 new() 约束,除非再补一个无参数构造方法。
十七、where T : 基类
可以限制 T 必须继承某个基类。
class Animal
{
public string Name { get; set; }
public void Eat()
{
Console.WriteLine(Name + " 正在吃东西");
}
}
class Dog : Animal
{
}
泛型方法:
static void Feed<T>(T animal) where T : Animal
{
animal.Eat();
}
使用:
Dog dog = new Dog { Name = "小狗" };
Feed(dog);
因为写了 where T : Animal,编译器知道 animal 一定拥有 Animal 中的成员,所以可以调用 Eat()。
十八、where T : 接口
接口约束非常常见。
interface IPrintable
{
void Print();
}
class Report : IPrintable
{
public void Print()
{
Console.WriteLine("打印报表");
}
}
泛型方法:
static void PrintItem<T>(T item) where T : IPrintable
{
item.Print();
}
使用:
Report report = new Report();
PrintItem(report);
如果没有接口约束,编译器不知道 T 一定有 Print() 方法。
有了约束后,就可以安全调用。
十九、多个约束
泛型方法可以有多个约束。
static T CreateEntity<T>() where T : class, IEntity, new()
{
T entity = new T();
entity.Id = 1;
return entity;
}
接口:
interface IEntity
{
int Id { get; set; }
}
这里:
where T : class, IEntity, new()
表示:
T必须是引用类型。T必须实现IEntity。T必须有 public 无参数构造方法。
注意:
如果有
new()约束,一般要放在最后。
二十、多个类型参数分别添加约束
如果方法有多个类型参数,可以分别添加约束。
static TResult ConvertEntity<TSource, TResult>(TSource source)
where TSource : IEntity
where TResult : class, new()
{
TResult result = new TResult();
Console.WriteLine("源对象 Id:" + source.Id);
return result;
}
这里:
where TSource : IEntity
约束 TSource。
where TResult : class, new()
约束 TResult。
当有多个类型参数时,约束要分别写清楚。
二十一、default(T)
泛型方法中经常使用 default(T)。
它表示:
返回某种类型的默认值。
例如:
static T GetDefault<T>()
{
return default(T);
}
不同类型的默认值:
| 类型 | 默认值 |
|---|---|
int |
0 |
double |
0 |
bool |
false |
char |
'\0' |
| 引用类型 | null |
| 结构体 | 所有字段都是默认值 |
调用:
int number = GetDefault<int>(); // 0
string text = GetDefault<string>(); // null
bool flag = GetDefault<bool>(); // false
新版 C# 中也可以简写为:
return default;
二十二、泛型方法配合 ref 使用
泛型方法可以配合 ref。
最经典例子就是交换两个变量。
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
使用:
int x = 10;
int y = 20;
Swap(ref x, ref y);
Console.WriteLine(x); // 20
Console.WriteLine(y); // 10
字符串也可以:
string a = "A";
string b = "B";
Swap(ref a, ref b);
Console.WriteLine(a); // B
Console.WriteLine(b); // A
注意:
使用
ref时,传入的两个变量类型必须一致。
下面这样不行:
int number = 10;
double price = 3.14;
// 错误:T 不能同时是 int 和 double
Swap(ref number, ref price);
二十三、泛型方法配合 out 使用
泛型方法也可以配合 out。
例如模拟一个安全转换方法:
static bool TryGetFirst<T>(List<T> list, out T value)
{
if (list.Count > 0)
{
value = list[0];
return true;
}
value = default(T);
return false;
}
使用:
List<string> names = new List<string> { "小明", "小红" };
if (TryGetFirst(names, out string firstName))
{
Console.WriteLine(firstName);
}
如果列表为空:
value = default(T);
return false;
这样调用者可以通过返回值知道是否成功。
二十四、泛型方法配合 params 使用
params 表示可变数量参数,也可以和泛型配合。
static void PrintAll<T>(params T[] values)
{
foreach (T value in values)
{
Console.WriteLine(value);
}
}
使用:
PrintAll(1, 2, 3, 4);
PrintAll("A", "B", "C");
分析:
PrintAll(1, 2, 3, 4)中,T是intPrintAll("A", "B", "C")中,T是string
注意:
PrintAll(1, "A");
这种写法类型不统一,编译器可能会推断成公共类型,比如 object,也可能需要你明确指定。
如果希望保存混合类型,最好认真考虑是否应该使用 object,或者定义更清晰的数据结构。
二十五、泛型方法作为工具方法
泛型方法很适合写工具方法。
例如判断是否为默认值:
static bool IsDefault<T>(T value)
{
return EqualityComparer<T>.Default.Equals(value, default(T));
}
使用:
Console.WriteLine(IsDefault(0)); // True
Console.WriteLine(IsDefault(10)); // False
Console.WriteLine(IsDefault<string>(null)); // True
这里使用:
EqualityComparer<T>.Default
是为了更安全地比较泛型值。
不推荐简单写:
value == default(T)
因为泛型 T 不一定支持 == 运算符。
二十六、泛型方法配合委托
泛型方法经常和 Func、Action 一起使用。
例如重复执行某个操作:
static void Repeat<T>(T value, int count, Action<T> action)
{
for (int i = 0; i < count; i++)
{
action(value);
}
}
使用:
Repeat("你好", 3, text =>
{
Console.WriteLine(text);
});
输出:
你好
你好
你好
再看一个转换方法:
static List<TResult> ConvertAll<TSource, TResult>(
List<TSource> source,
Func<TSource, TResult> converter)
{
List<TResult> result = new List<TResult>();
foreach (TSource item in source)
{
result.Add(converter(item));
}
return result;
}
使用:
List<string> words = new List<string> { "CSharp", "Java", "Go" };
List<int> lengths = ConvertAll(words, word => word.Length);
这里:
TSource是stringTResult是int
二十七、泛型方法配合 LINQ 思想
LINQ 中很多方法本质上就是泛型方法。
例如:
var result = numbers.Where(n => n > 10);
Where 可以处理 List<int>,也可以处理 List<string>、List<Student>。
因为它是泛型的。
我们也可以写一个简单版的 Where:
static List<T> Where<T>(List<T> source, Func<T, bool> predicate)
{
List<T> result = new List<T>();
foreach (T item in source)
{
if (predicate(item))
{
result.Add(item);
}
}
return result;
}
使用:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
List<int> evenNumbers = Where(numbers, n => n % 2 == 0);
学生列表:
List<Student> passedStudents = Where(students, s => s.Score >= 60);
同一个 Where<T> 方法可以处理不同类型列表。
二十八、泛型扩展方法
扩展方法也可以是泛型方法。
例如给所有对象增加一个简单的打印方法:
static class Extensions
{
public static void Print<T>(this T value)
{
Console.WriteLine(value);
}
}
使用:
100.Print();
"hello".Print();
DateTime.Now.Print();
再写一个判断列表是否为空的扩展方法:
static class ListExtensions
{
public static bool IsNullOrEmpty<T>(this List<T> list)
{
return list == null || list.Count == 0;
}
}
使用:
List<int> numbers = new List<int>();
if (numbers.IsNullOrEmpty())
{
Console.WriteLine("列表为空");
}
泛型扩展方法在工具库中非常常见。
二十九、异步泛型方法
泛型方法也可以是异步方法。
例如:
static async Task<T> GetValueAsync<T>(T value)
{
await Task.Delay(1000);
return value;
}
使用:
int number = await GetValueAsync(100);
string text = await GetValueAsync("hello");
再看一个带转换逻辑的异步泛型方法:
static async Task<TResult> ExecuteAsync<TResult>(Func<Task<TResult>> action)
{
Console.WriteLine("开始执行");
TResult result = await action();
Console.WriteLine("执行完成");
return result;
}
使用:
int result = await ExecuteAsync(async () =>
{
await Task.Delay(1000);
return 100;
});
注意:
异步泛型方法的返回类型通常是
Task<T>或ValueTask<T>,而不是直接返回T。
三十、泛型方法重载
泛型方法可以和普通方法重载。
static void Print(int value)
{
Console.WriteLine("int:" + value);
}
static void Print<T>(T value)
{
Console.WriteLine("generic:" + value);
}
调用:
Print(100);
Print("hello");
输出:
int:100
generic:hello
当有更匹配的普通方法时,编译器通常会优先选择普通方法。
也可以有多个泛型重载:
static void Show<T>(T value)
{
}
static void Show<T1, T2>(T1 value1, T2 value2)
{
}
注意:
重载太多会让调用规则变复杂,教学和实际开发中都要保持清晰。
三十一、泛型方法不能只靠返回值重载
下面这种写法是不允许的:
static int GetValue<T>()
{
return 1;
}
static string GetValue<T>()
{
return "hello";
}
原因是:
C# 方法重载不能只靠返回值类型区分。
方法签名主要看方法名和参数列表。
如果两个方法只有返回值不同,调用时编译器无法可靠判断该选哪个。
正确做法是改方法名,或者增加参数,或者使用不同的泛型设计。
三十二、泛型方法中的类型判断
有时你可能想根据 T 的具体类型做不同处理。
static void ShowType<T>(T value)
{
Console.WriteLine(typeof(T).Name);
}
调用:
ShowType(100); // Int32
ShowType("hello"); // String
也可以判断:
static void PrintSpecial<T>(T value)
{
if (value is string text)
{
Console.WriteLine("字符串长度:" + text.Length);
}
else
{
Console.WriteLine(value);
}
}
但要注意:
如果一个泛型方法里写了大量类型判断,可能说明这个方法并不适合设计成泛型。
泛型更适合“类型不同,但处理逻辑相同或相似”的场景。
三十三、泛型方法和 object 方法的对比
用 object:
static object Echo(object value)
{
return value;
}
调用:
object result = Echo(100);
int number = (int)result;
问题:
- 返回值需要强制转换。
- 类型错误可能运行时才发现。
- 值类型可能有装箱拆箱。
用泛型:
static T Echo<T>(T value)
{
return value;
}
调用:
int number = Echo(100);
string text = Echo("hello");
优点:
- 不需要强制转换。
- 编译期能检查类型。
- 对值类型更友好。
一句话:
object是把类型信息藏起来,泛型是把类型信息保留下来。
三十四、注意事项一:T 是类型,不是变量
T 表示类型参数,不是一个普通变量。
正确用法:
static T Echo<T>(T value)
{
return value;
}
List<T> list = new List<T>();
错误理解:
// T 不是一个可以直接打印或赋值的普通变量
可以这样讲:
T出现在类型应该出现的位置,表示“这里将来会换成某种具体类型”。
三十五、注意事项二:不能随便调用 T 的属性和方法
错误写法:
static void PrintName<T>(T item)
{
Console.WriteLine(item.Name);
}
编译器不知道 T 有没有 Name。
解决方式:使用约束。
interface IHasName
{
string Name { get; }
}
static void PrintName<T>(T item) where T : IHasName
{
Console.WriteLine(item.Name);
}
或者不用泛型,直接写明确类型:
static void PrintName(Student student)
{
Console.WriteLine(student.Name);
}
原则:
泛型不是动态类型,编译器只允许使用它确定存在的成员。
三十六、注意事项三:new T() 必须有约束
错误写法:
static T Create<T>()
{
return new T();
}
正确写法:
static T Create<T>() where T : new()
{
return new T();
}
原因:
编译器不知道 T 是否一定有 public 无参数构造方法。
三十七、注意事项四:类型推断不是万能的
这个可以推断:
static void Print<T>(T value)
{
}
Print(100);
这个不能推断:
static T CreateDefault<T>()
{
return default(T);
}
var value = CreateDefault(); // 错误
因为没有参数可以帮助推断 T。
要写:
var value = CreateDefault<int>();
三十八、注意事项五:不要滥用泛型方法
泛型方法适合:
- 方法逻辑相同
- 类型可能不同
- 希望保留类型安全
不适合:
- 只处理一种明确类型
- 每种类型的处理逻辑完全不同
- 为了看起来高级而泛型
不推荐:
static void Save<T>(T value)
{
if (value is Student)
{
// 保存学生
}
else if (value is Product)
{
// 保存商品
}
else if (value is Order)
{
// 保存订单
}
}
这种写法看似通用,实际把多种业务混在一起。
更推荐根据业务拆开,或者使用接口、多态、策略模式等设计。
三十九、注意事项六:多个类型参数要命名清楚
不推荐:
static U Convert<T, U>(T value)
{
// ...
}
如果业务稍微复杂,T 和 U 很难看懂。
更推荐:
static TResult Convert<TInput, TResult>(TInput value)
{
// ...
}
命名清楚后,别人一看就知道:
TInput是输入类型TResult是结果类型
四十、注意事项七:泛型方法中比较值要小心
下面写法可能不能编译:
static bool AreEqual<T>(T a, T b)
{
return a == b; // 可能错误
}
因为不是所有类型都支持 ==。
更通用的写法:
static bool AreEqual<T>(T a, T b)
{
return EqualityComparer<T>.Default.Equals(a, b);
}
使用:
Console.WriteLine(AreEqual(1, 1)); // True
Console.WriteLine(AreEqual("A", "A")); // True
Console.WriteLine(AreEqual("A", "B")); // False
四十一、注意事项八:泛型方法中的 null 判断
如果没有约束,T 可能是值类型,也可能是引用类型。
static bool IsNull<T>(T value)
{
return value == null;
}
这类代码在不同 C# 版本和可空上下文中可能会有警告或限制。
如果你明确只想处理引用类型,可以加约束:
static bool IsNull<T>(T value) where T : class
{
return value == null;
}
如果想判断默认值,更通用:
static bool IsDefault<T>(T value)
{
return EqualityComparer<T>.Default.Equals(value, default(T));
}
四十二、注意事项九:返回 default(T) 时要让调用者知道含义
例如:
static T Find<T>(List<T> list, Func<T, bool> condition)
{
foreach (T item in list)
{
if (condition(item))
{
return item;
}
}
return default(T);
}
如果找不到,返回 default(T)。
但问题是:
T是int时,默认值是0T是string时,默认值是nullT是bool时,默认值是false
调用者可能分不清:
返回的是找到的真实值,还是没找到时的默认值?
更清晰的写法可以使用 bool + out:
static bool TryFind<T>(List<T> list, Func<T, bool> condition, out T result)
{
foreach (T item in list)
{
if (condition(item))
{
result = item;
return true;
}
}
result = default(T);
return false;
}
使用:
if (TryFind(numbers, n => n > 10, out int result))
{
Console.WriteLine("找到了:" + result);
}
else
{
Console.WriteLine("没找到");
}
四十三、注意事项十:泛型方法不是动态方法
泛型方法在编译时仍然是强类型的。
不能因为写了 T,就随便调用任何成员。
例如:
static void DoSomething<T>(T value)
{
// value.Fly(); // 不行
// value.Run(); // 不行
// value.Name; // 不行
}
除非通过约束告诉编译器:
interface IRunnable
{
void Run();
}
static void DoSomething<T>(T value) where T : IRunnable
{
value.Run();
}
记住:
泛型强调类型安全,不是绕过类型检查。
四十四、完整示例:泛型查找方法
下面写一个通用查找方法,用来从列表中查找第一个符合条件的元素。
using System;
using System.Collections.Generic;
class Student
{
public int Id { get; set; }
public string Name { get; set; }
public int Score { get; set; }
}
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 3, 5, 8, 10 };
int firstEven = Find(numbers, n => n % 2 == 0);
Console.WriteLine("第一个偶数:" + firstEven);
List<Student> students = new List<Student>
{
new Student { Id = 1, Name = "小明", Score = 95 },
new Student { Id = 2, Name = "小红", Score = 80 },
new Student { Id = 3, Name = "小刚", Score = 59 }
};
Student highScoreStudent = Find(students, s => s.Score >= 90);
Console.WriteLine("高分学生:" + highScoreStudent.Name);
}
static T Find<T>(List<T> list, Func<T, bool> condition)
{
foreach (T item in list)
{
if (condition(item))
{
return item;
}
}
return default(T);
}
}
这个例子中:
static T Find<T>(List<T> list, Func<T, bool> condition)
表示:
- 传入一个
List<T> - 传入一个判断条件
Func<T, bool> - 返回一个
T
当传入 List<int> 时,T 是 int。
当传入 List<Student> 时,T 是 Student。
四十五、完整示例:泛型转换方法
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
List<string> words = new List<string> { "CSharp", "Java", "Go" };
List<int> lengths = ConvertAll(words, word => word.Length);
foreach (int length in lengths)
{
Console.WriteLine(length);
}
List<int> numbers = new List<int> { 1, 2, 3 };
List<string> texts = ConvertAll(numbers, n => "数字:" + n);
foreach (string text in texts)
{
Console.WriteLine(text);
}
}
static List<TResult> ConvertAll<TSource, TResult>(
List<TSource> source,
Func<TSource, TResult> converter)
{
List<TResult> result = new List<TResult>();
foreach (TSource item in source)
{
TResult convertedItem = converter(item);
result.Add(convertedItem);
}
return result;
}
}
这个方法:
static List<TResult> ConvertAll<TSource, TResult>(
List<TSource> source,
Func<TSource, TResult> converter)
可以把一种类型的列表转换成另一种类型的列表。
例如:
List<string>转List<int>List<int>转List<string>
这就是泛型方法的强大之处:
方法逻辑固定,但输入类型和输出类型都可以变化。
四十六、完整示例:带约束的泛型方法
using System;
using System.Collections.Generic;
interface IEntity
{
int Id { get; set; }
}
class Student : IEntity
{
public int Id { get; set; }
public string Name { get; set; }
}
class Product : IEntity
{
public int Id { get; set; }
public string Title { get; set; }
}
class Program
{
static void Main()
{
List<Student> students = new List<Student>
{
new Student { Id = 1, Name = "小明" },
new Student { Id = 2, Name = "小红" }
};
Student student = FindById(students, 2);
Console.WriteLine(student.Name);
List<Product> products = new List<Product>
{
new Product { Id = 10, Title = "键盘" },
new Product { Id = 20, Title = "鼠标" }
};
Product product = FindById(products, 20);
Console.WriteLine(product.Title);
}
static T FindById<T>(List<T> list, int id) where T : IEntity
{
foreach (T item in list)
{
if (item.Id == id)
{
return item;
}
}
return default(T);
}
}
重点:
where T : IEntity
因为有这个约束,方法内部才能写:
item.Id
否则编译器不知道 T 有没有 Id 属性。
四十七、课堂讲解建议
讲泛型方法时,可以按下面顺序:
- 先用重复代码引出问题,比如
PrintInt、PrintString。 - 再用
Print<T>展示泛型方法的基本写法。 - 强调
T是类型占位符,不是普通变量。 - 讲显式指定类型和自动类型推断。
- 讲泛型方法的返回值和多个类型参数。
- 用
Swap<T>讲ref场景。 - 用
Find<T>讲Func<T, bool>场景。 - 最后讲
where约束和常见错误。
学生最容易混淆的点:
<T>写在方法名后面。T是类型,不是对象。- 不是所有地方都能自动推断
T。 - 泛型方法里不能随便访问
T.Name、T.Id,除非加约束。 new T()必须有where T : new()。- 方法逻辑如果针对每种类型都不同,就不一定适合泛型。
可以用这句话帮助理解:
泛型方法就是“方法模板”,调用时把具体类型填进去,方法就能用这个类型安全地工作。
四十八、练习题
练习 1:打印任意类型
写一个泛型方法 Print<T>,可以打印任意类型的值。
参考答案:
static void Print<T>(T value)
{
Console.WriteLine(value);
}
使用:
Print(100);
Print("你好");
Print(3.14);
练习 2:返回传入的值
写一个泛型方法 Echo<T>,传入什么值就返回什么值。
参考答案:
static T Echo<T>(T value)
{
return value;
}
使用:
int number = Echo(100);
string text = Echo("hello");
练习 3:交换两个变量
写一个泛型方法 Swap<T>,交换两个变量。
参考答案:
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
使用:
int x = 1;
int y = 2;
Swap(ref x, ref y);
练习 4:获取列表第一个元素
写一个泛型方法 GetFirst<T>,返回列表中的第一个元素。如果列表为空,返回默认值。
参考答案:
static T GetFirst<T>(List<T> list)
{
if (list.Count > 0)
{
return list[0];
}
return default(T);
}
练习 5:查找符合条件的元素
写一个泛型方法 Find<T>,从列表中查找第一个符合条件的元素。
参考答案:
static T Find<T>(List<T> list, Func<T, bool> condition)
{
foreach (T item in list)
{
if (condition(item))
{
return item;
}
}
return default(T);
}
使用:
List<int> numbers = new List<int> { 1, 2, 3, 4 };
int result = Find(numbers, n => n > 2);
练习 6:带约束的打印方法
定义一个接口 IPrintable,再写一个泛型方法,只能打印实现了 IPrintable 的对象。
参考答案:
interface IPrintable
{
void Print();
}
static void PrintItem<T>(T item) where T : IPrintable
{
item.Print();
}
四十九、总结
泛型方法是 C# 中非常常用的语法,适合处理“逻辑相同,但类型不同”的问题。
可以记住下面几句话:
- 泛型方法是在方法名后面写
<T>的方法。 T是类型参数,表示调用时才确定的类型。- 泛型方法可以减少重复代码。
- 泛型方法比
object更类型安全。 - 调用泛型方法时,可以显式指定类型,也可以让编译器自动推断。
- 类型推断主要依靠参数,不是万能的。
- 泛型方法可以有返回值,也可以有多个类型参数。
where可以给泛型方法添加约束。new T()需要where T : new()。- 泛型方法中不能随便调用
T的成员,除非通过约束保证它存在。 - 泛型方法常和
List<T>、Func<T, bool>、Action<T>、LINQ 思想一起使用。 - 不要滥用泛型,只有类型确实需要变化时才使用。
一句话概括:
泛型方法让我们用一个方法安全地处理多种类型,是写通用工具方法、集合处理方法和业务复用逻辑的重要基础。