Руководство. Изучение функции C# 11 — статические виртуальные члены в интерфейсах

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

Из этого руководства вы узнаете, как выполнять следующие задачи:

  • Определите интерфейсы со статическими элементами.
  • Используйте интерфейсы для определения классов, реализующих интерфейсы с определенными операторами.
  • Создайте универсальные алгоритмы, использующие статические методы интерфейса.

Необходимые компоненты

Вам потребуется настроить компьютер для запуска .NET 7, который поддерживает C# 11. Компилятор C# 11 доступен начиная с Visual Studio 2022 версии 17.3 или пакета SDK для .NET 7.

Статические абстрактные методы интерфейса

Начнем с примера. Следующий метод возвращает середину двух double чисел:

public static double MidPoint(double left, double right) =>
    (left + right) / (2.0);

Та же логика будет работать для любого числового типа: int, short, longили floatdecimalлюбого типа, представляющего число. Необходимо иметь способ использовать + операторы и / операторы, а также определить значение для 2. Интерфейс можно использовать System.Numerics.INumber<TSelf> для записи предыдущего метода в качестве следующего универсального метода:

public static T MidPoint<T>(T left, T right)
    where T : INumber<T> => (left + right) / T.CreateChecked(2);  // note: the addition of left and right may overflow here; it's just for demonstration purposes

Любой тип, реализующий INumber<TSelf> интерфейс, должен включать определение для operator +и для operator /. Знаменатель определяется T.CreateChecked(2) для создания значения 2 для любого числового типа, что заставляет знаменатель совпадать с двумя параметрами. INumberBase<TSelf>.CreateChecked<TOther>(TOther) создает экземпляр типа из указанного значения и создает OverflowException исключение, если значение выходит за пределы представляющего диапазона. (Эта реализация может привести к переполнению, если left и right оба являются достаточно большими значениями. Существуют альтернативные алгоритмы, которые могут избежать этой потенциальной проблемы.)

Вы определяете статические абстрактные члены в интерфейсе с помощью знакомого синтаксиса: вы добавляете static модификаторы abstract к любому статическому элементу, который не предоставляет реализацию. В следующем примере определяется интерфейс, который можно применить к любому типу IGetNext<T> , который переопределяет operator ++:

public interface IGetNext<T> where T : IGetNext<T>
{
    static abstract T operator ++(T other);
}

Ограничение, которое реализует аргумент типа, Tгарантирует IGetNext<T> , что сигнатура для оператора включает содержащий тип или его аргумент типа. Многие операторы применяют, что его параметры должны соответствовать типу или быть параметром типа, ограниченным для реализации содержащего типа. Без этого ограничения оператор ++ не может быть определен в интерфейсе IGetNext<T> .

Вы можете создать структуру, которая создает строку символов "A", где каждый шаг добавляет другой символ в строку с помощью следующего кода:

public struct RepeatSequence : IGetNext<RepeatSequence>
{
    private const char Ch = 'A';
    public string Text = new string(Ch, 1);

    public RepeatSequence() {}

    public static RepeatSequence operator ++(RepeatSequence other)
        => other with { Text = other.Text + Ch };

    public override string ToString() => Text;
}

Как правило, можно создать любой алгоритм, в котором может потребоваться определить ++ значение "создать следующее значение этого типа". Использование этого интерфейса создает четкий код и результаты:

var str = new RepeatSequence();

for (int i = 0; i < 10; i++)
    Console.WriteLine(str++);

В предыдущем примере создаются следующие выходные данные:

A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA

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

Арифметические операторы в универсальных типах

Сценарий мотивации для разрешения статических методов, включая операторы, в интерфейсах — поддерживать универсальные математические алгоритмы. Библиотека базовых классов .NET 7 содержит определения интерфейса для многих арифметических операторов и производные интерфейсы, которые объединяют многие арифметические операторы в интерфейсе INumber<T> . Давайте применим эти типы к созданию записи, которая может использовать любой Point<T> числовой тип для T. Точка может быть перемещена + некоторыми XOffset и YOffset с помощью оператора.

Начните с создания консольного приложения с помощью dotnet new или Visual Studio.

Общедоступный интерфейс для этого Translation<T>Point<T> кода должен выглядеть следующим образом:

// Note: Not complete. This won't compile yet.
public record Translation<T>(T XOffset, T YOffset);

public record Point<T>(T X, T Y)
{
    public static Point<T> operator +(Point<T> left, Translation<T> right);
}

Тип используется record для обоих Translation<T> типов: Point<T> оба хранят два значения, и они представляют хранилище данных, а не сложное поведение. Реализация operator + будет выглядеть следующим образом:

public static Point<T> operator +(Point<T> left, Translation<T> right) =>
    left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };

Для компиляции предыдущего IAdditionOperators<TSelf, TOther, TResult> кода необходимо объявить, что T поддерживает интерфейс. Этот интерфейс включает статический operator + метод. Он объявляет три параметра типа: один для левого операнда, один для правого операнда и один для результата. Некоторые типы реализуются + для различных операндов и типов результатов. Добавьте объявление, которое аргумент типа T реализует IAdditionOperators<T, T, T>:

public record Point<T>(T X, T Y) where T : IAdditionOperators<T, T, T>

После добавления этого ограничения Point<T> класс может использовать + его оператор добавления. Добавьте то же ограничение для Translation<T> объявления:

public record Translation<T>(T XOffset, T YOffset) where T : IAdditionOperators<T, T, T>;

Ограничение IAdditionOperators<T, T, T> запрещает разработчику создавать Translation класс с помощью типа, который не соответствует ограничению для добавления в точку. Вы добавили необходимые ограничения в параметр Translation<T> типа, поэтому Point<T> этот код работает. Вы можете протестировать, добавив код, как показано ниже, в объявлениях Translation и Point в файле Program.cs :

var pt = new Point<int>(3, 4);

var translate = new Translation<int>(5, 10);

var final = pt + translate;

Console.WriteLine(pt);
Console.WriteLine(translate);
Console.WriteLine(final);

Этот код можно сделать более повторно используемым, заявив, что эти типы реализуют соответствующие арифметические интерфейсы. Первое изменение заключается в объявлении IAdditionOperators<Point<T>, Translation<T>, Point<T>> реализации Point<T, T> интерфейса. Тип Point использует различные типы для операндов и результатов. Тип Point уже реализует эту сигнатуру operator + , поэтому добавление интерфейса в объявление необходимо:

public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
    where T : IAdditionOperators<T, T, T>

Наконец, при добавлении полезно иметь свойство, определяющее значение аддитивного удостоверения для этого типа. Существует новый интерфейс для этой функции: IAdditiveIdentity<TSelf,TResult> Перевод {0, 0} является аддитивной идентификацией: результирующая точка совпадает с левым операндом. Интерфейс IAdditiveIdentity<TSelf, TResult> определяет одно свойство readonly, AdditiveIdentityкоторое возвращает значение удостоверения. Для Translation<T> реализации этого интерфейса требуется несколько изменений:

using System.Numerics;

public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>
    where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    public static Translation<T> AdditiveIdentity =>
        new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);
}

Здесь есть несколько изменений, поэтому давайте рассмотрим их по одному. Сначала вы объявляете, что Translation тип реализует IAdditiveIdentity интерфейс:

public record Translation<T>(T XOffset, T YOffset) : IAdditiveIdentity<Translation<T>, Translation<T>>

Далее можно попробовать реализовать элемент интерфейса, как показано в следующем коде:

public static Translation<T> AdditiveIdentity =>
    new Translation<T>(XOffset: 0, YOffset: 0);

Предыдущий код не компилируется, так как 0 зависит от типа. Ответ: используйте IAdditiveIdentity<T>.AdditiveIdentity для 0. Это изменение означает, что ограничения теперь должны включать T в себя реализующие IAdditiveIdentity<T>. Это приводит к следующей реализации:

public static Translation<T> AdditiveIdentity =>
    new Translation<T>(XOffset: T.AdditiveIdentity, YOffset: T.AdditiveIdentity);

Теперь, когда вы добавили это ограничение, необходимо добавить то же ограничение Translation<T>в Point<T>:

using System.Numerics;

public record Point<T>(T X, T Y) : IAdditionOperators<Point<T>, Translation<T>, Point<T>>
    where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
    public static Point<T> operator +(Point<T> left, Translation<T> right) =>
        left with { X = left.X + right.XOffset, Y = left.Y + right.YOffset };
}

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

  • Напишите метод, зависящий от INumber<T> интерфейса, чтобы этот метод мог использоваться с любым числовым типом.
  • Создайте тип, основанный на интерфейсах сложения для реализации типа, поддерживающего только одну математические операции. Этот тип объявляет поддержку тех же интерфейсов, чтобы его можно было создавать другими способами. Алгоритмы записываются с помощью самого естественного синтаксиса математических операторов.

Поэкспериментируйте с этими функциями и зарегистрируйте отзывы. Вы можете использовать пункт меню "Отправить отзыв" в Visual Studio или создать новую проблему в репозитории roslyn на сайте GitHub. Создание универсальных алгоритмов, работающих с любым числовым типом. Алгоритмы сборки с помощью этих интерфейсов, где аргумент типа может реализовать только подмножество возможностей типа. Даже если вы не создаете новые интерфейсы, использующие эти возможности, вы можете экспериментировать с ними в алгоритмах.

См. также