Generic Math: суперфича C#, доступная в .NET 6 Preview 7

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

10 августа 2021 года Microsoft в блоге опубликовала информацию о свежевыпущенном .NET 6 Preview 7.

Помимо добавления очередной порции синтаксического сахара, расширения функционала библиотек, улучшения поддержки UTF-8 и т.д., в данное обновление была включена демонстрация суперфичи — абстрактные статические методы интерфейсов и реализованная на её основе возможность использования арифметических операторов в дженериках:

T Add<T>(T lhs, T rhs)
    where T : INumber<T>
{
    return lhs + rhs;
}

Введение

До настоящего времени в C# не было возможности абстрагироваться от статических методов и писать обобщённый код. Особенно проблематично это для методов, которые существуют только в виде статических методов, например, для операторов.

Например, в LINQ to objects функции .Max, .Sum, .Average и т.д. реализованы отдельно для каждого из простых типов, а для пользовательских типов предлагается передавать делегат. Это и неудобно, и неэффективно: при многократном дублировании кода есть возможность ошибиться, а вызов делегата не даётся бесплатно (впрочем, уже идут обсуждения о реализации zero-cost делегатов в JIT-компиляторе).

Нововведение добавляет возможность писать обобщённый код относительно, например, числовых типов, на которые наложены ограничения в виде интерфейсов с нужными операторами. Таким образом, алгоритмы могут выражены в следующем виде:

// Interface specifies static properties and operators
interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

// Classes and structs (including built-ins) can implement interface
struct Int32 : …, IAddable<Int32>
{
    static Int32 I.operator +(Int32 x, Int32 y) => x + y; // Explicit
    public static int Zero => 0;                          // Implicit
}

// Generic algorithms can use static members on T
public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;                   // Call static operator
    foreach (T t in ts) { result += t; } // Use `+`
    return result;
}

// Generic method can be applied to built-in and user-defined types
int sixtyThree = AddAll(new [] { 1, 2, 4, 8, 16, 32 });

Реализация

Синтаксис

Статические члены, которые являются частью контракта интерфейса, объявляются с использованием ключевых слов static и abstract.

Хотя слово static было бы идеально для описания подобных методов, в одном из недавних обновлений была добавлена возможность объявлять вспомогательные статические методы в интерфейсах. Поэтому, чтобы отличать вспомогательные методы от статических членов контракта, было решено использовать модификатор abstract.

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

Вызвать статические методы интерфейса можно только через обобщённый тип и только если на тип наложено соответствующее ограничение:

public static T AddAll<T>(T[] ts) where T : IAddable<T>
{
    T result = T.Zero;            // Correct
    T result2 = IAddable<T>.Zero; // Incorrect
}

Также стоит понимать, что статические методы не были виртуальным и никогда ими не будут:

interface IStatic
{
    static abstract int StaticValue { get; }
    int Value { get; }
}

class Impl1 : IStatic
{
    public static int StaticValue => 1;
    public int Value => 1;
}

class Impl2 : Impl1, IStatic
{
    public static int StaticValue => 2;
    public int Value => 2;
}

static void Print<T>(T obj)
    where T : IStatic
{
    Console.WriteLine("{0}, {1}", T.StaticValue, obj.Value);
}

static void Test()
{
    Impl1 obj1 = new Impl1();
    Impl2 obj2 = new Impl2();
    Impl1 obj3 = obj2;

    Print(obj1);    // 1, 1
    Print(obj2);    // 2, 2
    Print(obj3);    // 1, 2
}

Вызов статического метода интерфейса определяется на этапе компиляции (на самом деле, JIT-компиляции, а не сборки C# кода). Таким образом, можно утверждать: ура, в C# завезли статический полиморфизм!

Под капотом

Посмотрим на сгенерированный IL-код для простейшей функции, суммирующей два числа:

.method private hidebysig static !!0/*T*/
  Sum<(class [System.Runtime]System.INumber`1<!!0/*T*/>) T>(
    !!0/*T*/ lhs,
    !!0/*T*/ rhs
  ) cil managed
{
  .maxstack 8

  // [4903 17 - 4903 34]
  IL_0000: ldarg.0      // lhs
  IL_0001: ldarg.1      // rhs
  IL_0002: constrained. !!0/*T*/
  IL_0008: call         !2/*T*/ class [System.Runtime]System.IAdditionOperators`3<!!0/*T*/, !!0/*T*/, !!0/*T*/>::op_Addition(!0/*T*/, !1/*T*/)
  IL_000d: ret

} // end of method GenericMathTest::Sum

Ничего примечательного: просто невиртуальный вызов статического метода интерфейса для типа T (для виртуальных вызовов используется callvirt). Оно и понятно: как можно сделать виртуальный вызов без объекта?

Поначалу у меня была мысль, что это сахар, сделанный через какие-нибудь магические объекты, создаваемые в единственном экземпляре для каждой пары тип-интерфейс, но нет: это честная реализация новой фичи на уровне JIT-компилятора: для простых типов компилятор генерирует инструкцию соответствующей операции, для остальных типов — вызывает соответствующий метод. Из этого можно сделать вывод, что код, использующий новые возможности, не сможет работать на более старых рантаймах.

Также стоить ожидать, что JIT-компилятор будет компилировать метод для каждой комбинации обобщённых типов, для которых вызываются статические методы интерфейсов. То есть производительность обобщённых методов, вызывающих статические методы интерфейсов, не должна отличаться от производительности частных реализаций.

Статус

Несмотря на то, что есть возможность пощупать эту возможность уже сейчас, она запланирована к релизу только в .NET 7, а после релиза .NET 6 останется в состоянии preview. Сейчас эта фича находится в состоянии активной разработки, детали её реализации могут измениться, поэтому просто брать и использовать её пока нельзя.

Попробовать на практике

Чтобы поиграться с новой возможностью, нужно добавить свойство EnablePreviewFeatures=true в файл проекта и подключить NuGet пакет System.Runtime.Experimental:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <EnablePreviewFeatures>true</EnablePreviewFeatures>
    <LangVersion>preview</LangVersion>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Runtime.Experimental" Version="6.0.0-preview.7.21377.19" />
  </ItemGroup>

</Project>

Само собой, должен быть установлен .NET 6 Preview 7 SDK и в качестве целевой платформы указано net6.0.

Мои впечатления

Попробовал — очень понравилось. То, что я давно ждал, потому что раньше проблему приходилось решать через костыли, например, так:

struct IOperationProvider<T>
{
    T Sum(T lhs, T rhs)
}

void SomeProcessing<T, TOperation>(...)
    where TOperation : IOperationProvider<T>
{
    T var1 = ...;
    T var2 = ...;
    T sum = default(TOperation).Sum(var1, var2);  // This is zero cost!
}

Альтарнатива такому костылю: реализация типом T интерфейса IOperation и вызов var1.Sum(var2). Но в данном случае теряется производительность из-за виртуальных вызовов, да и банально не во все классы можно залезть и добавить интерфейс.

Ещё один положительный момент — производительность. Я немного позапускал бенчмарки: скорость работы обычного кода и кода с generic арифметикой оказалась одинаковой. То есть мои ранее описанные предположения относительно JIT-компиляции кода оказались верны.

А что вот немного расстроило, так это то, что с типами-перечислениями эта фича не работает. Сравнивать их придётся по-прежнему через EqualityComparer<T>.Default.Equals.

Также не понравилось, что приходится использовать слово-костыль abstract. Похоже, сложность C# достигла уже того уровня, что добавление новых фишек без ущерба для старого функционала становится затруднительным, и фактически приходим к тому, что сейчас происходит с C++.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Хорошая фича?
55.56% Да, давно хотелось её видеть 5
0% Не особо нужна 0
44.44% Хватит из C# делать C++! 4
Проголосовали 9 пользователей. Воздержались 3 пользователя.
Источник: https://habr.com/ru/post/572902/


Интересные статьи

Интересные статьи

BaseN кодировки используются для кодирования двоичных данных в текстовый вид. Где N это размер текстового алфавита используемого для кодирования. Применяются BaseN кодиро...
Предисловие Однажды в далекой-далекой галактике… потребовалось нам реализовать аутентификацию пользователей с помощью учетной записи ЕСИА на ГосУслугах. Т.к. обитаем мы в галактике .Net, первым ...
Привет, друзья! Меня зовут Петр, я представитель малого белорусского бизнеса со штатом чуть более 20 сотрудников. В данной статье хочу поделиться негативным опытом покупки 1С-Битрикс. ...
Тема статьи навеяна результатами наблюдений за методикой создания шаблонов различными разработчиками, чьи проекты попадали мне на поддержку. Порой разобраться в, казалось бы, такой простой сущности ка...
Практически все коммерческие интернет-ресурсы создаются на уникальных платформах соответствующего типа. Среди них наибольшее распространение получил Битрикс24.