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


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

C# 11 и .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 оба являются достаточно большими значениями. Существуют альтернативные алгоритмы, которые могут избежать этой потенциальной проблемы.)

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

См. также