C# Academy · 课时

扩展接口与泛型

一次为多种类型添加行为。

第 2 / 4 课13 个步骤

扩展接口与泛型 是 CoddyKit 上的免费 C# Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

扩展接口

您可以编写一个扩展方法,使其 this 参数为接口。每个实现该接口的类型都会立即获得这个方法。这正是 LINQ 如何为 IEnumerable<T> 添加数十个方法的方式。

在可枚举序列上创建泛型扩展

通过让方法对 T 使用泛型,并扩展 IEnumerable<T>,这个辅助方法就可以适用于任何序列:列表、数组、查询结果等。

using System;
using System.Collections.Generic;

public static class EnumerableExtensions
{
    public static bool IsEmpty<T>(this IEnumerable<T> source)
    {
        foreach (var item in source) return false;
        return true;
    }
}

public class Program
{
    public static void Main()
    {
        var nums = new List<int> { 1, 2, 3 };
        Console.WriteLine(nums.IsEmpty());
        Console.WriteLine(new int[0].IsEmpty());
    }
}

迭代序列

在扩展内部,您可以像处理任何 IEnumerable<T> 一样处理该参数。这里我们不依赖 LINQ 来计算总和。

using System;
using System.Collections.Generic;

public static class EnumerableExtensions
{
    public static int SumValues(this IEnumerable<int> source)
    {
        int total = 0;
        foreach (var n in source) total += n;
        return total;
    }
}

public class Program
{
    public static void Main()
    {
        var nums = new List<int> { 10, 20, 30 };
        Console.WriteLine(nums.SumValues());
    }
}

返回新序列

扩展也可以返回序列。结合 yield return 后,它们就能成为采用 LINQ 风格的延迟执行运算符。

using System;
using System.Collections.Generic;

public static class EnumerableExtensions
{
    public static IEnumerable<T> EveryOther<T>(this IEnumerable<T> source)
    {
        bool take = true;
        foreach (var item in source)
        {
            if (take) yield return item;
            take = !take;
        }
    }
}

public class Program
{
    public static void Main()
    {
        var letters = new List<string> { "a", "b", "c", "d", "e" };
        foreach (var l in letters.EveryOther())
            Console.WriteLine(l);
    }
}

泛型类型约束

您可以限制泛型参数,使其必须具备特定能力。这里的 where T : IComparable<T> 让我们能够比较元素以找出最大值。

using System;
using System.Collections.Generic;

public static class EnumerableExtensions
{
    public static T MaxItem<T>(this IEnumerable<T> source) where T : IComparable<T>
    {
        bool started = false;
        T best = default;
        foreach (var item in source)
        {
            if (!started || item.CompareTo(best) > 0) { best = item; started = true; }
        }
        return best;
    }
}

public class Program
{
    public static void Main()
    {
        var nums = new List<int> { 3, 9, 1, 7 };
        Console.WriteLine(nums.MaxItem());
    }
}

扩展自定义接口

您自己的接口同样可以受益。请先定义接口,然后通过扩展附加共享行为,而不是在每个实现类型中重复编写。

using System;

public interface INamed { string Name { get; } }

public class Dog : INamed { public string Name => "Rex"; }

public static class NamedExtensions
{
    public static string Greeting(this INamed n) => "Hello, " + n.Name;
}

public class Program
{
    public static void Main()
    {
        var d = new Dog();
        Console.WriteLine(d.Greeting());
    }
}

多个类型参数

扩展可以使用多个类型参数实现泛型化。这个投影辅助方法使用一个函数映射序列,作用与 LINQ 的 Select 类似。

using System;
using System.Collections.Generic;

public static class EnumerableExtensions
{
    public static IEnumerable<TResult> Map<TSource, TResult>(
        this IEnumerable<TSource> source, Func<TSource, TResult> f)
    {
        foreach (var item in source) yield return f(item);
    }
}

public class Program
{
    public static void Main()
    {
        var nums = new List<int> { 1, 2, 3 };
        foreach (var s in nums.Map(n => "#" + n))
            Console.WriteLine(s);
    }
}

与内置 LINQ 结合

您自己的扩展方法可以与 LINQ 共存。您可以在一个流畅的管道中,将自定义运算符与标准运算符链接起来。

using System;
using System.Collections.Generic;
using System.Linq;

public static class EnumerableExtensions
{
    public static IEnumerable<T> EveryOther<T>(this IEnumerable<T> source)
    {
        bool take = true;
        foreach (var item in source) { if (take) yield return item; take = !take; }
    }
}

public class Program
{
    public static void Main()
    {
        var nums = Enumerable.Range(1, 10);
        var result = nums.Where(n => n > 2).EveryOther().ToList();
        Console.WriteLine(string.Join(", ", result));
    }
}

扩展 IEnumerable 可覆盖多种类型

由于数组、列表、字典、哈希集和查询结果都实现了 IEnumerable<T>,一个扩展就能同时覆盖它们。

using System;
using System.Collections.Generic;

public static class EnumerableExtensions
{
    public static int CountItems<T>(this IEnumerable<T> source)
    {
        int n = 0;
        foreach (var item in source) n++;
        return n;
    }
}

public class Program
{
    public static void Main()
    {
        Console.WriteLine(new[] { 1, 2, 3 }.CountItems());
        Console.WriteLine(new HashSet<string> { "a", "b" }.CountItems());
    }
}

此模式为何易于扩展

接口扩展无需继承就能提供多态辅助方法。只需针对接口编写一次逻辑,所有当前和未来的实现类型都可以直接使用。

using System;
using System.Collections.Generic;

public static class EnumerableExtensions
{
    public static string JoinWith<T>(this IEnumerable<T> source, string sep)
    {
        return string.Join(sep, source);
    }
}

public class Program
{
    public static void Main()
    {
        var nums = new List<int> { 1, 2, 3 };
        Console.WriteLine(nums.JoinWith(" -> "));
    }
}

亲自尝试

请在 IEnumerable<T> 上构建一个小型泛型运算符,并将它与 LINQ 链接起来。注意它如何适用于任何序列类型。

using System;
using System.Collections.Generic;
using System.Linq;

public static class EnumerableExtensions
{
    public static IEnumerable<T> WithoutDuplicatesByKey<T, TKey>(
        this IEnumerable<T> source, Func<T, TKey> keySelector)
    {
        var seen = new HashSet<TKey>();
        foreach (var item in source)
            if (seen.Add(keySelector(item))) yield return item;
    }
}

public class Program
{
    public static void Main()
    {
        var words = new[] { "apple", "avocado", "banana", "cherry", "blueberry" };
        var firstPerLetter = words.WithoutDuplicatesByKey(w => w[0]);
        Console.WriteLine(string.Join(", ", firstPerLetter));
    }
}

快速检查

思考一下 LINQ 风格的扩展是如何声明的。

回顾

扩展接口,尤其是 IEnumerable<T>,是采用 LINQ 风格编程的基础。

  • 使方法具有泛型,并将接口用作 this 类型。
  • 使用 yield return 构建延迟执行的运算符。
  • 应用 where 约束,要求类型具备 IComparable<T> 等能力。
  • 一个接口扩展可以覆盖所有实现类型,包括未来的实现类型。
免费开始

用 AI 导师学习 C# — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
93
课程
346

常见问题解答

「扩展接口与泛型」课时是免费的吗?

是的 — 「扩展接口与泛型」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「扩展接口与泛型」这节课中我会学到什么?

一次为多种类型添加行为。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「扩展接口与泛型」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 定义扩展方法
  2. 扩展接口与泛型
  3. 扩展方法解析
  4. 设计优质扩展
← 返回 C# Academy