设计优质扩展
应用最佳实践并避开常见问题。
设计优质扩展 是 CoddyKit 上的免费 C# Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。
设计令人喜爱的扩展
扩展方法功能强大,但强大的功能也容易被滥用。优秀的扩展用起来像类型本身自然具备的功能,易于发现,并且绝不会让调用者感到意外。本课将介绍实用的指导原则。
扩展行为,而非身份
对于在管道中读起来顺畅的实用行为,优先使用扩展。如果某个方法确实属于您拥有的类型,就应将它实现为真正的成员。
using System;
public static class TimeExtensions
{
// A pleasant, focused helper
public static bool IsWeekend(this DayOfWeek day)
=> day == DayOfWeek.Saturday || day == DayOfWeek.Sunday;
}
public class Program
{
public static void Main()
{
Console.WriteLine(DayOfWeek.Sunday.IsWeekend());
Console.WriteLine(DayOfWeek.Monday.IsWeekend());
}
}尽可能保持方法纯粹
返回新值且避免副作用的扩展最容易理解和链接。请尽量实现输入值、输出值的形式。
using System;
public static class StringExtensions
{
// Pure: depends only on input, returns a new string
public static string Truncate(this string s, int max)
=> s.Length <= max ? s : s.Substring(0, max) + "...";
}
public class Program
{
public static void Main()
{
Console.WriteLine("Extension methods are great".Truncate(10));
}
}防范 null 输入
由于调用者可以在 null 接收对象上调用扩展,请有意决定应如何处理这种情况。您可以平稳地容忍 null,也可以抛出清晰的 ArgumentNullException。
using System;
public static class StringExtensions
{
public static string SafeUpper(this string s)
{
if (s is null) throw new ArgumentNullException(nameof(s));
return s.ToUpper();
}
}
public class Program
{
public static void Main()
{
try { ((string)null).SafeUpper(); }
catch (ArgumentNullException ex) { Console.WriteLine("Caught: " + ex.ParamName); }
Console.WriteLine("ok".SafeUpper());
}
}以便于发现为目标命名
开发人员通常通过输入句点并查看 IntelliSense 来查找扩展。清晰的、以动词为基础且能描述结果的名称,可以让您的辅助方法更容易被发现,也能做到自我说明。
using System;
using System.Collections.Generic;
public static class CollectionExtensions
{
public static bool HasItems<T>(this ICollection<T> c)
=> c != null && c.Count > 0;
}
public class Program
{
public static void Main()
{
var list = new List<int> { 1 };
Console.WriteLine(list.HasItems());
}
}将相关扩展统一分组
请将同一概念的扩展放在一个命名良好的静态类和命名空间中。这样可以让导入更有意义,也让用户能够准确引入所需的辅助方法。
using System;
using System.Collections.Generic;
public static class EnumerableExtensions
{
public static IEnumerable<T> TakeFirst<T>(this IEnumerable<T> source, int n)
{
int taken = 0;
foreach (var item in source)
{
if (taken++ >= n) yield break;
yield return item;
}
}
}
public class Program
{
public static void Main()
{
var nums = new List<int> { 1, 2, 3, 4 };
Console.WriteLine(string.Join(",", nums.TakeFirst(2)));
}
}不要隐藏令人意外的行为
一个看起来开销很小、实际上却执行昂贵操作或有状态操作的扩展,会违背使用者的预期。请让开销显而易见;如果它查询数据库或修改状态,就应在名称中体现出来。
using System;
public static class MathExtensions
{
// Cheap and obvious: no hidden cost
public static int ClampMin(this int value, int min)
=> value < min ? min : value;
}
public class Program
{
public static void Main()
{
Console.WriteLine((-3).ClampMin(0));
Console.WriteLine(5.ClampMin(0));
}
}避免扩展 object
扩展 object 会让您的方法污染程序中的每一种类型,使 IntelliSense 到处变得杂乱。请改为针对合理的最具体类型或接口。
using System;
public static class IntExtensions
{
// Specific to int, not object - far less noisy
public static string ToOrdinal(this int n)
{
if (n % 10 == 1 && n % 100 != 11) return n + "st";
if (n % 10 == 2 && n % 100 != 12) return n + "nd";
if (n % 10 == 3 && n % 100 != 13) return n + "rd";
return n + "th";
}
}
public class Program
{
public static void Main()
{
Console.WriteLine(1.ToOrdinal());
Console.WriteLine(22.ToOrdinal());
}
}序列优先返回 IEnumerable
对于序列运算符,请返回 IEnumerable<T> 并使用延迟执行。这样便于组合,也可以避免强迫调用者立即生成可能并不需要的数据。
using System;
using System.Collections.Generic;
public static class SeqExtensions
{
public static IEnumerable<int> Doubled(this IEnumerable<int> source)
{
foreach (var n in source) yield return n * 2;
}
}
public class Program
{
public static void Main()
{
var nums = new List<int> { 1, 2, 3 };
foreach (var n in nums.Doubled()) Console.WriteLine(n);
}
}优秀扩展的检查清单
发布扩展前,请问自己:名称是否清晰?它是否是纯操作,或者副作用是否显而易见?是否处理了 null?目标类型是否足够具体?是否进行了合理分组?如果所有答案都是肯定的,您就拥有了其他人会乐于使用的辅助方法。
using System;
public static class StringExtensions
{
public static string OrDefault(this string s, string fallback)
=> string.IsNullOrEmpty(s) ? fallback : s;
}
public class Program
{
public static void Main()
{
Console.WriteLine("".OrDefault("N/A"));
Console.WriteLine("value".OrDefault("N/A"));
}
}亲自尝试
应用这份检查清单:指定具体的目标类型,使用清晰的名称,容忍 null,并返回纯结果。这个小型扩展符合每一项指导原则。
using System;
using System.Collections.Generic;
public static class DictionaryExtensions
{
// Clear name, specific type, null-safe, pure
public static TValue GetOrDefault<TKey, TValue>(
this IReadOnlyDictionary<TKey, TValue> dict, TKey key, TValue fallback)
{
if (dict == null) return fallback;
return dict.TryGetValue(key, out var value) ? value : fallback;
}
}
public class Program
{
public static void Main()
{
var ages = new Dictionary<string, int> { ["Ann"] = 30 };
Console.WriteLine(ages.GetOrDefault("Ann", -1));
Console.WriteLine(ages.GetOrDefault("Bob", -1));
}
}快速检查
应用设计指导原则。
回顾
设计良好的扩展用起来自然且可预测。
- 优先使用纯的、返回值且便于链接的辅助方法。
- 决定并记录对 null 的处理方式。
- 使用清晰的、以动词为基础的名称,以便于发现。
- 针对具体类型或接口,而不是
object。 - 将相关辅助方法分组,并为序列返回
IEnumerable<T>。
用 AI 导师学习 C# — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 93
- 课程
- 346
常见问题解答
「设计优质扩展」课时是免费的吗?
是的 — 「设计优质扩展」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。
「设计优质扩展」这节课中我会学到什么?
应用最佳实践并避开常见问题。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 C# Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「设计优质扩展」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 C# Academy 课中编写并运行代码吗?
能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。