
В С# 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# на пенсию.
Комментарии (16)

eeeeeeeeeeee
26.11.2025 05:49В контексте file-based apps (https://habr.com/ru/articles/965532/, https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/tutorials/file-based-programs) выглядит очень интересно. Возможно, некоторые команды переедут с bash / python на c#. Осталось только дождаться, когда умельцы напишут nuget-пакеты со всеми утилитами
Но если честно, есть некоторое ощущение хаоса. Все эти shell-штуки не сочетаются с "нормальной" разработкой. Очень странный курс развития выбрали майкрософты

Gromilo
26.11.2025 05:49Видимо слава питона не даёт покоя. Тоже хотят, чтобы программы распространялись копипастой

iamkisly
26.11.2025 05:49Слишком долго dotnet был нишевым windows only решением, теперь им "надо бежать в двое быстрее, чтобы оставаться на месте" и хорошо что у кампании есть на это деньги

voroninp
26.11.2025 05:49Ещё б они поменьше ИИ в каждую щель пихали, не смещая акцент с нормальной разработки. В команде EF три человека осталось. MAUI тоже подрезали.

CitizenOfDreams
26.11.2025 05:49Дисклеймер: Я не призываю никого писать такой код.
Если в языке есть возможность писать такой код - кто-то будет писать такой код.

equmag
26.11.2025 05:49extension members просили довольно давно и это отличное нововведение, хотя два года я их и не ждал. Возможность расширять свойства и статические классы (вроде Math) также даёт много возможностей, которые я весьма приветствую. Уже печально известный .IsNullOrEmpty в виде свойства разойдется наверно по тысячам кодовых баз.
По некоторой причине есть категория людей, которая катастрофически не хочет давать разработчикам новую силу, потому что найдется тот, кто применит ее во зло. Видимо стакан у некоторых людей всегда наполовину пуст

withkittens
26.11.2025 05:49Уже печально известный .IsNullOrEmpty в виде свойства
Не работает это, компилятор резолвит
"".IsNullOrEmptyкак method group.

iamkisly
26.11.2025 05:49Да как обычно сейчас github переполнится шиткодом, как и при введении предыдущих фич.. все ж захотят попробовать. А затем на этом будет обучаться copilot, лол
Popou
Оххх сколько хауса будет, наверное появиться анализаторы запрещающие создавать расширение операторов для встроенных классов, ну или вообще запретят расширять операторы
simplepersonru
> сколько хауса
p.s. хаоса, но мой комент только ради спойлера
VBDUnit
Артхауса же. А вообще штука годная, даже не для артхаусного кода. Наконец‑то можно будет сократить пепекторы до вменяемого размера
withkittens
Нет, конечно. Extension operators - это не "случайно так получилось, что теперь с этим делать?", а фича.