Approfondimento della programmazione funzionale C# (12) Immutabilità, tipo anonimo e tupla

Approfondimento della programmazione funzionale C# (12) Immutabilità, tipo anonimo e tupla

[LINQ tramite serie C#]

[Serie di approfondimento programmazione funzionale C#]

Ultima versione:https://weblogs.asp.net/dixin/functional-csharp-immutability-anonymous-type-and-tuple

L'immutabilità è un aspetto importante del paradigma funzionale. Come accennato in precedenza, la programmazione imperativa/orientata agli oggetti è solitamente con stato e la programmazione funzionale incoraggia l'immutabilità senza cambiamento di stato. Nella programmazione C# esistono molti tipi di immutabilità, ma possono essere classificati in 2 livelli:immutabilità di un certo valore e immutabilità dello stato interno di un valore. Prendi come esempio la variabile locale, una variabile locale può essere chiamata immutabile, se una volta assegnata non c'è modo di riassegnarla; una variabile locale può anche essere chiamata immutabile, se una volta inizializzato il suo stato interno, non c'è modo di modificarne lo stato in uno stato diverso.

In generale, l'immutabilità può rendere la programmazione più semplice in molti casi, poiché elimina una delle principali fonti di bug. Il valore immutabile e lo stato immutabile possono anche semplificare ampiamente la programmazione simultanea/parallela/multithread, perché sono thread-safe per natura. Lo svantaggio dell'immutabilità è, a quanto pare, per modificare un valore immutabile o uno stato immutabile, è necessario creare un'altra nuova istanza con la mutazione, che può causare un sovraccarico delle prestazioni.

Valore immutabile

Molti linguaggi funzionali supportano il valore immutabile. In contrasto con variabile. Una volta assegnato un valore con qualcosa, non può essere riassegnato in modo che non possa essere modificato in nient'altro. Ad esempio, in F#, un valore non è modificabile per impostazione predefinita, a meno che non sia specificata la parola chiave mutable:

let value = new Uri("https://weblogs.asp.net/dixin") // Immutable value.
value <- null // Cannot be compiled. Cannot reassign to value.

let mutable variable = new Uri("https://weblogs.asp.net/dixin") // Mutable variable.
variable <- null // Can reassign to variable.

Essendo un linguaggio simile al C, la variabile C# è modificabile per impostazione predefinita. C# ha alcune altre funzionalità del linguaggio per il valore immutabile.

Costante

C# ha una parola chiave const per definire la costante del tempo di compilazione, che non può essere modificata in fase di esecuzione. Tuttavia, funziona solo per tipi primitivi, stringhe e riferimenti null:

internal static partial class Immutability
{
    internal static void Const()
    {
        const int immutable1 = 1;
        const string immutable2 = "https://weblogs.asp.net/dixin";
        const object immutale3 = null;
        const Uri immutable4 = null;
        const Uri immutable5 = new Uri(immutable2); // Cannot be compiled.
    }
}

utilizzo di istruzione e foreach

C# supporta anche il valore immutabile in alcune istruzioni, come le precedenti istruzioni using e foreach:

internal static void ForEach(IEnumerable<int> source)
{
    foreach (int immutable in source)
    {
        // Cannot reassign to immutable.
    }
}

internal static void Using(Func<IDisposable> disposableFactory)
{
    using (IDisposable immutable = disposableFactory())
    {
        // Cannot reassign to immutable.
    }
}

questo riferimento per la classe

Nella definizione della classe, questa parola chiave può essere utilizzata nei membri della funzione di istanza. Si riferisce all'istanza corrente della classe ed è immutabile:

internal partial class Device
{
    internal void InstanceMethod()
    {
        // Cannot reassign to this.
    }
}

Per impostazione predefinita, questo riferimento è modificabile per la definizione della struttura, che verrà discussa più avanti.

Ingresso di sola lettura e uscita di sola lettura della funzione

Il parametro della funzione menzionato passato per riferimento di sola lettura (nel parametro) è immutabile nella funzione e il risultato della funzione risintonizzato da riferimento di sola lettura (ref readonly return) è immutabile per il chiamante della funzione:

internal static void ParameterAndReturn<T>(Span<T> span)
{
    ref readonly T Last(in Span<T> immutableInput)
    {
        // Cannot reassign to immutableInput.
        int length = immutableInput.Length;
        if (length > 0)
        {
            return ref immutableInput[length - 1];
        }
        throw new ArgumentException("Span is empty.", nameof(immutableInput));
    }

    ref readonly T immutableOutput = ref Last(in span);
    // Cannot reassign to immutableOutput.
}

Variabile locale per riferimento di sola lettura (rif. variabile di sola lettura)

C# 7.2 introduce il riferimento di sola lettura per la variabile locale. In C#, quando si definisce e si inizializza una nuova variabile locale con qualche variabile locale esistente, ci sono 3 casi:

  • Per copia:assegna direttamente alla variabile locale. Se viene assegnata un'istanza del tipo di valore, tale istanza del tipo di valore viene copiata in una nuova istanza; se viene assegnata un'istanza del tipo di riferimento, tale riferimento viene copiato. Pertanto, quando la nuova variabile locale viene riassegnata, la variabile locale precedente non viene influenzata.
  • Per riferimento:assegnare alla variabile locale con la parola chiave ref. La nuova variabile locale può essere vista virtualmente come un puntatore o alias della variabile locale esistente. Quindi, quando la nuova variabile locale viene riassegnata, equivale a riassegnare la precedente variabile locale
  • Per riferimento di sola lettura:assegnare alla variabile locale con le parole chiave di sola lettura ref. La nuova variabile locale può anche essere vista virtualmente come puntatore o alias, ma in questo caso la nuova variabile locale è immutabile e non può essere riassegnata.
internal static void ReadOnlyReference()
{
    int value = 1;
    int copyOfValue = value; // Assign by copy.
    copyOfValue = 10; // After the assignment, value does not change.
    ref int mutaleRefOfValue = ref value; // Assign by reference.
    mutaleRefOfValue = 10; // After the reassignment, value changes too.
    ref readonly int immutableRefOfValue = ref value; // Assign by readonly reference.
    immutableRefOfValue = 0; // Cannot be compiled. Cannot reassign to immutableRefOfValue.

    Uri reference = new Uri("https://weblogs.asp.net/dixin");
    Uri copyOfReference = reference; // Assign by copy.
    copyOfReference = new Uri("https://flickr.com/dixin"); // After the assignment, reference does not change.
    ref Uri mutableRefOfReference = ref reference; // Assign by reference.
    mutableRefOfReference = new Uri("https://flickr.com/dixin"); // After the reassignment, reference changes too.
    ref readonly Uri immutableRefOfReference = ref reference; // Assign by readonly reference.
    immutableRefOfReference = null; // Cannot be compiled. Cannot reassign to immutableRefOfReference.
}

Valore immutabile nell'espressione di query LINQ

Nell'espressione di query LINQ introdotta da C# 3,0, le clausole from, join, let possono dichiarare valori e anche la parola chiave into query può dichiarare un valore. Questi valori sono tutti immutabili:

internal static void QueryExpression(IEnumerable<int> source1, IEnumerable<int> source2)
{
    IEnumerable<IGrouping<int, int>> query =
        from immutable1 in source1
        // Cannot reassign to immutable1.
        join immutable2 in source2 on immutable1 equals immutable2 into immutable3
        // Cannot reassign to immutable2, immutable3.
        let immutable4 = immutable1
        // Cannot reassign to immutable4.
        group immutable4 by immutable4 into immutable5
        // Cannot reassign to immutable5.
        select immutable5 into immutable6
        // Cannot reassign to immutable6.
        select immutable6;
}

L'espressione di query è uno zucchero sintattico delle chiamate ai metodi di query, che verrà discusso in dettaglio nel capitolo LINQ to Objects.

Stato immutabile (tipo immutabile)

Una volta creata un'istanza da un tipo immutabile, i dati interni dell'istanza non possono essere modificati. In C#, string (System.String) è un tipo non modificabile. Una volta creata una stringa, non esiste alcuna API per modificare quella stringa. Ad esempio, string.Remove non modifica la stringa, ma restituisce sempre una nuova stringa con i caratteri specificati rimossi. Al contrario, il generatore di stringhe (System.Text.StringBuilder) è un tipo mutabile. Ad esempio, StringBuilder.Remove modifica effettivamente la stringa per rimuovere i caratteri specificati. Nella libreria principale, la maggior parte delle classi sono tipi mutabili e la maggior parte delle strutture sono tipi immutabili.

Campo costante del tipo

Quando si definisce il tipo (classe o struttura), un campo con il modificatore const non è modificabile. Ancora una volta, funziona solo per tipi primitivi, stringhe e riferimenti nulli.

namespace System
{
    public struct DateTime : IComparable, IComparable<DateTime>, IConvertible, IEquatable<DateTime>, IFormattable, ISerializable
    {
        private const int DaysPerYear = 365;
        // Compiled to:
        // .field private static literal int32 DaysPerYear = 365

        private const int DaysPer4Years = DaysPerYear * 4 + 1;
        // Compiled to:
        // .field private static literal int32 DaysPer4Years = 1461

        // Other members.
    }
}

Classe immutabile con campo di istanza di sola lettura

Quando il modificatore di sola lettura viene utilizzato per un campo, il campo può essere inizializzato solo dal costruttore e non può essere riassegnato in seguito. Quindi una classe immutabile può essere immutabile definendo tutti i campi di istanza come di sola lettura:

internal partial class ImmutableDevice
{
    private readonly string name;

    private readonly decimal price;
}

Con il summenzionato zucchero sintattico della proprietà auto, la definizione del campo di sola lettura può essere generata automaticamente. Di seguito è riportato un esempio di tipo di dati modificabile con stato di lettura e scrittura e tipo di dati immutabile con stato di sola lettura archiviato nei campi di istanza di sola lettura:

internal partial class MutableDevice
{
    internal string Name { get; set; }

    internal decimal Price { get; set; }
}

internal partial class ImmutableDevice
{
    internal ImmutableDevice(string name, decimal price)
    {
        this.Name = name;
        this.Price = price;
    }

    internal string Name { get; }

    internal decimal Price { get; }
}

Apparentemente, l'istanza MutableDevice costruita può cambiare il suo stato interno memorizzato dai campi e l'istanza ImmutableDevice non può:

internal static void State()
{
    MutableDevice mutableDevice = new MutableDevice() { Name = "Microsoft Band 2", Price = 249.99M };
    // Price drops.
    mutableDevice.Price -= 50M;

    ImmutableDevice immutableDevice = new ImmutableDevice(name: "Surface Book", price: 1349.00M);
    // Price drops.
    immutableDevice = new ImmutableDevice(name: immutableDevice.Name, price: immutableDevice.Price - 50M);
}

Poiché l'istanza di tipo immutabile non può cambiare stato, elimina una delle principali fonti di bug ed è sempre thread-safe. Ma questi vantaggi hanno un prezzo. È comune aggiornare alcuni dati esistenti a un valore diverso, ad esempio, avere uno sconto in base al prezzo corrente:

internal partial class MutableDevice
{
    internal void Discount() => this.Price = this.Price * 0.9M;
}

internal partial class ImmutableDevice
{
    internal ImmutableDevice Discount() => new ImmutableDevice(name: this.Name, price: this.Price * 0.9M);
}

Quando si sconta il prezzo, MutableDevice.Discount cambia direttamente lo stato. ImmutableDevice.Discount non può farlo, quindi deve costruire una nuova istanza con il nuovo stato, quindi restituire la nuova istanza, che è anche immutabile. Questo è un sovraccarico di prestazioni.

Molti tipi predefiniti di .NET sono strutture di dati immutabili, inclusa la maggior parte dei tipi di valore (tipi primitivi, System.Nullable, System.DateTime, System.TimeSpan e così via) e alcuni tipi di riferimento (string, System.Lazy, System.Linq.Expressions.Expression e suoi tipi derivati, ecc.). Microsoft fornisce anche un pacchetto NuGet di raccolte immutabili System.Collections.Immutable, con array, elenco, dizionario e così via non modificabili

Struttura immutabile (struttura di sola lettura)

La struttura seguente è definita con lo stesso schema della classe immutabile sopra. La struttura sembra immutabile:

internal partial struct Complex
{
    internal Complex(double real, double imaginary)
    {
        this.Real = real;
        this.Imaginary = imaginary;
    }

    internal double Real { get; }

    internal double Imaginary { get; }
}

Con lo zucchero sintattico della proprietà auto, vengono generati campi di sola lettura. Tuttavia, per la struttura, i campi di sola lettura non sono sufficienti per l'immutabilità. A differenza della classe, nei membri della funzione di istanza della struttura, questo riferimento è mutabile:

internal partial struct Complex
{
    internal Complex(Complex value) => this = value; // Can reassign to this.

    internal Complex Value
    {
        get => this;
        set => this = value; // Can reassign to this.
    }

    internal Complex ReplaceBy(Complex value) => this = value; // Can reassign to this.

    internal Complex Mutate(double real, double imaginary) => 
        this = new Complex(real, imaginary); // Can reassign to this.
}

Con questo mutabile, la struttura di cui sopra può ancora essere mutabile:

internal static void Structure()
{
    Complex complex1 = new Complex(1, 1);
    Complex complex2 = new Complex(2, 2);
    complex1.Real.WriteLine(); // 1
    complex1.ReplaceBy(complex2);
    complex1.Real.WriteLine(); // 2
}

Per affrontare questo scenario, C# 7,2 abilita il modificatore di sola lettura per la definizione della struttura. Per assicurarsi che la struttura sia immutabile, impone che tutti i campi di istanza siano di sola lettura e rende immutabile questo riferimento nei membri della funzione di istanza tranne il costruttore:

internal readonly partial struct ImmutableComplex
{
    internal ImmutableComplex(double real, double imaginary)
    {
        this.Real = real;
        this.Imaginary = imaginary;
    }

    internal ImmutableComplex(in ImmutableComplex value) => 
        this = value; // Can reassign to this only in constructor.

    internal double Real { get; }

    internal double Imaginary { get; }

    internal void InstanceMethod()
    {
        // Cannot reassign to this.
    }
}

Tipo anonimo immutabile

C# 3.0 introduce il tipo anonimo per rappresentare dati immutabili, senza fornire la definizione del tipo in fase di progettazione:

internal static void AnonymousType()
{
    var immutableDevice = new { Name = "Surface Book", Price = 1349.00M };
}

Poiché il nome del tipo è sconosciuto in fase di progettazione, l'istanza precedente è di tipo anonimo e il nome del tipo è rappresentato dalla parola chiave var. In fase di compilazione, viene generata la seguente definizione del tipo di dati immutabile:

[CompilerGenerated]
[DebuggerDisplay(@"\{ Name = {Name}, Price = {Price} }", Type = "<Anonymous Type>")]
internal sealed class AnonymousType0<TName, TPrice>
{
    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly TName name;

    [DebuggerBrowsable(DebuggerBrowsableState.Never)]
    private readonly TPrice price;

    [DebuggerHidden]
    public AnonymousType0(TName name, TPrice price)
    {
        this.name = name;
        this.price = price;
    }

    public TName Name => this.name;

    public TPrice Price => this.price;

    [DebuggerHidden]
    public override bool Equals(object value) =>
        value is AnonymousType0<TName, TPrice> anonymous
        && anonymous != null
        && EqualityComparer<TName>.Default.Equals(this.name, anonymous.name)
        && EqualityComparer<TPrice>.Default.Equals(this.price, anonymous.price);

    // Other members.
}

E la sintassi simile alla proprietà di impostazione sopra viene compilata nella normale chiamata del costruttore:

internal static void CompiledAnonymousType()
{
    AnonymousType0<string, decimal> immutableDevice = new AnonymousType0<string, decimal>(
        name: "Surface Book", price: 1349.00M);
}

Se sono presenti altri tipi anonimi diversi utilizzati nel codice, il compilatore C# genera più definizioni di tipi AnonymousType1, AnonymousType2 e così via. I tipi anonimi vengono riutilizzati da un'istanza diversa se le loro proprietà hanno lo stesso numero, nomi, tipi e ordine:

internal static void ReuseAnonymousType()
{
    var device1 = new { Name = "Surface Book", Price = 1349.00M };
    var device2 = new { Name = "Surface Pro 4", Price = 899.00M };
    var device3 = new { Name = "Xbox One S", Price = 399.00 }; // Price is of type double.
    var device4 = new { Price = 174.99M, Name = "Microsoft Band 2" };
    (device1.GetType() == device2.GetType()).WriteLine(); // True
    (device1.GetType() == device3.GetType()).WriteLine(); // False
    (device1.GetType() == device4.GetType()).WriteLine(); // False
}

Il nome della proprietà del tipo anonimo può essere dedotto dall'identificatore utilizzato per inizializzare la proprietà. Le seguenti 2 istanze di tipo anonimo sono equivalenti:

internal static void PropertyInference(Uri uri, int value)
{
    var anonymous1 = new { value, uri.Host };
    var anonymous2 = new { value = value, Host = uri.Host };
}

Il tipo anonimo può anche far parte di altri tipi, come array e parametro di tipo per il tipo generico, ecc:

internal static void AnonymousTypeParameter()
{
    var source = new[] // AnonymousType0<string, decimal>[].
    {
        new { Name = "Surface Book", Price = 1349.00M },
        new { Name = "Surface Pro 4", Price = 899.00M }
    };
    var query = // IEnumerable<AnonymousType0<string, decimal>>.
        source.Where(device => device.Price > 0);
}

Qui si deduce che la matrice di origine è di tipo AnonymousType0[], poiché ogni valore della matrice è di tipo AnonymousType0. La matrice T[] implementa l'interfaccia IEnumerable, quindi la matrice di origine implementa l'interfaccia IEnumerable>. Il suo metodo di estensione Where accetta una funzione di predicato AnonymousType0 –> bool e restituisce IEnumerable>.

Il compilatore C# utilizza il tipo anonimo per la clausola let nell'espressione di query LINQ. La clausola let viene compilata per selezionare la chiamata al metodo di query con una funzione di selezione che restituisce un tipo anonimo. Ad esempio:

internal static void Let(IEnumerable<int> source)
{
    IEnumerable<double> query =
        from immutable1 in source
        let immutable2 = Math.Sqrt(immutable1)
        select immutable1 + immutable2;
}

internal static void CompiledLet(IEnumerable<int> source)
{
    IEnumerable<double> query = source // from clause.
        .Select(immutable1 => new { immutable1, immutable2 = Math.Sqrt(immutable1) }) // let clause.
        .Select(anonymous => anonymous.immutable1 + anonymous.immutable2); // select clause.
}

I dettagli completi della compilazione delle espressioni di query sono trattati nel capitolo LINQ to Objects.

Inferenza del tipo di variabile locale

Oltre alla variabile locale di tipo anonimo, la parola chiave var può essere utilizzata anche per inizializzare una variabile locale di tipo esistente:

internal static void LocalVariable(IEnumerable<int> source, string path)
{
    var a = default(int); // int.
    var b = 1M; // decimal.
    var c = typeof(void); // Type.
    var d = from int32 in source where int32 > 0 select Math.Sqrt(int32); // IEnumerable<double>.
    var e = File.ReadAllLines(path); // string[].
}

Questo è solo uno zucchero sintattico. Il tipo della variabile locale viene dedotto dal tipo del valore iniziale. La compilazione della variabile locale tipizzata implicita non ha differenze rispetto alla variabile locale tipizzata in modo esplicito. Quando il tipo del valore iniziale è ambiguo, la parola chiave var non può essere utilizzata direttamente:

internal static void LocalVariableWithType()
{
    var f = (Uri)null;
    var g = (Func<int, int>)(int32 => int32 + 1);
    var h = (Expression<Func<int, int>>)(int32 => int32 + 1);
}

Per coerenza e leggibilità, questo tutorial utilizza la digitazione esplicita quando possibile, utilizza la digitazione implicita (var) quando necessario (per il tipo anonimo).

Tupla immutabile vs. tupla mutabile

Tuple è un altro tipo di struttura dati comunemente usata nella programmazione funzionale. È un elenco di valori finito e ordinato, generalmente immutabile nella maggior parte dei linguaggi funzionali. Per rappresentare la tupla, a partire da .NET Framework 3.5, viene fornita una serie di classi di tupla generiche con 1 ~ 8 parametri di tipo. Ad esempio, la seguente è la definizione di Tupla, che rappresenta una tupla a 2 (tupla di 2 valori):

namespace System
{
    [Serializable]
    public class Tuple<T1, T2> : IStructuralEquatable, IStructuralComparable, IComparable, ITuple
    {
        public Tuple(T1 item1, T2 item2)
        {
            this.Item1 = item1;
            this.Item2 = item2;
        }

        public T1 Item1 { get; }

        public T2 Item2 { get; }

        // Other members.
    }
}

Tutte le classi di tuple sono immutabili. L'ultimo C# 7.0 introduce la sintassi della tupla, che funziona con una serie di strutture di tupla generiche con 1 ~ 8 parametri di tipo. Ad esempio, 2-tuple è ora rappresentata dalla seguente struttura ValueTuple:

namespace System
{
    [StructLayout(LayoutKind.Auto)]
    public struct ValueTuple<T1, T2> : IEquatable<ValueTuple<T1, T2>>, IStructuralEquatable, IStructuralComparable, IComparable, IComparable<ValueTuple<T1, T2>>, ITupleInternal
    {
        public T1 Item1;

        public T2 Item2;

        public ValueTuple(T1 item1, T2 item2)
        {
            this.Item1 = item1;
            this.Item2 = item2;
        }

        public override bool Equals(object obj) => obj is ValueTuple<T1, T2> tuple && this.Equals(tuple);

        public bool Equals(ValueTuple<T1, T2> other) =>
            EqualityComparer<T1>.Default.Equals(this.Item1, other.Item1)
            && EqualityComparer<T2>.Default.Equals(this.Item2, other.Item2);

        public int CompareTo(ValueTuple<T1, T2> other)
        {
            int compareItem1 = Comparer<T1>.Default.Compare(this.Item1, other.Item1);
            return compareItem1 != 0 ? compareItem1 : Comparer<T2>.Default.Compare(this.Item2, other.Item2);
        }

        public override string ToString() => $"({this.Item1}, {this.Item2})";

        // Other members.
    }
}

La tupla del valore viene fornita per prestazioni migliori, poiché non gestisce l'allocazione dell'heap e la raccolta dei dati inutili. Tuttavia, tutte le strutture delle tuple di valori diventano tipi mutabili, in cui i valori sono solo campi pubblici. Per essere funzionale e coerente, questo tutorial utilizza solo tuple di valore e le usa solo come tipi immutabili.

Come mostra la definizione di tupla sopra, a differenza di list, i valori di tupla possono essere di diversi tipi:

internal static void TupleAndList()
{
    ValueTuple<string, decimal> tuple = new ValueTuple<string, decimal>("Surface Book", 1349M);
    List<string> list = new List<string>() { "Surface Book", "1349M" };
}

Il tipo tupla e il tipo anonimo sono concettualmente simili tra loro, sono entrambi un insieme di proprietà che restituiscono un elenco di valori. La differenza principale è che, in fase di progettazione, il tipo di tupla è definito e il tipo anonimo non è ancora definito. Pertanto, il tipo anonimo (var) può essere utilizzato solo per la variabile locale con valore iniziale da cui dedurre il tipo previsto e non può essere utilizzato come tipo di parametro, tipo restituito, tipo argomento, ecc.:

internal static ValueTuple<string, decimal> Method(ValueTuple<string, decimal> values)
{
    ValueTuple<string, decimal> variable1;
    ValueTuple<string, decimal> variable2 = default;
    IEnumerable<ValueTuple<string, decimal>> variable3;
    return values;
}

internal static var Method(var values) // Cannot be compiled.
{
    var variable1; // Cannot be compiled.
    var variable2 = default; // Cannot be compiled.
    IEnumerable<var> variable3; // Cannot be compiled.
    return values;
}

Costruzione, elemento e inferenza elemento

C# 7.0 introduce lo zucchero sintattico tupla, che offre grande praticità. Il tipo di tupla ValuTuple può essere semplificato in (T1, T2, T3, …) e la costruzione della tupla nuova ValueTuple(valore1, valore2, valore3, … ) può essere semplificato in (valore1, valore2, valore3, …):

internal static void TupleTypeLiteral()
{
    (string, decimal) tuple1 = ("Surface Pro 4", 899M);
    // Compiled to: 
    // ValueTuple<string, decimal> tuple1 = new ValueTuple<string, decimal>("Surface Pro 4", 899M);

    (int, bool, (string, decimal)) tuple2 = (1, true, ("Surface Studio", 2999M));
    // ValueTuple<int, bool, ValueTuple<string, decimal>> tuple2 = 
    //    new ValueTuple<int, bool, new ValueTuple<string, decimal>>(1, true, ("Surface Studio", 2999M))
}

Apparentemente, la tupla può essere il tipo di parametro/ritorno della funzione, proprio come altri tipi. Quando si utilizza la tupla come tipo restituito dalla funzione, la sintassi della tupla consente virtualmente alla funzione di restituire più valori:

internal static (string, decimal) MethodReturnMultipleValues()
// internal static ValueTuple<string, decimal> MethodReturnMultipleValues()
{
    string returnValue1 = default;
    int returnValue2 = default;

    (string, decimal) Function() => (returnValue1, returnValue2);
    // ValueTuple<string, decimal> Function() => new ValueTuple<string, decimal>(returnValue1, returnValue2);

    Func<(string, decimal)> function = () => (returnValue1, returnValue2);
    // Func<ValueTuple<string, decimal>> function = () => new ValueTuple<string, decimal>(returnValue1, returnValue2);

    return (returnValue1, returnValue2);
}

C# 7.0 introduce anche il nome dell'elemento per la tupla, in modo che a ogni valore del tipo di tupla possa essere assegnato un nome simile a una proprietà, con la sintassi (T1 Name1, T2 Name2, T3 Name3, ...) e ogni valore dell'istanza della tupla può anche dare un nome, con sintassi (Nome1:valore1, Nome2, valore2, Nome3 valore3, …). In modo che sia possibile accedere ai valori nella tupla con un nome significativo, invece dei nomi dei campi Item1, Item2, Item3, … effettivi. Anche questo è uno zucchero sintattico, in fase di compilazione, tutti i nomi degli elementi sono tutti sostituiti dai campi sottostanti.

internal static void ElementName()
{
    (string Name, decimal Price) tuple1 = ("Surface Pro 4", 899M);
    tuple1.Name.WriteLine();
    tuple1.Price.WriteLine();
    // Compiled to: 
    // ValueTuple<string, decimal> tuple1 = new ValueTuple<string, decimal>("Surface Pro 4", 899M);
    // TraceExtensions.WriteLine(tuple1.Item1);
    // TraceExtensions.WriteLine(tuple1.Item2)

    (string Name, decimal Price) tuple2 = (ProductNanme: "Surface Book", ProductPrice: 1349M);
    tuple2.Name.WriteLine(); // Element names on the right side are ignore.

    var tuple3 = (Name: "Surface Studio", Price: 2999M);
    tuple3.Name.WriteLine(); // Element names are available through var.

    ValueTuple<string, decimal> tuple4 = (Name: "Xbox One", Price: 179M);
    tuple4.Item1.WriteLine(); // Element names are not available on ValueTuple<T1, T2>.
    tuple4.Item2.WriteLine();

    (string Name, decimal Price) Function((string Name, decimal Price) tuple)
    {
        tuple.Name.WriteLine(); // Parameter element names are available in function.
        return (tuple.Name, tuple.Price - 10M);
    };
    var tuple5 = Function(("Xbox One S", 299M));
    tuple5.Name.WriteLine(); // Return value element names are available through var.
    tuple5.Price.WriteLine();

    Func<(string Name, decimal Price), (string Name, decimal Price)> function = tuple =>
    {
        tuple.Name.WriteLine(); // Parameter element names are available in function.
        return (tuple.Name, tuple.Price - 100M);
    };
    var tuple6 = function(("HoloLens", 3000M));
    tuple5.Name.WriteLine(); // Return value element names are available through var.
    tuple5.Price.WriteLine();
}

Simile all'inferenza della proprietà del tipo anonimo, C# 7.1 può dedurre il nome dell'elemento della tupla dall'identificatore utilizzato per inizializzare l'elemento. Le seguenti 2 tuple sono equivalenti:

internal static void ElementInference(Uri uri, int value)
{
    var tuple1 = (value, uri.Host);
    var tuple2 = (value: value, Host: uri.Host);
}

Decostruzione

A partire da C# 7.0, la parola chiave var può essere usata anche per decostruire la tupla in un elenco di valori. Questa sintassi è molto utile quando viene utilizzata con funzioni che restituiscono più valori rappresentati da tupla:

internal static void DeconstructTuple()
{
    (string, decimal) GetProductInfo() => ("HoLoLens", 3000M);
    var (name, price) = GetProductInfo();
    name.WriteLine(); // name is string.
    price.WriteLine(); // price is decimal.
}

Questo zucchero sintattico di decostruzione può essere utilizzato con qualsiasi tipo, a condizione che quel tipo abbia un'istanza Deconstruct o un metodo di estensione definito, in cui i valori sono parametri out. Prendi il tipo di dispositivo menzionato come esempio, ha 3 proprietà Nome, Descrizione e Prezzo, quindi il suo metodo Deconstruct può essere uno dei seguenti 2 moduli:

internal partial class Device
{
    internal void Deconstruct(out string name, out string description, out decimal price)
    {
        name = this.Name;
        description = this.Description;
        price = this.Price;
    }
}

internal static class DeviceExtensions
{
    internal static void Deconstruct(this Device device, out string name, out string description, out decimal price)
    {
        name = device.Name;
        description = device.Description;
        price = device.Price;
    }
}

Ora la parola chiave var può distruggere anche il dispositivo, che è appena compilato nella chiamata al metodo Destruct:

internal static void DeconstructDevice()
{
    Device GetDevice() => new Device() { Name = "Surface studio", Description = "All-in-one PC.", Price = 2999M };
    var (name, description, price) = GetDevice();
    // Compiled to:
    // string name; string description; decimal price;
    // surfaceStudio.Deconstruct(out name, out description, out price);
    name.WriteLine(); // Surface studio
    description.WriteLine(); // All-in-one PC.
    price.WriteLine(); // 2999
}

Scarta

Nella distruzione della tupla, poiché gli elementi vengono compilati in variabili out del metodo Destruct, qualsiasi elemento può essere scartato con il carattere di sottolineatura proprio come una variabile out:

internal static void Discard()
{
    Device GetDevice() => new Device() { Name = "Surface studio", Description = "All-in-one PC.", Price = 2999M };
    var (_, _, price1) = GetDevice();
    (_, _, decimal price2) = GetDevice();
}

Assegnazione tupla

Con la sintassi della tupla, ora C# può anche supportare l'assegnazione di tuple di fantasia, proprio come Python e altri linguaggi. L'esempio seguente assegna 2 valori a 2 variabili con una singola riga di codice, quindi scambia i valori di 2 variabili con una singola riga di codice:

internal static void TupleAssignment(int value1, int value2)
{
    (value1, value2) = (1, 2);
    // Compiled to:
    // value1 = 1; value2 = 2;

    (value1, value2) = (value2, value1);
    // Compiled to:
    // int temp1 = value1; int temp2 = value2;
    // value1 = temp2; value2 = temp1;
}

È facile calcolare il numero di Fibonacci con l'assegnazione di loop e tuple:

internal static int Fibonacci(int n)
{
    (int a, int b) = (0, 1);
    for (int i = 0; i < n; i++)
    {
        (a, b) = (b, a + b);
    }
    return a;
}

Oltre alle variabili, l'assegnazione della tupla funziona anche per altri scenari, come il tipo membro. L'esempio seguente assegna 2 valori a 2 proprietà con una singola riga di codice:

internal class ImmutableDevice
{
    internal ImmutableDevice(string name, decimal price) =>
        (this.Name, this.Price) = (name, price);

    internal string Name { get; }

    internal decimal Price { get; }
}

Immutabilità vs. sola lettura


Raccolta immutabile e raccolta di sola lettura

Microsoft fornisce raccolte non modificabili tramite il pacchetto System.Collections.Immutable NuGet, inclusi ImmutableArray, ImmutableDictionary, ImmutableHashSet, ImmutableList, ImmutableQueue, ImmutableSet, ImmutableStack, ecc. Come accennato in precedenza, provare a modificare una raccolta immutabile crea una nuova raccolta immutabile:

internal static void ImmutableCollection()
{
    ImmutableList<int> immutableList1 = ImmutableList.Create(1, 2, 3);
    ImmutableList<int> immutableList2 = immutableList1.Add(4); // Create a new collection.
    object.ReferenceEquals(immutableList1, immutableList2).WriteLine(); // False
}

.NET/Core fornisce anche raccolte di sola lettura, come ReadOnlyCollection, ReadOnlyDictionary e così via, che possono creare confusione. Queste raccolte di sola lettura sono in realtà un semplice wrapper di raccolte modificabili. Semplicemente non implementano ed espongono metodi come Aggiungi, Rimuovi, che vengono utilizzati per modificare la raccolta. Non sono né immutabili, né thread-safe. L'esempio seguente crea una raccolta non modificabile e una raccolta di sola lettura da un'origine mutabile. Quando la sorgente viene modificata, la raccolta immutabile apparentemente non viene modificata, ma la raccolta di sola lettura viene modificata:

internal static void ReadOnlyCollection()
{
    List<int> mutableList = new List<int>() { 1, 2, 3 };
    ImmutableList<int> immutableList = ImmutableList.CreateRange(mutableList);
    ReadOnlyCollection<int> readOnlyCollection = new ReadOnlyCollection<int>(mutableList);
    // ReadOnlyCollection<int> wraps a mutable source, just has no methods like Add, Remove, etc.

    mutableList.Add(4);
    immutableList.Count.WriteLine(); // 3
    readOnlyCollection.Count.WriteLine(); // 4
}