Share via


Självstudie: Utforska funktionen C# 11 – statiska virtuella medlemmar i gränssnitt

C# 11 och .NET 7 innehåller statiska virtuella medlemmar i gränssnitt. Med den här funktionen kan du definiera gränssnitt som innehåller överbelastade operatorer eller andra statiska medlemmar. När du har definierat gränssnitt med statiska medlemmar kan du använda dessa gränssnitt som begränsningar för att skapa generiska typer som använder operatorer eller andra statiska metoder. Även om du inte skapar gränssnitt med överbelastade operatorer kommer du förmodligen att dra nytta av den här funktionen och de allmänna matematiska klasser som aktiveras av språkuppdateringen.

I den här självstudien får du lära dig att:

  • Definiera gränssnitt med statiska medlemmar.
  • Använd gränssnitt för att definiera klasser som implementerar gränssnitt med definierade operatorer.
  • Skapa generiska algoritmer som förlitar sig på statiska gränssnittsmetoder.

Förutsättningar

Du måste konfigurera datorn för att köra .NET 7, som stöder C# 11. C# 11-kompilatorn är tillgänglig från och med Visual Studio 2022, version 17.3 eller .NET 7 SDK.

Statiska abstrakta gränssnittsmetoder

Vi börjar med ett exempel. Följande metod returnerar mittpunkten för två double tal:

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

Samma logik skulle fungera för alla numeriska typer: int, short, long, floatdecimaleller någon typ som representerar ett tal. Du måste ha ett sätt att använda operatorerna + och / och för att definiera ett värde för 2. Du kan använda System.Numerics.INumber<TSelf> gränssnittet för att skriva föregående metod som följande generiska metod:

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

Alla typer som implementerar INumber<TSelf> gränssnittet måste innehålla en definition för operator +, och för operator /. Nämnaren definieras av T.CreateChecked(2) för att skapa värdet 2 för valfri numerisk typ, vilket tvingar nämnaren att vara av samma typ som de två parametrarna. INumberBase<TSelf>.CreateChecked<TOther>(TOther) skapar en instans av typen från det angivna värdet och genererar en OverflowException om värdet hamnar utanför det representerarbara intervallet. (Den här implementeringen har potential för spill om left och right är båda tillräckligt stora värden. Det finns alternativa algoritmer som kan undvika det här potentiella problemet.)

Du definierar statiska abstrakta medlemmar i ett gränssnitt med välbekant syntax: Du lägger till static modifierarna och abstract till alla statiska medlemmar som inte tillhandahåller någon implementering. I följande exempel definieras ett IGetNext<T> gränssnitt som kan tillämpas på alla typer som åsidosätter operator ++:

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

Villkoret som typargumentet , Timplementerar säkerställer IGetNext<T> att signaturen för operatorn innehåller den innehållande typen eller dess typargument. Många operatorer framtvingar att dess parametrar måste matcha typen eller vara den typparameter som är begränsad för att implementera den innehållande typen. Utan den här begränsningen kunde operatorn ++ inte definieras i IGetNext<T> gränssnittet.

Du kan skapa en struktur som skapar en sträng med "A"-tecken där varje steg lägger till ytterligare ett tecken i strängen med hjälp av följande kod:

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

Mer allmänt kan du skapa valfri algoritm där du kanske vill definiera ++ att "producera nästa värde av den här typen". Med det här gränssnittet skapas tydlig kod och resultat:

var str = new RepeatSequence();

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

Föregående exempel genererar följande utdata:

A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA

Det här lilla exemplet visar motivationen för den här funktionen. Du kan använda naturlig syntax för operatorer, konstanta värden och andra statiska åtgärder. Du kan utforska dessa tekniker när du skapar flera typer som är beroende av statiska medlemmar, inklusive överlagrade operatorer. Definiera de gränssnitt som matchar dina typers funktioner och deklarera sedan dessa typers stöd för det nya gränssnittet.

Allmän matematik

Det motiverande scenariot för att tillåta statiska metoder, inklusive operatorer, i gränssnitt är att stödja generiska matematiska algoritmer. Basklassbiblioteket för .NET 7 innehåller gränssnittsdefinitioner för många aritmetiska operatorer och härledda gränssnitt som kombinerar många aritmetiska operatorer i ett INumber<T> gränssnitt. Nu ska vi använda dessa typer för att skapa en Point<T> post som kan använda valfri numerisk typ för T. Punkten kan flyttas av vissa XOffset och YOffset använda operatorn + .

Börja med att skapa ett nytt konsolprogram, antingen med hjälp dotnet new av eller Visual Studio.

Det offentliga gränssnittet för Translation<T> och Point<T> bör se ut som följande kod:

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

Du använder record typen för både typerna Translation<T> och Point<T> : Båda lagrar två värden, och de representerar datalagring snarare än avancerat beteende. Implementeringen av operator + skulle se ut som följande kod:

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

För att den tidigare koden ska kompileras måste du deklarera att T den IAdditionOperators<TSelf, TOther, TResult> stöder gränssnittet. Gränssnittet innehåller den operator + statiska metoden. Den deklarerar tre typparametrar: En för den vänstra operanden, en för den högra operanden och en för resultatet. Vissa typer implementerar + för olika typer av operander och resultat. Lägg till en deklaration som typargumentet implementerar TIAdditionOperators<T, T, T>:

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

När du har lagt till den begränsningen + kan klassen Point<T> använda för dess additionsoperator. Lägg till samma villkor för deklarationen Translation<T> :

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

Villkoret IAdditionOperators<T, T, T> hindrar en utvecklare som använder din klass från att skapa en Translation med hjälp av en typ som inte uppfyller villkoret för tillägget till en punkt. Du har lagt till de nödvändiga begränsningarna i typparametern för Translation<T> och Point<T> därför fungerar den här koden. Du kan testa genom att lägga till kod som följande ovanför deklarationerna för Translation och Point i din Program.cs-fil :

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

Du kan göra den här koden mer återanvändbar genom att deklarera att dessa typer implementerar lämpliga aritmetiska gränssnitt. Den första ändringen att göra är att deklarera att Point<T, T> implementerar IAdditionOperators<Point<T>, Translation<T>, Point<T>> gränssnittet. Typen Point använder olika typer för operander och resultatet. Typen Point implementerar redan en operator + med signaturen, så att lägga till gränssnittet i deklarationen är allt du behöver:

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

När du utför addition är det slutligen bra att ha en egenskap som definierar värdet för additiv identitet för den typen. Det finns ett nytt gränssnitt för den funktionen: IAdditiveIdentity<TSelf,TResult>. En översättning av {0, 0} är den additiva identiteten: Den resulterande punkten är densamma som den vänstra operanden. Gränssnittet IAdditiveIdentity<TSelf, TResult> definierar en skrivskyddad egenskap, AdditiveIdentity, som returnerar identitetsvärdet. Behöver Translation<T> några ändringar för att implementera det här gränssnittet:

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

Det finns några ändringar här, så vi går igenom dem en i taget. Först deklarerar du att Translation typen implementerar IAdditiveIdentity gränssnittet:

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

Sedan kan du prova att implementera gränssnittsmedlemmen enligt följande kod:

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

Föregående kod kompileras inte eftersom 0 den beror på typen. Svaret: Använd IAdditiveIdentity<T>.AdditiveIdentity för 0. Den ändringen innebär att dina begränsningar nu måste innehålla som T implementerar IAdditiveIdentity<T>. Detta resulterar i följande implementering:

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

Nu när du har lagt till den begränsningen Translation<T>måste du lägga till samma villkor i 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 };
}

Det här exemplet har gett dig en titt på hur gränssnitten för allmän matematisk sammansättning. Du har lärt dig att:

  • Skriv en metod som förlitade sig på INumber<T> gränssnittet så att metoden kunde användas med valfri numerisk typ.
  • Skapa en typ som förlitar sig på tilläggsgränssnitten för att implementera en typ som bara stöder en matematisk åtgärd. Den typen deklarerar sitt stöd för samma gränssnitt så att den kan bestå på andra sätt. Algoritmerna skrivs med den mest naturliga syntaxen för matematiska operatorer.

Experimentera med dessa funktioner och registrera feedback. Du kan använda menyalternativet Skicka feedback i Visual Studio eller skapa ett nytt problem på roslynlagringsplatsen på GitHub. Skapa generiska algoritmer som fungerar med valfri numerisk typ. Skapa algoritmer med hjälp av dessa gränssnitt där typargumentet bara kan implementera en delmängd av talliknande funktioner. Även om du inte skapar nya gränssnitt som använder dessa funktioner kan du experimentera med att använda dem i dina algoritmer.

Se även