Samouczek: eksplorowanie funkcji języka C# 11 — statyczne wirtualne elementy członkowskie w interfejsach

Język C# 11 i .NET 7 zawierają statyczne elementy wirtualne w interfejsach. Ta funkcja umożliwia definiowanie interfejsów obejmujących przeciążone operatory lub inne statyczne elementy członkowskie. Po zdefiniowaniu interfejsów ze statycznymi elementami członkowskimi można użyć tych interfejsów jako ograniczeń do tworzenia typów ogólnych, które używają operatorów lub innych metod statycznych. Nawet jeśli nie utworzysz interfejsów z przeciążonymi operatorami, prawdopodobnie skorzystasz z tej funkcji i ogólnych klas matematycznych włączonych przez aktualizację języka.

Z tego samouczka dowiesz się, jak wykonywać następujące czynności:

  • Zdefiniuj interfejsy ze statycznymi elementami członkowskimi.
  • Użyj interfejsów, aby zdefiniować klasy implementujące interfejsy ze zdefiniowanymi operatorami.
  • Utwórz ogólne algorytmy, które opierają się na metodach interfejsu statycznego.

Wymagania wstępne

Musisz skonfigurować maszynę do uruchamiania platformy .NET 7, która obsługuje język C# 11. Kompilator języka C# 11 jest dostępny od programu Visual Studio 2022 w wersji 17.3 lub zestawu .NET 7 SDK.

Statyczne metody interfejsu abstrakcyjnego

Zacznijmy od przykładu. Poniższa metoda zwraca punkt środkowy dwóch double liczb:

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

Ta sama logika działa dla dowolnego typu liczbowego: int, , longshort, floatdecimallub dowolnego typu reprezentującego liczbę. Musisz mieć sposób używania + operatorów i / i, aby zdefiniować wartość dla 2elementu . Za pomocą interfejsu System.Numerics.INumber<TSelf> można napisać poprzednią metodę jako następującą metodę ogólną:

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

Każdy typ, który implementuje INumber<TSelf> interfejs, musi zawierać definicję dla operator +, i dla operator /. Mianownik jest definiowany przez T.CreateChecked(2) element w celu utworzenia wartości 2 dla dowolnego typu liczbowego, co wymusza, aby mianownik był tego samego typu co dwa parametry. INumberBase<TSelf>.CreateChecked<TOther>(TOther) Tworzy wystąpienie typu z określonej wartości i zgłasza wartość OverflowException , jeśli wartość spadnie poza reprezentowany zakres. (Ta implementacja ma potencjał przepełnienia, jeśli left i right są wystarczająco duże wartości. Istnieją alternatywne algorytmy, które mogą uniknąć tego potencjalnego problemu).

Statyczne elementy abstrakcyjne są definiowane w interfejsie przy użyciu znanej składni: można dodać static modyfikatory i abstract do wszystkich statycznych elementów członkowskich, które nie zapewniają implementacji. W poniższym przykładzie zdefiniowano IGetNext<T> interfejs, który można zastosować do dowolnego typu, który zastępuje operator ++element :

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

Ograniczenie, które argument typu, T, implementuje IGetNext<T> zapewnia, że podpis dla operatora zawiera typ zawierający lub jego argument typu. Wiele operatorów wymusza, że jego parametry muszą być zgodne z typem lub być ograniczeniem parametru typu, aby zaimplementować typ zawierający. Bez tego ograniczenia ++ nie można zdefiniować operatora w interfejsie IGetNext<T> .

Można utworzyć strukturę, która tworzy ciąg znaków "A", gdzie każdy przyrost dodaje kolejny znak do ciągu przy użyciu następującego kodu:

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;
}

Ogólnie rzecz biorąc, można utworzyć dowolny algorytm, w którym można zdefiniować ++ wartość "generuj następną wartość tego typu". Użycie tego interfejsu generuje przejrzysty kod i wyniki:

var str = new RepeatSequence();

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

Powyższy przykład generuje następujące dane wyjściowe:

A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA

W tym małym przykładzie pokazano motywację dla tej funkcji. Można użyć składni naturalnej dla operatorów, wartości stałych i innych operacji statycznych. Te techniki można eksplorować podczas tworzenia wielu typów, które opierają się na statycznych elementach członkowskich, w tym przeciążonych operatorów. Zdefiniuj interfejsy zgodne z możliwościami typów, a następnie zadeklaruj obsługę tych typów dla nowego interfejsu.

Ogólna matematyka

Motywującym scenariuszem zezwalania na metody statyczne, w tym operatory, w interfejsach jest obsługa ogólnych algorytmów matematycznych . Biblioteka klas bazowych platformy .NET 7 zawiera definicje interfejsu dla wielu operatorów arytmetycznych i interfejsów pochodnych łączących wiele operatorów arytmetycznych w interfejsie INumber<T> . Zastosujmy te typy do utworzenia rekordu Point<T> , który może używać dowolnego typu liczbowego dla elementu T. Punkt może zostać przeniesiony przez niektóre XOffset i YOffset za pomocą + operatora .

Zacznij od utworzenia nowej aplikacji konsolowej przy użyciu programu dotnet new Lub Visual Studio.

Interfejs publiczny dla elementu Translation<T> i Point<T> powinien wyglądać podobnie do następującego kodu:

// 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);
}

Należy użyć record typu zarówno dla typów, jak Translation<T> i Point<T> : Oba przechowują dwie wartości i reprezentują magazyn danych, a nie zaawansowane zachowanie. Implementacja elementu będzie wyglądać podobnie do następującego operator + kodu:

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

Aby poprzedni kod został skompilowany, należy zadeklarować, że T obsługuje IAdditionOperators<TSelf, TOther, TResult> interfejs. Ten interfejs zawiera metodę statyczną operator + . Deklaruje trzy parametry typu: jeden dla lewego operandu, jeden dla prawego operandu i jeden dla wyniku. Niektóre typy implementują + różne typy operacji i wyników. Dodaj deklarację, T którą argument typu implementuje IAdditionOperators<T, T, T>:

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

Po dodaniu tego ograniczenia Point<T> klasa może użyć operatora dodawania + elementu . Dodaj to samo ograniczenie w deklaracji Translation<T> :

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

Ograniczenie IAdditionOperators<T, T, T> uniemożliwia deweloperowi utworzenie klasy przy użyciu Translation typu, który nie spełnia ograniczeń dodatku do punktu. Dodano niezbędne ograniczenia do parametru type dla Translation<T> parametru i Point<T> dlatego ten kod działa. Możesz przetestować, dodając kod podobny do poniższego powyżej deklaracji Translation i Point w pliku 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);

Ten kod może być bardziej wielokrotnego użytku, deklarując, że te typy implementują odpowiednie interfejsy arytmetyczne. Pierwszą zmianą, którą należy wprowadzić, jest zadeklarowanie, że Point<T, T> implementuje IAdditionOperators<Point<T>, Translation<T>, Point<T>> interfejs. Typ Point używa różnych typów dla operandów i wyniku. Typ Point już implementuje element operator + z tym podpisem, więc dodanie interfejsu do deklaracji jest potrzebne:

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

Na koniec, gdy wykonujesz dodawanie, warto mieć właściwość definiującą wartość tożsamości addytywnej dla tego typu. Istnieje nowy interfejs dla tej funkcji: IAdditiveIdentity<TSelf,TResult>. Translacja {0, 0} jest tożsamością addytywną: wynikowy punkt jest taki sam jak lewy operand. Interfejs IAdditiveIdentity<TSelf, TResult> definiuje jedną właściwość readonly , AdditiveIdentityktóra zwraca wartość tożsamości. Wymaga Translation<T> kilku zmian w celu zaimplementowania tego interfejsu:

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);
}

Istnieje kilka zmian w tym miejscu, więc przejdźmy przez nie jeden po drugim. Najpierw należy zadeklarować, że Translation typ implementuje IAdditiveIdentity interfejs:

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

Następnie możesz spróbować zaimplementować element członkowski interfejsu, jak pokazano w poniższym kodzie:

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

Powyższy kod nie zostanie skompilowany, ponieważ 0 zależy od typu. Odpowiedź: Użyj polecenia IAdditiveIdentity<T>.AdditiveIdentity dla .0 Ta zmiana oznacza, że ograniczenia muszą teraz obejmować T implementację IAdditiveIdentity<T>. Powoduje to następującą implementację:

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

Teraz, po dodaniu tego ograniczenia w systemie Translation<T>, musisz dodać to samo ograniczenie do Point<T>polecenia :

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 };
}

W tym przykładzie przedstawiono, jak interfejsy ogólnego redagowania matematycznego. W tym samouczku omówiono:

  • Napisz metodę, która polegała na interfejsie INumber<T> , aby można było użyć metody z dowolnym typem liczbowym.
  • Utwórz typ, który opiera się na interfejsach dodawania, aby zaimplementować typ, który obsługuje tylko jedną operację matematyczną. Ten typ deklaruje obsługę tych samych interfejsów, dzięki czemu może być komponowany w inny sposób. Algorytmy są pisane przy użyciu najbardziej naturalnej składni operatorów matematycznych.

Poeksperymentuj z tymi funkcjami i zarejestruj opinię. Możesz użyć elementu menu Wyślij opinię w programie Visual Studio lub utworzyć nowy problem w repozytorium roslyn w usłudze GitHub. Twórz ogólne algorytmy, które działają z dowolnym typem liczbowym. Tworzenie algorytmów przy użyciu tych interfejsów, w których argument typu może implementować tylko podzbiór możliwości przypominających liczbę. Nawet jeśli nie utworzysz nowych interfejsów korzystających z tych możliwości, możesz eksperymentować z użyciem ich w algorytmach.

Zobacz też