Funktionale C#-Programmierung im Detail (12) Unveränderlichkeit, anonymer Typ und Tupel

Funktionale C#-Programmierung im Detail (12) Unveränderlichkeit, anonymer Typ und Tupel

[LINQ via C#-Reihe]

[Eingehende Serie zur funktionalen Programmierung in C#]

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

Unveränderlichkeit ist ein wichtiger Aspekt des funktionalen Paradigmas. Wie bereits erwähnt, ist die imperative/objektorientierte Programmierung normalerweise zustandsbehaftet, und die funktionale Programmierung fördert die Unveränderlichkeit ohne Zustandsänderung. In der C#-Programmierung gibt es viele Arten von Unveränderlichkeit, aber sie können in zwei Ebenen eingeteilt werden:Unveränderlichkeit eines Werts und Unveränderlichkeit des internen Zustands eines Werts. Nehmen Sie als Beispiel eine lokale Variable. Eine lokale Variable kann als unveränderlich bezeichnet werden, wenn sie nach ihrer Zuweisung nicht mehr neu zugewiesen werden kann. Eine lokale Variable kann auch als unveränderlich bezeichnet werden, wenn es nach der Initialisierung ihres internen Zustands keine Möglichkeit gibt, ihren Zustand in einen anderen Zustand zu ändern.

Im Allgemeinen kann die Unveränderlichkeit das Programmieren in vielen Fällen vereinfachen, da sie eine wichtige Fehlerquelle beseitigt. Unveränderlicher Wert und unveränderlicher Zustand können auch die gleichzeitige/parallele/Multithread-Programmierung erheblich vereinfachen, da sie von Natur aus Thread-sicher sind. Der Nachteil der Unveränderlichkeit besteht offensichtlich darin, dass zum Ändern eines unveränderlichen Werts oder unveränderlichen Zustands eine weitere neue Instanz mit der Mutation erstellt werden muss, was zu einem Leistungsaufwand führen kann.

Unveränderlicher Wert

Viele funktionale Sprachen unterstützen unveränderliche Werte. Im Gegensatz zu variabel. Sobald einem Wert etwas zugewiesen wurde, kann er nicht neu zugewiesen werden, sodass er nicht in etwas anderes geändert werden kann. Beispielsweise ist ein Wert in F# standardmäßig unveränderlich, es sei denn, das veränderliche Schlüsselwort ist angegeben:

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.

Als C-ähnliche Sprache ist die C#-Variable standardmäßig veränderbar. C# hat einige andere Sprachfunktionen für unveränderliche Werte.

Konstante

C# verfügt über ein const-Schlüsselwort zum Definieren der Kompilierzeitkonstante, die zur Laufzeit nicht geändert werden kann. Es funktioniert jedoch nur für primitive Typen, Strings und Nullreferenzen:

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

using-Anweisung und foreach-Anweisung

C# unterstützt auch unveränderliche Werte in einigen Anweisungen, wie den oben erwähnten using- und foreach-Anweisungen:

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

diese Referenz für die Klasse

In der Klassendefinition kann dieses Schlüsselwort in Instanzfunktionsmitgliedern verwendet werden. Es bezieht sich auf die aktuelle Instanz der Klasse und ist unveränderlich:

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

Standardmäßig ist diese Referenz für die Strukturdefinition änderbar, was später besprochen wird.

Schreibgeschützte Eingabe und schreibgeschützte Ausgabe der Funktion

Der oben erwähnte Funktionsparameter, der durch eine schreibgeschützte Referenz (in Parameter) übergeben wird, ist in der Funktion unveränderlich, und das Funktionsergebnis, das durch eine schreibgeschützte Referenz (ref readonly return) neu eingestellt wird, ist für den Aufrufer der Funktion unveränderlich:

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

Lokale Variable durch schreibgeschützte Referenz (ref readonly variable)

C# 7.2 führt eine schreibgeschützte Referenz für lokale Variablen ein. Wenn in C# eine neue lokale Variable mit einer vorhandenen lokalen Variablen definiert und initialisiert wird, gibt es drei Fälle:

  • Durch Kopieren:direkt an lokale Variable zuweisen. Wenn eine Werttypinstanz zugewiesen wird, wird diese Werttypinstanz in eine neue Instanz kopiert; Wenn eine Referenztypinstanz zugewiesen wird, wird diese Referenz kopiert. Wenn also die neue lokale Variable neu zugewiesen wird, wird die vorherige lokale Variable nicht beeinflusst.
  • Durch Referenz:Weisen Sie der lokalen Variablen mit dem Schlüsselwort ref zu. Die neue lokale Variable kann virtuell als Zeiger oder Alias ​​der bestehenden lokalen Variablen betrachtet werden. Wenn also die neue lokale Variable neu zugewiesen wird, entspricht dies der Neuzuweisung der vorherigen lokalen Variablen
  • Durch Readonly-Referenz:Weisen Sie der lokalen Variablen mit den ref Readonly-Schlüsselwörtern zu. Die neue lokale Variable kann auch virtuell als Zeiger oder Alias ​​angesehen werden, aber in diesem Fall ist die neue lokale Variable unveränderlich und kann nicht neu zugewiesen werden.
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.
}

Unveränderlicher Wert im LINQ-Abfrageausdruck

Im LINQ-Abfrageausdruck, der von C# 3.0 eingeführt wurde, können die Klauseln from, join, let Werte deklarieren, und das Schlüsselwort into query kann auch Werte deklarieren. Diese Werte sind alle unveränderlich:

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

Der Abfrageausdruck ist ein syntaktischer Zucker von Abfragemethodenaufrufen, der ausführlich im Kapitel LINQ to Objects besprochen wird.

Unveränderlicher Zustand (unveränderlicher Typ)

Sobald eine Instanz aus einem unveränderlichen Typ erstellt wurde, können die internen Daten der Instanz nicht mehr geändert werden. In C# ist string (System.String) ein unveränderlicher Typ. Sobald eine Zeichenfolge erstellt wurde, gibt es keine API, um diese Zeichenfolge zu ändern. Beispielsweise ändert string.Remove die Zeichenfolge nicht, sondern gibt immer eine neu konstruierte Zeichenfolge zurück, bei der bestimmte Zeichen entfernt wurden. Im Gegensatz dazu ist String Builder (System.Text.StringBuilder) ein änderbarer Typ. Beispielsweise ändert StringBuilder.Remove tatsächlich die Zeichenfolge, um die angegebenen Zeichen zu entfernen. In der Kernbibliothek sind die meisten Klassen veränderliche Typen und die meisten Strukturen unveränderliche Typen.

Konstantes Feld des Typs

Beim Definieren des Typs (Klasse oder Struktur) ist ein Feld mit dem const-Modifizierer unveränderlich. Auch hier funktioniert es nur für primitive Typen, Zeichenfolgen und Nullreferenzen.

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

Unveränderliche Klasse mit schreibgeschütztem Instanzfeld

Wenn der Readonly-Modifizierer für ein Feld verwendet wird, kann das Feld nur vom Konstruktor initialisiert und später nicht neu zugewiesen werden. Eine unveränderliche Klasse kann also unveränderlich sein, indem alle Instanzfelder als schreibgeschützt definiert werden:

internal partial class ImmutableDevice
{
    private readonly string name;

    private readonly decimal price;
}

Mit dem oben erwähnten syntaktischen Zucker der Auto-Eigenschaft kann die schreibgeschützte Felddefinition automatisch generiert werden. Das Folgende ist ein Beispiel für veränderliche Datentypen mit Lese-/Schreibstatus und unveränderliche Datentypen mit Nur-Lese-Status, die in Nur-Lese-Instanzfeldern gespeichert sind:

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

Anscheinend kann eine konstruierte MutableDevice-Instanz ihren internen Zustand ändern, der von Feldern gespeichert wird, und eine ImmutableDevice-Instanz kann nicht:

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

Da die Instanz des unveränderlichen Typs den Zustand nicht ändern kann, beseitigt sie eine wichtige Fehlerquelle und ist immer Thread-sicher. Aber diese Vorteile haben ihren Preis. Es ist üblich, einige vorhandene Daten auf einen anderen Wert zu aktualisieren, z. B. einen Rabatt basierend auf dem aktuellen Preis:

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

Beim Abzinsen des Preises ändert MutableDevice.Discount direkt den Status. ImmutableDevice.Discount kann dies nicht, daher muss es eine neue Instanz mit dem neuen Status erstellen und dann die neue Instanz zurückgeben, die ebenfalls unveränderlich ist. Dies ist ein Leistungsmehraufwand.

Viele integrierte .NET-Typen sind unveränderliche Datenstrukturen, darunter die meisten Werttypen (primitive Typen, System.Nullable, System.DateTime, System.TimeSpan usw.) und einige Referenztypen (String, System.Lazy, System.Linq.Expressions.Expression und seine abgeleiteten Typen usw.). Microsoft stellt auch ein NuGet-Paket mit unveränderlichen Sammlungen System.Collections.Immutable bereit, mit unveränderlichem Array, Liste, Wörterbuch usw.

Unveränderliche Struktur (schreibgeschützte Struktur)

Die folgende Struktur wird mit dem gleichen Muster wie die obige unveränderliche Klasse definiert. Die Struktur sieht unveränderlich aus:

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

    internal double Real { get; }

    internal double Imaginary { get; }
}

Mit der syntaktischen Eigenschaft auto werden schreibgeschützte Felder generiert. Für die Struktur sind schreibgeschützte Felder jedoch nicht ausreichend für die Unveränderlichkeit. Im Gegensatz zur Klasse ist diese Referenz in den Instanzfunktionsmitgliedern der Struktur änderbar:

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

Mit mutable this kann die obige Struktur immer noch änderbar sein:

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
}

Um dieses Szenario zu adressieren, aktiviert C# 7.2 den Readonly-Modifizierer für die Strukturdefinition. Um sicherzustellen, dass die Struktur unveränderlich ist, erzwingt sie, dass alle Instanzfelder schreibgeschützt sind, und macht diese Referenz in Instanzfunktionsmitgliedern unveränderlich, mit Ausnahme von Konstruktor:

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

Unveränderlicher anonymer Typ

C# 3.0 führt anonyme Typen ein, um unveränderliche Daten darzustellen, ohne die Typdefinition zur Entwurfszeit bereitzustellen:

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

Da der Typname zur Entwurfszeit unbekannt ist, handelt es sich bei der obigen Instanz um einen anonymen Typ, und der Typname wird durch das Schlüsselwort var dargestellt. Zur Kompilierzeit wird die folgende unveränderliche Datentypdefinition generiert:

[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.
}

Und die obige Einstellungseigenschaft-ähnliche Syntax wird zum normalen Konstruktoraufruf kompiliert:

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

Wenn im Code andere unterschiedliche anonyme Typen verwendet werden, generiert der C#-Compiler weitere Typdefinitionen AnonymousType1, AnonymousType2 usw. Anonyme Typen werden durch unterschiedliche Instanziierung wiederverwendet, wenn ihre Eigenschaften dieselbe Nummer, Namen, Typen und Reihenfolge haben:

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
}

Der Eigenschaftsname des anonymen Typs kann aus dem Bezeichner abgeleitet werden, der zum Initialisieren der Eigenschaft verwendet wird. Die folgenden zwei Instanziierungen anonymer Typen sind äquivalent:

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

Anonymer Typ kann auch Teil anderer Typen sein, wie z. B. Array und Typparameter für generische Typen usw.:

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

Hier wird abgeleitet, dass das Quellarray vom Typ AnonymousType0[] ist, da jeder Arraywert vom Typ AnonymousType0 ist. Array T[] implementiert die IEnumerable-Schnittstelle, sodass das Quellarray die IEnumerable>-Schnittstelle implementiert. Seine Where-Erweiterungsmethode akzeptiert eine AnonymousType0 –> bool-Prädikatfunktion und gibt IEnumerable>.

zurück

Der C#-Compiler verwendet einen anonymen Typ für die let-Klausel im LINQ-Abfrageausdruck. Die let-Klausel wird zum Select-Abfragemethodenaufruf mit einer Auswahlfunktion kompiliert, die einen anonymen Typ zurückgibt. Zum Beispiel:

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

Die vollständigen Details zur Kompilierung von Abfrageausdrücken werden im Kapitel LINQ to Objects behandelt.

Inferenz vom lokalen Variablentyp

Neben lokalen Variablen des anonymen Typs kann auch das Schlüsselwort var verwendet werden, um lokale Variablen des vorhandenen Typs zu initialisieren:

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[].
}

Dies ist nur ein syntaktischer Zucker. Der Typ der lokalen Variablen wird vom Typ des Anfangswerts abgeleitet. Die Kompilierung von implizit typisierten lokalen Variablen unterscheidet sich nicht von explizit typisierten lokalen Variablen. Wenn der Typ des Anfangswerts mehrdeutig ist, kann das Schlüsselwort var nicht direkt verwendet werden:

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

Aus Gründen der Konsistenz und Lesbarkeit verwendet dieses Tutorial nach Möglichkeit die explizite Typisierung und bei Bedarf die implizite Typisierung (var) (für anonyme Typen).

Unveränderliches Tupel vs. veränderliches Tupel

Tuple ist eine andere Art von Datenstruktur, die häufig in der funktionalen Programmierung verwendet wird. Es ist eine endliche und geordnete Liste von Werten, die in den meisten funktionalen Sprachen normalerweise unveränderlich ist. Zur Darstellung von Tupeln wird seit .NET Framework 3.5 eine Reihe generischer Tupelklassen mit 1 bis 8 Typparametern bereitgestellt. Das Folgende ist beispielsweise die Definition von Tuple, das ein 2-Tupel (Tupel aus 2 Werten) darstellt:

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

Alle Tupelklassen sind unveränderlich. Das neueste C# 7.0 führt die Tupelsyntax ein, die mit einer Reihe generischer Tupelstrukturen mit 1 bis 8 Typparametern arbeitet. Beispielsweise wird 2-Tuple jetzt durch die folgende ValueTuple-Struktur dargestellt:

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

Das Wertetupel wird für eine bessere Leistung bereitgestellt, da es keine Heap-Zuweisung und Garbage Collection verwaltet. Alle Werttupelstrukturen werden jedoch zu änderbaren Typen, bei denen die Werte nur öffentliche Felder sind. Um funktional und konsistent zu sein, verwendet dieses Tutorial nur Werttupel und verwendet sie nur als unveränderliche Typen.

Wie die obige Tupeldefinition zeigt, können Tupelwerte im Gegensatz zur Liste von verschiedenen Typen sein:

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

Tupeltyp und anonymer Typ sind konzeptionell ähnlich, sie sind beide eine Reihe von Eigenschaften, die eine Liste von Werten zurückgeben. Der Hauptunterschied besteht darin, dass zur Entwurfszeit der Tupeltyp definiert wird und der anonyme Typ noch nicht definiert ist. Daher kann der anonyme Typ (var) nur für lokale Variablen mit Anfangswert verwendet werden, um auf den erwarteten Typ zu schließen, und kann nicht als Parametertyp, Rückgabetyp, Typargument usw. verwendet werden:

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

Konstruktion, Element und Elementschluss

C# 7.0 führt tupelsyntaktischen Zucker ein, was großen Komfort bringt. Der Tupeltyp ValuTuple kann vereinfacht werden zu (T1, T2, T3, …), und die Tupelkonstruktion neu ValueTuple(value1, value2, value3, …). ) kann vereinfacht werden zu (Wert1, Wert2, Wert3, …):

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

Anscheinend kann Tuple der Parameter/Rückgabetyp einer Funktion sein, genau wie andere Typen. Bei Verwendung von Tuple als Funktionsrückgabetyp ermöglicht die Tuple-Syntax praktisch, dass die Funktion mehrere Werte zurückgibt:

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 führt auch den Elementnamen für Tupel ein, sodass jedem Wert des Tupeltyps ein eigenschaftsähnlicher Name mit der Syntax (T1 Name1, T2 Name2, T3 Name3, …) gegeben werden kann, und jeder Wert der Tupelinstanz kann dies auch einen Namen erhalten, mit Syntax (Name1:Wert1, Name2, Wert2, Name3 Wert3, …). Damit auf die Werte im Tupel mit einem aussagekräftigen Namen zugegriffen werden kann, anstatt mit den eigentlichen Feldnamen Item1, Item2, Item3, …. Dies ist auch ein syntaktischer Zucker, zur Kompilierzeit werden alle Elementnamen durch die zugrunde liegenden Felder ersetzt.

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

Ähnlich wie bei der Eigenschaftsableitung des anonymen Typs kann C# 7.1 den Elementnamen des Tupels aus dem Bezeichner ableiten, der zum Initialisieren des Elements verwendet wurde. Die folgenden 2 Tupel sind äquivalent:

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

Dekonstruktion

Seit C# 7.0 kann das Schlüsselwort var auch verwendet werden, um Tupel in eine Werteliste zu zerlegen. Diese Syntax ist sehr nützlich, wenn sie mit Funktionen verwendet wird, die mehrere Werte zurückgeben, die durch Tupel dargestellt werden:

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

Dieser syntaktische Dekonstruktionszucker kann mit jedem Typ verwendet werden, solange für diesen Typ eine Deconstruct-Instanz oder eine Erweiterungsmethode definiert ist, bei der die Werte als Ausgangsparameter verwendet werden. Nehmen Sie den oben erwähnten Gerätetyp als Beispiel. Er hat 3 Eigenschaften Name, Beschreibung und Preis, also kann seine Dekonstruktionsmethode eine der folgenden 2 Formen sein:

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

Jetzt kann das var-Schlüsselwort auch Device zerstören, was einfach zum Destruct-Methodenaufruf kompiliert wird:

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
}

Verwerfen

Da bei der Tupelzerstörung die Elemente in out-Variablen der Destruct-Methode kompiliert werden, kann jedes Element mit Unterstrich verworfen werden, genau wie eine out-Variable:

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

Tupelzuweisung

Mit der Tupelsyntax kann C# jetzt auch ausgefallene Tupelzuweisungen unterstützen, genau wie Python und andere Sprachen. Das folgende Beispiel weist 2 Werten 2 Variablen mit einer einzigen Codezeile zu und tauscht dann die Werte von 2 Variablen mit einer einzigen Codezeile aus:

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

Es ist einfach, die Fibonacci-Zahl mit Schleifen- und Tupelzuweisung zu berechnen:

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

Neben Variablen funktioniert die Tupelzuweisung auch für andere Szenarien, wie z. B. Typmember. Das folgende Beispiel weist zwei Eigenschaften mit einer einzigen Codezeile zwei Werte zu:

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

    internal string Name { get; }

    internal decimal Price { get; }
}

Unveränderlichkeit vs. schreibgeschützt


Unveränderliche Sammlung vs. schreibgeschützte Sammlung

Microsoft stellt unveränderliche Sammlungen über das System.Collections.Immutable NuGet-Paket bereit, einschließlich ImmutableArray, ImmutableDictionary, ImmutableHashSet, ImmutableList, ImmutableQueue, ImmutableSet, ImmutableStack usw. Wie bereits erwähnt, erstellt der Versuch, eine unveränderliche Sammlung zu ändern, eine neue unveränderliche Sammlung:

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 bietet auch schreibgeschützte Sammlungen wie ReadOnlyCollection, ReadOnlyDictionary usw., was verwirrend sein kann. Diese schreibgeschützten Sammlungen sind eigentlich ein einfacher Wrapper für veränderliche Sammlungen. Sie implementieren und zeigen nur keine Methoden wie Add, Remove, die zum Ändern der Sammlung verwendet werden. Sie sind weder unveränderlich noch threadsicher. Das folgende Beispiel erstellt eine unveränderliche Sammlung und eine schreibgeschützte Sammlung aus einer veränderlichen Quelle. Wenn die Quelle geändert wird, wird die unveränderliche Sammlung anscheinend nicht geändert, aber die schreibgeschützte Sammlung wird geändert:

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
}