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

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

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


  1. Popou
    26.11.2025 05:49

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