Примечание
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
C# 11 и .NET 7 включают статические виртуальные члены в интерфейсы. Эта функция позволяет определять интерфейсы, включающие перегруженные операторы или другие статические члены. Определив интерфейсы со статическими элементами, эти интерфейсы можно использовать в качестве ограничений для создания универсальных типов, использующих операторы или другие статические методы. Даже если вы не создаете интерфейсы с перегруженными операторами, вы, скорее всего, получите выгоду от этой функции и универсальных математических классов, включенных обновлением языка.
Из этого руководства вы узнаете, как выполнять следующие задачи:
- Определите интерфейсы со статическими элементами.
- Используйте интерфейсы для определения классов, реализующих интерфейсы с определенными операторами.
- Создайте универсальные алгоритмы, использующие статические методы интерфейса.
Требования
- Последняя версия .NET SDK
- Visual Studio Code редактор
- C# DevKit
Статические абстрактные методы интерфейса
Начнем с примера. Следующий метод возвращает середину двух double
чисел:
public static double MidPoint(double left, double right) =>
(left + right) / (2.0);
Та же логика будет работать для любого числового типа: int
, short
, long
или float
decimal
любого типа, представляющего число. Необходимо иметь способ использовать +
операторы и /
операторы, а также определить значение для 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. Создание универсальных алгоритмов, работающих с любым числовым типом. Создавайте алгоритмы с помощью этих интерфейсов, где аргумент типа может поддерживать только подмножество числовых возможностей. Даже если вы не создаете новые интерфейсы, использующие эти возможности, вы можете экспериментировать с ними в алгоритмах.