Расширения типов позволяют добавлять новые компоненты - методы, свойства и операторы к уже существующим типам без создания нового производного типа, перекомпиляции или иного изменения исходного типа. Даже если нет доступа к исходному коду этого типа. Для расширения типов применяются два типа синтаксиса: методы расширений и блоки расширений. Рассмотрим, как их можно использовать.
Методы расширений (extension methods) позволяют добавлять новые методы в уже существующие типы без создания нового производного класса. Эта функциональность бывает особенно полезна, когда нам хочется добавить в некоторый тип новый метод, но сам тип (класс или структуру) мы изменить не можем, поскольку у нас нет доступа к исходному коду. Либо если мы не можем использовать стандартный механизм наследования, например, если классы определенны с модификатором sealed.
Например, нам надо добавить для типа string новый метод:
string s = "Привет мир";
char c = 'и';
int i = s.CharCount(c);
Console.WriteLine(i);
public static class StringExtension
{
public static int CharCount(this string str, char c)
{
int counter = 0;
for (int i = 0; i < str.Length; i++)
{
if (str[i] == c)
counter++;
}
return counter;
}
}
Для того, чтобы создать метод расширения, вначале надо создать статический класс, который и будет содержать этот метод. В данном случае это класс
StringExtension. Затем объявляем статический метод. Суть нашего метода расширения - подсчет количества определенных символов в строке.
Собственно метод расширения - это обычный статический метод, который в качестве первого параметра всегда принимает такую конструкцию:
this имя_типа название_параметра, то есть в нашем случае this string str. Так как наш метод будет относиться к типу string,
то мы и используем данный тип.
Затем у всех строк мы можем вызвать данный метод:
int i = s.CharCount(c);Причем нам уже не надо указывать первый параметр. Значения для остальных параметров передаются в обычном порядке.
Применение методов расширения очень удобно, но при этом надо помнить, что метод расширения никогда не будет вызван, если он имеет ту же сигнатуру, что и метод, изначально определенный в типе.
Также следует учитывать, что методы расширения действуют на уровне пространства имен. То есть, если добавить в проект другое пространство имен, то метод не будет применяться к строкам, и в этом случае надо будет подключить пространство имен метода через директиву using.
Начиная с версии C# 14 можно использовать новый синтаксис для определения новых компонентов уже существующих типов - с помощью блоков extension, которые далее мы будем именовать блоками расширений
Блоки расширения поддерживают определение методов, свойств и операторов. С помощью блоков расширения можно определять два типа расширений:
Расширения экземпляров: расширяют экземпляр типа
Статические расширения: расширяют сам тип
Синтаксически блоки расширений - это блоки кода, которые предваряются словом extension:
extension (тип получатель | тип){
// определения
// методов
// свойств
// операторов
}
После слова extension в круглых скобках указывается тип, для которого определяется расширение. Для расширений экземпляров после типа также указывается получатель - по сути переменная этого типа, через которую внутри блока расширения можно ссылаться на объект типа.
Затем в фигурных скобках располагаются определения методов, свойств и операторов, которыми расширяется указанный тип.
Блоки расширения размещаются в статическом классе, который не является обобщенным и не вложен в другой класс.
Причем, стоит отметить, что extension - это не ключевое слово, а контекстное (контекстуальное) слово, и мы вполне его можем использовать в другом значение, например, в качестве имени переменной:
var extension = "ext";
Console.WriteLine(extension);
Расширения экземпляров
Расширения экземпляров применяются к эземпляру типа. Например, используем блоки расширений для добавления к типу string метода подсчета вхождений определенного символа:
static class StringExtension
{
extension(string str)
{
public int CharCount(char c)
{
int counter = 0;
for (int i = 0; i < str.Length; i++)
{
if (str[i] == c) counter++;
}
return counter;
}
}
}
Здесь блок расширения определен для типа string, а в качестве получателя выступает str. То есть через строку str мы сможем ссылать на строку, к которой применяется
расширение. В данном случае внутри блока определен только один метод CharCount, который получает символ и вычисляет количество его вхождений в строке.
Далее мы можем применить этот метод:
string s = "Привет мир";
char c = 'и';
int i = s.CharCount(c);
Console.WriteLine(i); // 2
static class StringExtension
{
extension(string str)
{
public int CharCount(char c)
{
int counter = 0;
for (int i = 0; i < str.Length; i++)
{
if (str[i] == c) counter++;
}
return counter;
}
}
}
Аналогичным образом можно определять свойства. Например, добавим для строки свойство, которое будет показывать, является ли строка целым числом:
string str1 = "Hello world";
Console.WriteLine(str1.IsNumber); // false
string str2 = "1234567";
Console.WriteLine(str2.IsNumber); // true
string str3 = "123456yyy678";
Console.WriteLine(str3.IsNumber); // false
static class StringExtension
{
extension(string str)
{
public bool IsNumber
{
get
{
for (int i = 0; i < str.Length; i++)
{
if (str[i] < '0' || str[i] > '9') return false;
}
return true;
}
}
}
}
Здесь для строк добавлено свойство IsNumber, которое просто проходит по всем символам строки и, если символ выходит за границы диапазона [1-9], то возвращает false
(то есть строка не является целым числом).
Статические расширения
Статические расширения применяются к типу в целом. Методы и свойства статических расширений определяются с модификатором static. Например:
Console.WriteLine(string.Default);
static class StringExtension
{
extension(string) // для статических расширений указываем только тип
{
public static string Default => "Hello World";
}
}
Здесь для типа string определено расширение StringExtension с одним свойством - Default, которое просто возвращает некоторую строку по умолчанию.
Поскольку это расширение статическое, для для обращения к его компонентам применяется имя типа: string.Default
Другой распространенный пример статических расширений - определение операторов. Например, добавим для строк операции сравнения:
Console.WriteLine("hello" > "ello"); // true
Console.WriteLine("hello" < "ello"); // false
static class StringExtension
{
extension(string)
{
public static bool operator >(string str1, string str2) => str1.Length > str2.Length;
public static bool operator <(string str1, string str2) => str1.Length < str2.Length;
public static string Default => "Hello World";
}
}
В данном случае сравниваем строки по их длине.
Другой пример - попробуем сложить два массива:
int[] nums1 = [1, 2, 3, 4];
int[] nums2 = [6, 7, 8];
int[] nums3 = nums1 + nums2; // ! ошибка
Однако при попытке скомпилировать этот код, компилятор нам выбросит ошибку, так как для массивов типа int[] оператор сложения не определен.
Теперь используем статические расширения, чтобы решить данную задачу:
int[] nums1 = [1, 2, 3, 4];
int[] nums2 = [6, 7, 8];
int[] nums3 = nums1 + nums2;
foreach(int i in nums3)
{
Console.Write(i);
Console.Write(" "); // пробел - разделитель между числами
}
Console.WriteLine();
static class ArrayExtension
{
extension(int[])
{
public static int[] operator +(int[] numbers, int[] other)
{
// возвращаемый массив, который объединяет оба исходных
int[] new_numbers = new int[numbers.Length + other.Length];
int j = 0; // индекс итогового массива
// добавляем элементы из первого массива
for(int i = 0; i < numbers.Length; i++, j++)
{
new_numbers[j] = numbers[i];
}
// добавляем элементы из второго массива
for(int i = 0; i < other.Length; i++, j++)
{
new_numbers[j] = other[i];
}
return new_numbers;
}
}
}
Поскольку это статическое расширение, то блок расширений определен для типа (без указания получателя):
extension(int[])В блоке определен только оператор сложения, который в принципе должен быть статическим. Логика оператора проста - создаем новый массив, который объекдиняет оба складываемых массива
Благодаря этому мы сложем объединить два массива в один. Консольный вывод прогарммы:
1 2 3 4 6 7 8 Причем в одном классе расширений можно объединять статические расширения и расширения экземпляров:
int[] nums1 = [1, 2, 3, 4];
int[] nums2 = [6, 7, 8];
int[] nums3 = nums1 + nums2;
nums3.Print();
static class ArrayExtension
{
// расширение экземпляра
extension(int[] numbers)
{
public void Print()
{
foreach(int i in numbers)
{
Console.Write(i);
Console.Write(" ");
}
Console.WriteLine();
}
}
// статическое расширение
extension(int[])
{
public static int[] operator +(int[] numbers, int[] other)
{
int[] new_numbers = new int[numbers.Length + other.Length];
int j = 0;
for(int i = 0; i < numbers.Length; i++, j++)
{
new_numbers[j] = numbers[i];
}
for(int i = 0; i < other.Length; i++, j++)
{
new_numbers[j] = other[i];
}
return new_numbers;
}
}
}