to the top
close form
Для получения триального ключа
заполните форму ниже
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Бесплатная лицензия PVS-Studio для специалистов Microsoft MVP
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
Мне интересно попробовать плагин на:
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

close form
check circle
Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

>
>
>
Generic Math: суперфича C#, доступная в…

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

27 Окт 2021
Автор:

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

Мы опубликовали и перевели эту статью с разрешения правообладателя. Автор статьи – DistortNeo. Оригинал опубликован на сайте Habr.

[Ссылка на статью Microsoft Announcing .NET 6 Preview 7.]

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

T Add<T>(T lhs, T rhs)
    where T : INumber<T>
{
    return lhs + rhs;
}
0878_Generic_Math_in_CSharp_ru/image1.png

Введение

До настоящего времени в 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 ....::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.

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

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

interface IOperationProvider<T>
{
    T Sum(T lhs, T rhs)
}
void SomeProcessing<T, TOperation>(...)
    where TOperation : struct, 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++.

Популярные статьи по теме
Под капотом SAST: как инструменты анализа кода ищут дефекты безопасности

Дата: 26 Янв 2023

Автор: Сергей Васильев

Сегодня речь о том, как SAST-решения ищут дефекты безопасности. Расскажу, как разные подходы к поиску потенциальных уязвимостей дополняют друг друга, зачем нужен каждый из них и как теория ложится на…
Wave Function Collapse для процедурной генерации в Unity

Дата: 24 Янв 2023

Автор: Андрей Москалёв

Wave Function Collapse – это алгоритм, c помощью которого можно реализовать генерацию чего угодно, что можно было бы описать с помощью правил или конкретным примером. В этой статье мы рассмотрим, как…
PVS-Studio научился анализировать Blazor компоненты

Дата: 10 Янв 2023

Автор: Алексей Авдеев

Всем привет. Перед вами небольшая статья о добавлении анализа Blazor компонентов в PVS-Studio. По ходу рассказа постараемся предугадать ваши немые вопросы по теме и ответить на них. Приятного прочтен…
Создание .NET библиотеки от А до Я

Дата: 05 Янв 2023

Автор: Гость

Думаете о создании .NET библиотеки, но не знаете, в какую сторону двигаться? Уже разрабатываете нечто подобное, но хочется открыть для себя что-то новое? Ищете варианты расширить автоматизацию? Не зн…
Топ-10 ошибок, найденных в C#-проектах за 2022 год

Дата: 28 Дек 2022

Автор: Никита Липилин

За 2022 год разработчики PVS-Studio написали много статей, в которых рассказали об ошибках, найденных в различных Open Source проектах. Пришло время подвести итоги и представить десяток самых интерес…

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

Следующие комментарии next comments
close comment form
Unicorn with delicious cookie
Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо