Объявление расширения (справочник по C#)

Начиная с C# 14, негенерические static class объявления верхнего уровня могут использовать extension блоки для объявления членов расширения. Члены расширения являются методами или свойствами и могут быть экземплярами или статическими элементами. Более ранние версии C# позволяют использовать методы расширения путем добавления this в качестве модификатора к первому параметру статического метода, объявленного в негенерическом статичном классе верхнего уровня.

Справочные документы по языку C# описывают последнюю выпущенную версию языка C#. Она также содержит начальную документацию по функциям в общедоступных предварительных версиях для предстоящего языкового выпуска.

Документация определяет любую функцию, впервые представленную в последних трех версиях языка или в текущих общедоступных предварительных версиях.

Подсказка

Чтобы узнать, когда функция впервые появилась в C#, ознакомьтесь со статьей об истории версий языка C#.

Блок extension задает тип и приемник для членов расширения. Методы, свойства, индексаторы и операторы можно объявить внутри extension объявления. В следующем примере объявляется один блок расширения, определяющий метод расширения экземпляра, свойство экземпляра и статический метод оператора.

Замечание

Все примеры, приведенные в этой статье, включают в себя XML-комментарии для членов и блока расширения. Узел в блоке extension описывает расширенный тип и параметр приемника. Компилятор C# копирует этот узел в созданный элемент для всех членов в блоке расширения. В этих примерах демонстрируется предпочтительный стиль создания XML-документации для членов расширения.

/// <summary>
/// Contains extension members for numeric sequences.
/// </summary>
public static class NumericSequences
{
    /// <summary>
    /// Defines extensions for integer sequences.
    /// </summary>
    /// <param name="sequence">The sequence used as a receiver.</param>
    extension(IEnumerable<int> sequence)
    {
        /// <summary>
        /// Adds a scalar value to each element in the sequence.
        /// </summary>
        /// <param name="operand">The amount to add.</param>
        /// <returns>
        /// A new sequence where each value contains the updated value.
        /// </returns>
        public IEnumerable<int> AddValue(int operand)
        {
            foreach (var item in sequence)
            {
                yield return item + operand;
            }
        }

        /// <summary>
        /// Gets the median value of the sequence.
        /// </summary>
        /// <remarks>
        /// This value is calculated when requested.
        /// </remarks>
        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]) / 2;
                }
                else
                {
                    // Odd number of elements: return the middle element
                    return sortedList[middleIndex];
                }
            }
        }

        /// <summary>
        /// Concatenates two sequences of integers into a single sequence.
        /// </summary>
        /// <remarks>The resulting sequence enumerates all elements from <paramref name="left"/> in order,
        /// followed by all elements from <paramref name="right"/>. Enumeration is deferred and performed lazily as the
        /// returned sequence is iterated.</remarks>
        /// <param name="left">The first sequence of integers to concatenate. Cannot be null.</param>
        /// <param name="right">The second sequence of integers to concatenate. Cannot be null.</param>
        /// <returns>A sequence that contains the elements of the first sequence followed by the
        /// elements of the second sequence.</returns>
        public static IEnumerable<int> operator +(IEnumerable<int> left, IEnumerable<int> right)
            => left.Concat(right);
    }
}

Определенный extension определяет приемник: sequence, который является IEnumerable<int>. Тип приемника может быть негенерным, открытым универсальным или закрытым универсальным типом. Имя sequence находится в области каждого элемента экземпляра, объявленного в этом расширении. Метод расширения и свойство оба имеют доступ к sequence.

Вы обращаетесь к любому из членов расширения, как будто они были членами типа приемника:

IEnumerable<int> numbers = Enumerable.Range(1, 10);
numbers = numbers.AddValue(10);

var median = numbers.Median;

var combined = numbers + Enumerable.Range(100, 10);

Вы можете объявить любое количество членов в одном блоке, если они совместно используют один и тот же получатель. Кроме того, можно объявить столько блоков расширения в одном классе. Разные расширения не обязаны объявлять одинаковый тип или название получателя. Параметр расширения не должен включать имя параметра, если только члены являются статическими:

/// <summary>
/// Provides static extensions for the <see cref="IEnumerable{Int32}"/> type.
/// </summary>
extension(IEnumerable<int>)
{
    // Method:
    /// <summary>
    /// Generates a sequence of integers, starting from a specified value and incrementing by a given amount.
    /// </summary>
    /// <param name="low">The starting value of the sequence.</param>
    /// <param name="count">The number of integers to generate. Must be non-negative.</param>
    /// <param name="increment">The value by which to increment each subsequent integer in the sequence.</param>
    /// <returns>
    /// An enumerable collection of integers, beginning with the specified starting value and containing the
    /// specified number of elements, each incremented by the given amount.
    /// </returns>
    public static IEnumerable<int> Generate(int low, int count, int increment)
    {
        for (int i = 0; i < count; i++)
            yield return low + (i * increment);
    }

    // Property:
    /// <summary>
    /// Gets an empty sequence of integers representing the identity element for sequence operations.
    /// </summary>
    /// <remarks>
    /// This property can be used as a neutral starting point when aggregating or composing
    /// sequences of integers. The returned sequence is always empty and does not allocate any storage.
    /// </remarks>
    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;
    }
}

Метод можно вызвать AddValue из любого другого IEnumerable<int> метода, как если бы он был членом интерфейса:

IEnumerable<int> numbers = Enumerable.Range(1, 10);
numbers = numbers.AddValue(10);

Обе формы методов расширения создают один и тот же промежуточный язык (IL). Вызывающие не могут различать их. Фактически можно преобразовать существующие методы расширения в новый синтаксис члена без критического изменения. Форматы являются совместимыми как на двоичном уровне, так и на уровне исходного кода.

Универсальные блоки расширений

Если указать параметры типа для члена расширения, объявленного в блоке расширения, зависит от того, где требуется параметр типа:

  • Добавьте параметр type в extension объявление, когда параметр типа используется в приемнике.
  • Добавьте параметр типа в объявление члена, если тип отличается от любого параметра типа, указанного в приемнике.
  • Нельзя указать один и тот же параметр типа в обоих местах.

В следующем примере показан блок расширения, для IEnumerable<T> которого два члена расширения требуют второго параметра типа:

/// <summary>
/// Contains generic extension members for sequences.
/// </summary>
public static class GenericExtensions
{
    /// <summary>
    /// Defines extensions for generic sequences.
    /// </summary>
    /// <typeparam name="TReceiver">The type of elements in the receiver sequence.</typeparam>
    /// <param name="source">The sequence used as a receiver.</param>
    extension<TReceiver>(IEnumerable<TReceiver> source)
    {
        /// <summary>
        /// Returns a sequence containing a specified number of elements from the source, starting at a given index.
        /// </summary>
        /// <param name="start">The zero-based index at which to begin retrieving elements. Must be greater than or equal to 0.</param>
        /// <param name="count">The number of elements to return. Must be greater than or equal to 0.</param>
        /// <returns>
        /// An <see cref="IEnumerable{TReceiver}"/> that contains up to <paramref name="count"/> elements from the
        /// source sequence, starting at the element at position <paramref name="start"/>. If <paramref name="start"/>
        /// is greater than the number of elements in the source, an empty sequence is returned.
        /// </returns>
        public IEnumerable<TReceiver> Spread(int start, int count)
            => source.Skip(start).Take(count);

        /// <summary>
        /// Returns a sequence that contains the elements of the original sequence followed by the elements of a
        /// specified sequence, each transformed by a converter function.
        /// </summary>
        /// <remarks>
        /// Enumeration of the returned sequence will not start until the sequence is iterated.
        /// The converter function is applied to each element of the appended sequence as it is enumerated.
        /// </remarks>
        /// <typeparam name="TArg">The type of the elements in the sequence to append.</typeparam>
        /// <param name="second">The sequence whose elements are to be appended after being converted. Cannot be null.</param>
        /// <param name="Converter">A function to convert each element of the appended sequence to the result type. Cannot be null.</param>
        /// <returns>
        /// An IEnumerable<TReceiver> that contains the elements of the original sequence followed by the converted
        /// elements of the specified sequence.
        /// </returns>
        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);
            }
        }

        /// <summary>
        /// Returns a sequence that consists of the elements of the specified collection, transformed by the provided
        /// converter, followed by the elements of the current sequence.
        /// </summary>
        /// <remarks>
        /// Enumeration of the returned sequence will not start until the sequence is iterated.
        /// Both the input collection and the converter function must not be null; otherwise, an exception will be
        /// thrown at enumeration time.
        /// </remarks>
        /// <typeparam name="TArg">The type of the elements in the collection to prepend.</typeparam>
        /// <param name="second">The collection whose elements are to be transformed and prepended to the current sequence. Cannot be null.</param>
        /// <param name="converter">A function to convert each element of the prepended collection to the target type. Cannot be null.</param>
        /// <returns>
        /// An IEnumerable<TReceiver> that contains the converted elements of the specified collection followed by the
        /// elements of the current sequence.
        /// </returns>
        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;
            }
        }

        /// <summary>
        /// Gets an empty sequence of the receiver's element type.
        /// </summary>
        /// <remarks>
        /// This property can be used as a neutral starting point when aggregating or composing
        /// sequences of the receiver's element type. The returned sequence is always empty and does not allocate any storage.
        /// </remarks>
        public static IEnumerable<TReceiver> Identity => Enumerable.Empty<TReceiver>();
    }
}

Члены 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# 15, можно объявить индексаторы в блоке extension . Индексатор расширений позволяет индексировать приемник, как если бы индексатор был объявлен в типе приемника. Так как индексаторы всегда являются элементами экземпляров, блок расширения, объявляющий индексатор, должен предоставить именованный параметр приемника. Индексаторы расширений поддерживают те же функции, что и get обычные индексаторы, включая set и методы доступа, методы доступа с поддержкой выражений и возвращающие ссылки методы доступа.

В следующем примере объявляется индексатор IEnumerable<int> только для получения, возвращающий элемент по указанной позиции:

/// <summary>
/// Contains an extension indexer for numeric sequences.
/// </summary>
public static class SequenceIndexer
{
    /// <summary>
    /// Defines an indexer for integer sequences.
    /// </summary>
    /// <param name="sequence">The sequence used as a receiver.</param>
    extension(IEnumerable<int> sequence)
    {
        /// <summary>
        /// Gets the element at the specified position in the sequence.
        /// </summary>
        /// <param name="index">The zero-based position of the element to retrieve.</param>
        /// <value>The element at the specified position.</value>
        public int this[int index] => sequence.ElementAt(index);
    }
}

Индексировать приемник, как будто индексатор был членом типа приемника:

int third = numbers[2];

Доступ к индексу и диапазону

Члены расширения также могут предоставлять элементы, необходимые для неявного Index и Range доступа. Свойство Length или Count свойство и целочисленный индексатор поддерживают индексирование с конца. Метод Slice(int, int) поддерживает доступ к диапазону. Эти члены могут быть элементами экземпляра, членами расширения или сочетанием обоих элементов.

Например, Readings упаковывает массив без объявления элементов индексирования. Его расширения предоставляют Countцелочисленный индексатор и Slice:

/// <summary>Stores sensor readings.</summary>
public sealed class Readings(int[] values)
{
    /// <summary>Gets the stored readings.</summary>
    public int[] Values { get; } = values;
}

/// <summary>Provides indexing for sensor readings.</summary>
public static class ReadingExtensions
{
    /// <summary>Defines indexing members for readings.</summary>
    /// <param name="readings">The readings to access.</param>
    extension(Readings readings)
    {
        /// <summary>Gets the number of readings.</summary>
        public int Count => readings.Values.Length;

        /// <summary>Gets a reading by position.</summary>
        /// <param name="index">The zero-based position.</param>
        public int this[int index] => readings.Values[index];

        /// <summary>Gets a range of readings.</summary>
        /// <param name="start">The first position.</param>
        /// <param name="length">The number of readings.</param>
        public Readings Slice(int start, int length)
            => new(readings.Values.AsSpan(start, length).ToArray());
    }
}

Эти члены обеспечивают обычный индексирование, индексирование от конечных, диапазонов и шаблонов списка:

Readings readings = new([10, 20, 30, 40]);
Console.WriteLine(readings[^1]);                    // output: 40
Console.WriteLine(readings[1..3][0]);               // output: 20
Console.WriteLine(readings is [10, .., 40]);        // output: True

Поиск и ограничения

Для подстановки индексатора компилятор пытается применить элементы в следующем порядке:

  1. Индексатор, объявленный типом приемника.
  2. Неявный индекс или доступ к диапазону с помощью элементов экземпляра.
  3. Индексатор расширений.
  4. Неявный индекс или доступ к диапазону с помощью элементов расширения.

Более ранняя применимая форма имеет приоритет над более поздней. Например, индексатор целых чисел экземпляра со свойством экземпляра Length может обрабатываться receiver[^1] перед индексатором расширения, который напрямую принимает Index.

Индексаторы расширений и связанные члены имеют следующие ограничения:

  • Индексаторы расширений не используются для доступа к элементам строки или массива, даже если объявлять расширения для этих типов.
  • Дерево выражений не может содержать свойство расширения или доступ индексатора.
  • Расширения Length и Count свойства не участвуют в оптимизации распространения выражений коллекции.

См. также

Спецификация языка C#

Дополнительные сведения см. в спецификации языка C#. Спецификация языка является авторитетным источником синтаксиса и использования языка C#.