CodeGym /课程 /C# SELF /用于比较的lambda表达式和委托

用于比较的lambda表达式和委托

C# SELF
第 30 级 , 课程 2
可用

1. 入门

想象一下,你有一个学生列表,你需要先按年龄排序,再按姓氏,然后按平均分排序,但只针对所有考试都通过的学生。如果每次这种“一次性”比较都要写一个实现了IComparer<T>的新类,你的项目很快就会变成一堆小比较器类的垃圾场。这很麻烦:代码又臃肿又难读。

遇到这种情况,C#给了我们更优雅的方案:可以直接把比较逻辑传给Sort方法,而不用新建一个类。

为此我们需要用到委托和它们的精简兄弟——lambda表达式

委托——我们的灵活小帮手

在聊lambda之前,先搞清楚什么是委托。简单说,委托就是一种可以存储方法引用的类型。听起来有点玄乎?你可以这样想:

比如你有个待办事项清单,其中有些事项其实是“指令”或“配方”。委托就像一个特殊变量,可以存储这种“配方”(方法)的引用。等你要做这件事时,只要调用这个委托变量,它就会“调用”它指向的方法。

在C#里,委托常用于创建回调(稍后调用方法,通常响应事件)、事件处理(比如按钮点击),当然也可以把方法作为参数传给其他方法(让方法能调用另一个方法),这正是我们现在排序要用的。

List<T>.Sort()方法有好几个重载(版本),其中一个接受一个叫Comparison<T>的特殊委托。

2. Comparison<T>委托

Comparison<T>是什么?

Comparison<T>是.NET内置的委托,专门用来比较两个同类型T的对象。它的“配方”是:接受两个T类型的对象(叫xy),返回一个整数(int):

  • 负数(比如-1),如果x“小于”y
  • 0),如果x“等于”y
  • 正数(比如1),如果x“大于”y

IComparable.CompareToIComparer.Compare也是按这个规则工作的。也就是说,逻辑一样,只不过现在我们可以把它当“方法变量”传递,而不是写个新类。

来看个例子。回到我们的学生。假设有个Student类:

public class Student
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public double AverageGrade { get; set; }

    public Student(string firstName, string lastName, int age, double averageGrade)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
        AverageGrade = averageGrade;
    }

    public void PrintInfo()
    {
        Console.WriteLine($"学生: {FirstName} {LastName}, 年龄: {Age}, 分数: {AverageGrade:F2}");
    }
}

现在,如果要用委托按年龄排序学生列表,可以写一个静态方法,符合Comparison<Student>的签名:

public class Program
{
    // 符合Comparison<Student>委托签名的方法
    // 用来比较两个学生的年龄
    public static int CompareStudentsByAge(Student student1, Student student2)
    {
        // 用内置的CompareTo方法比较数字,
        // 返回-1、0或1
        return student1.Age.CompareTo(student2.Age);
    }

    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("伊万", "彼得罗夫", 20, 4.5),
            new Student("玛丽亚", "西多罗娃", 22, 4.8),
            new Student("阿列克谢", "伊万诺夫", 19, 3.9),
            new Student("叶莲娜", "科兹洛娃", 20, 4.2) // 两个同龄学生
        };

        Console.WriteLine("--- 排序前的学生列表 ---");        
        foreach (var s in students)
            s.PrintInfo();

        Console.WriteLine("--- 按年龄排序学生(用委托) ---");
        students.Sort(CompareStudentsByAge); //把CompareStudentsByAge方法作为参数传递

        foreach (var s in students)
            s.PrintInfo();
    }
}

代码解析:

  1. 我们写了一个静态方法CompareStudentsByAge,接受两个学生,返回int,符合Comparison<Student>的约定。
  2. Main里创建了学生列表。
  3. 当我们调用students.Sort(CompareStudentsByAge);时,并不是立刻调用CompareStudentsByAge()!只是传递方法引用List<T>.Sort()会在排序时多次调用我们的CompareStudentsByAge,每次传不同的学生对。这就像你给别人一个送货地址,而不是直接把货车开过去。

这种方式比每次都写个比较器类方便多了。但还能更进一步!

3. 欢迎lambda表达式

其实,哪怕写一个单独的方法像CompareStudentsByAge,如果比较逻辑很简单、只用一两次,也会觉得有点多余。为此C#引入了lambda表达式

lambda表达式是什么?其实就是匿名方法,或者说“无家可归的方法”。就是你可以直接在需要的地方写一小段代码(方法),不用单独声明。就像随手写个便签贴在任务上,而不是写一份完整手册。

lambda表达式的主要符号是=>(念作“箭头”或“映射到”)。它把参数和方法体分开。

lambda表达式的基本语法

假如你有个委托(方法引用),要传给Sort()函数:

public static int CompareStudentsByAge(Student student1, Student student2)
{
   return student1.Age.CompareTo(student2.Age);
}

students.Sort(CompareStudentsByAge); //把CompareStudentsByAge方法作为参数传递

其实可以更简洁地写:

//把匿名方法作为参数传递
students.Sort( (Student student1, Student student2) => student1.Age.CompareTo(student2.Age) );

这里我们直接写了方法的两部分:

  • 参数:(Student student1, Student student2)
  • 方法体:student1.Age.CompareTo(student2.Age)

这种简洁的写法就叫lambda表达式(参数) => 表达式

它是怎么工作的

C#编译器遇到lambda表达式时,会自动生成一个真正的方法。

比如你有这样的代码:

students.Sort( (s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade) );

编译结果大致是这样:

public static int CompareStudents_Lambda123(Student s1, Student s2)
{
   return s2.AverageGrade.CompareTo(s1.AverageGrade);
}

students.Sort( CompareStudents_Lambda123 );

4. 排序和lambda表达式的例子

我们把学生的例子用lambda表达式重写一下:

public class Program
{
    public static void Main(string[] args)
    {
        List<Student> students = new List<Student>
        {
            new Student("伊万", "彼得罗夫", 20, 4.5),
            new Student("玛丽亚", "西多罗娃", 22, 4.8),
            new Student("阿列克谢", "伊万诺夫", 19, 3.9),
            new Student("叶莲娜", "科兹洛娃", 20, 4.2)
        };

        Console.WriteLine("--- 排序前的学生列表 ---");        
        foreach (var s in students)
            s.PrintInfo();

        // 现在比较逻辑直接写在这里,“就地”
        Console.WriteLine("--- 按年龄排序学生(用lambda表达式) ---");
        students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));

        foreach (var s in students)
            s.PrintInfo();

        // 要按降序排序,只要结果乘以-1
        Console.WriteLine("\n--- 按平均分降序排序学生 ---");
        // s2.CompareTo(s1) 代替 s1.CompareTo(s2)
        students.Sort((s1, s2) => s2.AverageGrade.CompareTo(s1.AverageGrade));

        foreach (var s in students)
            s.PrintInfo();
    }
}

这里发生了什么?

  1. students.Sort((student1, student2) => student1.Age.CompareTo(student2.Age));
    • student1student2Sort传给我们匿名方法的参数(类似于xyComparison<T>里)。
    • =>是lambda操作符。
    • student1.Age.CompareTo(student2.Age)是lambda表达式的主体。这里只是一个表达式,结果就是返回值。
  2. 要按平均分降序排序,只要把s1s2CompareTo里换一下顺序。这是反转排序顺序的经典技巧。

为什么这样方便?

  • 简洁:不用为每个小的比较逻辑写方法或类。
  • 易读:比较逻辑就在Sort()调用旁边,简单情况更容易理解。
  • 灵活:可以随时“就地”改排序条件。

5. 委托和lambda——绝配

你可能会问:lambda表达式和委托是一样的吗,还是不一样?

其实,lambda表达式只是语法糖,用来创建委托实例或表达式树(Expression Tree,后面会讲)。编译器看到lambda表达式时,会自动把它变成合适的委托实例。比如List<T>.Sort()需要Comparison<T>委托,编译器就知道(student1, student2) => student1.Age.CompareTo(student2.Age)要变成Comparison<Student>

所以,lambda表达式让我们能写很简洁的代码,而委托就是“容器”,负责传递和执行这些代码。它们是好搭档!

什么时候用什么?

  • IComparable<T>:当你的类型有自然、明显的排序方式时用。比如商品按货号排序。这是“默认”顺序。
  • IComparer<T>:当你需要多次复用的比较逻辑,但又不想“污染”主类,或者有多种排序方式时用。比如一个IComparer按价格排序,另一个按名称,在不同地方用。
  • 委托(Comparison<T>)和lambda表达式:最适合一次性、临时的排序,逻辑简单不需要单独类。大多数C#排序任务都用这个方式。也是把逻辑传给其他方法(比如过滤的FindFindAll,查找的FindIndex)的好方法,我们之前讲过。
特点 IComparable<T> IComparer<T> Comparison<T> / lambda表达式
定义在哪? 在类T 在单独的比较器类里 可以是方法或匿名表达式
灵活性 固定的“自然”顺序 多种可复用顺序 临时(就地),针对某次方法调用
样板代码 少,写在类里 中等(单独类) 最少(lambda尤其简洁)
用法示例
someList.Sort()
someList.Sort(new MyComparer())
someList.Sort((x, y) => x.Prop.CompareTo(y.Prop))
可读性 适合自然顺序 取决于比较器名字 简单、特定比较时很棒

6. 实战应用和未来展望

lambda表达式不仅仅是排序的“语法糖”。它是现代C#代码里无处不在的强大工具。你会经常遇到它们:

  • 在LINQ(Language Integrated Query)里: 这是lambda表达式最常见的用法。LINQ让你能写SQL风格的集合查询,lambda用来定义过滤、排序、投影条件。我们很快会学LINQ,你会看到lambda让它超级强大又好用。
  • 事件处理: lambda能很简洁地描述某个事件发生时要做什么(比如UI按钮点击)。
  • 异步编程: 用来定义要并行执行的任务。
  • 各种.NET API: .NET标准库很多方法都接受委托(也就是lambda表达式)作为参数,让你能加自定义逻辑。

所以,学会lambda表达式,不只是提升排序技能,更是理解现代C#代码和库的关键一步。这是每个面试都很看重、每个项目都用得上的技能!

7. 常见错误和细节

用委托和lambda表达式做比较时,有几个点要注意:

比较结果不对: 记住,CompareTo或你的比较逻辑必须返回负数、零或正数。如果你不小心返回了别的,比如truefalse,排序就会出错甚至报错。Sort方法要的是数字结果,因为它不仅要知道元素是否相等,还要知道哪个“大”。

处理null值: 如果你的集合里可能有null,在null对象上调用方法(比如student1.Age.CompareTo(...),如果student1null)会抛出NullReferenceException。所以你的比较逻辑要显式处理null。一般规则是,null比任何非null都“小”。如果两个都是null,算相等。如果一个null,另一个不是,null“小”。

// 在lambda表达式里处理null的例子
students.Sort((s1, s2) => {
    if (s1 == null && s2 == null) return 0;
    if (s1 == null) return -1; // null最小
    if (s2 == null) return 1;  // 非null比null大
    return s1.Age.CompareTo(s2.Age); // 都非null时比较
});

好在实际项目里,集合很少有null,但一定要记住这个点!

性能: 虽然lambda表达式很方便,但如果在非常“热”的循环里或者超大集合里滥用,性能可能比高度优化的IComparer类略差(那些类可能经过专门测试和调优)。不过大多数日常场景下,这点差别可以忽略,代码的可读性和简洁性更重要。

复杂的多级比较: 就像我们按姓再按名排序的例子,lambda表达式可以嵌套多个条件。这比写十个if在一行里舒服多了!关键是——总是先判断第一个比较的结果(lastNameComparison != 0),再进入下一级。

lambda表达式和委托是C#的基础概念,让你能写出更灵活、函数式的代码。理解并会用它们,你的代码会更简洁、高效、现代。多多练习,很快你就能用得得心应手!

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION