Esercitazione: Ridurre le allocazioni di memoria con la sicurezza ref
L'ottimizzazione delle prestazioni per un'applicazione .NET prevede spesso l'uso di due tecniche. In primo luogo, ridurre il numero e le dimensioni delle allocazioni dell'heap. In secondo luogo, ridurre la frequenza di copia dei dati. Visual Studio offre strumenti eccezionali che consentono di analizzare il modo in cui l'applicazione usa la memoria. Dopo aver determinato dove l'app effettua allocazioni non necessarie, è possibile apportare modifiche per ridurre al minimo tali allocazioni. Si convertono i tipi class
in tipi struct
. Si usano le funzionalità di sicurezza ref
per mantenere la semantica e ridurre al minimo la copia aggiuntiva.
Usare Visual Studio 17.5 per un'esperienza ottimale con questa esercitazione. Lo strumento di allocazione di oggetti .NET usato per analizzare l'utilizzo della memoria fa parte di Visual Studio. È possibile usare Visual Studio Code e la riga di comando per eseguire l'applicazione e apportare tutte le modifiche. Non sarà tuttavia possibile visualizzare i risultati dell'analisi delle modifiche.
L'applicazione che verrà usata è una simulazione di un'applicazione IoT che monitora diversi sensori per determinare se un intruso è entrato in un museo segreto con oggetti di valore. I sensori IoT inviano costantemente dati che misurano la combinazione di ossigeno (O2) e anidride carbonica (CO2) nell'aria. Segnalano anche la temperatura e l'umidità relativa. Ognuno di questi valori varia leggermente tutto il tempo. Quando una persona entra nella stanza, tuttavia, il cambiamento è leggermente più significativo e sempre nella stessa direzione: l'ossigeno diminuisce, l'anidride carbonica aumenta, la temperatura aumenta, così come l'umidità relativa. Quando i sensori si combinano per mostrare aumenti, viene attivato l'allarme intruso.
In questa esercitazione si eseguirà l'applicazione, si eseguiranno misurazioni sulle allocazioni di memoria, quindi si miglioreranno le prestazioni riducendo il numero di allocazioni. Il codice sorgente è disponibile nell'esplorazione esempi.
Esplorare l'applicazione iniziale
Scaricare l'applicazione ed eseguire l'esempio di avvio. L'applicazione iniziale funziona correttamente, ma poiché alloca molti oggetti di piccole dimensioni con ogni ciclo di misurazione, le prestazioni diminuiscono lentamente man mano che vengono eseguite nel tempo.
Press <return> to start simulation
Debounced measurements:
Temp: 67.332
Humidity: 41.077%
Oxygen: 21.097%
CO2 (ppm): 404.906
Average measurements:
Temp: 67.332
Humidity: 41.077%
Oxygen: 21.097%
CO2 (ppm): 404.906
Debounced measurements:
Temp: 67.349
Humidity: 46.605%
Oxygen: 20.998%
CO2 (ppm): 408.707
Average measurements:
Temp: 67.349
Humidity: 46.605%
Oxygen: 20.998%
CO2 (ppm): 408.707
Molte righe rimosse.
Debounced measurements:
Temp: 67.597
Humidity: 46.543%
Oxygen: 19.021%
CO2 (ppm): 429.149
Average measurements:
Temp: 67.568
Humidity: 45.684%
Oxygen: 19.631%
CO2 (ppm): 423.498
Current intruders: 3
Calculated intruder risk: High
Debounced measurements:
Temp: 67.602
Humidity: 46.835%
Oxygen: 19.003%
CO2 (ppm): 429.393
Average measurements:
Temp: 67.568
Humidity: 45.684%
Oxygen: 19.631%
CO2 (ppm): 423.498
Current intruders: 3
Calculated intruder risk: High
È possibile esplorare il codice per ottenere informazioni sul funzionamento dell'applicazione. Il programma principale esegue la simulazione. Quando si preme <Enter>
, crea una stanza e raccoglie alcuni dati di base iniziali:
Console.WriteLine("Press <return> to start simulation");
Console.ReadLine();
var room = new Room("gallery");
var r = new Random();
int counter = 0;
room.TakeMeasurements(
m =>
{
Console.WriteLine(room.Debounce);
Console.WriteLine(room.Average);
Console.WriteLine();
counter++;
return counter < 20000;
});
Dopo aver stabilito i dati di base, esegue la simulazione nella stanza, in cui un generatore di numeri casuali determina se un intruso è entrato nella stanza:
counter = 0;
room.TakeMeasurements(
m =>
{
Console.WriteLine(room.Debounce);
Console.WriteLine(room.Average);
room.Intruders += (room.Intruders, r.Next(5)) switch
{
( > 0, 0) => -1,
( < 3, 1) => 1,
_ => 0
};
Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
Console.WriteLine();
counter++;
return counter < 200000;
});
Altri tipi contengono le misurazioni, una misura con debouncing che corrisponde alla media delle ultime 50 misurazioni e alla media di tutte le misurazioni effettuate.
Eseguire quindi l'applicazione usando lo strumento di allocazione di oggetti .NET. Assicurarsi di usare la build Release
, non la build Debug
. Scegliere Profiler prestazioni dal menu Debug. Selezionare solo l'opzione Rilevamento allocazioni oggetti .NET. Eseguire l'applicazione fino al completamento. Il profiler misura le allocazioni di oggetti e segnala le allocazioni e i cicli di Garbage Collection. Verrà visualizzato un grafico simile all'immagine seguente:
Il grafico precedente mostra che il lavoro per ridurre al minimo le allocazioni offrirà vantaggi in termini di prestazioni. Nel grafico degli oggetti attivi viene visualizzato un modello simile ai denti di una sega. Ciò indica che vengono creati numerosi oggetti che diventano rapidamente elementi superflui. Vengono raccolti in un secondo momento, come illustrato nel grafico delta dell'oggetto. Le barre rosse verso il basso indicano un ciclo di Garbage Collection.
Esaminare quindi la scheda Allocazioni sotto i grafici. Questa tabella mostra quali tipi presentano il numero più elevato di allocazioni:
Il tipo System.String è responsabile per la maggior parte delle allocazioni. L'attività più importante deve essere quella di ridurre al minimo la frequenza delle allocazioni di stringhe. Questa applicazione stampa costantemente numerosi output formattati nella console. Per questa simulazione si vogliono mantenere i messaggi, quindi ci si concentrerà sulle due righe successive: il tipo SensorMeasurement
e il tipo IntruderRisk
.
Fare doppio clic sulla riga SensorMeasurement
. È possibile notare che tutte le allocazioni vengono eseguite nel metodo static
SensorMeasurement.TakeMeasurement
. È possibile visualizzare il metodo nel frammento di codice seguente:
public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
return new SensorMeasurement
{
CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
Room = room,
TimeRecorded = DateTime.Now
};
}
Ogni misura alloca un nuovo oggetto SensorMeasurement
, ovvero un tipo class
. Ogni SensorMeasurement
creata causa un'allocazione dell'heap.
Modificare le classi in struct
Il codice seguente illustra la dichiarazione iniziale di SensorMeasurement
:
public class SensorMeasurement
{
private static readonly Random generator = new Random();
public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
return new SensorMeasurement
{
CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
Room = room,
TimeRecorded = DateTime.Now
};
}
private const double CO2Concentration = 409.8; // increases with people.
private const double O2Concentration = 0.2100; // decreases
private const double TemperatureSetting = 67.5; // increases
private const double HumiditySetting = 0.4500; // increases
public required double CO2 { get; init; }
public required double O2 { get; init; }
public required double Temperature { get; init; }
public required double Humidity { get; init; }
public required string Room { get; init; }
public required DateTime TimeRecorded { get; init; }
public override string ToString() => $"""
Room: {Room} at {TimeRecorded}:
Temp: {Temperature:F3}
Humidity: {Humidity:P3}
Oxygen: {O2:P3}
CO2 (ppm): {CO2:F3}
""";
}
Il tipo è stato originariamente creato come class
perché contiene numerose misure double
. È più grande rispetto a quanto si vuole copiare nei percorsi critici. Tale decisione ha tuttavia comportato un numero elevato di allocazioni. Modificare il tipo da class
a struct
.
Il passaggio da class
a struct
introduce alcuni errori del compilatore perché il codice originale usava controlli di riferimento null
in alcuni punti. Il primo si trova nella classe DebounceMeasurement
, nel metodo AddMeasurement
:
public void AddMeasurement(SensorMeasurement datum)
{
int index = totalMeasurements % debounceSize;
recentMeasurements[index] = datum;
totalMeasurements++;
double sumCO2 = 0;
double sumO2 = 0;
double sumTemp = 0;
double sumHumidity = 0;
for (int i = 0; i < debounceSize; i++)
{
if (recentMeasurements[i] is not null)
{
sumCO2 += recentMeasurements[i].CO2;
sumO2+= recentMeasurements[i].O2;
sumTemp+= recentMeasurements[i].Temperature;
sumHumidity += recentMeasurements[i].Humidity;
}
}
O2 = sumO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
CO2 = sumCO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
Temperature = sumTemp / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
Humidity = sumHumidity / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
}
Il tipo DebounceMeasurement
contiene una matrice di 50 misurazioni. Le letture per un sensore vengono segnalate come media delle ultime 50 misurazioni. Ciò riduce gli elementi non significativi nelle letture. Prima che siano state acquisite 50 letture complete, questi valori sono null
. Il codice verifica la presenza del riferimento null
per segnalare la media corretta all'avvio del sistema. Dopo aver modificato il tipo SensorMeasurement
in uno struct, è necessario usare un test diverso. Il tipo SensorMeasurement
include una string
per l'identificatore della stanza, quindi è possibile usare tale test:
if (recentMeasurements[i].Room is not null)
Gli altri tre errori del compilatore si trovano tutti nel metodo che esegue ripetutamente le misurazioni in una stanza:
public void TakeMeasurements(Func<SensorMeasurement, bool> MeasurementHandler)
{
SensorMeasurement? measure = default;
do {
measure = SensorMeasurement.TakeMeasurement(Name, Intruders);
Average.AddMeasurement(measure);
Debounce.AddMeasurement(measure);
} while (MeasurementHandler(measure));
}
Nel metodo iniziale la variabile locale per SensorMeasurement
è un riferimento che ammette i valori Null:
SensorMeasurement? measure = default;
Ora che SensorMeasurement
è un struct
anziché una class
, l'oggetto che ammette i valori Null è un tipo di valore che ammette i valori Null. È possibile modificare la dichiarazione in un tipo di valore per correggere gli errori rimanenti del compilatore:
SensorMeasurement measure = default;
Ora che gli errori del compilatore sono stati risolti, è necessario esaminare il codice per assicurarsi che la semantica non sia stata modificata. Poiché i tipi struct
vengono passati per valore, le modifiche apportate ai parametri del metodo non sono visibili dopo la restituzione del metodo.
Importante
La modifica di un tipo da class
a struct
può modificare la semantica del programma. Quando un tipo class
viene passato a un metodo, tutte le mutazioni apportate nel metodo vengono apportate all'argomento. Quando un tipo struct
viene passato a un metodo, le modifiche apportate al metodo vengono apportate a una copia dell'argomento. Ciò significa che qualsiasi metodo che modifica i rispettivi argomenti in base alla progettazione deve essere aggiornato per usare il modificatore ref
in qualsiasi tipo di argomento modificato da class
a struct
.
Il tipo SensorMeasurement
non include metodi che modificano lo stato, quindi questo non è un problema in questo esempio. È possibile dimostrarlo aggiungendo il modificatore readonly
allo struct SensorMeasurement
:
public readonly struct SensorMeasurement
Il compilatore applica la natura readonly
dello struct SensorMeasurement
. Se l'ispezione del codice non ha rilevato un metodo che ha modificato lo stato, il compilatore lo segnalerà all'utente. L'app continua a essere compilata senza errori, quindi questo tipo è readonly
. L'aggiunta del modificatore readonly
quando si modifica un tipo da class
a struct
consente di trovare membri che modificano lo stato dello struct
.
Evitare di eseguire copie
È stato rimosso un numero elevato di allocazioni non necessarie dall'app. Il tipo SensorMeasurement
non viene visualizzato nella tabella in nessun punto.
Esegue ora operazioni aggiuntive per copiare la struttura SensorMeasurement
ogni volta che viene usata come parametro o come valore restituito. Lo struct SensorMeasurement
contiene quattro valori double, un DateTime e una string
. Tale struttura è significativamente più grande di un riferimento. Aggiungere i modificatori ref
o in
alle posizioni in cui viene usato il tipo SensorMeasurement
.
Il passaggio successivo consiste nel trovare i metodi che restituiscono una misura o accettare una misurazione come argomento e usare i riferimenti laddove possibile. Iniziare nello struct SensorMeasurement
. Il metodo TakeMeasurement
statico crea e restituisce un nuovo SensorMeasurement
:
public static SensorMeasurement TakeMeasurement(string room, int intruders)
{
return new SensorMeasurement
{
CO2 = (CO2Concentration + intruders * 10) + (20 * generator.NextDouble() - 10.0),
O2 = (O2Concentration - intruders * 0.01) + (0.005 * generator.NextDouble() - 0.0025),
Temperature = (TemperatureSetting + intruders * 0.05) + (0.5 * generator.NextDouble() - 0.25),
Humidity = (HumiditySetting + intruders * 0.005) + (0.20 * generator.NextDouble() - 0.10),
Room = room,
TimeRecorded = DateTime.Now
};
}
Questa opzione verrà lasciata invariata, con restituzione per valore. Se si prova a usare la restituzione per ref
, si otterrà un errore del compilatore. Non è possibile restituire un ref
a una nuova struttura creata localmente nel metodo. La progettazione dello struct non modificabile prevede che sia solo possibile impostare i valori della misura in fase di costruzione. Questo metodo deve creare un nuovo struct di misurazione.
Esaminare di nuovo DebounceMeasurement.AddMeasurement
. È necessario aggiungere il modificatore in
al parametro measurement
:
public void AddMeasurement(in SensorMeasurement datum)
{
int index = totalMeasurements % debounceSize;
recentMeasurements[index] = datum;
totalMeasurements++;
double sumCO2 = 0;
double sumO2 = 0;
double sumTemp = 0;
double sumHumidity = 0;
for (int i = 0; i < debounceSize; i++)
{
if (recentMeasurements[i].Room is not null)
{
sumCO2 += recentMeasurements[i].CO2;
sumO2+= recentMeasurements[i].O2;
sumTemp+= recentMeasurements[i].Temperature;
sumHumidity += recentMeasurements[i].Humidity;
}
}
O2 = sumO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
CO2 = sumCO2 / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
Temperature = sumTemp / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
Humidity = sumHumidity / ((totalMeasurements > debounceSize) ? debounceSize : totalMeasurements);
}
In questo modo si evita un'operazione di copia. Il parametro in
è un riferimento alla copia già creata dal chiamante. È anche possibile salvare una copia con il metodo TakeMeasurement
nel tipo Room
. Questo metodo illustra il modo in cui il compilatore garantisce la sicurezza quando si passano argomenti per ref
. Il metodo TakeMeasurement
iniziale nel tipo Room
accetta un argomento di Func<SensorMeasurement, bool>
. Se si prova ad aggiungere il modificatore in
o ref
a tale dichiarazione, il compilatore segnala un errore. Non è possibile passare un argomento ref
a un'espressione lambda. Il compilatore non può garantire che l'espressione chiamata non copi il riferimento. Se l'espressione lambda acquisisce il riferimento, tale riferimento potrebbe avere una durata superiore al valore a cui fa riferimento. L'accesso all'esterno del contesto ref safe comporta un danneggiamento della memoria. Le regole di sicurezza ref
non lo consentono. Per altre informazioni, vedere la panoramica delle funzionalità di sicurezza Ref.
Mantenere la semantica
I set finali di modifiche non avranno un impatto significativo sulle prestazioni di questa applicazione perché i tipi non vengono creati in percorsi critici. Queste modifiche illustrano alcune delle altre tecniche che è possibile usare nell'ottimizzazione delle prestazioni. Di seguito viene esaminata la classe Room
iniziale:
public class Room
{
public AverageMeasurement Average { get; } = new ();
public DebounceMeasurement Debounce { get; } = new ();
public string Name { get; }
public IntruderRisk RiskStatus
{
get
{
var CO2Variance = (Debounce.CO2 - Average.CO2) > 10.0 / 4;
var O2Variance = (Average.O2 - Debounce.O2) > 0.005 / 4.0;
var TempVariance = (Debounce.Temperature - Average.Temperature) > 0.05 / 4.0;
var HumidityVariance = (Debounce.Humidity - Average.Humidity) > 0.20 / 4;
IntruderRisk risk = IntruderRisk.None;
if (CO2Variance) { risk++; }
if (O2Variance) { risk++; }
if (TempVariance) { risk++; }
if (HumidityVariance) { risk++; }
return risk;
}
}
public int Intruders { get; set; }
public Room(string name)
{
Name = name;
}
public void TakeMeasurements(Func<SensorMeasurement, bool> MeasurementHandler)
{
SensorMeasurement? measure = default;
do {
measure = SensorMeasurement.TakeMeasurement(Name, Intruders);
Average.AddMeasurement(measure);
Debounce.AddMeasurement(measure);
} while (MeasurementHandler(measure));
}
}
Questo tipo contiene diverse proprietà. Alcuni tipi sono class
. La creazione di un oggetto Room
comporta più allocazioni. Uno per l'oggetto Room
stesso e uno per ognuno dei membri di un tipo class
contenuto nell'oggetto. È possibile convertire due di queste proprietà da tipi class
a tipi struct
, ovvero i tipi DebounceMeasurement
e AverageMeasurement
. Verrà ora illustrata la trasformazione con entrambi i tipi.
Modificare il tipo DebounceMeasurement
da class
a struct
. Ciò introduce un errore del compilatore CS8983: A 'struct' with field initializers must include an explicitly declared constructor
. È possibile risolvere questo problema aggiungendo un costruttore vuoto senza parametri:
public DebounceMeasurement() { }
Per altre informazioni su questo requisito, vedere l'articolo di riferimento sul linguaggio relativo agli struct.
L'override Object.ToString() non modifica i valori dello struct. È possibile aggiungere il modificatore readonly
alla dichiarazione del metodo. Il tipo DebounceMeasurement
è modificabile, quindi è necessario assicurarsi che le modifiche non influiscano sulle copie eliminate. Il metodo AddMeasurement
modifica lo stato dell'oggetto. Viene chiamato dalla classe Room
, nel metodo TakeMeasurements
. Queste modifiche devono essere mantenute dopo la chiamata del metodo. È possibile modificare la proprietà Room.Debounce
in modo da restituire un riferimento a una singola istanza del tipo DebounceMeasurement
:
private DebounceMeasurement debounce = new();
public ref readonly DebounceMeasurement Debounce { get { return ref debounce; } }
Nell'esempio precedente sono state apportate alcune modifiche. In primo luogo, la proprietà è una proprietà di sola lettura che restituisce un riferimento di sola lettura all'istanza di proprietà di questa stanza. È ora supportata da un campo dichiarato inizializzato quando viene creata un'istanza dell'oggetto Room
. Dopo aver apportato queste modifiche, si aggiornerà l'implementazione del metodo AddMeasurement
. Viene usato il campo sottostante privato, debounce
, non la proprietà di sola lettura Debounce
. In questo modo, le modifiche vengono apportate alla singola istanza creata durante l'inizializzazione.
La stessa tecnica funziona con la proprietà Average
. Prima di tutto, modificare il tipo AverageMeasurement
da class
a struct
e aggiungere il modificatore readonly
nel metodo ToString
:
namespace IntruderAlert;
public struct AverageMeasurement
{
private double sumCO2 = 0;
private double sumO2 = 0;
private double sumTemperature = 0;
private double sumHumidity = 0;
private int totalMeasurements = 0;
public AverageMeasurement() { }
public readonly double CO2 => sumCO2 / totalMeasurements;
public readonly double O2 => sumO2 / totalMeasurements;
public readonly double Temperature => sumTemperature / totalMeasurements;
public readonly double Humidity => sumHumidity / totalMeasurements;
public void AddMeasurement(in SensorMeasurement datum)
{
totalMeasurements++;
sumCO2 += datum.CO2;
sumO2 += datum.O2;
sumTemperature += datum.Temperature;
sumHumidity+= datum.Humidity;
}
public readonly override string ToString() => $"""
Average measurements:
Temp: {Temperature:F3}
Humidity: {Humidity:P3}
Oxygen: {O2:P3}
CO2 (ppm): {CO2:F3}
""";
}
Modificare quindi la classe Room
seguendo la stessa tecnica usata per la proprietà Debounce
. La proprietà Average
restituisce un readonly ref
al campo privato per la misurazione media. Il metodo AddMeasurement
modifica i campi interni.
private AverageMeasurement average = new();
public ref readonly AverageMeasurement Average { get { return ref average; } }
Evitare la conversione boxing
È necessaria una modifica finale per migliorare le prestazioni. Il programma principale stampa le statistiche per la stanza, inclusa la valutazione dei rischi:
Console.WriteLine($"Current intruders: {room.Intruders}");
Console.WriteLine($"Calculated intruder risk: {room.RiskStatus}");
La chiamata a ToString
generato esegue la conversione boxing del valore dell'enumerazione. È possibile evitare la conversione scrivendo un override nella classe Room
che formatta la stringa in base al valore del rischio stimato:
public override string ToString() =>
$"Calculated intruder risk: {RiskStatus switch
{
IntruderRisk.None => "None",
IntruderRisk.Low => "Low",
IntruderRisk.Medium => "Medium",
IntruderRisk.High => "High",
IntruderRisk.Extreme => "Extreme",
_ => "Error!"
}}, Current intruders: {Intruders.ToString()}";
Modificare quindi il codice nel programma principale per chiamare questo nuovo metodo ToString
:
Console.WriteLine(room.ToString());
Eseguire l'app usando il profiler ed esaminare la tabella aggiornata per individuare le allocazioni.
Sono state rimosse numerose allocazioni e sono state migliorate le prestazioni dell'app.
Uso della sicurezza dei riferimenti nell'applicazione
Queste tecniche consentono l'ottimizzazione delle prestazioni di basso livello. Possono aumentare le prestazioni nell'applicazione quando vengono applicate ai percorsi critici e quando si è misurato l'impatto prima e dopo le modifiche. Nella maggior parte dei casi si seguirà il ciclo seguente:
- Misurare le allocazioni: determinare quali tipi presentano il numero più elevato di allocazioni e quando è possibile ridurre le allocazioni dell'heap.
- Convertire la classe in uno struct: in molti casi i tipi possono essere convertiti da
class
astruct
. L'app usa lo spazio dello stack invece di effettuare allocazioni dell'heap. - Mantenere la semantica: la conversione di una
class
in unostruct
può influire sulla semantica per i parametri e i valori restituiti. Qualsiasi metodo che modifica i parametri deve ora contrassegnare i parametri con il modificatoreref
. Ciò garantisce che le modifiche vengano apportate all'oggetto corretto. Analogamente, se un valore restituito di una proprietà o di un metodo deve essere modificato dal chiamante, tale valore restituito deve essere contrassegnato con il modificatoreref
. - Evitare le copie: quando si passa uno struct di grandi dimensioni come parametro, è possibile contrassegnare il parametro con il modificatore
in
. È possibile passare un riferimento in meno byte e assicurarsi che il metodo non modifichi il valore originale. È anche possibile restituire valorireadonly ref
per restituire un riferimento che non può essere modificato.
Usando queste tecniche è possibile migliorare le prestazioni nei percorsi critici del codice.