目 录CONTENT

文章目录

CSharp(四十六) 泛型方法的定义、使用和注意事项

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. 减少重复代码
  2. 保持类型安全
  3. 提高代码复用性

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) 时,Tint
  • Print("你好") 时,Tstring
  • Print(3.14) 时,Tdouble
  • Print(true) 时,Tbool

六、显式指定类型和自动推断类型

调用泛型方法有两种方式。

1. 显式指定类型

Print<int>(100);
Print<string>("你好");

这里我们明确告诉编译器:

T 是 int
T 是 string

2. 自动推断类型

Print(100);
Print("你好");

编译器会根据传入参数自动判断:

  • 100int,所以 Tint
  • "你好"string,所以 Tstring

实际开发中,能自动推断时通常直接省略类型参数。


七、什么时候不能自动推断类型

有些泛型方法不能自动推断类型。

例如:

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) 中的 Tint
  • GetFirst(names) 中的 Tstring

泛型方法的返回值类型会跟着 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)

这里:

  • TInputstring
  • TResultint
Map(100, number => "数字是:" + number)

这里:

  • TInputint
  • TResultstring

多个类型参数适合处理“输入类型和输出类型不同”的场景。


十、泛型方法和普通方法的区别

普通方法:

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>("当前值是:");

这里有两个类型参数:

  • 类上的 Tint
  • 方法上的 TMessagestring

注意:

类的类型参数和方法的类型参数可以不同,各自有自己的作用范围。

不推荐在方法里再写同名 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 可能是 intstringDateTime,它们不一定有 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) 中,Tint
  • PrintAll("A", "B", "C") 中,Tstring

注意:

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 不一定支持 == 运算符。


二十六、泛型方法配合委托

泛型方法经常和 FuncAction 一起使用。

例如重复执行某个操作:

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);

这里:

  • TSourcestring
  • TResultint

二十七、泛型方法配合 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)
{
    // ...
}

如果业务稍微复杂,TU 很难看懂。

更推荐:

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)

但问题是:

  • Tint 时,默认值是 0
  • Tstring 时,默认值是 null
  • Tbool 时,默认值是 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> 时,Tint

当传入 List<Student> 时,TStudent


四十五、完整示例:泛型转换方法

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 属性。


四十七、课堂讲解建议

讲泛型方法时,可以按下面顺序:

  1. 先用重复代码引出问题,比如 PrintIntPrintString
  2. 再用 Print<T> 展示泛型方法的基本写法。
  3. 强调 T 是类型占位符,不是普通变量。
  4. 讲显式指定类型和自动类型推断。
  5. 讲泛型方法的返回值和多个类型参数。
  6. Swap<T>ref 场景。
  7. Find<T>Func<T, bool> 场景。
  8. 最后讲 where 约束和常见错误。

学生最容易混淆的点:

  • <T> 写在方法名后面。
  • T 是类型,不是对象。
  • 不是所有地方都能自动推断 T
  • 泛型方法里不能随便访问 T.NameT.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# 中非常常用的语法,适合处理“逻辑相同,但类型不同”的问题。

可以记住下面几句话:

  1. 泛型方法是在方法名后面写 <T> 的方法。
  2. T 是类型参数,表示调用时才确定的类型。
  3. 泛型方法可以减少重复代码。
  4. 泛型方法比 object 更类型安全。
  5. 调用泛型方法时,可以显式指定类型,也可以让编译器自动推断。
  6. 类型推断主要依靠参数,不是万能的。
  7. 泛型方法可以有返回值,也可以有多个类型参数。
  8. where 可以给泛型方法添加约束。
  9. new T() 需要 where T : new()
  10. 泛型方法中不能随便调用 T 的成员,除非通过约束保证它存在。
  11. 泛型方法常和 List<T>Func<T, bool>Action<T>、LINQ 思想一起使用。
  12. 不要滥用泛型,只有类型确实需要变化时才使用。

一句话概括:

泛型方法让我们用一个方法安全地处理多种类型,是写通用工具方法、集合处理方法和业务复用逻辑的重要基础。

0
博主关闭了当前页面的评论