Ранее в прошлой статье были рассмотрены блоки расширений, которые были добавлены в язык C# начиная с версии C# 14 и которые позволяют добавить функциональность к уже имеющимся типам без изменения исходного кода или наследования. Сейчас же отдельно рассмотрим обобщенные блоки расширений, которые типизируются одним или несколькими параметрами типа - подобно обобщенным типам и методам. Распространенным сценарием, где такие блоки могут понадобиться, это определение операторов.
Для начала рассмотрим простейший, хотя и не очень практичный пример:
string text = "Hello";
text >>= null;
static class Operators{
extension<T>(T self) where T: class?{
public void operator >>= (T _) => Console.WriteLine(self);
}
}
Здесь мы определяем для произвольного ссылочного типа оператор >>= (оператор сдвига вправо с последующим присвоением).
Прежде всего определяем блок расширений:
extension<T>(T self) where T: class?{
Этот блок типизирован параметром T, вместо которого будет передаваться некоторый тип данных. И как в случае с обобщенными типами и методами мы можем установить ограничения для обобщеного
блока расширений с помощью оператора where. В данном случае с помощью выражения where T: class? мы говорим, что тип, который будет передаваться вместо T, будет представлять класс или null
Также стоит обратить внимание, что это нестатический блок, и в данном случае мы определяем составной оператор присвоения - а начиная с C# 14 эти операторы можно определить с помощью нестатических методов. И в данном случае все, что делает наш оператор - это просто выводит значение объекта (левого операнда) на консоль:
public void operator >>= (T _) => Console.WriteLine(self);Поскольку это бинарный оператор, то нам через параметр надо передать правый операнд. Но здесь для упрощения примера он не используется, поэтому вместо названия параметра используется символ _. Конкретный тип этого параметра пока не важен.
И применение оператора:
string text = "Hello";
text >>= null;
Здесь к переменной text, которая представляет ссылочный тип string, применяем наш оператор >>=. Правый операнд здесь не важен, за тем исключением, что он должен представлять тот же тип, что и левый операнд
(так как параметр оператора имеет тип T) или null. Для примера я выбрал в качестве правого операнда null, хотя можно было бы использовать и строку:
string text = "Hello";
text >>= "world";
В любом случае, поскольку в коде оператора определен только вывод на консоль, то соответственно никакого сдвига или присвоения не происходило, и мы бы просто увидели вывод строки на консоль.
Но теперь пойдем дальше и рассмотрим более практический пример:
string text = "Hello METANIT.COM";
text >>= Console.WriteLine;
text >>= FramePrint;
text >>= mes => Console.WriteLine($"<{mes}>");
void FramePrint(string message) => Console.WriteLine($"***** {message} *****");
static class Operators{
extension<T>(T self) where T: class{
public void operator >>= (Action<T> op) => op(self);
}
}
Здесь изменено определение оператора - теперь в качестве параметра он принимает делегат Action<T> - то есть некоторое действие, которое имеет один параметр типа T - типа, к которому и применяется данный оператор.
А в коде оператора просто вызываем данный делегат.
Подобному делегату, например, соответствует метод Console.WriteLine, поэтому мы можем написать
text >>= Console.WriteLine;Либо мы можем определить свои методы или лямбда-выражения, которые будут использоваться в качестве правого операнда:
text >>= FramePrint;
text >>= mes => Console.WriteLine($"<{mes}>");
void FramePrint(string message) => Console.WriteLine($"***** {message} *****");
Консольный вывод подобной программы:
Hello METANIT.COM
***** Hello METANIT.COM *****
<Hello METANIT.COM>
И поскольку мы определили блок расширений как обобщенный, то тот же самый оператор мы можем применять к любому ссылочному типу. Например:
Person person = new("Bob", 44);
person >>= Console.WriteLine; // Bob (44)
person >>= (p) => p.Age = 25;
person >>= Console.WriteLine; // Bob (25)
person >>= SetDefault;
person >>= Console.WriteLine; // Tom (41)
void SetDefault(Person p)
{
p.Age = 41;
p.Name = "Tom";
}
class Person{
public string Name {get;set;}
public int Age {get;set;}
public Person(string name, int age)
{
Name = name;
Age = age;
}
public override string ToString() => $"{Name} ({Age})";
}
static class Operators{
extension<T>(T self) where T: class{
public void operator >>= (Action<T> op) => op(self);
}
}
В данном случае применяем оператор к кастомному типу Person. Причем поскольку это ссылочный тип, то мы можем в делегате-правом операнде изменять его свойства и доступные поля. Консольный вывод программы:
Значимые типы
В примере выше в качестве параметра типа в блоке расширения можно было использовать ссылочные типов (where T: class). Если же нам потребуется определить подобный оператор, но для значимых типов,
то кроме установки соответствующего ограничения (where T: struct) необходимо использовать модификатор ref для параметра расширения:
int n = 123;
n >>= Console.WriteLine;
static class Operators{
extension<T>(ref T self) where T: struct{
public void operator >>= (Action<T> op) => op(self);
}
}
Блок расширений может использовать несколько параметров типа, например, один для левого операнда, другой - для правого операнда, либо даже один операнд может использовать сразу несколько параметров типа. Например, рассмотрим применение нескольких параметров на примере статического блока расширения и для этого возьмем следующую задачу:
// проверяем строку и устанавливаем значение по умолчанию, если ввод отсутствует
var checkNum = (string? val) => val?.Length > 0 ? val : "11";
Console.Write("Введите число: ");
int num =
Console.ReadLine() // считываем число в виде строки
| checkNum // проверяем введенную строку (string -> string)
| Int32.Parse // парсим строку в число (string -> int)
| Square // возводим в квадрат (int -> int)
;
num >>= Console.WriteLine;
int Square(int n) => n * n;
static class Operators{
extension<T1, T2>(T1) {
public static T2 operator | (T1 self, Func<T1, T2> op) => op(self);
}
extension<T>(ref T self) where T: struct{
public void operator >>= (Action<T> op) => op(self);
}
}
Здесь блок расширения применяет два параметра типа - T1 и T2:
extension<T1, T2>(T1) {
public static T2 operator | (T1 self, Func<T1, T2> op) => op(self);
}
Причем сам блок расширения формально определен для типа T1. Внутри расширения определяем оператор |, первый параметр которого представляет тип T1 - тип левого операнда.
А вот правый операнд представляет делегат Func<T1, T2> - некоторое действие, которое принимает значение типа T1 и возвращает значение типа T2. Результат делегата и будет результатом всего оператора.
Причем важно отметить, что T1 и T2 могут представлять как разные типы, так и один и тот же тип.
Посмотрим на применение этого оператора:
int num =
Console.ReadLine() // считываем число в виде строки
| checkNum // проверяем введенную строку (string -> string)
| Int32.Parse // парсим строку в число (string -> int)
| Square // возводим в квадрат (int -> int)
;
Здесь мы имеем следующий порядок выполнения:
Console.ReadLine()Считываем число в виде строки
| checkNumВ качестве левого операнда оператору
|передается ранее введенная строка. Правый операнд - лямбда-выражениеcheckNum, которое проверяет строку, и, если эта строка равна null или не содержит символов, возвращает значение по умолчанию - "11"| Int32.ParseВ качестве левого операнда оператору
|передается строка - результатcheckNum. Правый операнд - методInt32.Parse(), который принимает строку и преобразует ее в целое число. Поэтому здесь оператор возвращает значение типаintВ данном случае не проверяется, если пользователь ввел не конвертируемую в число строку, но, я думаю, не составит труда добавить подобную логику по аналогии.
| SquareВ качестве левого операнда оператору
|передается полученное отInt32.Parseцелое число. Правый операнд - методSquare, который возводит переданное число в квадрат.
Примеры работы программы:
eugene@Eugene:/dotnet/c#/testapp$ dotnet run
Введите число: 15
225
eugene@Eugene:/dotnet/c#/testapp$ dotnet run
Введите число: -3
9
eugene@Eugene:/dotnet/c#/testapp$ dotnet run
Введите число:
121
eugene@Eugene:/dotnet/c#/testapp$ dotnet run