Теперь можно
Теперь можно ?

В С# 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# код, если что
Это C# код, если что

Теперь аналогичное поведение можно реализовать и в 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.

И что будет дальше?

Осталось только приделать pipe-оператор, каррирование и discriminated unions
Осталось только приделать pipe-оператор, каррирование и discriminated unions

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

Комментарии (16)


  1. Popou
    26.11.2025 05:49

    Оххх сколько хауса будет, наверное появиться анализаторы запрещающие создавать расширение операторов для встроенных классов, ну или вообще запретят расширять операторы


    1. simplepersonru
      26.11.2025 05:49

      > сколько хауса

      p.s. хаоса, но мой комент только ради спойлера


      1. VBDUnit
        26.11.2025 05:49

        Артхауса же. А вообще штука годная, даже не для артхаусного кода. Наконец‑то можно будет сократить пепекторы до вменяемого размера


    1. withkittens
      26.11.2025 05:49

      ну или вообще запретят расширять операторы

      Нет, конечно. Extension operators - это не "случайно так получилось, что теперь с этим делать?", а фича.


  1. 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-штуки не сочетаются с "нормальной" разработкой. Очень странный курс развития выбрали майкрософты


    1. Gromilo
      26.11.2025 05:49

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


    1. iamkisly
      26.11.2025 05:49

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


      1. voroninp
        26.11.2025 05:49

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


    1. maxcat
      26.11.2025 05:49

      Давно могли бы на ps переехать, если бы хотели


  1. roqin
    26.11.2025 05:49

    Ну discriminated unions вроде в какой-то следующей версии обещают. Может быть.


  1. Gromilo
    26.11.2025 05:49

    Мы вот для коллекции используем линку через методы расширения, а для одиночных значений так нельзя :(

    Поэтому написал свой Pipe оператор.

    Выглядит так: `order?.Pipe(ExtractData) ?? EmptyData();`


    1. voroninp
      26.11.2025 05:49

      Так нельзя — как именно нельзя?


  1. CitizenOfDreams
    26.11.2025 05:49

    Дисклеймер: Я не призываю никого писать такой код.

    Если в языке есть возможность писать такой код - кто-то будет писать такой код.


  1. equmag
    26.11.2025 05:49

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

    По некоторой причине есть категория людей, которая катастрофически не хочет давать разработчикам новую силу, потому что найдется тот, кто применит ее во зло. Видимо стакан у некоторых людей всегда наполовину пуст


    1. withkittens
      26.11.2025 05:49

      Уже печально известный .IsNullOrEmpty в виде свойства 

      Не работает это, компилятор резолвит "".IsNullOrEmpty как method group.


  1. iamkisly
    26.11.2025 05:49

    Да как обычно сейчас github переполнится шиткодом, как и при введении предыдущих фич.. все ж захотят попробовать. А затем на этом будет обучаться copilot, лол