Condividi tramite


Esercitazione: Esplorare la funzionalità di C# 11 - Membri virtuali statici nelle interfacce

C# 11 e .NET 7 includono membri virtuali statici nelle interfacce. Questa funzionalità consente di definire interfacce che includono operatori di overload o altri membri statici. Una volta definite le interfacce con membri statici, è possibile usarle come vincoli per creare tipi generici che usano gli operatori o altri metodi statici. Anche se non si creano interfacce con operatori di overload, questa funzionalità e le classi matematiche generiche abilitate dall'aggiornamento del linguaggio saranno comunque utili.

Questa esercitazione illustra come:

  • Definire interfacce con membri statici.
  • Usare le interfacce per definire classi che implementano interfacce con operatori definiti.
  • Creare algoritmi generici che si basano su metodi di interfaccia statici.

Prerequisiti

È necessario configurare il computer per l'esecuzione di .NET 7, che supporta C# 11. Il compilatore C# 11 è disponibile a partire da Visual Studio 2022 versione 17.3 o .NET 7 SDK.

Metodi di interfaccia astratti statici

Iniziamo con un esempio. Il metodo seguente restituisce il punto intermedio di due numeri double:

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

La stessa logica funziona per qualsiasi tipo numerico: int, short, long, float, decimal o qualsiasi tipo che rappresenta un numero. È necessario disporre di un modo per usare gli operatori + e / e per definire un valore per 2. È possibile usare l'interfaccia System.Numerics.INumber<TSelf> per scrivere il metodo precedente come metodo generico seguente:

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

Qualsiasi tipo che implementa l'interfaccia INumber<TSelf> deve includere una definizione per operator + e per operator /. Il denominatore è definito da T.CreateChecked(2) per creare il valore 2 per qualsiasi tipo numerico, il che impone che il denominatore sia dello stesso tipo dei due parametri. INumberBase<TSelf>.CreateChecked<TOther>(TOther) crea un'istanza del tipo dal valore specificato e genera un'eccezione OverflowException se il valore non rientra nell'intervallo rappresentabile. Questa implementazione può causare un overflow se left e right sono entrambi valori sufficientemente grandi. Sono disponibili algoritmi alternativi che consentono di evitare questo potenziale problema.

I membri astratti statici in un'interfaccia si definiscono usando una sintassi familiare: si aggiungono i modificatori static e abstract a qualsiasi membro statico che non fornisce un'implementazione. Nell'esempio seguente viene definita un'interfaccia IGetNext<T> che può essere applicata a qualsiasi tipo che esegue l'override di operator ++:

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

Il vincolo che l'argomento tipo, T, implementi IGetNext<T> garantisce che la firma per l'operatore includa il tipo contenitore o il relativo argomento tipo. Molti operatori richiedono che i relativi parametri corrispondano al tipo o al parametro di tipo vincolato a implementare il tipo contenitore. Senza questo vincolo, l'operatore ++ non può essere definito nell'interfaccia IGetNext<T>.

È possibile creare una struttura che crea una stringa di caratteri "A" in cui ogni incremento aggiunge un altro carattere alla stringa usando il codice seguente:

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

Più in generale, è possibile creare un algoritmo in cui definire ++ per indicare di "produrre il valore successivo di questo tipo". L'uso di questa interfaccia produce codice e risultati chiari:

var str = new RepeatSequence();

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

L'esempio precedente produce l'output seguente:

A
AA
AAA
AAAA
AAAAA
AAAAAA
AAAAAAA
AAAAAAAA
AAAAAAAAA
AAAAAAAAAA

Questo piccolo esempio illustra la motivazione per questa funzionalità. È possibile usare la sintassi naturale per operatori, valori costanti e altre operazioni statiche. È possibile esplorare queste tecniche quando si creano più tipi che si basano su membri statici, inclusi gli operatori di overload. Definire le interfacce che corrispondono alle funzionalità dei tipi e quindi dichiarare il supporto di tali tipi per la nuova interfaccia.

Operazioni matematiche generiche

Il motivo per cui consentire metodi statici, inclusi gli operatori, nelle interfacce è quello di supportare gli algoritmi matematici generici. La libreria di classi base .NET 7 contiene definizioni di interfaccia per molti operatori aritmetici e interfacce derivate che combinano numerosi operatori aritmetici in un'interfaccia INumber<T>. Applicare questi tipi per compilare un record Point<T> che può usare qualsiasi tipo numerico per T. Il punto può essere spostato in base a determinati valori di XOffset e YOffset usando l'operatore +.

Per iniziare, creare una nuova applicazione console usando dotnet new o Visual Studio.

L'interfaccia pubblica per Translation<T> e Point<T> deve essere simile al codice seguente:

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

Il tipo record viene usato per entrambi i tipi Translation<T> e Point<T>: entrambi archiviano due valori e rappresentano la risorsa di archiviazione dei dati anziché un comportamento sofisticato. L'implementazione di operator + sarà simile al codice seguente:

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

Per la compilazione del codice precedente, è necessario dichiarare che T supporta l'interfaccia IAdditionOperators<TSelf, TOther, TResult>. Tale interfaccia include il metodo statico operator +. Dichiara tre parametri di tipo: uno per l'operando sinistro, uno per l'operando destro e uno per il risultato. Alcuni tipi implementano + per diversi tipi di operandi e risultati. Aggiungere una dichiarazione per indicare che l'argomento tipo, T, implementa IAdditionOperators<T, T, T>:

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

Dopo aver aggiunto tale vincolo, la classe Point<T> può usare + come operatore di addizione. Aggiungere lo stesso vincolo nella dichiarazione di Translation<T>:

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

Il vincolo IAdditionOperators<T, T, T> impedisce a uno sviluppatore che usa la classe di creare un oggetto Translation usando un tipo che non soddisfa il vincolo per l'aggiunta a un punto. Sono stati aggiunti i vincoli necessari al parametro di tipo per Translation<T> e Point<T> quindi questo codice funziona. È possibile eseguire il test aggiungendo codice simile al seguente sopra le dichiarazioni di Translation e Point nel file 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);

È possibile rendere questo codice più riutilizzabile dichiarando che questi tipi implementano le interfacce aritmetiche appropriate. La prima modifica da apportare consiste nel dichiarare che Point<T, T> implementa l'interfaccia IAdditionOperators<Point<T>, Translation<T>, Point<T>>. Il tipo Point usa tipi diversi per gli operandi e il risultato. Il tipo Point implementa già un oggetto operator + con tale firma, quindi l'aggiunta dell'interfaccia alla dichiarazione è sufficiente:

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

Infine, quando si esegue l'addizione, è utile avere una proprietà che definisce il valore di identità additiva per tale tipo. È disponibile una nuova interfaccia per tale funzionalità: IAdditiveIdentity<TSelf,TResult>. Uno spostamento di {0, 0} è l'identità additiva: il punto risultante è lo stesso dell'operando sinistro. L'interfaccia IAdditiveIdentity<TSelf, TResult> definisce una proprietà di sola lettura, AdditiveIdentity, che restituisce il valore dell'identità. L'oggetto Translation<T> richiede alcune modifiche per implementare questa interfaccia:

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

Qui sono presenti alcune modifiche, che esamineremo singolarmente. In primo luogo, si dichiara che il tipo Translation implementa l'interfaccia IAdditiveIdentity:

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

A questo punto è possibile provare a implementare il membro dell'interfaccia, come illustrato nel codice seguente:

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

Il codice precedente non viene compilato, perché 0 dipende dal tipo. La soluzione è usare IAdditiveIdentity<T>.AdditiveIdentity per 0. Questa modifica implica che i vincoli devono ora includere che T implementa IAdditiveIdentity<T>. Il risultato è l'implementazione seguente:

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

Dopo aver aggiunto tale vincolo in Translation<T>, è necessario aggiungere lo stesso vincolo a 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 };
}

In questo esempio è stato illustrato come si compongono le interfacce per la matematica generica. Contenuto del modulo:

  • Scrivere un metodo basato sull'interfaccia INumber<T> in modo che tale metodo possa essere usato con qualsiasi tipo numerico.
  • Creare un tipo basato sulle interfacce di addizione per implementare un tipo che supporta solo un'operazione matematica. Tale tipo dichiara il supporto per le stesse interfacce in modo che possa essere composto in altri modi. Gli algoritmi vengono scritti usando la sintassi più naturale degli operatori matematici.

Provare a usare queste funzionalità e registrare il feedback. È possibile usare la voce di menu Invia feedback in Visual Studio oppure creare un nuovo problema nel repository roslyn in GitHub. Creare algoritmi generici che funzionano con qualsiasi tipo numerico. Creare algoritmi con queste interfacce in cui l'argomento tipo può implementare solo un subset di funzionalità di tipo numerico. Anche se non si creano nuove interfacce che usano queste funzionalità, è possibile provare a usarle negli algoritmi.

Vedi anche