
В С# 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# на пенсию.
