Поделиться через


Руководство. Изучение статических виртуальных элементов в интерфейсах

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

В этом руководстве вы узнаете, как:

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

Предпосылки

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

Начнем с примера. Следующий метод возвращает середину двух 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 };

Чтобы предыдущий код T скомпилировался, необходимо объявить, что IAdditionOperators<TSelf, TOther, TResult> поддерживает интерфейс. Этот интерфейс включает статический 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);

Этот код можно сделать более повторно используемым, заявив, что эти типы реализуют соответствующие арифметические интерфейсы. Первое изменение, которое нужно сделать, это объявить, что Point<T, T> реализует интерфейс IAdditionOperators<Point<T>, Translation<T>, Point<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> определяет одно свойство только для чтения, 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. Создание универсальных алгоритмов, работающих с любым числовым типом. Создание алгоритмов с использованием этих интерфейсов, где аргумент типа реализует только подмножество числовых возможностей. Даже если вы не создаете новые интерфейсы, использующие эти возможности, вы можете экспериментировать с ними в алгоритмах.

См. также