从 C# 14 开始,顶级非泛型 static class
声明可以使用 extension
容器声明 扩展成员。 扩展成员是方法或属性,可以显示为实例或静态成员。 早期版本的 C# 通过向顶级非泛型静态类中声明的静态方法的第一个参数添加修饰符来启用this
。
该 extension
块指定扩展成员的类型和接收器。 可以在声明中声明方法和属性 extension
。 以下示例声明一个定义实例扩展方法和实例属性的扩展块。
public static class NumericSequences
{
extension(IEnumerable<int> sequence)
{
public IEnumerable<int> AddValue(int operand)
{
foreach (var item in sequence)
{
yield return item + operand;
}
}
public int Median
{
get
{
var sortedList = sequence.OrderBy(n => n).ToList();
int count = sortedList.Count;
int middleIndex = count / 2;
if (count % 2 == 0)
{
// Even number of elements: average the two middle elements
return (sortedList[middleIndex - 1] + sortedList[middleIndex]);
}
else
{
// Odd number of elements: return the middle element
return sortedList[middleIndex];
}
}
}
public int this[int index] => sequence.Skip(index).First();
}
}
extension
定义了接收方:sequence
,它是一个 IEnumerable<int>
。 接收方类型可以是非泛型、开放泛型或封闭的泛型类型。 该名称 sequence
位于该扩展中声明的每个实例成员的范围内。 扩展方法和属性都访问 sequence
。
任何扩展成员都可以被访问,就好像它们是接收方类型的成员一样:
IEnumerable<int> numbers = Enumerable.Range(1, 10);
numbers = numbers.AddValue(10);
var median = numbers.Median;
只要成员共享相同的接收方,就可以在单个容器中声明任意数量的成员。 还可以在单个类中声明任意数量的扩展块。 不同的扩展不需要声明相同的接收方类型或名称。 如果唯一的成员是静态的,则扩展参数不需要包含参数名称:
extension(IEnumerable<int>)
{
// Method:
public static IEnumerable<int> Generate(int low, int count, int increment)
{
for (int i = 0; i < count; i++)
yield return low + (i * increment);
}
// Property:
public static IEnumerable<int> Identity => Enumerable.Empty<int>();
}
静态扩展可以像调用接收器类型的静态成员一样调用:
var newSequence = IEnumerable<int>.Generate(5, 10, 2);
var identity = IEnumerable<int>.Identity;
重要
扩展不会引入成员声明 的范围 。 在单个类中声明的所有成员(即使在多个扩展中)必须具有唯一签名。 生成的签名在其名称中包含静态成员的接收方类型,以及扩展实例成员的接收方参数。
以下示例演示了使用修饰符的 this
扩展方法:
public static class NumericSequenceExtensionMethods
{
public static IEnumerable<int> AddValue(this IEnumerable<int> sequence, int operand)
{
foreach (var item in sequence)
yield return item + operand;
}
}
Add
方法可以像IEnumerable<int>
接口的成员一样从其他任何方法中被调用。
IEnumerable<int> numbers = Enumerable.Range(1, 10);
numbers = numbers.AddValue(10);
这两种扩展方法都生成相同的中间语言(IL)。 呼叫者不能区分它们。 事实上,你可以将现有扩展方法转换为新的成员语法,而不会引发破坏性变更。 格式既支持二进制兼容,也支持源代码兼容。
泛型扩展块
在扩展块中为声明的扩展成员指定类型参数的位置取决于需要该类型参数的位置:
- 在接收方中使用类型参数时,将类型参数添加到
extension
声明中。 - 当类型与接收器上指定的任何类型参数不同时,请将类型参数添加到成员声明中。
- 不能在这两个位置指定相同的类型参数。
以下示例显示了 IEnumerable<T>
的一个扩展块,其中两个扩展成员需要第二个类型参数:
public static class GenericExtensions
{
extension<TReceiver>(IEnumerable<TReceiver> source)
{
public IEnumerable<TReceiver> Spread(int start, int count)
=> source.Skip(start).Take(count);
public IEnumerable<TReceiver> Append<TArg>(IEnumerable<TArg> second, Func<TArg, TReceiver> Converter)
{
foreach(TReceiver item in source)
{
yield return item;
}
foreach (TArg item in second)
{
yield return Converter(item);
}
}
public IEnumerable<TReceiver> Prepend<TArg>(IEnumerable<TArg> second, Func<TArg, TReceiver> Converter)
{
foreach (TArg item in second)
{
yield return Converter(item);
}
foreach (TReceiver item in source)
{
yield return item;
}
}
}
}
成员 Append
和 Prepend
指定了用于转换的额外类型参数。 所有成员都不重复接收方的类型参数。
等效的扩展方法声明演示如何对这些类型参数进行编码:
public static class GenericExtensions
{
public static IEnumerable<T> Spread<T>(this IEnumerable<T> source, int start, int count)
=> source.Skip(start).Take(count);
public static IEnumerable<T1> Append<T1, T2>(this IEnumerable<T1> source, IEnumerable<T2> second, Func<T2, T1> Converter)
{
foreach (T1 item in source)
{
yield return item;
}
foreach (T2 item in second)
{
yield return Converter(item);
}
}
public static IEnumerable<T1> Prepend<T1, T2>(this IEnumerable<T1> source, IEnumerable<T2> second, Func<T2, T1> Converter)
{
foreach (T2 item in second)
{
yield return Converter(item);
}
foreach (T1 item in source)
{
yield return item;
}
}
}
另请参阅
C# 语言规范
有关详细信息,请参阅 C# 语言规范。 语言规范是 C# 语法和用法的明确来源。