Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
I membri virtuali statici dell'interfaccia consentono di definire interfacce che includono operatori di overload o altri membri statici. Dopo aver definito le interfacce con membri statici, è possibile usare tali interfacce come vincoli per creare tipi generici che usano operatori o altri metodi statici. Anche se non crei interfacce con operatori sovraccaricati, è probabile che tu tragga vantaggio da questa funzionalità e dalle classi matematiche generiche abilitate dall'aggiornamento del linguaggio.
In questa esercitazione si apprenderà come:
- Definire le 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
Metodi di interfaccia astratta statici
Iniziamo con un esempio. Il metodo seguente restituisce il punto intermedio di due double numeri:
public static double MidPoint(double left, double right) =>
(left + right) / (2.0);
La stessa logica funzionerebbe per qualsiasi tipo numerico: int, shortlong, , floatdecimalo qualsiasi tipo che rappresenta un numero. È necessario avere 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, che forza il denominatore allo 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 ha il rischio di overflow se left e right sono entrambi valori abbastanza grandi. Esistono algoritmi alternativi che possono evitare questo potenziale problema.
Si definiscono membri astratti statici in un'interfaccia usando una sintassi familiare: si aggiungono i static modificatori 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 implementato dall'argomento di tipo , T, IGetNext<T> garantisce che la firma per l'operatore includa il tipo contenitore o il relativo argomento di tipo. Molti operatori richiedono che i loro parametri debbano corrispondere al tipo o essere parametri di tipo vincolati per implementare il tipo che li contiene. 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 compilare qualsiasi algoritmo in cui si potrebbe voler definire ++ per indicare "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 sovraccarichi. Definire le interfacce che corrispondono alle funzionalità dei tipi e quindi dichiarare il supporto di tali tipi per la nuova interfaccia.
Matematica generica
Lo scenario motivante per consentire metodi statici, inclusi gli operatori, nelle interfacce consiste nel supportare algoritmi matematici generici . La libreria di classi di base .NET 7 contiene definizioni di interfaccia per molti operatori aritmetici e interfacce derivate che combinano molti operatori aritmetici in un'interfaccia INumber<T> . Si applicano questi tipi per compilare un Point<T> record che può usare qualsiasi tipo numerico per T. È possibile spostare il punto da alcuni 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> dovrebbe 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 utilizzato sia per i tipi Translation<T> che Point<T>: entrambi archiviano due valori e rappresentano l'archiviazione di 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 };
Affinché il codice precedente venga compilato, è necessario dichiarare che T supporta l'interfaccia IAdditionOperators<TSelf, TOther, TResult> . Tale interfaccia include il operator + metodo statico. 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 operando e risultati. Aggiungere una dichiarazione che l'argomento tipo, T, implementi 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 l'operatore + di addizione. Aggiungere lo stesso vincolo nella Translation<T> dichiarazione:
public record Translation<T>(T XOffset, T YOffset) where T : IAdditionOperators<T, T, T>;
Il IAdditionOperators<T, T, T> vincolo impedisce a uno sviluppatore che usa la classe di creare un Translation oggetto 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 di 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 Point tipo usa tipi diversi per gli operandi e il risultato. Il Point tipo 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 identità additiva per quel tipo. È disponibile una nuova interfaccia per tale funzionalità: IAdditiveIdentity<TSelf,TResult>. Una traduzione di {0, 0} è l'identità aggiuntiva: il punto risultante è lo stesso dell'operando sinistro. L'interfaccia IAdditiveIdentity<TSelf, TResult> definisce una proprietà di sola lettura, AdditiveIdentity, che restituisce il valore Identity. Sono necessarie alcune modifiche al Translation<T> 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, quindi esaminiamole una alla sola. In primo luogo, si dichiara che il Translation tipo 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 . Risposta: Usare IAdditiveIdentity<T>.AdditiveIdentity per 0. Questa modifica significa che i vincoli devono ora includere che T implementa IAdditiveIdentity<T>. Ciò comporta 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 };
}
Questo esempio ti ha dato un'idea di come le interfacce per la matematica generica si compongano. Ecco cosa hai imparato a fare:
- Scrivere un metodo basato sull'interfaccia
INumber<T>in modo che il metodo possa essere usato con qualsiasi tipo numerico. - Creare un tipo che si basa 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.
Sperimentare queste funzionalità e registrare commenti e suggerimenti. È possibile usare la voce di menu Invia commenti e suggerimenti in Visual Studio oppure creare un nuovo problema nel repository roslyn in GitHub. Creare algoritmi generici che funzionano con qualsiasi tipo numerico. Creare algoritmi usando queste interfacce in cui l'argomento di tipo implementa 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.