Übersetzung vorschlagen
 
Andere Vorschläge:

progress indicator
Keine anderen Vorschläge
MSDN Magazin > Home > Ausgaben > 2009 > MSDN Magazin Juli 2009 >  CLR von allen Seiten: Tupelerstellungen
Inhalt anzeigen:  Englisch mit deutscher ÜbersetzungInhalt anzeigen: Englisch mit deutscher Übersetzung
Dies sind maschinell übersetzte Inhalte, die von den Mitgliedern der Community bearbeitet werden können. Sie können die Übersetzung verbessern, indem Sie auf den jeweils zum Satz gehörenden Link "Bearbeiten" klicken.Mithilfe des Dropdown-Steuerelements "Inhalt anzeigen" links oben auf der Seite können Sie zudem bestimmen, ob nur der englische Originaltext, nur die deutsche Übersetzung oder beides nebeneinander angezeigt werden.
CLR Inside Out
Building Tuple
Matt Ellis

This column is based on a prerelease version of the Microsoft .NET Framework 4. Details are subject to change.
The upcoming 4.0 release of Microsoft .NET Framework introduces a new type called System.Tuple. System.Tuple is a fixed-size collection of heterogeneously typed data. Like an array, a tuple has a fixed size that can't be changed once it has been created. Unlike an array, each element in a tuple may be a different type, and a tuple is able to guarantee strong typing for each element. Consider trying to use an array to store a string and integer value together, as in the following:
        static void Main(string[] args) {
            object[] t = new object[2];
            t[0] = "Hello";
            t[1] = 4;

            PrintStringAndInt((string) t[0], (int) t[1]);
        }

        static void PrintStringAndInt(string s, int i) {
            Console.WriteLine("{0} {1}", s, i);
        }
The above code is both awkward to read and dangerous from a type safety point of view. We have no guarantee that the elements of the tuple are the correct type when we go to use them. What if, instead of an integer, we put a string in the second element in our tuple, as follows:
        static void Main(string[] args) {
            object[] t = new object[2];
            t[0] = "Hello";
            t[1] = "World";

            PrintStringAndInt((string) t[0], (int) t[1]);
        }

        static void PrintStringAndInt(string s, int i) {
            Console.WriteLine("{0} {1}", s, i);
        }
This code would compile, but at run time, the call to PrintStringAndInt would fail with an InvalidCastException when we tried to cast the string "World" to an integer. Let's now take a look at what the code might look like with a tuple type:
        static void Main(string[] args) {
            Tuple<string, int> t = new Tuple<string, int>("Hello", 4);
            PrintStringAndInt(t.Item1, t.Item2);
        }

        static void PrintStringAndInt(string s, int i) {
            Console.WriteLine("{0} {1}", s, i);
        }
With the tuple type, we've been able to remove the cast operators from our code, giving us better error checking at compile time. When constructing and using elements from the tuple, the compiler will ensure our types are correct.
As this example shows, it's obvious that a tuple is a useful type to have in the Microsoft .NET Framework. At first, it might seem like a very easy enhancement. However, we'll discuss some of the interesting design challenges that the team faced while designing System.Tuple. We'll also explore more about what we added to the Microsoft .NET Framework and why these additions were made.

Existing Tuple Types
There is already one example of a tuple floating around the Microsoft .NET Framework, in the System.Collections.Generic namespace: KeyValuePair. While KeyValuePair<TKey, TValue> can be thought of as the same as Tuple<T1, T2>, since they are both types that hold two things, KeyValuePair feels different from Tuple because it evokes a relationship between the two values it stores (and with good reason, as it supports the Dictionary class). Furthermore, tuples can be arbitrarily sized, whereas KeyValuePair holds only two things: a key and a value.
Within the framework, many teams created, each for its own use, a private version of a tuple but didn't share their versions across teams. The Base Class Libraries (BCL) team looked at all of these uses when designing their version of tuple. After other teams heard that the BCL team would be adding a tuple in Microsoft .NET Framework 4, other teams felt a strong desire to start using tuples in their code, too. The Managed Extensibility Framework (MEF) team even took a look at the draft of one of BCL's specs before implementing a version of tuple in their code. They later released this version as part of a consumer technology preview, with a supporting comment that it was a temporary implementation until tuples were added to the Microsoft .NET Framework!
Interoperability
One of the biggest set of customers inside Microsoft for tuple was the language teams themselves. While C# and VB.NET languages do not have the concept of a tuple as part of the core language, it is a common feature of many functional languages. When targeting such languages to the Micrososft.NET Framework, language developers had to define a managed representation of a tuple, which leads to unnecessary duplication. One language suffering that problem was F#, which previously had defined its own tuple type in FSharp.Core.dll but will now use the tuple added in Microsoft .NET Framework 4.
In addition to enabling the removal of duplicate types, having a common type ensures that it is easy to call functions across language boundaries. Consider what would happen if C# had added a tuple type (as well as new syntax to support it) to the language, but didn't use the same managed representation as F#. If this were the case, any time you wanted to call a method from an F# assembly that took a tuple as an argument, you would be unable to use the normal C# syntax for a tuple or pass an existing C# tuple. Instead, you would be forced to convert your C# tuple to an F# tuple, and then call the method. It's our hope that by providing a common tuple type, we'll make interoperability between managed languages with tuples that much easier.
Using Tuple from C#
While some languages like F# have special syntax for tuples, you can use the new common tuple type from any language. Revisiting the first example, we can see that while useful, tuples can be overly verbose in languages without syntax for a tuple:
    class Program {
        static void Main(string[] args) {
            Tuple<string, int> t = new Tuple<string, int>("Hello", 4);
            PrintStringAndInt(t.Item1, t.Item2);
        }

        static void PrintStringAndInt(string s, int i) {
            Console.WriteLine("{0} {1}", s, i);
        }
    }
Using the var keyword from C# 3.0, we can remove the type signature on the tuple variable, which allows for somewhat more readable code.
            var t = new Tuple<string, int>("Hello", 4);
We've also added some factory methods to a static Tuple class which makes it easier to build tuples in a language that supports type inference, like C#.
            var t = Tuple.Create("Hello", 4);
Don't be fooled by the apparent lack of types here. The type of t is still Tuple<string, int> and the compiler won't allow you to treat the elements as different types.
Now that we've seen how the tuple type works, we can take a look at the design behind the type itself.
Reference or Value Type?
At first glance, there isn't much to the tuple type, and it seems like something that could be designed and implemented over a long weekend. However, looks are often deceiving, and there were some very interesting design decisions that needed to be made during its development.
The first major decision was whether to treat tuples either as a reference or value type. Since they are immutable any time you want to change the values of a tuple, you have to create a new one. If they are reference types, this means there can be lots of garbage generated if you are changing elements in a tuple in a tight loop. F# tuples were reference types, but there was a feeling from the team that they could realize a performance improvement if two, and perhaps three, element tuples were value types instead. Some teams that had created internal tuples had used value instead of reference types, because their scenarios were very sensitive to creating lots of managed objects. They found that using a value type gave them better performance. In our first draft of the tuple specification, we kept the two-, three-, and four-element tuples as value types, with the rest being reference types. However, during a design meeting that included representatives from other languages it was decided that this "split" design would be confusing, due to the slightly different semantics between the two types. Consistency in behavior and design was determined to be of higher priority than potential performance increases. Based on this input, we changed the design so that all tuples are reference types, although we asked the F# team to do some performance investigation to see if it experienced a speedup when using a value type for some sizes of tuples. It had a good way to test this, since its compiler, written in F#, was a good example of a large program that used tuples in a variety of scenarios. In the end, the F# team found that it did not get a performance improvement when some tuples were value types instead of reference types. This made us feel better about our decision to use reference types for tuple.
Tuples of Arbitrary Length
Theoretically, a tuple can contain as many elements as needed, but we were able to create only a finite number of classes to represent tuples. This raised two major questions: how many generic parameters should the largest tuple have, and how should the larger tuples be encoded? Deciding on how many generic parameters to have in the largest tuple ended up being somewhat arbitrary. Since Microsoft .NET Framework 4 introduces new versions of the Action and Func delegates that take up to eight generic parameters, we chose to have System.Tuple work the same way. One nice feature of this design is that there's a correspondence between these two types. We could add an Apply method to each tuple that takes a correspondingly sized Action or Func of the same type and pass each element from the tuple as an argument to the delegate. While we ultimately didn't add this in order to keep the surface area of the type clean, it's something that could be added in a later release or with extension methods. There is certainly a nice symmetry in the number of generic parameters that belong to Tuple, Action, and Func. An aside: After System.Tuple was finished being designed, the owners of Action and Func created more instances of each type; they went up to 16 generic parameters. Ultimately, we decided not to follow suit: the decision on sizing was somewhat arbitrary to begin with, and we didn't feel the time we would spend adding eight more tuple types would be worth it.
Once we answered our first question, we still had to figure out a way to represent tuples with more than eight elements. We decided that the last element of an eight-element tuple would be called "Rest" and we would require it to be a tuple. The elements of this tuple would be the eighth, ninth, and so on, elements of the overall tuple. Therefore, users who wanted an eight-element tuple would create two instances of the tuple classes. The first would be the eight-element tuple, which would contain the first seven items of the tuple, and the second would be a one-element tuple that held the last tuple. In C#, it might look like this:
            Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8));
In languages like F# that already have concrete syntax for tuples, the compiler handles this encoding for you.

For languages that provide syntax for interacting with tuples, the names of the properties used to access each element aren't very interesting, since they are shielded from the developer. This is a very important question for languages like C#, where you have to interact with the type directly. We started with the idea of using Item1, Item2, and so on as the property names for the elements, but we received some interesting feedback from our framework design reviewers. They said that while these property names make sense when someone thinks about them at first glance, it gives the feeling that the type was auto-generated, instead of designed. They suggested that we name the properties First, Second, and so on, which felt more accessible. Ultimately, we rejected this feedback for a few reasons: first, we liked the experience of being able to change the element you access by changing one character of the property name; second, we felt that the English names were difficult to use (typing Fourth or Sixth is more work than Item4 or Item6) and would lead to a very weird IntelliSense experience, since property names would be alphabetized instead of showing up in numerical order.
Interfaces Implemented
System.Tuple implements very few interfaces and no generic interfaces. The F# team was very concerned about tuple being a very lightweight type, because it uses so many different generic instantiations of the type across its product. If Tuple were to implement a large number of generic interfaces, they felt it would have an impact on their working set and NGen image size. In light of this argument, we were unable to find compelling reasons for Tuple to implement interfaces like IEquatable<T> and IComparable<T>, even though it overrides Equals and implements IComparable.
Structural Equality, Comparison, and Equivalence
The most interesting challenge we faced when designing Tuple was to figure out how to support the following:
  • structural equality
  • structural comparison
  • partial equivalence relations
We did as much design work around these concepts as we did for Tuple itself. Structural equality and comparison relate to what Equals means for a type like Tuple that simply holds other data.
In F#, equality over tuples and arrays is structural. This means that two arrays or tuples are equal if all their elements are the same. This differs from C#. By default, the contents of arrays and tuples don't matter for equality. It is the location in memory that is important.
Since this idea of structural equality and comparison is already part of the F# specification, the F# team had already come up with a partial solution to the problem. However, it applied only to the types that the team created. Since it also needed structural equality over arrays, the compiler generated special code to test if it was doing equality comparison on an array. If so, it would do a structural comparison instead of just calling the Equals method on Array. The design team iterated on a few different designs on how to solve these sorts of structural equality and comparison problems. It settled on creating a few interfaces that structural types needed to implement.
IStructualEquatable and IStructuralComparable
IStructualEquatable and IStructuralComparable interfaces provide a way for a type to opt in to structural equality or comparison. They also provide a way for a comparer to be used for each element in the object. Using these interfaces, as well as a specially defined comparer, we can have deep equality over tuples and arrays when it is required, but we don't have to force the semantics on all users of the type. The design is deceptively simple:
    public interface IStructuralComparable {
        Int32 CompareTo(Object other, IComparer comparer);
    }

    public interface IStructuralEquatable {
        Boolean Equals(Object other, IEqualityComparer comparer);
        Int32 GetHashCode(IEqualityComparer comparer);
    }
These interfaces work by separating the act of iterating over elements in the structural type, for which the implementer of the interface is responsible, from the actual comparison, for which the caller is responsible. Comparers can decide if they want to provide a deep structural equality (by recursively calling the IStructualEquatable or IStructualComparable methods, if the elements implement them), a shallow one (by not doing so), or something completely different. Both Tuple and Array now implement both of these interfaces explicitly.
Partial Equivalence Relations
Tuple also needed to support the semantics of partial equivalence relations. An example of a partial equivalence relation in the Microsoft .NET Framework is the relationship between NaN and other floating-point numbers. For example: NaN<NaN is false, but the same holds for NaN>NaN and NaN == NaN and NaN != NaN. This is due to NaN being fundamentally incomparable, since it does not represent any number. While we are able to encode this sort of relationship with operators, the same does not hold for the CompareTo method on IComparable. This is because there is no value that CompareTo can return that signals the two values are incomparable.
F# requires that the structural equality on tuples also works with partial equivalence relationships. Therefore in F#, [NaN, NaN] == [NaN, NaN] is false, but so is [NaN, NaN] != [NaN, NaN].
Our first tentative solution was to have overloaded operators on Tuple. This worked by using operators on the underlying types, if they existed, and by falling back to Equals or CompareTo, if they did not. A second option was to create a new interface like IComparable but with a different return type so that it could signal cases where things were not comparable. Ultimately, we decided that we would hold off on building something like this until we saw more examples of partial equivalence being needed across the Microsoft .NET Framework. Instead, we recommended that F# implement this sort of logic in the IComparer and IEqualityComparer methods that they passed into the IStructrual variants of CompareTo and Equals methods by detecting these cases and having some sort of out-of-band signaling mechanism when it encountered NaNs.

Worth the Effort
Though it took a great deal more design iteration than anyone expected, we were able to create a tuple type that we feel is flexible enough for use in a variety of languages, regardless of syntactic support for tuples. At the same time, we've created interfaces that help describe the important concepts of structural equality and comparison, which have value in the Microsoft .NET Framework outside of tuple itself.
Over the past few months, the F# team has updated its compiler to use System.Tuple as the underlying type for all F# tuples. This ensures that we can start building toward a common tuple type across the Microsoft .NET ecosystem. In addition to this exciting development, tuple was demoed at this year's Professional Developers Conference as a new feature for Microsoft .NET Framework 4 and received much applause from the crowd. Watching the video and seeing how excited developers are to start using tuples has made all the time spent on this deceptively simple feature all the more worthwhile.

Send your questions and comments to clrinout@microsoft.com.

Matt Ellis is a Software Design Engineer on the Base Class Libraries team responsible for diagnostics, isolated storage, and other little features like Tuple. When he's not thinking about frameworks or programming language design, he spends his time with his wife Victoria and their two dogs, Nibbler and Snoopy.

CLR von allen Seiten
Tupelerstellungen
Matt Ellis

Dieser Artikel basiert auf einer Vorabversion von Microsoft .NET Framework 4. Änderungen vorbehalten.
Mit der bevorstehenden Veröffentlichung von Microsoft .NET Framework 4.0 wird ein neuer Typ namens System.Tuple eingeführt. System.Tuple ist eine Auflistung fester Größe von heterogen eingegebenen Daten. Wie ein Array hat ein Tupel eine feste Größe, die nicht geändert werden kann, nachdem er erstellt wurde. Im Gegensatz zu einem Array weist jedes Element in einem Tupel möglicherweise einen anderen Typ auf, und ein Tupel kann eine starke Typisierung für jedes Element sicherstellen. Versuchen Sie einmal, ein Array zu verwenden, um eine Zeichenfolge und einen Integer-Wert zusammen (wie im folgenden Beispiel) zu speichern:
        static void Main(string[] args) {
            object[] t = new object[2];
            t[0] = "Hello";
            t[1] = 4;

            PrintStringAndInt((string) t[0], (int) t[1]);
        }

        static void PrintStringAndInt(string s, int i) {
            Console.WriteLine("{0} {1}", s, i);
        }
Der obige Code ist ungeeigneter zum Lesen und aus Sicht der Sicherheit gefährlich. Wir haben keine Garantie, dass die Elemente des Tupels den richtigen Typ aufweisen, wenn wir sie verwenden. Was geschieht, wenn wir anstelle einer ganzen Zahl eine Zeichenfolge in das zweite Element unseres Tupels eingeben, wie im folgenden Beispiel:
        static void Main(string[] args) {
            object[] t = new object[2];
            t[0] = "Hello";
            t[1] = "World";

            PrintStringAndInt((string) t[0], (int) t[1]);
        }

        static void PrintStringAndInt(string s, int i) {
            Console.WriteLine("{0} {1}", s, i);
        }
Dieser Code würde kompiliert, zur Laufzeit gibt der Aufruf von PrintStringAndInt jedoch einen Fehler des Typs InvalidCastException zurück, wenn versucht wird, die Zeichenfolge "World" in eine ganze Zahl umzuwandeln. Nun betrachten wir, wie ein Code mit einem Tupel-Typ aussehen könnte:
        static void Main(string[] args) {
            Tuple<string, int> t = new Tuple<string, int>("Hello", 4);
            PrintStringAndInt(t.Item1, t.Item2);
        }

        static void PrintStringAndInt(string s, int i) {
            Console.WriteLine("{0} {1}", s, i);
        }
Mit dem Tupel-Typ konnten wir die Umwandlungsoperatoren aus unserem Code entfernen und so eine bessere Fehlerüberprüfung zur Kompilierzeit durchführen. Beim Erstellen und Verwenden von Elementen aus dem Tupel stellt der Compiler sicher, dass unsere Typen richtig sind.
Wie dieses Beispiel zeigt, ist es offensichtlich, dass ein Tupel ein nützlicher Typ in Microsoft .NET Framework ist. Zunächst mag es wie eine sehr einfache Verbesserung scheinen. Wir erörtern nun jedoch einige interessante Design-Herausforderungen, mit denen das Team beim Entwerfen von System.Tuple konfrontiert wurde. Darüber hinaus untersuchen wir, was Microsoft .NET Framework hinzugefügt wurde und warum diese Ergänzungen vorgenommen wurden.

Vorhandene Tupel-Typen
Es gibt bereits ein Beispiel für einen Tupel, der in Microsoft .NET Framework im System.Collections.Generic-Namespace im Umlauf ist: KeyValuePair. Während KeyValuePair<TKey, TValue> als identisch zu Tupel <T1, T2> betrachtet werden kann, da beide Typen zwei Dinge enthalten, ist der KeyValuePair-Tupel anders, da er eine Beziehung zwischen den beiden Werten aufruft, die er speichert (und mit gutem Grund, da er die Dictionary-Klasse unterstützt). Darüber hinaus können Tupel beliebig vergrößert und verkleinert werden, während KeyValuePair nur zwei Dinge enthält: einen Schlüssel und Wert.
Innerhalb des Frameworks erstellten viele Teams, jedes für sich, eine eigene Version eines Tupels, gaben diese jedoch nicht an andere Teams weiter. Das Basisklassenbibliotheksteam (Base Class Libraries, BCL) betrachtete alle diese Verwendungsmöglichkeiten beim Entwurf ihrer Tupel-Versionen. Nachdem andere Teams gehört hatten, dass das BCL-Team ein Tupel in Microsoft .NET Framework 4 hinzufügen würde, verspürten andere Teams den Wunsch, auch Tupel in ihrem Code zu verwenden. Das Managed Extensibility Framework (MEF)-Team warf vor der Implementierung einer Tupel-Version in ihrem Code sogar einen Blick auf den Entwurf einer BCL-Spezifikation. Sie veröffentlichten diese Version später als Teil einer Consumer Technology-Voransicht mit dem unterstützenden Kommentar, dass es sich um eine temporäre Implementierung handelt, bis Tupel in Microsoft .NET Framework hinzugefügt wurden.
Interoperabilität
Eine der größten Kundengrupppen innerhalb von Microsoft für Tupel war das Sprachteam selbst. Obwohl C#- und VB.NET-Sprachen nicht über das Tupelkonzept als Teil der Kernsprache verfügen, ist es ein allgemeines Feature von vielen funktionalen Sprachen. Wenn solche Sprachen auf Microsoft .NET Framework ausgerichtet wurden, mussten Sprachenentwickler eine verwaltete Repräsentation eines Tupels definieren, was zu einer unnötigen Duplizierung führt. Eine Sprache mit diesem Problem war F#, die zuvor seinen eigenen Tupel-Typ in FSharp.Core.dll definiert hatte, aber jetzt den Tupel verwendet, der in Microsoft .NET Framework 4 hinzugefügt wurde.
Zusätzlich zu der möglichen Entfernung von doppelten Typen gewährleistet ein allgemeiner Typ, dass Funktionen über Sprachgrenzen hinweg einfach aufgerufen werden können. Überlegen Sie, was geschieht, wenn mit C# ein Tupel-Typ (und die neue Syntax zur Unterstützung) zur Sprache hinzugefügt, aber nicht die gleiche verwaltete Repräsentation als F# verwendet worden wäre. Wenn dies der Fall wäre, könnten Sie nicht die normale C#-Syntax für ein Tupel verwenden oder einen bestehenden C#-Tupel weitergeben, wenn Sie eine Methode aus einer F#-Assembly aufrufen, die als Argument ein Tupel hat. Stattdessen würden Sie gezwungen werden, Ihre C#-Tupel in ein F#-Tupel zu konvertieren und dann die Methode aufzurufen. Wir hoffen, dass die Interoperabilität zwischen verwalteten Sprachen mit Tupeln viel einfacher wird, indem ein allgemeiner Tupel-Typ bereitgestellt wird.
Verwendung von Tupel in C#
Während einige Sprachen wie F# über eine spezielle Syntax für Tupel verfügen, können Sie den neuen allgemeinen Tupel-Typ mit einer beliebigen Sprache verwenden. Ein erneuter Blick auf das erste Beispiel zeigt, dass Tupel zwar nützlich sind, in Sprachen ohne Syntax für ein Tupel jedoch sehr umfangreich werden können:
    class Program {
        static void Main(string[] args) {
            Tuple<string, int> t = new Tuple<string, int>("Hello", 4);
            PrintStringAndInt(t.Item1, t.Item2);
        }

        static void PrintStringAndInt(string s, int i) {
            Console.WriteLine("{0} {1}", s, i);
        }
    }
Mit dem Var-Schlüsselwort aus C# 3.0 können wir die Typsignatur für die Tupel-Variable entfernen, was einen etwas besser lesbaren Code ermöglicht.
            var t = new Tuple<string, int>("Hello", 4);
Wir haben auch einige Factorymethoden zu einer statischen Tupel-Klasse hinzugefügt, um leichter Tupel in einer Sprache erstellen zu können, die Typrückschluss unterstützen, beispielsweise C#.
            var t = Tuple.Create("Hello", 4);
Lassen Sie sich nicht durch das offensichtliche Fehlen von Typen täuschen. Der Typ t ist weiterhin Tuple<string, int>, und der Compiler wird nicht zulassen, dass Sie die Elemente als verschiedene Typen behandeln.
Nun, da wir die Funktionsweise des Tupel-Typs gesehen haben, können wir den Entwurf hinter dem Typ selbst betrachten.
Verweis- oder Werttyp?
Auf den ersten Blick ist der Tupel-Typ unscheinbar, und er scheint wie etwas, das an einem langen Wochenende entworfen und implementiert werden könnte. Der erste Eindruck täuscht jedoch oft, und es wurden einige sehr interessante Entwurfsentscheidungen getroffen, die während der Entwicklung erforderlich waren.
Die erste wichtige Entscheidung war, ob Tupel-Typen als Verweis- oder Werttypen behandelt werden sollen. Da sie unveränderlich sind, müssen Sie jedes Mal einen neuen Tupel erstellen, wenn Sie die Werte eines Tupels ändern möchten. Wenn sie Verweistypen sind, bedeutet dies, dass jede Menge überflüssige Daten generiert werden, wenn Sie Elemente in einem Tupel in einer engen Schleife ändern. F#-Tupel waren Verweistypen, das Team war jedoch der Meinung, dass es eine Leistungsverbesserung erkennen konnte, wenn zwei oder vielleicht drei Tupel-Typen stattdessen Werttypen waren. Einige Teams, die interne Tupel erstellten, hatten anstelle von Verweistypen Werttypen verwendet, da ihre Szenarien sehr empfindlich bei der Erstellung einer Vielzahl von verwalteten Objekten reagierten. Diese fanden heraus, dass das Verwenden eines Werttyps eine bessere Leistung ergab. In unserem ersten Entwurf der Tupelspezifikation behielten wir die Tupel aus zwei, drei und vier Elementen als Werttyp bei, der Rest bestand aus Verweistypen. Allerdings wurde während einer Entwurfsbesprechung mit Vertretern anderer Sprachen entschieden, dass dieser "Teilen"-Entwurf aufgrund der etwas anderen Semantik zwischen den beiden Typen verwirrend wäre. Konsistenz in Verhalten und Entwurf wurde mit höherer Priorität als potenzielle Leistungssteigerungen festgelegt. Basierend auf dieser Eingabe änderten wir den Entwurf, sodass alle Tupel Verweistypen sind, obwohl wir das F#-Team baten, einige Leistungsuntersuchung durchzuführen, um zu sehen, ob eine Beschleunigung festgestellt wird, wenn ein Werttyp für einige Tupelgrößen verwendet wird. Es war eine gute Möglichkeit, dies zu testen, da der Compiler in F# geschrieben und ein gutes Beispiel für ein großes Programm war, das Tupel in einer Vielzahl von Szenarien verwendet. Am Ende fand das F#-Team heraus, dass es keine Leistungsverbesserung erhielt, wenn einige Tupel Werttypen anstelle von Verweistypen waren. Dadurch fühlten wir uns bei unserer Entscheidung, Tupel als Verweistypen zu verwenden, besser.
Tupel willkürlicher Länge
Theoretisch kann ein Tupel beliebig viele Elemente enthalten, aber wir konnten nur eine begrenzte Anzahl von Klassen zur Darstellung von Tupeln erstellen. Das warf zwei wichtige Fragen auf: wie viele generische Parameter sollte das größte Tupel aufweisen, und wie sollten die größeren Tupel codiert sein? Die Entscheidung, wie viele generische Parameter im größten Tupel enthalten sein können, wurde etwas willkürlich getroffen. Da mit Microsoft .NET Framework 4 neue Versionen der Action- und Func-Delegaten eingeführt werden, die bis zu acht generische Parameter haben, beschlossen wir, dass System.Tuple auf die gleiche Weise funktionieren sollte. Ein nützliches Feature dieses Entwurfs besteht darin, dass eine Entsprechung zwischen diesen beiden Typen vorhanden ist. Wir könnten jedem Tupel mit entsprechend goßem Action oder Func des gleichen Typs eine Apply-Methode hinzufügen und jedes Element aus dem Tupel als Argument an den Delegaten übergeben. Obwohl wir dies letzten Endes nicht hinzugefügt haben, um die Oberfläche des Typs sauber zu halten, ist es etwas, das in einer späteren Version oder mit Erweiterungsmethoden hinzugefügt werden könnte. Es ist sicherlich eine schöne Symmetrie bei der Anzahl von generischen Parametern, die Tuple, Action und Func angehören. Übrigens: Nach dem Entwurf von System.Tuple erstellten die Besitzer von Action und Func weitere Instanzen jedes Typs; Sie verwendeten bis zu 16 generische Parameter. Letztlich entschieden wir, dies nicht so zu handhaben: Die Entscheidung bezüglich der Größe war zunächst etwas willkürlich, und wir fanden, dass es zuviel Zeit kosten würde, acht weitere Tupel-Typen hinzuzufügen.
Nachdem wir unsere erste Frage beantwortet hatten, mussten wir eine Möglichkeit finden, Tupel mit mehr als acht Elementen darzustellen. Wir beschlossen, dass das letzte Element eines Tupels mit acht Elementen "Rest" genannt würde und ein Tupel sein musste. Die Elemente dieses Tupels wären das achte, neunte Element usw. des gesamten Tupels. Benutzer, die ein Tupel mit acht Elementen wollten, würden daher zwei Instanzen der Tupel-Klassen erstellen. Die erste wäre der Tupel mit acht Elementen, der die ersten sieben Elemente des Tupel enthalten würde, und die zweite wäre ein Tupel mit einem Element, das den letzten Tupel enthält. In C# könnte dies wie folgt aussehen:
            Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8));
In Sprachen wie F#, die bereits konkrete Syntax für Tupel haben, übernimmt der Compiler diese Codierung für Sie.

Bei Sprachen, die Syntax für die Interaktion mit Tupeln bereitstellen, sind die Namen der Eigenschaften für den Zugriff auf jedes Element nicht sehr interessant, da Sie vom Entwickler abgeschirmt sind. Dies ist eine sehr wichtige Frage bei Sprachen wie C#, bei denen Sie mit dem Typ direkt interagieren müssen. Wir begannen mit der Vorstellung, Item1, Item2 usw. als Eigenschaftennamen für die Elemente zu verwenden, aber wir erhielten interessantes Feedback von unseren Framework-Entwurfsüberprüfern. Sie sagten, obwohl diese Eigenschaftennamen sinnvoll sind, wenn jemand nur einen kurzen Blick darauf wirft, kommt das Gefühl auf, dass der Typ automatisch generiert und nicht entwickelt wurde. Sie schlugen vor, die Eigenschaften First, Second usw. zu nennen, was zugänglicher erscheint. Schließlich lehnten wir dieses Feedback aus einigen wenigen Gründen ab: Zunächst schätzten wir die Erfahrung, das Element ändern zu können, auf das zugegriffen wird, indem ein Zeichen des Eigenschaftennamens geändert wird; und zweitens hatten wir das Gefühl, dass die englischen Namen schwierig zu verwenden sind (Fourth oder Sixth eingeben ist mehr Arbeit als Item4 oder Item6) und würde zu einer äußerst merkwürdigen IntelliSense-Erfahrung führen, da Eigenschaftennamen alphabetisiert statt in numerischer Reihenfolge angezeigt werden würden.
Implementierte Schnittstellen
Für System.Tuple werden sehr wenige Schnittstellen und keine generischen Schnittstellen implementiert. Das F#-Team war sehr besorgt darüber, dass der Tupel eine sehr einfacher Typ ist, da er so viele verschiedene generische Instanziierungen des Typs innerhalb des Produkts verwendet. Sie dachten, dass dies Auswirkungen auf Ihre Arbeitsseiten und die NGen-Imagegröße hätte, wenn Tupel eine große Anzahl von generischen Schnittstellen implementieren sollten. Angesichts dieses Arguments konnten wir keine zwingenden Gründe dafür finden, Tupel zum Implementieren von Schnittstellen wie IEquatable<T> und IComparable<T> zu verwenden, auch wenn es Equals überschreibt und IComparable implementiert.
Strukturgleichheit, -vergleich und -gleichwertigkeit
Die interessanteste Herausforderung, mit der wir beim Entwerfen von Tupel konfrontiert wurden, war herauszufinden, wie damit Folgendes unterstützt werden konnte:
  • Strukturgleichheit
  • Strukturvergleich
  • Teilgleichheitsbeziehungen
Wir haben soviel Entwurfarbeit für diese Konzepte geleistet wie für die Tupel selbst. Strukturgleichheit und -vergleich beziehen sich darauf, was gleich für einen Typ wie Tupel bedeutet, der einfach andere Daten enthält.
In F# ist Gleichheit über Tupel und Arrays strukturell. Dies bedeutet, dass zwei Arrays oder Tupel gleich sind, wenn deren Elemente identisch sind. Dies unterscheidet sich von C#. Standardmäßig ist der Inhalt von Arrays und Tupeln für Gleichheit nicht von Bedeutung. Es ist der Speicherort im Speicher, der wichtig ist.
Da diese Idee von Strukturgleichheit und -vergleich bereits Teil der F#-Spezifikation ist, hatte das F#-Team bereits eine teilweise Lösung des Problems. Allerdings galt es nur für die Typen, die das Team erstellte. Da es auch Strukturgleichheit über Arrays benötigte, generierte der Compiler speziellen Code zum Testen bei einem Gleichheitsvergleich auf einem Array. Wenn dies der Fall ist, würde es einen strukturellen Vergleich anstelle von nur einem Aufruf der Equals-Methode machen. Das Entwicklungsteam durchlief einige verschiedene Möglichkeiten zum Lösen von Strukturgleichheits- und Vergleichsproblemen dieser Art. Es einigte sich auf das Erstellen von ein paar Schnittstellen, die strukturelle Typen implementieren müssen.
IStructualEquatable und IStructuralComparable
IStructualEquatable- und IStructuralComparable-Schnittstellen bieten eine Möglichkeit für einen Typ, ein Opt-In für Strukturgleichheit oder -vergleich durchzuführen. Darüber hinaus bieten sie eine Möglichkeit, einen Comparer für jedes Element in dem Objekt zu verwenden. Die Verwendung dieser Schnittstellen sowie eines speziell definierten Comparers ermöglicht ggf. eine weitreichende Gleichheit über Tupel und Arrays, aber wir müssen keine Semantik für alle Benutzer des Typs erzwingen. Der Entwurf ist trügerisch einfach:
    public interface IStructuralComparable {
        Int32 CompareTo(Object other, IComparer comparer);
    }

    public interface IStructuralEquatable {
        Boolean Equals(Object other, IEqualityComparer comparer);
        Int32 GetHashCode(IEqualityComparer comparer);
    }
Diese Schnittstellen arbeiten, indem der Vorgang des Durchlaufens der Elemente im strukturierten Typ, für den der Implementierer der Schnittstelle verantwortlich ist, und der tatsächliche Vergleich, für den der Aufrufer verantwortlich ist, getrennt werden. Vergleiche können entscheiden, ob Sie eine tiefe strukturelle Gleichheit (durch rekursives Aufrufen der Methoden IStructualEquatable oder IStructualComparable, wenn die Elemente diese implementieren) bereitstellen möchten (auf diese Weise), oder eine flache oder etwas völlig anderes. Tupel und Arrayimplementieren jetzt beide diese Schnittstellen explizit.
Teilgleichheitbeziehungen
Tupel werden auch zur Unterstützung der Semantik der partiellen Gleichheitsbeziehungen benötigt. Ein Beispiel für eine teilweise Gleichheitsbeziehung in Microsoft .NET Framework ist die Beziehung zwischen NaN und anderen Gleitkommazahlen. Beispiel: NaN<NaN ist false, aber dasselbe gilt für NaN>NaN und NaN == NaN und NaN != NaN. Dies liegt daran, dass NaN grundlegend nicht vergleichbar ist, da es keine Zahl darstellt. Während wir diese Art von Beziehung mit Operatoren codieren können, gilt das Gleiche nicht für die CompareTo-Methode auf IComparable. Grund ist, dass CompareTo keinen Wert zurückgeben kann, der signalisiert, dass die beiden Werte nicht vergleichbar sind.
F# erfordert, dass die strukturelle Gleichheit auf Tupeln auch mit teilweisen Gleichheitsbeziehungen funktioniert. Daher ist in F# [NaN, NaN] [NaN, NaN] == false, [NaN, NaN] != [NaN, NaN] jedoch auch.
Unsere erste vorläufige Lösung waren Überladungsoperatoren auf Tupeln. Dies funktionierte mit Operatoren auf zugrunde liegenden Typen, wenn diese vorhanden waren, und durch Ausweichen auf Equals oder CompareTo, wenn dies nicht der Fall war. Eine zweite Option war es, eine neue Schnittstelle wie IComparable zu erstellen, jedoch mit einem anderen Rückgabetyp, sodass Anfragen signalisiert werden konnten, wenn Dinge nicht vergleichbar waren. Schließlich haben wir beschlossen, dass wir das Erstellen von etwas wie diesem anhalten würden, bis wir weitere Beispiele für partielle Gleichheit gesehen hatten, die in Microsoft .NET Framework benötigt wird. Stattdessen empfahlen wir, dass F# diese Art von Logik in die Methoden IComparer und IEqualityComparer implementiert, die Sie in die IStructrual-Varianten der CompareTo- und der Equals-Methode übergaben, indem sie diese Fällen erkennen und eine Art von Out-of-Band-Signalisierungsmechanismus verwenden, wenn NaN auftritt.

Lohnt sich der Aufwand?
Obwohl viel mehr Entwurfdurchläufe als jeder erwartet hatte nötig waren, konnten wir einen Tupel-Typ erstellen, von dem wir glauben, dass er flexibel genug für die Verwendung in einer Vielzahl von Sprachen ist, unabhängig von der syntaktischen Unterstützung für Tupel. Zur gleichen Zeit haben wir Schnittstellen erstellt, die helfen, die wichtigen Konzepte der strukturellen Gleichheits- und Vergleichsoperatoren zu beschreiben, die außer den Tupeln Werte in Microsoft .NET Framework haben.
In den vergangenen Monaten aktualisierte das F#-Team den Compiler, damit es System.Tuple als zugrunde liegenden Typ für alle F#-Tupel verwendet. Dadurch wird sichergestellt, dass wir beginnen können, einen allgemeinen Tupel-Typ über das Microsoft .NET-Ökosystem zu erstellen. Zusätzlich zu dieser interessanten Entwicklung wurden Tupel bei der diesjährigen Professional Developers Conference als neues Feature für Microsoft .NET Framework 4 demonstriert und empfingen viel Applaus von der Menge. Die Videos anzuschauen und zu sehen, wie aufgeregt die Entwickler darauf warten, Tupel zu verwenden, führte dazu, dass alle auf dieses trügerisch einfache Feature aufgewendete Zeit sich noch mehr lohnt.

Senden Sie Ihre Fragen und Kommentare an clrinout@microsoft.com.

Matt Ellis ist eine Software Design Engineer im Basisklassenbibliotheks-Team und zuständig für Diagnose, isolierten Speicher und andere kleine Features wie Tupel. Wenn er sich nicht über Frameworks oder Programmierungs-Sprachdesign Gedanken macht, verbringt er seine Zeit mit seiner Ehefrau Victoria und ihren zwei Hunden, Nibbler und Snoopy.

Page view tracker