
В С# 14 появился новый синтаксис расширений (extension members), позволяющий добавлять методы, свойства и даже перегружать операторы для существующих типов без создания врапперов и без изменения исходных типов.
Благодаря этому, стал возможен, например, вот такой код:
var str = "Hello, C# code!"
| No
| ReplaceToFSharp
| ToUpper;
Console.WriteLine(str); // NO! HELLO F#-LIKE CODE!
Дисклеймер: Я не призываю никого писать такой код. Статья написана в юмористических целях, но c долей полезной информации.
Блоки расширений
Нововведение, о котором сегодня пойдёт речь, – extension members. Мне больше нравится называть «блоками расширений», поэтому дальше я буду использовать именно этот термин.
Блоки расширений позволяют определять сразу несколько расширений для одного типа. Например класс c обычными методами расширения:
public static class Extensions
{
public static bool IsNullOrEmpty(this string source) =>
string.IsNullOrEmpty(source);
public static bool IsNullOrWhiteSpace(this string source) =>
string.IsNullOrWhiteSpace(source);
}
теперь можно записать вот так:
public static class ExtensionMembers
{
extension(string source)
{
public bool IsNullOrEmpty() =>
string.IsNullOrEmpty(source);
public bool IsNullOrWhiteSpace() =>
string.IsNullOrWhiteSpace(source);
}
}
Но кроме методов, можно определить расширение в виде оператора… И это открывает простор для полёта фантазии. Например, теперь можно написать расширение и умножать строки как в Python:
Console.WriteLine("C# goes b" + "r" * 10);
public static class ExtensionMembers
{
extension(string source)
{
public static string operator *(string str, int count)
{
return string.Concat(Enumerable.Repeat(str, count));
}
}
}
Точно так же можно складывать массивы:
int[] a = [1, 2, 3];
int[] b = [4, 5, 6];
var concat = a + b;
Console.WriteLine(string.Join(", ", concat));
public static class ExtensionMembers
{
extension<T>(T[] arr)
{
public static T[] operator +(T[] a, T[] b)
{
var result = new T[a.Length + b.Length];
Array.Copy(a, result, a.Length);
Array.Copy(b, 0, result, a.Length, b.Length);
return result;
}
}
}
А помните мемы про JavaScript со складыванием и вычитанием строк и чисел?

Теперь аналогичное поведение можно реализовать и в C#!
Console.WriteLine("5" - 3); // 2
Console.WriteLine("5" + 3); // 53
Console.WriteLine("10" - "4"); // 6
public static class ExtensionMembers
{
extension(string source)
{
public static int operator -(string str, int number) =>
int.Parse(str) - number;
public static string operator +(string str, int number) =>
str + number.ToString();
public static int operator -(string a, string b) =>
int.Parse(a) - int.Parse(b);
}
}
JavaScript разработчикам будет значительно проще вкатиться в С#.
Ну и напоследок – превратим C# в F#. Для этого нужно объявить обобщённое расширение для оператора | (побитовое ИЛИ) и всё – pipe-оператор из говна и палок готов.
public static class FunctionalExtensions
{
extension<T, TResult>(T)
{
public static TResult operator |(T source, Func<T, TResult> f) =>
f(source);
}
}

Теперь можно объявить несколько статических методов и делать цепочку вызовов функций прямо как в функциональных языках.
using static StringExtensions;
var str = "Hello, C# code!"
| No
| ReplaceToFSharp
| ToUpper;
Console.WriteLine(str); // NO! HELLO F#-LIKE CODE!
public static class StringExtensions
{
public static string No(string source) =>
$"No! {source}";
public static string ReplaceToFSharp(string source) =>
source.Replace("C#", "F#-like");
public static string ToUpper(string source) =>
source.ToUpper();
}
Что же под капотом?
Синтаксический сахар (или синтаксическая соль, тут уж кому как) который мы использовали, компилируется в довольно страшную конструкцию.
Например, код имитирующий F#, упрощённо можно записать вот так.
var str = FunctionalExtensions.op_BitwiseOr(
FunctionalExtensions.op_BitwiseOr(
FunctionalExtensions.op_BitwiseOr(
"Hello, C# code!",
new Func<string, string>(No)
),
new Func<string, string>(ReplaceToFSharp)
),
new Func<string, string>(ToUpper)
);
Кстати, код выше компилируется и выполняется, потому что компилятор для оператора | генерирует статический дженерик op_BitwiseOr, доступный из пользовательского кода как обычный метод:
public static class FunctionalExtensions
{
public static TResult op_BitwiseOr<T, TResult>(T source, Func<T, TResult> f)
{
return f(source);
}
}
А как было раньше?
Раньше тоже можно было писать похожий код, но если класс недоступен для изменений, как тот же string, то нужно было писать враппер. Пример кода в функциональном стиле с использованием структуры-враппера:
using static FunctionalString;
var str = new FunctionalString("Hello, C# code!")
| No
| ReplaceToFSharp
| ToUpper;
Console.WriteLine(str); // Output: NO, HELLO F#-LIKE CODE!
public record struct FunctionalString(string Value)
{
public static FunctionalString operator |(FunctionalString fs, Func<FunctionalString, FunctionalString> f) =>
f(fs);
public static implicit operator FunctionalString(string s) =>
new(s);
public static implicit operator string(FunctionalString fs) =>
fs.Value;
public static FunctionalString No(FunctionalString fs) =>
$"No! {fs.Value}";
public static FunctionalString ReplaceToFSharp(FunctionalString fs) =>
fs.Value.Replace("C#", "F#-like");
public static FunctionalString ToUpper(FunctionalString fs) =>
fs.Value.ToUpper();
}
Как видим, код не такой элегантный, как в C# 14.
И что будет дальше?

Разработчикам C# осталось лишь добавить pipe-оператор, каррирование, discriminated unions и можно, как минимум, отправлять F# на пенсию.
Popou
Оххх сколько хауса будет, наверное появиться анализаторы запрещающие создавать расширение операторов для встроенных классов, ну или вообще запретят расширять операторы