Juni 2018

Band 33, Nummer 6

.NET Framework - Tupel-Probleme: Warum c# Tupel abrufen, unterbrechen Sie die Richtlinien

Durch Markierung Michaelis | Juni 2018

Wieder in der Ausgabe August 2017 des MSDN Magazin ich hat einen ausführlichen Artikel über C#-7.0 und die Unterstützung für das Tupel (msdn.com/magazine/mt493248). Zum Zeitpunkt habe ich über die Tatsache, dass Tupeltyps mit c# 7.0 (intern vom Typ ValueTuple <>...) hält mehrere Richtlinien von einem gut strukturierten Werttyp ist, nämlich eingeführt:

• Felder, die öffentliche oder geschützte sind nicht deklarieren (stattdessen mit einer Eigenschaft kapseln).

• Führen Sie keine änderbaren Werttypen definiert.

• Nicht erstelle Werttypen größer als 16 Byte groß.

Diese Richtlinien seit c# 1.0 vorhanden waren, und noch hier in c# 7.0, sie haben ausgelöst wurden, Wind zur Definition der System.ValueTuple <>...-Datentyps. Technisch gesehen System.ValueTuple <>... ist eine Familie von Datentypen mit demselben Namen aber unterschiedlichen Stelligkeit (insbesondere die Anzahl von Typparametern). Was ist daher besonders zu diesem bestimmten Datentyp, die nicht mehr folgende Richtlinien lang eingehalten gelten? Und wie unser Verständnis der Umstände können sich diese Richtlinien gelten – oder sind nicht anwendbar – helfen Sie uns, ihre Anwendung zur Definition von Werttypen zu optimieren?

Beginnen Sie die Diskussion mit dem Schwerpunkt auf Kapselung und die Vorteile der Eigenschaften im Vergleich zu Felder ein. Betrachten Sie z. B. einen Bogen Werttyp, der einen Teil der Umfang eines Kreises darstellt. Es ist von den Radius des Kreises, den Startwinkel des ersten Punkts in den Bogen (in Grad) und den mittelpunktswinkel (in Grad) des letzten Punkts in den Bogen entsprechend definiert Abbildung 1.

Abbildung 1 definieren einen Bogen

public struct Arc
{
  public Arc (double radius, double startAngle, double sweepAngle)
  {
    Radius = radius;
    StartAngle = startAngle;
    SweepAngle = sweepAngle;
  }

  public double Radius;
  public double StartAngle;
  public double SweepAngle;

  public double Length
  {
    get
    {
      return Math.Abs(StartAngle - SweepAngle)
        / 360 * 2 * Math.PI * Radius;
    }
  }

  public void Rotate(double degrees)
  {
    StartAngle += degrees;
    SweepAngle += degrees;
  }

  // Override object.Equals
  public override bool Equals(object obj)
  {
    return (obj is Arc)
      && Equals((Arc)obj);
  }

        // Implemented IEquitable<T>
  public bool Equals(Arc arc)
  {
    return (Radius, StartAngle, SweepAngle).Equals(
      (arc.Radius, arc.StartAngle, arc.SweepAngle));
  }

  // Override object.GetHashCode
  public override int GetHashCode() =>
    return (Radius, StartAngle, SweepAngle).GetHashCode();

  public static bool operator ==(Arc lhs, Arc rhs) =>
    lhs.Equals(rhs);

  public static bool operator !=(Arc lhs, Arc rhs) =>
    !lhs.Equals(rhs);
}

Deklarieren Sie Felder, die öffentliche oder geschützte sind nicht

In dieser Deklaration ist Bogen ein Werttyp (mit der Schlüsselwort-Struktur definiert) mit drei öffentliche Felder, die die Merkmale des Bogens zu definieren. Ja, ich könnte Eigenschaften verwendet haben, aber ich öffentliche Felder in diesem Beispiel verwenden, insbesondere, da er die erste Führungslinie verletzt möchten – nicht deklarieren, die öffentliche oder geschützte Felder.

Durch Nutzung der öffentlichen Felder anstelle von Eigenschaften der Definition des Bogens verfügt nicht über die grundlegendste eines objektorientierten Entwurfsprinzipien – Kapselung. Angenommen, was geschieht, wenn habe ich mich entschieden so ändern Sie die interne Datenstruktur für die Verwendung von des Radius, Winkel und Bogen Länge, z. B. statt der mittelpunktswinkel starten? Auf diese Weise wird offensichtlich die Schnittstelle für Bogen unterbrochen, und alle Clients sind gezwungen werden, um eine Änderung vorzunehmen.

Auf ähnliche Weise müssen mit den Definitionen von Radius, StartAngle und SweepAngle, keine Validierung. RADIUS, könnte z. B. einen negativen Wert zugewiesen werden. Und zwar negative Werte für StartAngle und SweepAngle zulässige, wäre ein Wert größer als 360 Grad nicht. Da Bogen mit öffentlichen Felder definiert ist, besteht leider keine Möglichkeit zum Hinzufügen von Validierung zum Schutz vor dieser Werte. Ja, ich konnte Überprüfung in Version 2 hinzufügen, indem Sie die Felder, Eigenschaften ändern, aber dies wird deshalb die Versionskompatibilität der Bogen-Struktur unterbrochen. Alle vorhandenen kompilierten Code, der die Felder aufgerufen wird unterbrochen zur Laufzeit, als würde der code (selbst wenn neu kompiliert), der das Feld als übergibt einen Ref-Parameter.

Angesichts der Richtlinie, dass öffentliche oder geschützte Felder nicht sein sollte, ist es Folgendes zu beachten, dass die Eigenschaften, vor allem mit Standardwerten, einfacher als explizite Felder, die durch Eigenschaften, vielen Dank für die Eigenschaft-Initialisierer in c# 6.0 unterstützt gekapselte definiert wurde. Dieser Code

public double SweepAngle { get; set; } = 180;

ist dies einfacher:

private double _SweepAngle = 180;

public double SweepAngle {
  get { return _SweepAngle; }
  set { _SweepAngle = value; }
}

Die Eigenschaft-Initialisierer-Unterstützung ist wichtig, weil eine automatisch implementierte Eigenschaft, die für Initialisierung erforderlich, ohne einen begleitenden Konstruktor müssten. Als Ergebnis der Richtlinie: "Sollten Sie über Felder automatisch implementierte Eigenschaften" (selbst bei privaten Feldern) macht Sinn, beide, da der Code präziser ist und Sie nicht mehr können ändern, Felder von außerhalb ihrer enthaltende Eigenschaft. All dies begünstigt noch eine andere Richtlinie "Vermeiden Sie den Zugriff auf Felder von außerhalb ihrer enthaltenen Eigenschaften" die Daten weiter oben beschriebenen Kapselung Prinzipals auch von anderen Klassenmembern haben vor.

An diesem Punkt können an der C#-7.0 Tupeltyps ValueTuple <>... zurückzugeben. Trotz der Richtlinie zu verfügbar gemachten Feldern ist ValueTuple < T1, T2 >, z. B. wie folgt definiert:

public struct ValueTuple<T1, T2>
  : IComparable<ValueTuple<T1, T2>>, ...
{
  public T1 Item1;
  public T2 Item2;
  // ...
}

Was ist der speziellen ValueTuple <>...? Im Gegensatz zu den meisten Datenstrukturen war das C#-7.0-Tupel, künftig als Tupel bezeichnet nicht über das gesamte Objekt (z. B. eine Person oder Kartenstapel-Objekt). Stattdessen war es Informationen über die einzelnen Teile gruppiert nach dem Zufallsprinzip aus Gründen der Transport, sodass sie von einer Methode ohne die aufwändig mit out oder Ref-Parameter zurückgegeben werden können. Mads Torgersen verwendet die Analogie von einer Vielzahl von Personen, die gerade auf dem gleichen Bus –, in denen der Bus wie ein Tupel ist und die Personen sind, wie die Elemente im Tupel. Die Elemente werden in einem Tupel return-Parameter gruppiert, da sie alle bestimmt sind, an den Aufrufer zurückgeben nicht verwendet werden, da sie alle anderen Zuordnungen miteinander unbedingt. Tatsächlich ist es wahrscheinlich, dass der Aufrufer klicken Sie dann die Werte aus der Tupel abrufen und mit ihnen, einzeln anstatt als eine Einheit arbeiten.

Die Bedeutung der einzelnen Elemente statt der gesamten macht dem Konzept der Kapselung weniger beeindruckenden. Angenommen, in einem Tupel Elemente vollständig nicht miteinander verknüpft werden können, ist häufig nicht erforderlich, auf diese Weise zu kapseln, dass Element1, z. B. ändern Item2 auswirken kann. (Im Gegensatz dazu, würde das Ändern der Länge des Bogens erfordern eine Änderung in eine oder beide der der Winkel Kapselung muss also.) Darüber hinaus sind keine ungültigen Werte für die Elemente in einem Tupel gespeichert. In den Datentyp des Elements selbst, nicht in die Zuordnung einer der Eigenschaften der auf das Tupel, würde keine Validierung erzwungen werden.

Aus diesem Grund Eigenschaften für das Tupel ausnahmslos stellen keine, und es ist kein denkbaren zukünftigen Wert bereitgestellten konnte. Kurz gesagt, wenn Sie vorhaben, einen Typ definieren, deren Daten mit keine Notwendigkeit für die Validierung änderbar ist, können Sie auch Felder verwenden. Ein weiterer Grund, dass Sie Eigenschaften nutzen möchten, möglicherweise werden unterschiedliche Zugriff zwischen der Getter und Setter aufweisen. Allerdings werden nicht vorausgesetzt Veränderlichkeit akzeptabel ist, Sie möchten Eigenschaften mit unterschiedlichen Getter/Setter Eingabehilfen entweder nutzen. All dies löst eine andere Frage – Tupeltyps muss änderbare?

Legen Sie keine änderbaren Werttypen

Die nächste Führungslinie zu berücksichtigen ist, die änderbare Werttyps. Noch einmal: der Bogen wird (dargestellt in den Code in Abbildung 2) gegen die Richtlinie verstößt. Es ist offensichtlich, wenn Sie darüber nachdenken, einen Werttyp übergibt eine Kopie, so dass die Änderung der Kopierens nicht vom Aufrufer Observable-Objekt. Allerdings dagegen den Code in Abbildung 2 veranschaulicht das Konzept der nur ändern, die Lesbarkeit des Codes nicht. Aus Sicht der Lesbarkeit mag es die Änderungen des Bogens.

Abbildung 2 Werttypen werden kopiert, damit der Aufrufer die Änderung berücksichtigt keine

[TestMethod]
public void PassByValue_Modify_ChangeIsLost()
{
  void Modify(Arc paramameter) { paramameter.Radius++; }
  Arc arc = new Arc(42, 0, 90);
  Modify(arc);
  Assert.AreEqual<double>(42, arc.Radius);
}

Was verwirrend ist, die in der Reihenfolge für einen Entwickler Wert Kopierverhalten zu erwarten ist, müssten sie wissen, dass Bogen ein Werttyp ist. Es ist jedoch nichts offensichtlich aus dem Quellcode, der das Verhalten des Wert-Typ angibt (obwohl um ziemlich sein, der Visual Studio-IDE einen Werttyp als eine Struktur angezeigt, wenn Sie den Datentyp zeigen). Man könnte vielleicht argumentieren, dass C#-Programmierer Werttyp im Vergleich zu verweistypsemantik, informiert sein sollten, dass das Verhalten in Abbildung 2 wird erwartet. Jedoch sollten Sie das Szenario in abbildung3Wenn ist das Kopierverhalten nicht so offensichtlich.

Abbildung 3 änderbaren Werts Typen weisen unerwartet Verhalten.

public class PieShape
{
  public Point Center { get; }
  public Arc Arc { get; }

  public PieShape(Arc arc, Point center = default)
  {
    Arc = arc;
    Center = center;
  }
}

public class PieShapeTests
{
  [TestMethod]
  public void Rotate_GivenArcOnPie_Fails()
  {
    PieShape pie = new PieShape(new Arc(42, 0, 90));
    Assert.AreEqual<double>(90, pie.Arc.SweepAngle);
    pie.Arc.Rotate(42);
    Assert.AreEqual<double>(90, pie.Arc.SweepAngle);
  }
}

Beachten Sie, dass trotz des Bogens Aufruf Rotate-Funktion der Bogen tatsächlich nie dreht. Warum ist das so? Dieses Verhalten verwirrend ist aufgrund der Kombination von zwei Faktoren ab. Zunächst ist Bogen ein Werttyp, der zu übergebenden Wert statt als Verweis übergeben werden. Daher das Kreisdiagramm aufrufen. ARC gibt eine Kopie des Bogens, anstatt die Rückgabe von der gleichen Instanz des Bogens an, die instanziiert wurde im Konstruktor zurück. Dies wäre ein Problem, wenn es für den zweiten Faktor wurde nicht. So ändern Sie die Instanz des Bogens im Kreis gespeicherten richtet sich an der Aufruf von drehen, aber in Wirklichkeit ändert er die Kopie aus der Bogen-Eigenschaft zurückgegeben. Und daher wir verfügen über die Richtlinie, die "Nicht änderbare Werttypen definiere."

Als zuvor Tupel in c# 7.0 ignorieren diese Richtlinie und macht öffentliche Felder, die per Definition ValueTuple <>... änderbare vornehmen. Trotz dieser Verletzung, ValueTuple <>... nicht beeinträchtigt werden, die gleichen Nachteile als Bogen. Der Grund ist, dass die einzige Möglichkeit zum Ändern der Tupel über das Feld "Item" ist. Der C#-Compiler lässt jedoch nicht die Änderung des Felds (oder einer Eigenschaft) von einem enthaltenden Typ (gibt an, ob der enthaltende Typ ein Verweistyp, Werttyp oder sogar ein Array oder andere Art von Auflistung wird) zurückgegeben. Der folgende Code wird z. B. nicht kompiliert:

pie.Arc.Radius = 0;

Noch wird mit diesem Code:

pie.Arc.Radius++;

Diese Anweisungen fehl, mit der Meldung "Fehler CS1612: Kann den Rückgabewert von "PieShape.Arc" ändern, da es sich nicht um eine Variable handelt." Die Richtlinie ist also nicht unbedingt genau. Anstatt alle änderbaren Werttypen zu vermeiden, ist der Schlüssel, um zu vermeiden, veränderliche Funktionen (Lese-/Schreibeigenschaften sind zulässig). Das wissen, natürlich annimmt, dass die Wertsemantik in angezeigten Abbildung 2 sind eindeutig genug, sodass Verhalten der systeminternen Wert erwartet wird.

Erstellen Sie nicht Werttypen größer als 16 Bytes

Diese Richtlinie ist erforderlich, aufgrund der Häufigkeit der Werttyp kopiert wird. Tatsächlich werden mit Ausnahme von ref- oder out-Parameter Werttypen praktisch kopiert jedes Mal, wenn darauf zugegriffen werden. Dies ist "true", ob eine Werttypinstanz zu einem anderen zuweisen (z. B. Bogen = Bogen in Abbildung 3) oder eines Methodenaufrufs (z. B. in angezeigten Modify(arc) Abbildung 2). Die Richtlinie werden aus Gründen der Leistung Wert Schriftgröße gering zu halten.

In der Wirklichkeit liegt, dass die Größe eines ValueTuple <>... können häufig größer als 128 Bits (16 Byte), da eine ValueTuple <>... sieben Einzelelemente (und sogar ein höherer bei Angabe von einem anderen Tupel für die achte Typparameter) enthalten kann. Warum ist Klicken Sie dann das C#-7.0-Tupel als Werttyp definiert?

Wie bereits erwähnt, seit das Tupel als eine Sprachfunktion, die auf mehreren Rückgabewerten zu aktivieren, ohne die komplexe Syntax erforderlich out- oder Ref Parameter. Das allgemeine Muster wurde dann zum Erstellen und ein Tupel zurück, und löschen es an den Aufrufer zurück. Übergeben ein Tupel der Stapel über ein Rückgabeparameter ist tatsächlich ähnelt eine Gruppe von Argumente im Stapel für einen Methodenaufruf übergeben. Anders gesagt sind return Tupel mit einzelnen Parameterlisten symmetrisch, soweit Speicher kopiert werden.

Wenn Sie das Tupel, wie ein Verweistyp deklariert, dann wäre es notwendig, den Typ auf dem Heap zu erstellen und initialisieren Sie sie mit der Elementwerte – möglicherweise kopieren, entweder einen Wert oder Verweis auf den Heap. In beiden Fällen ein Kopiervorgang von Arbeitsspeicher erforderlich ist, ähnlich dem von einem Werttyp-Memory-Kopie. Darüber hinaus wird zu einem späteren Zeitpunkt in der Zeit, wenn das Tupel Verweis nicht mehr zugänglich ist, der Garbage Collector müssen, mit den Speicher wiederherstellen. Das heißt, umfasst ein Tupel Verweis weiterhin Speicher kopieren sowie zusätzlichen Druck auf den Garbage Collector, wodurch ein Type-Wert-Tupel effizientere Möglichkeit. (In den seltenen Fällen, dass ein Wert-Tupel ist effizienter, nicht können Sie weiterhin die Version der Verweis, Tupel <>... verwenden.)

Während zueinander, um das Hauptthema des Artikels verwendet wird, beachten Sie die Implementierung von Equals und GetHashCode in Abbildung 1. Sie können sehen, wie Tupel eine Verknüpfung zum Implementieren der Equals und GetHashCode bereitzustellen. Weitere Informationen finden Sie unter "Using Tupel Außerkraftsetzung auf Gleichheit und GetHashCode".

Zusammenfassung

Auf den ersten Blick können überraschende für Tupel als unveränderlich Werttypen definiert sein scheinen. Nachdem alle ist minimal, die Anzahl der unveränderliche Werttypen, die in .NET Core und .NET Framework gefunden und stehen langjähriges Programmierungsrichtlinien, die für Werttypen unveränderlich und gekapselten mit Eigenschaften werden aufrufen. Es gibt auch der Einfluss des Merkmals unveränderlichen standardmäßig Ansatz für f#, die C#-Sprache-Designern, geben Sie eine Kurzschreibweise, um unveränderliche Variablen zu deklarieren oder unveränderliche Typen definieren gezwungen. (Zwar keine solche Kurzschreibweise derzeit Diskussion für C#-8.0, wurden nur-Lese Strukturen hinzugefügt C# 7.2 als Mittel zum sicherstellen, dass eine Struktur unveränderlich ist.)

Wenn Sie mit den Details befassen, sehen Sie eine Reihe wichtiger Faktoren. Hierzu gehören:

• Verweistypen eine zusätzliche Leistungseinbußen mit Garbagecollection zu erzwingen.

• Tupel sind in der Regel kurzlebige.

• Tupel Elemente keine vorhersehbaren benötigen Kapselung mit Eigenschaften.

• Sogar Tupeln, die Groß (durch Wert Typ Richtlinien) haben keine Arbeitsspeicher erheblich Kopiervorgänge, die weit über eine Reference-Tupel-Implementierung.

Zusammenfassend stehen zahlreiche Faktoren, die ein Type-Wert-Tupel mit öffentlichen Felder trotz der Standardrichtlinien begünstigen. Am Ende sind Richtlinien nur, dass Richtlinien. Nicht ignorieren, aber ausreichend angegeben werden – und würde ich schlage vor, explizit dokumentiert – Ursache ist OK, um außerhalb der Zeilen in manchen Fällen Farbe.

Weitere Informationen zu den Richtlinien zum Definieren von Werttypen sowohl Überschreiben von Equals und GetHashCode sehen Sie sich Kapitel 9 und 10 in meinem wichtige C#-Buch: "Wichtige c# 7.0" (IntelliTect.com/EssentialCSharp), die im Mai out sein soll.


Mark Michaelis ist der Gründer von IntelliTect und arbeitet als leitender technischer Architekt und Trainer. Nahezu zwei Jahrzehnten er wurde Microsoft MVP und ist seit 2007 Microsoft Regional Director. Michaelis fungiert auf mehrere Microsoft-Softwareentwurf überprüfen Teams, z. B. c#, Mi Crosoft Azure, SharePoint und Visual Studio ALM. Er spricht auf Entwicklerkonferenzen und hat zahlreiche Bücher, einschließlich seiner letzten geschrieben "vorangestellten sential c# 6.0 (5. Edition)" (itl.tc/EssentialCSharp). Sie können ihn auf Facebook unter facebook.com/Mark.Michaelis, über seinen Blog unter IntelliTect.com/Mark, auf Twitter: @markmichaelis oder per E-Mail unter mark@IntelliTect.com erreichen.