From 8250791dff7b77bf38754417c45109d847fb48f6 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Mon, 10 Oct 2016 11:42:10 -0400 Subject: [PATCH 1/4] Create Tuples content in C# 7 content Outline and start on first draft. --- docs/csharp/csharp-7.md | 17 +- docs/csharp/tuples.md | 182 ++++++++++++++++-- samples/snippets/csharp/tuples/tuples.sln | 22 +++ .../snippets/csharp/tuples/tuples/App.config | 6 + .../snippets/csharp/tuples/tuples/Program.cs | 67 +++++++ .../tuples/tuples/Properties/AssemblyInfo.cs | 36 ++++ .../csharp/tuples/tuples/packages.config | 4 + .../csharp/tuples/tuples/tuples.csproj | 57 ++++++ 8 files changed, 371 insertions(+), 20 deletions(-) create mode 100644 samples/snippets/csharp/tuples/tuples.sln create mode 100644 samples/snippets/csharp/tuples/tuples/App.config create mode 100644 samples/snippets/csharp/tuples/tuples/Program.cs create mode 100644 samples/snippets/csharp/tuples/tuples/Properties/AssemblyInfo.cs create mode 100644 samples/snippets/csharp/tuples/tuples/packages.config create mode 100644 samples/snippets/csharp/tuples/tuples/tuples.csproj diff --git a/docs/csharp/csharp-7.md b/docs/csharp/csharp-7.md index d9f61a19de91e..db8117034bd0b 100644 --- a/docs/csharp/csharp-7.md +++ b/docs/csharp/csharp-7.md @@ -57,7 +57,6 @@ Add a sample at RC that shows how if statements scope out variables. --> -This small language change improves your productivity in important ways: * The code is easier to read. - You declare the out variable where you use it, not on another line above. * No need to assign an initial value. @@ -78,9 +77,10 @@ that contain multiple fields to represent the data members. The fields are not validated, and you cannot define your own methods > [!NOTE] -> Tuples were available as an API class before C# 7, but had many limitations. -> Most importantly, the members of these tuples were named `Item1`, `Item2` -> and so on. The language features for Tuples address this limitations. +> Tuples were available before C# 7 as an API, but had many limitations. +> Most importantly, the members of these tuples were named +> `Item1`, `Item2` and so on. The language support enables semantic names +> for the fields of a Tuple. You can create a tuple by assigning each member to a value: @@ -116,6 +116,13 @@ with the names on the left side, `first` and `second`. The examples above show the basic syntax to declare tuples. Tuples are most useful as return types for `private` and `internal` methods. Tuples provide a simple syntax for those methods to return multiple discrete values: +You save the work of authoring a `class` or a `struct` that +defines the type returned. There is no need for creating a new symbol. + +Creating a tuple is more efficient and more productive. +It is a simpler, lightweight syntax to define a data structure that carries +more than one value. The example method below returns the minimimum and maximum +values found in a sequence of integers: [!code-csharp[TupleReturningMethod](../../samples/snippets/csharp/new-in-7/new-in-7/program.cs#08_TupleReturningMethod "Tuple returning method")] @@ -337,7 +344,7 @@ the return value is a reference: Now, the second `WriteLine` statement in the example above will print out the value `24`, indicating that the storage in the matrix has been modified. The local variable has been declared with the `ref` modifier, -and it will take a `ref` return. You must initialize a `ref` varaible when +and it will take a `ref` return. You must initialize a `ref` variable when it is declared, you cannot split the declaration and the initialization. The C# language has two other rules that protect you from mis-using diff --git a/docs/csharp/tuples.md b/docs/csharp/tuples.md index 399e5482c076d..712a747661505 100644 --- a/docs/csharp/tuples.md +++ b/docs/csharp/tuples.md @@ -1,10 +1,11 @@ --- title: Tuples | C# Guide -description: Learn about the unnamed tuple types in C# +description: Learn about unnamed and named tuple types in C# keywords: .NET, .NET Core, C# -author: dotnet-bot +author: BillWagner +ms-author: wiwagn manager: wpickett -ms.date: 10/04/2016 +ms.date: 11/01/2016 ms.topic: article ms.prod: .net-core ms.technology: .net-core-technologies @@ -12,19 +13,157 @@ ms.devlang: csharp ms.assetid: ee8bf7c3-aa3e-4c9e-a5c6-e05cc6138baa --- -# 🔧 Tuples +# C# Tuple types + +C# Tuples are types that you define using a lightweight syntax. The advantages +include a simpler syntax, rules for conversions based on arity and types, and +consistent rules for copies and assignments. As a tradeoff, Tuples do not +support some of the object oriented idioms associated with inheritance. You +can get an overview in the section on [Tuples in the What's new in C# 7](csharp-7.md#tuples) topic. + +In this topic, you'll learn the language rules governing Tuples in C# 7, +different ways to use them, and initial guidance on working with Tuples. + +Let's start with the reasons for adding new Tuple support. Methods return +a single object. Tuples enable you to package multiple values in that single +object more easily. + +The .NET Framework already had a `Tuple` generic classes. These classes, +however, suffered from two limitations. For one, the `Tuple` classes named +their fields `Item1`, `Item2`, and so on. Those names carry no semantic +information. Using these `Tuple` types makes it harder to know the meaning +of each of the fields. Another concern is that the `Tuple` class is a +reference type. Using the `Tuple` type means allocating objects. On hot +paths, this can have a measurable impact on your application's performance. + +To avoid those deficiencies, you could create a `class` or a `struct` as +an object to carry multiple fields. Unfortunately, that's more work for you, +and it obscures your design intent. Making a `struct` or `class` implies +that you are defining a type with both data and behavior. + +The new language features for tuples, combined with a new set of generic +classes in the framework address thease deficiencies. These new tuples +use the new `ValueTuple` generic struct. As the name implies, this type is a `struct` +instead of a `class`. There are different versions of this struct to support +tuples with different numbers of fields. New language support provides semantic +names for the fields of the tuple type, along with features to make constructing +or accessing tuple fields easy. + +The language features and the `ValueTuple` generic structs enforce the rule that +these tuple types do not have any behavior (methods) associated with them. +All the `ValueTuple` types are *mutable structs*. Each member field is a +public field. That makes them very lightweight. However, that means tuples +should not be used where immutability is important. + +Tuples are both simpler and more flexible data containers than `class` and +`struct` types. Let's explore those differences. + +## Named and unnamed tuples + +The `ValueTuple` struct has fields named `Item1`, `Item2`, `Item3` and so on. +These names are the only names you can use for *unnamed tuples*. When you +do not provide any alternative field names to a tuple, you've created an +unnamed tuple: + +[!code-csharp[UnnamedTuple](../../samples/snippets/csharp/tuples/tuples/program.cs#01_UnNamedTuple "Unnamed tuple")] + +However, when you initialize a tuple, you can use new language features +that give better names to each field. Doing so creates a *named tuple*. +Named tuples still have fields named `Item1`, `Item2`, `Item3` and so on. +But they also have synonyms for any of those fields that you have named. +You creat a named tuple by specifying the names for each field. One way +is to specify the names as part of the tuple initialization: + +[!code-csharp[NamedTuple](../../samples/snippets/csharp/tuples/tuples/program.cs#02_NamedTuple "Named tuple")] + +These synonyms are handled by the compiler and the language so that you +can use named tuples effectively. Inside a single assembly, the compiler +replaces the names you've defined with `Item*` equivalents. Inside the +compiled Microsoft Intermediate Language (MSIL), the names you've given +do not exist. + +The compiler must communicate those names you created for tuples that +are returned from public methods or properties. In those cases, the compiler +adds a `TupleElementNames` attribute on the method. This attribute contains +a `TransformNames` list property that contains the names give to each of +the fields in the Tuple. + +> [!NOTE] +> Development Tools, such as Visual Studio, also read that metadata, +> and provide intellisense and other features using the metadata +> field names. + +It is important to understand these underlying fundamentals of +the new tuples and the `ValueTuple` type in order to understand +the rules for assigning named tuples to each other. + +## Assignment and tuples + +The language supports assignment between tuple types that have +the same number of fields, and the same types for each of those +fields. Those types must be exact compile-time matches. Other +conversions are not considered for assignments. Let's look at the kinds +of assignments that are allowed by tuples that may appear to be different +types. + +First, consider these variables. + +[!code-csharp[VariableCreation](../../samples/snippets/csharp/tuples/tuples/program.cs#03_VariableCreation "Variable creation")] + +The first two variables, `unnamed` and `anonymous` do not have semantic +names provided for the fields. The field names are `Item1` and `Item2`. +The last two variables, `named` and `differentName` have semantic names +given for the fields. Note that these two tuples have different names +for the fields. + +All four of these tuples have the same number of fields (referred to as 'arity') +and the types of those fields are exactly the same. Therefore, all of these +assignments work: + +[!code-csharp[VariableAssignment](../../samples/snippets/csharp/tuples/tuples/program.cs#04_VariableAssignment "Variable assignment")] + +Notice that the names of the tuples are not assigned. The values of the +fields are assigned following the order of the fields in the tuple. + +Tuples of different types or numbers of fields are not assignable: + +```csharp +// Does not compile. +// CS0029: Cannot assign Tuple(int,int,int) to Tuple(int, string) +var differentShape = (1, 2, 3); +named = differentShape; +``` + +## Tuples as method return values + + +.. Declaring. + +.. are they always named? + +.. calling and naming + + + +## deconstruction + +.. deconstructing to var + +.. create deconstruct method + + +## Guidance + +### Not Public + +### LINQ Queries + +### Multiple values + + + + -> **Note** -> -> This topic hasn’t been written yet! -> -> We welcome your input to help shape the scope and approach. You can track the status and provide input on this -> [issue](https://github.com/dotnet/docs/issues/1113) at GitHub. -> -> If you would like to review early drafts and outlines of this topic, please leave a note with your contact information in the issue. -> -> Learn more about how you can contribute on [GitHub](https://github.com/dotnet/docs/blob/master/CONTRIBUTING.md). -> + +> **Note** +> +> This topic hasn’t been written yet! +> +> We welcome your input to help shape the scope and approach. You can track the status and provide input on this +> [issue](https://github.com/dotnet/docs/issues/1113) at GitHub. +> +> If you would like to review early drafts and outlines of this topic, please leave a note with your contact information in the issue. +> +> Learn more about how you can contribute on [GitHub](https://github.com/dotnet/docs/blob/master/CONTRIBUTING.md). +> + diff --git a/samples/snippets/csharp/tuples/tuples.sln b/samples/snippets/csharp/tuples/tuples.sln new file mode 100644 index 0000000000000..52634fdf48ea0 --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.25906.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "tuples", "tuples\tuples.csproj", "{9B28122A-B30F-4318-9245-E016004A206C}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {9B28122A-B30F-4318-9245-E016004A206C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9B28122A-B30F-4318-9245-E016004A206C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9B28122A-B30F-4318-9245-E016004A206C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9B28122A-B30F-4318-9245-E016004A206C}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/samples/snippets/csharp/tuples/tuples/App.config b/samples/snippets/csharp/tuples/tuples/App.config new file mode 100644 index 0000000000000..88fa4027bda39 --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples/App.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/samples/snippets/csharp/tuples/tuples/Program.cs b/samples/snippets/csharp/tuples/tuples/Program.cs new file mode 100644 index 0000000000000..3fa620526ba9a --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples/Program.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace tuples +{ + class Program + { + static void Main(string[] args) + { + InitializationStatements(); + + AssignmentStatements(); + } + + private static void AssignmentStatements() + { + #region 03_VariableCreation + // The 'arity' and 'shape' of all these + // tuples are compatible. The only + // difference is the field names being + // used. + var unnamed = (42, "The meaning of life"); + var anonymous = (16, "a perfect square"); + var named = (answer: 42, message: "The meaning of life"); + var differentNamed = (secretConstant: 42, label: "The meaning of life"); + #endregion + + #region 04_VariableAssignment + // unnamed to named: + unnamed = named; + + // named to unnamed: + named = unnamed; + // 'named' still has fields that can be referred to + // as 'answer', and 'message': + Console.WriteLine($"{named.answer}, {named.message}"); + + // unnamed to unnamed: + anonymous = unnamed; + + // named tuples. + named = differentNamed; + // The field names are not assigned. + // 'named' still has fields that can be referred to + // as 'answer' and 'message': + Console.WriteLine($"{named.answer}, {named.message}"); + #endregion + + } + + private static void InitializationStatements() + { + #region 01_UnNamedTuple + var unnamed = ("one", "two"); + #endregion + + #region 02_NamedTuple + var named = (first: "one", second: "two"); + #endregion + + + } + } +} diff --git a/samples/snippets/csharp/tuples/tuples/Properties/AssemblyInfo.cs b/samples/snippets/csharp/tuples/tuples/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000..4e94284514579 --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("tuples")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("tuples")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9b28122a-b30f-4318-9245-e016004a206c")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/samples/snippets/csharp/tuples/tuples/packages.config b/samples/snippets/csharp/tuples/tuples/packages.config new file mode 100644 index 0000000000000..f9c3485db8966 --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/samples/snippets/csharp/tuples/tuples/tuples.csproj b/samples/snippets/csharp/tuples/tuples/tuples.csproj new file mode 100644 index 0000000000000..ac7382038a3d7 --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples/tuples.csproj @@ -0,0 +1,57 @@ + + + + + Debug + AnyCPU + {9B28122A-B30F-4318-9245-E016004A206C} + Exe + tuples + tuples + v4.5.2 + 512 + true + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + ..\packages\System.ValueTuple.4.3.0-preview1-24530-04\lib\netstandard1.0\System.ValueTuple.dll + True + + + + + + + + + + + + + + + + + + \ No newline at end of file From 1c36689c0aa1c092e6c8374991c11035da364aa4 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Tue, 22 Nov 2016 14:55:38 -0500 Subject: [PATCH 2/4] finish tuples article and samples Ready for a final proofread --- docs/csharp/tuples.md | 208 +++++++++++++++--- .../snippets/csharp/tuples/tuples/Person.cs | 49 +++++ .../snippets/csharp/tuples/tuples/Program.cs | 61 +++++ .../csharp/tuples/tuples/ProjectionSample.cs | 78 +++++++ .../csharp/tuples/tuples/Statistics.cs | 187 ++++++++++++++++ .../csharp/tuples/tuples/tuples.csproj | 3 + 6 files changed, 552 insertions(+), 34 deletions(-) create mode 100644 samples/snippets/csharp/tuples/tuples/Person.cs create mode 100644 samples/snippets/csharp/tuples/tuples/ProjectionSample.cs create mode 100644 samples/snippets/csharp/tuples/tuples/Statistics.cs diff --git a/docs/csharp/tuples.md b/docs/csharp/tuples.md index 712a747661505..619da6259258b 100644 --- a/docs/csharp/tuples.md +++ b/docs/csharp/tuples.md @@ -136,58 +136,198 @@ named = differentShape; ## Tuples as method return values +One of the most common uses for Tuples is as a method return +value. It may often be natural for a method to compute more than +one value. Let's walk through one example. Consider this method +that computes the standard deviation for a sequence of numbers: -.. Declaring. - -.. are they always named? - -.. calling and naming - - - -## deconstruction - -.. deconstructing to var - -.. create deconstruct method +[!code-csharp[StandardDeviation](../../samples/snippets/csharp/tuples/tuples/statistics.cs#05_StandardDeviation "Compute Standard Deviation")] +> [!NOTE] +> These examples compute the uncorrected sample standard deviation. +> The corrected sample standard deviation would formula uses (N-1) +> instead of N for the final division, where N is the sample size. +> Consult a statistics text for more details. -## Guidance -### Not Public +This follows the textbook formula for the standard deviation. It produces +the correct answer, but it's a very inefficient implementation. This +method enumerates the sequence twice: Once to produce the average, and +once to produce the average of the square of the difference of the average. +(Remember that LINQ queries are evaluated lazily, so the computation of +the differences from the mean and the average of those differences makes +only one enumeration.) -### LINQ Queries +There is an alternative formula that computes standard deviation using +only one enumeration of the sequence. This computation produces two +values as it enumerates the sequence: the sum of all items in the sequence, +and the sum of the each value squared: -### Multiple values +[!code-csharp[SumOfSquaresFormula](../../samples/snippets/csharp/tuples/tuples/statistics.cs#06_SumOfSquaresFormula "Compute Standard Deviation using the sum of squares")] +Ths version enumerates the sequence exactly once. But, it's not very +reusable code. As you keep working, you'll find that many different +statistical computations use both the sum of the sequence, and the sum +of the squares of the sequence. Let's refactor this method and write +a utility method that produces both of those values, along with the +number of items in the collection. +This is where tuples come in very useful. +Let's update this method so the three values computed during the enumeration +are stored in a tuple. That creates with this version: +[!code-csharp[TupleVersion](../../samples/snippets/csharp/tuples/tuples/statistics.cs#07_TupleVersion "Refactor to use tuples")] +Visual Studio's Refactoring suport makes it easy to extract the functionality +for the core statistics into a private method. That gives you a `private static` +method that returns the tuple type with the three values of `Sum`, `SumOfSquares`, and `Count`: - +You must address the fields of this tuple as `Item1`, `Item2`, and `Item3`. +## Deconstruction -> **Note** -> -> This topic hasn’t been written yet! -> -> We welcome your input to help shape the scope and approach. You can track the status and provide input on this -> [issue](https://github.com/dotnet/docs/issues/1113) at GitHub. -> -> If you would like to review early drafts and outlines of this topic, please leave a note with your contact information in the issue. -> -> Learn more about how you can contribute on [GitHub](https://github.com/dotnet/docs/blob/master/CONTRIBUTING.md). -> +You can unpackage all the items in a tuple by *deconstructng* the tuple +returned by a method. Therer are two different approaches to deconstructing +tuples. First, you can expelicitly declare the type of each field inside +parentheses to create discrete variables for each of the fields in the tuple: +[!code-csharp[Deconstruct](../../samples/snippets/csharp/tuples/tuples/statistics.cs#10_Deconstruct "Deconstruct")] + +You can also declare implicitly typed variables for each field in a tuple +by using the `var` keyword outside the parentheses: + +[!code-csharp[DeconstructToVar](../../samples/snippets/csharp/tuples/tuples/statistics.cs#11_DeconstructToVar "Deconstruct to Var")] + +It is also legal to use the `var` keyword with any, or all of the variable +declarations inside the parenthesis. Note that you cannot use a specific +type outside the parentheses, even if every field in the tuple has the +same type. + +### Deconstring user defined types + +Any tuple type can be deconstructed as shown above. It's also easy +to enable deconstruction on any user defined (classes, structs, or +even interfaces). + +The type author can define one or more `Deconstruct` methods that +assign values to any number of `out` variables representing the +data elements that make up the type. For example, the following +`Person` type defines a `Deconstruct` method that deconstructs +a person object into the fields representing the first name +and last name: + +[!code-csharp[TypeWithDeconstructMethod](../../samples/snippets/csharp/tuples/tuples/person.cs#12_TypeWithDeconstructMethod "Type with a deconstruct method")] + +The deconstruct method enables assignment from a `Person` to a +tuple with two strings, representing the `FirstName` and +`LastName` properties. + +You can enable deconstruction even for types you did not author. +The `Deconstruct` method can be an extenion method that unpackages +the accessible data members of an object. The example below shows +a `Stedent` type, derived from the `Person` type, and an extension +method that deconstructs a `Student` into three variables, representing +the `FirstName`, the `LastName` and the `GPA`: + +[!code-csharp[ExtensionDeconstructMethod](../../samples/snippets/csharp/tuples/tuples/person.cs#13_ExtensionDeconstructMethod "Type with a deconstruct extension method")] + +A `Student` object now has two accessible `Deconstruct` methods: the extension method +declared for `Student` types, and the member of the `Person` type. Both are in scope, +and that enables a `Student` to be deconstructed into either two variables or three. +If you assign a student to three variables, the first name, last name, and GPA are +all returned. If you assign a student to two variables, only the first name and +the last name are returned. + +You should be very careful defining multiple `Deconstruct` methods in a +class or a class hierarchy. Multiple `Deconstruct` methods that have the +same number of `out` parameters can quickly cause ambiguities. Callers may +not be able to easily call the desired `Deconstruct` method without introducing +ambiguities. + +In this examle, there is minimal chance for an ambiguious call because the +`Deconstrcut` method for `Person` has two output fields, and the `Deconstruct` +method for `Student` has three. + +## Conclusion + +The addition of Tuples, both named and unnaemed, with language support +enables very concise syntax for working with data types that contain +more than one element. Even so, they are most useful for utility methods +that are `private`, or `internal`. Create user defined types, either +`class` or `struct` types when your public methods return a value +that has multiple fields. + +Another idiom where tuples can be very useful is when you are authoring +LINQ queries where the final result is a projection that some, but not +all, of the properties of the objects being selected. + +You would traditionally project the results of the query into a sequence +of objects that were an anonymous type. That presented many limitations, +primarily because anonymous types could not conveniently be named in the +return type for a method. Alternatives using `object` or `dynamic` as the +type of the result came with significant performance costs. + +Returning a sequence of a tuple type is easy, and the names and types +of the fields are available at compile time and through IDE tools. +For example, consider a ToDo list application. You might define a +class similar to the following to represent a single entry in the ToDo list: + +[!code-csharp[ToDoItem](../../samples/snippets/csharp/tuples/tuples/projectionsample.cs#14_ToDoItem "To Do Item")] + +Your mobile applications may support a compact form of the current ToDo items +that only displays the title. That LINQ query would make a projection that +include the the ID and the title. A method that returns a sequence of tuples +expresses that design very well: + +[!code-csharp[QueryReturningTuple](../../samples/snippets/csharp/tuples/tuples/projectionsample.cs#15_QueryReturningTuple "Query returning a tuple")] + +The named tuple can be part of the signature. It lets the compiler and IDE +tools provide static checking that you are using the result correctly. The +named tuple also carries the static type information so there is no need +to use expensive run time features like reflection or dynamic binding to +work with the results. + +The new language and library support for named tuples makes it much easier +to work with designs that use data structures that store multiple fields +but do not need to define behavior, as classes and structs do. It's +easy and concise to use tuples for those types. You get all the benefits +static type checking, without needing to author types using the more +verbose `class` or `struct` syntax. diff --git a/samples/snippets/csharp/tuples/tuples/Person.cs b/samples/snippets/csharp/tuples/tuples/Person.cs new file mode 100644 index 0000000000000..815f866618b0a --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples/Person.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace tuples +{ + #region 12_TypeWithDeconstructMethod + public class Person + { + public string FirstName { get;} + public string LastName { get; } + + public Person(string first, string last) + { + FirstName = first; + LastName = last; + } + public void Deconstruct(out string firstName, out string lastName) + { + firstName = FirstName; + lastName = LastName; + } + } + #endregion + + #region 13_ExtensionDeconstructMethod + public class Student : Person + { + public double GPA { get; } + public Student(string first, string last, double gpa) : + base(first, last) + { + GPA = gpa; + } + } + + public static class Extensions + { + public static void Deconstruct(this Student s, out string first, out string last, out double gpa) + { + first = s.FirstName; + last = s.LastName; + gpa = s.GPA; + } + } + #endregion +} diff --git a/samples/snippets/csharp/tuples/tuples/Program.cs b/samples/snippets/csharp/tuples/tuples/Program.cs index 3fa620526ba9a..fb86b061ee88a 100644 --- a/samples/snippets/csharp/tuples/tuples/Program.cs +++ b/samples/snippets/csharp/tuples/tuples/Program.cs @@ -13,6 +13,30 @@ static void Main(string[] args) InitializationStatements(); AssignmentStatements(); + + var sample = new List{ 2.0, 4.0, 6.0, 8.0 }; + + Console.WriteLine(StatisticsVersionOne.StandardDeviation(sample)); + + Console.WriteLine(StatisticsVersionTwo.StandardDeviation(sample)); + Console.WriteLine(StatisticsVersionThree.StandardDeviation(sample)); + Console.WriteLine(StatisticsVersionFour.StandardDeviation(sample)); + + var p2 = new Person("Bill", "Wagner"); + var (first, last) = p2; + Console.WriteLine(first); + Console.WriteLine(last); + + var s1 = new Student("Bill", "Wagner", 4.5); + var (fName, lName, gpa) = s1; + + var (f, l) = s1; + + var source = new ProjectionSample(); + var sequence = source.GetCurrentItemsMobileList(); + foreach (var item in sequence) + Console.WriteLine($"{item.ID}, {item.Title}"); + } private static void AssignmentStatements() @@ -60,8 +84,45 @@ private static void InitializationStatements() #region 02_NamedTuple var named = (first: "one", second: "two"); #endregion + } + private static double VersionThree(IEnumerable sequence) + { + var computation = (sum: 0.0, sumOfSquares: 0.0, items: 0); + + foreach (var item in sequence) + { + computation.items++; + computation.sum += item; + computation.sumOfSquares += item * item; + } + + var variance = computation.sumOfSquares - computation.sum * computation.sum / computation.items; + return Math.Sqrt(variance / computation.items); + } + + private static double VersionFour(IEnumerable sequence) + { + var coreStats = ComputeCoreStats(sequence); + + var variance = coreStats.sumOfSquares - coreStats.sum * coreStats.sum / coreStats.items; + return Math.Sqrt(variance / coreStats.items); + } + + // Remove names, and it can be an unnamed tuple. + private static (double sum, double sumOfSquares, int items) ComputeCoreStats(IEnumerable sequence) + { + double total = 0; + double sumOfSquares = 0; + int items = 0; + foreach (var item in sequence) + { + items++; + total += item; + sumOfSquares += item * item; + } + return (total, sumOfSquares, items); } } } diff --git a/samples/snippets/csharp/tuples/tuples/ProjectionSample.cs b/samples/snippets/csharp/tuples/tuples/ProjectionSample.cs new file mode 100644 index 0000000000000..863afb84b01de --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples/ProjectionSample.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace tuples +{ + #region 14_ToDoItem + public class ToDoItem + { + public int ID { get; set; } + public bool IsDone { get; set; } + public DateTime DueDate { get; set; } + public string Title { get; set; } + public string Notes { get; set; } + + } + #endregion + + + public class ProjectionSample + { + private List AllItems = new List + { + new ToDoItem + { + ID=0, + IsDone = false, + DueDate = DateTime.Now + TimeSpan.FromDays(2), + Title = "answer email", + Notes = "This happens every day" + }, + new ToDoItem + { + ID=1, + IsDone = false, + DueDate = DateTime.Now + TimeSpan.FromDays(3), + Title = "Review open PRs", + Notes = "Look for working code, good tests, and structure" + }, + new ToDoItem + { + ID=2, + IsDone = false, + DueDate = DateTime.Now + TimeSpan.FromDays(1), + Title = "Create updated issues", + Notes = "Need to track what's getting done" + }, + new ToDoItem + { + ID=3, + IsDone = false, + DueDate = DateTime.Now + TimeSpan.FromDays(5), + Title = "Plan vacation", + Notes = "The first step is determining where we should go" + }, + new ToDoItem + { + ID=4, + IsDone = false, + DueDate = DateTime.Now + TimeSpan.FromDays(7), + Title = "make reservations for vacation", + Notes = "Once we decide on a location, find lodging" + } + }; + + #region 15_QueryReturningTuple + internal IEnumerable<(int ID, string Title)> GetCurrentItemsMobileList() + { + return from item in AllItems + where !item.IsDone + orderby item.DueDate + select (item.ID, item.Title); + } + #endregion + } +} diff --git a/samples/snippets/csharp/tuples/tuples/Statistics.cs b/samples/snippets/csharp/tuples/tuples/Statistics.cs new file mode 100644 index 0000000000000..7be15fe112609 --- /dev/null +++ b/samples/snippets/csharp/tuples/tuples/Statistics.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace tuples +{ + public static class StatisticsVersionOne + { + #region 05_StandardDeviation + public static double StandardDeviation(IEnumerable sequence) + { + // Step 1: Compute the Mean: + var mean = sequence.Average(); + + // Step 2: Compute the square of the differences between each number + // and the mean: + var squaredMeanDifferences = from n in sequence + select (n - mean) * (n - mean); + // Step 3: Find the mean of those squared differences: + var meanOfSquaredDifferences = squaredMeanDifferences.Average(); + + // Step 4: Standard Deviation is the square root of that mean: + var standardDeviation = Math.Sqrt(meanOfSquaredDifferences); + return standardDeviation; + } + #endregion + } + + public static class StatisticsVersionTwo + { + #region 06_SumOfSquaresFormula + public static double StandardDeviation(IEnumerable sequence) + { + double sum = 0; + double sumOfSquares = 0; + double count = 0; + + foreach (var item in sequence) + { + count++; + sum += item; + sumOfSquares += item * item; + } + + var variance = sumOfSquares - sum * sum / count; + return Math.Sqrt(variance / count); + } + #endregion + } + + public static class StatisticsVersionThree + { + #region 07_TupleVersion + public static double StandardDeviation(IEnumerable sequence) + { + var computation = (sum: 0.0, sumOfSquares: 0.0, count: 0); + + foreach (var item in sequence) + { + computation.count++; + computation.sum += item; + computation.sumOfSquares += item * item; + } + + var variance = computation.sumOfSquares - computation.sum * computation.sum / computation.count; + return Math.Sqrt(variance / computation.count); + } + #endregion + } + + public static class StatisticsVersionFour + { + #region 08_TupleMethodVersion + public static double StandardDeviation(IEnumerable sequence) + { + (double Sum, double SumOfSquares, int Count) computation = ComputeSumsAnSumOfdSquares(sequence); + + var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count; + return Math.Sqrt(variance / computation.Count); + } + + private static (double Sum, double SumOfSquares, int Count) ComputeSumsAnSumOfdSquares(IEnumerable sequence) + { + var computation = (sum: 0.0, sumOfSquares: 0.0, count: 0); + + foreach (var item in sequence) + { + computation.count++; + computation.sum += item; + computation.sumOfSquares += item * item; + } + + return computation; + } + #endregion + } + + public static class StatisticsVersionFive + { + #region 09_CleanedTupleVersion + public static double StandardDeviation(IEnumerable sequence) + { + var computation = ComputeSumAndSumOfSquares(sequence); + + var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count; + return Math.Sqrt(variance / computation.Count); + } + + private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSquares(IEnumerable sequence) + { + double sum = 0; + double sumOfSquares = 0; + int count = 0; + + foreach (var item in sequence) + { + count++; + sum += item; + sumOfSquares += item * item; + } + + return (sum, sumOfSquares, count); + } + #endregion + } + + + public static class StatisticsVersionSix + { + #region 10_Deconstruct + public static double StandardDeviation(IEnumerable sequence) + { + (double sum, double sumOfSquares, int count) = ComputeSumAndSumOfSquares(sequence); + + var variance = sumOfSquares - sum * sum / count; + return Math.Sqrt(variance / count); + } + #endregion + + private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSquares(IEnumerable sequence) + { + double sum = 0; + double sumOfSquares = 0; + int count = 0; + + foreach (var item in sequence) + { + count++; + sum += item; + sumOfSquares += item * item; + } + + return (sum, sumOfSquares, count); + } + } + + public static class StatisticsVersionSeven + { + #region 11_DeconstructToVar + public static double StandardDeviation(IEnumerable sequence) + { + var (sum, sumOfSquares, count) = ComputeSumAndSumOfSquares(sequence); + + var variance = sumOfSquares - sum * sum / count; + return Math.Sqrt(variance / count); + } + #endregion + + private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSquares(IEnumerable sequence) + { + double sum = 0; + double sumOfSquares = 0; + int count = 0; + + foreach (var item in sequence) + { + count++; + sum += item; + sumOfSquares += item * item; + } + + return (sum, sumOfSquares, count); + } + } +} diff --git a/samples/snippets/csharp/tuples/tuples/tuples.csproj b/samples/snippets/csharp/tuples/tuples/tuples.csproj index ac7382038a3d7..0b67c09df8e5c 100644 --- a/samples/snippets/csharp/tuples/tuples/tuples.csproj +++ b/samples/snippets/csharp/tuples/tuples/tuples.csproj @@ -46,8 +46,11 @@ + + + From c1465582975333f4140e824782031c02697d6dd3 Mon Sep 17 00:00:00 2001 From: Bill Wagner Date: Wed, 23 Nov 2016 11:21:57 -0500 Subject: [PATCH 3/4] proofread, remove wrenches This is now ready for review. --- docs/csharp/tuples.md | 164 ++++++++++++++++++++++-------------------- docs/toc.md | 2 +- 2 files changed, 87 insertions(+), 79 deletions(-) diff --git a/docs/csharp/tuples.md b/docs/csharp/tuples.md index 619da6259258b..1333219236e9f 100644 --- a/docs/csharp/tuples.md +++ b/docs/csharp/tuples.md @@ -5,7 +5,7 @@ keywords: .NET, .NET Core, C# author: BillWagner ms-author: wiwagn manager: wpickett -ms.date: 11/01/2016 +ms.date: 11/23/2016 ms.topic: article ms.prod: .net-core ms.technology: .net-core-technologies @@ -16,7 +16,7 @@ ms.assetid: ee8bf7c3-aa3e-4c9e-a5c6-e05cc6138baa # C# Tuple types C# Tuples are types that you define using a lightweight syntax. The advantages -include a simpler syntax, rules for conversions based on arity and types, and +include a simpler syntax, rules for conversions based on arity and types of fields, and consistent rules for copies and assignments. As a tradeoff, Tuples do not support some of the object oriented idioms associated with inheritance. You can get an overview in the section on [Tuples in the What's new in C# 7](csharp-7.md#tuples) topic. @@ -26,24 +26,26 @@ different ways to use them, and initial guidance on working with Tuples. Let's start with the reasons for adding new Tuple support. Methods return a single object. Tuples enable you to package multiple values in that single -object more easily. +object more easily. You don't need behavior on these types, but you want to +package multiple values in a single object. -The .NET Framework already had a `Tuple` generic classes. These classes, -however, suffered from two limitations. For one, the `Tuple` classes named +The .NET Framework already had `Tuple` generic classes. These classes, +however, had two major limitations. For one, the `Tuple` classes named their fields `Item1`, `Item2`, and so on. Those names carry no semantic -information. Using these `Tuple` types makes it harder to know the meaning -of each of the fields. Another concern is that the `Tuple` class is a -reference type. Using the `Tuple` type means allocating objects. On hot +information. Using these `Tuple` types does not enable communicating the +meaning of each of the fields. Another concern is that the `Tuple` classes are +reference types. Using one of the `Tuple` types means allocating objects. On hot paths, this can have a measurable impact on your application's performance. -To avoid those deficiencies, you could create a `class` or a `struct` as -an object to carry multiple fields. Unfortunately, that's more work for you, +To avoid those deficiencies, you could create a `class` or a `struct` +to carry multiple fields. Unfortunately, that's more work for you, and it obscures your design intent. Making a `struct` or `class` implies -that you are defining a type with both data and behavior. +that you are defining a type with both data and behavior. Many times, you +simply want to store multiple values in a single object. The new language features for tuples, combined with a new set of generic classes in the framework address thease deficiencies. These new tuples -use the new `ValueTuple` generic struct. As the name implies, this type is a `struct` +use the new `ValueTuple` generic structs. As the name implies, this type is a `struct` instead of a `class`. There are different versions of this struct to support tuples with different numbers of fields. New language support provides semantic names for the fields of the tuple type, along with features to make constructing @@ -60,7 +62,8 @@ Tuples are both simpler and more flexible data containers than `class` and ## Named and unnamed tuples -The `ValueTuple` struct has fields named `Item1`, `Item2`, `Item3` and so on. +The `ValueTuple` struct has fields named `Item1`, `Item2`, `Item3` and so on, +just like the existing `Tuple` types. These names are the only names you can use for *unnamed tuples*. When you do not provide any alternative field names to a tuple, you've created an unnamed tuple: @@ -77,16 +80,18 @@ is to specify the names as part of the tuple initialization: [!code-csharp[NamedTuple](../../samples/snippets/csharp/tuples/tuples/program.cs#02_NamedTuple "Named tuple")] These synonyms are handled by the compiler and the language so that you -can use named tuples effectively. Inside a single assembly, the compiler -replaces the names you've defined with `Item*` equivalents. Inside the -compiled Microsoft Intermediate Language (MSIL), the names you've given -do not exist. +can use named tuples effectively. IDEs and editors can read these semantic names +using the Roslyn APIs. This enables you to reference the fields of a named +tuple by those semantic names anywhere in the same assembly. The compiler +replaces the names you've defined with `Item*` equivalents when generating +the compiled output. The compiled Microsoft Intermediate Language (MSIL) +does not include the names you've given these fields. The compiler must communicate those names you created for tuples that are returned from public methods or properties. In those cases, the compiler adds a `TupleElementNames` attribute on the method. This attribute contains a `TransformNames` list property that contains the names give to each of -the fields in the Tuple. +the fields in the Tuple. > [!NOTE] > Development Tools, such as Visual Studio, also read that metadata, @@ -100,13 +105,12 @@ the rules for assigning named tuples to each other. ## Assignment and tuples The language supports assignment between tuple types that have -the same number of fields, and the same types for each of those +the same number of fields and the same types for each of those fields. Those types must be exact compile-time matches. Other conversions are not considered for assignments. Let's look at the kinds -of assignments that are allowed by tuples that may appear to be different -types. +of assignments that are allowed between tuple types. -First, consider these variables. +Consider these variables used in the following examples: [!code-csharp[VariableCreation](../../samples/snippets/csharp/tuples/tuples/program.cs#03_VariableCreation "Variable creation")] @@ -117,7 +121,7 @@ given for the fields. Note that these two tuples have different names for the fields. All four of these tuples have the same number of fields (referred to as 'arity') -and the types of those fields are exactly the same. Therefore, all of these +and the types of those fields are identical. Therefore, all of these assignments work: [!code-csharp[VariableAssignment](../../samples/snippets/csharp/tuples/tuples/program.cs#04_VariableAssignment "Variable assignment")] @@ -145,10 +149,11 @@ that computes the standard deviation for a sequence of numbers: > [!NOTE] > These examples compute the uncorrected sample standard deviation. -> The corrected sample standard deviation would formula uses (N-1) -> instead of N for the final division, where N is the sample size. -> Consult a statistics text for more details. - +> The corrected sample standard deviation would formula would divide +> the sum of the squared differences from the mean by (N-1) instead +> of N, as the `Average` extension method does. Consult a statistics +> text for more details on the differences between these formulas +> for standard deviation. This follows the textbook formula for the standard deviation. It produces the correct answer, but it's a very inefficient implementation. This @@ -167,7 +172,8 @@ and the sum of the each value squared: Ths version enumerates the sequence exactly once. But, it's not very reusable code. As you keep working, you'll find that many different -statistical computations use both the sum of the sequence, and the sum +statistical computations use the number of items in the sequence, +the sum of the sequence, and the sum of the squares of the sequence. Let's refactor this method and write a utility method that produces both of those values, along with the number of items in the collection. @@ -175,7 +181,7 @@ number of items in the collection. This is where tuples come in very useful. Let's update this method so the three values computed during the enumeration -are stored in a tuple. That creates with this version: +are stored in a tuple. That creates this version: [!code-csharp[TupleVersion](../../samples/snippets/csharp/tuples/tuples/statistics.cs#07_TupleVersion "Refactor to use tuples")] @@ -221,6 +227,38 @@ private static (double, double, int) ComputeSumAndSumOfSquares(IEnumerable Date: Tue, 29 Nov 2016 15:58:10 -0500 Subject: [PATCH 4/4] respond to all feedback. Reviews from @svick and @rpetrusha --- docs/csharp/csharp-7.md | 16 ++--- docs/csharp/tuples.md | 66 ++++++++++--------- docs/toc.md | 2 +- .../csharp/new-in-7/new-in-7/Program.cs | 8 +-- .../snippets/csharp/tuples/tuples/Person.cs | 3 +- .../snippets/csharp/tuples/tuples/Program.cs | 45 ++++++------- .../csharp/tuples/tuples/ProjectionSample.cs | 3 +- .../csharp/tuples/tuples/Statistics.cs | 32 ++++----- 8 files changed, 91 insertions(+), 84 deletions(-) diff --git a/docs/csharp/csharp-7.md b/docs/csharp/csharp-7.md index db8117034bd0b..51031edb61697 100644 --- a/docs/csharp/csharp-7.md +++ b/docs/csharp/csharp-7.md @@ -94,12 +94,12 @@ names to each of the members of the tuple: [!code-csharp[NamedTuple](../../samples/snippets/csharp/new-in-7/new-in-7/program.cs#05_NamedTuple "Named tuple")] > [!NOTE] -> The new tuples features require the @System.ValueTuple type. For Visual Studio 15 +> The new tuples features require the `System.ValueTuple` type. For Visual Studio 15 > Preview 5 and earlier preview releases, you must add the NuGet package "System.ValueTuple", > available in the pre-release stream. -The `namedLetters` tuple contains fields referred to as `alpha` and -`beta`. In a tuple assignment, you can also specify the names of the fields +The `namedLetters` tuple contains fields referred to as `Alpha` and +`Beta`. In a tuple assignment, you can also specify the names of the fields on the right hand side of the assignment: [!code-csharp[ImplicitNamedTuple](../../samples/snippets/csharp/new-in-7/new-in-7/program.cs#06_ImplicitNamedTuple "Implicitly named tuple")] @@ -110,14 +110,14 @@ left and right hand side of the assignment: [!code-csharp[NamedTupleConflict](../../samples/snippets/csharp/new-in-7/new-in-7/program.cs#07_NamedTupleConflict "Named tuple conflict")] The line above generates a warning, `CS8123`, telling you that the names on the right -side of the assignment, `alpha` and `beta` are ignored because they conflict -with the names on the left side, `first` and `second`. +side of the assignment, `Alpha` and `Beta` are ignored because they conflict +with the names on the left side, `First` and `Second`. The examples above show the basic syntax to declare tuples. Tuples are most useful as return types for `private` and `internal` methods. Tuples provide a simple syntax for those methods to return multiple discrete values: You save the work of authoring a `class` or a `struct` that -defines the type returned. There is no need for creating a new symbol. +defines the type returned. There is no need for creating a new type. Creating a tuple is more efficient and more productive. It is a simpler, lightweight syntax to define a data structure that carries @@ -129,12 +129,12 @@ values found in a sequence of integers: Using tuples in this way offers several advantages: * You save the work of authoring a `class` or a `struct` that defines the type returned. -* You do not need to create new symbol. +* You do not need to create new type. * The language enhancements removes the need to call the @System.Tuple.Create%60%601(%60%600) methods. The declaration for the method provides the names for the fields of the tuple that is returned. When you call the method, the return value is a -tuple whose fields are `max` and `min`: +tuple whose fields are `Max` and `Min`: [!code-csharp[CallingTupleMethod](../../samples/snippets/csharp/new-in-7/new-in-7/program.cs#09_CallingTupleMethod "Calling a tuple returning method")] diff --git a/docs/csharp/tuples.md b/docs/csharp/tuples.md index 1333219236e9f..17e165c986024 100644 --- a/docs/csharp/tuples.md +++ b/docs/csharp/tuples.md @@ -13,10 +13,11 @@ ms.devlang: csharp ms.assetid: ee8bf7c3-aa3e-4c9e-a5c6-e05cc6138baa --- -# C# Tuple types +# C# Tuple types # C# Tuples are types that you define using a lightweight syntax. The advantages -include a simpler syntax, rules for conversions based on arity and types of fields, and +include a simpler syntax, rules for conversions based on number (referred to as "arity") +and types of fields, and consistent rules for copies and assignments. As a tradeoff, Tuples do not support some of the object oriented idioms associated with inheritance. You can get an overview in the section on [Tuples in the What's new in C# 7](csharp-7.md#tuples) topic. @@ -24,12 +25,16 @@ can get an overview in the section on [Tuples in the What's new in C# 7](csharp- In this topic, you'll learn the language rules governing Tuples in C# 7, different ways to use them, and initial guidance on working with Tuples. +> [!NOTE] +> The new tuples features require the `System.ValueTuple` type. For Visual Studio 2017 +> RC and earlier preview releases, you must add the NuGet package "System.ValueTuple", +> available in the pre-release stream. + Let's start with the reasons for adding new Tuple support. Methods return a single object. Tuples enable you to package multiple values in that single -object more easily. You don't need behavior on these types, but you want to -package multiple values in a single object. +object more easily. -The .NET Framework already had `Tuple` generic classes. These classes, +The .NET Framework already has generic `Tuple` classes. These classes, however, had two major limitations. For one, the `Tuple` classes named their fields `Item1`, `Item2`, and so on. Those names carry no semantic information. Using these `Tuple` types does not enable communicating the @@ -43,8 +48,8 @@ and it obscures your design intent. Making a `struct` or `class` implies that you are defining a type with both data and behavior. Many times, you simply want to store multiple values in a single object. -The new language features for tuples, combined with a new set of generic -classes in the framework address thease deficiencies. These new tuples +The new language features for tuples, combined with a new set of +classes in the framework, address these deficiencies. These new tuples use the new `ValueTuple` generic structs. As the name implies, this type is a `struct` instead of a `class`. There are different versions of this struct to support tuples with different numbers of fields. New language support provides semantic @@ -52,7 +57,7 @@ names for the fields of the tuple type, along with features to make constructing or accessing tuple fields easy. The language features and the `ValueTuple` generic structs enforce the rule that -these tuple types do not have any behavior (methods) associated with them. +you cannot add any behavior (methods) to these tuple types. All the `ValueTuple` types are *mutable structs*. Each member field is a public field. That makes them very lightweight. However, that means tuples should not be used where immutability is important. @@ -63,7 +68,7 @@ Tuples are both simpler and more flexible data containers than `class` and ## Named and unnamed tuples The `ValueTuple` struct has fields named `Item1`, `Item2`, `Item3` and so on, -just like the existing `Tuple` types. +similer to the properties defined in the existing `Tuple` types. These names are the only names you can use for *unnamed tuples*. When you do not provide any alternative field names to a tuple, you've created an unnamed tuple: @@ -74,7 +79,7 @@ However, when you initialize a tuple, you can use new language features that give better names to each field. Doing so creates a *named tuple*. Named tuples still have fields named `Item1`, `Item2`, `Item3` and so on. But they also have synonyms for any of those fields that you have named. -You creat a named tuple by specifying the names for each field. One way +You create a named tuple by specifying the names for each field. One way is to specify the names as part of the tuple initialization: [!code-csharp[NamedTuple](../../samples/snippets/csharp/tuples/tuples/program.cs#02_NamedTuple "Named tuple")] @@ -90,7 +95,7 @@ does not include the names you've given these fields. The compiler must communicate those names you created for tuples that are returned from public methods or properties. In those cases, the compiler adds a `TupleElementNames` attribute on the method. This attribute contains -a `TransformNames` list property that contains the names give to each of +a `TransformNames` list property that contains the names given to each of the fields in the Tuple. > [!NOTE] @@ -141,15 +146,14 @@ named = differentShape; ## Tuples as method return values One of the most common uses for Tuples is as a method return -value. It may often be natural for a method to compute more than -one value. Let's walk through one example. Consider this method +value. Let's walk through one example. Consider this method that computes the standard deviation for a sequence of numbers: [!code-csharp[StandardDeviation](../../samples/snippets/csharp/tuples/tuples/statistics.cs#05_StandardDeviation "Compute Standard Deviation")] > [!NOTE] > These examples compute the uncorrected sample standard deviation. -> The corrected sample standard deviation would formula would divide +> The corrected sample standard deviation formula would divide > the sum of the squared differences from the mean by (N-1) instead > of N, as the `Average` extension method does. Consult a statistics > text for more details on the differences between these formulas @@ -175,8 +179,7 @@ reusable code. As you keep working, you'll find that many different statistical computations use the number of items in the sequence, the sum of the sequence, and the sum of the squares of the sequence. Let's refactor this method and write -a utility method that produces both of those values, along with the -number of items in the collection. +a utility method that produces all three of those values. This is where tuples come in very useful. @@ -192,7 +195,7 @@ method that returns the tuple type with the three values of `Sum`, `SumOfSquares [!code-csharp[TupleMethodVersion](../../samples/snippets/csharp/tuples/tuples/statistics.cs#08_TupleMethodVersion "After extracting utility method")] The language enables a couple more options that you can use, if you want -to make a fwe quick edits by hand. First, you can use the `var` +to make a few quick edits by hand. First, you can use the `var` declaration to initialize the tuple result from the `ComputeSumAndSumOfSquares` method call. You can also create three discrete variables inside the `ComputeSumAndSumOfSquares` method. The final version is below: @@ -202,8 +205,8 @@ method call. You can also create three discrete variables inside the This final version can be used for any method that needs those three values, or any subset of them. -The langauge supports other options in managing the names of the fields -in these tuple returning methods. +The language supports other options in managing the names of the fields +in these tuple-returning methods. You can remove the field names from the return value declaration and return an unnamed tuple: @@ -263,8 +266,8 @@ work with the results. ## Deconstruction You can unpackage all the items in a tuple by *deconstructng* the tuple -returned by a method. Therer are two different approaches to deconstructing -tuples. First, you can expelicitly declare the type of each field inside +returned by a method. There are two different approaches to deconstructing +tuples. First, you can explicitly declare the type of each field inside parentheses to create discrete variables for each of the fields in the tuple: [!code-csharp[Deconstruct](../../samples/snippets/csharp/tuples/tuples/statistics.cs#10_Deconstruct "Deconstruct")] @@ -299,12 +302,13 @@ and last name: [!code-csharp[TypeWithDeconstructMethod](../../samples/snippets/csharp/tuples/tuples/person.cs#12_TypeWithDeconstructMethod "Type with a deconstruct method")] -The deconstruct method enables assignment from a `Person` to a -tuple with two strings, representing the `FirstName` and -`LastName` properties. +The deconstruct method enables assignment from a `Person` to two strings, +representing the `FirstName` and `LastName` properties: + +[!code-csharp[Deconstruct Type](../../samples/snippets/csharp/tuples/tuples/program.cs#12A_DeconstructType "Deconstruct a class type")] You can enable deconstruction even for types you did not author. -The `Deconstruct` method can be an extenion method that unpackages +The `Deconstruct` method can be an extension method that unpackages the accessible data members of an object. The example below shows a `Student` type, derived from the `Person` type, and an extension method that deconstructs a `Student` into three variables, representing @@ -314,18 +318,20 @@ the `FirstName`, the `LastName` and the `GPA`: A `Student` object now has two accessible `Deconstruct` methods: the extension method declared for `Student` types, and the member of the `Person` type. Both are in scope, -and that enables a `Student` to be deconstructed into a tuple with either two fields or three. -If you assign a student to a tuple with three fields, the first name, last name, and GPA are -all returned. If you assign a student to a tuple with two fields, only the first name and +and that enables a `Student` to be deconstructed into either two variables or three. +If you assign a student to three variabless, the first name, last name, and GPA are +all returned. If you assign a student to two variables, only the first name and the last name are returned. +[!code-csharp[Deconstruct extension method](../../samples/snippets/csharp/tuples/tuples/program.cs#13A_DeconstructExtension "Deconstruct a class type using an extension method")] + You should be very careful defining multiple `Deconstruct` methods in a class or a class hierarchy. Multiple `Deconstruct` methods that have the same number of `out` parameters can quickly cause ambiguities. Callers may not be able to easily call the desired `Deconstruct` method. In this example, there is minimal chance for an ambiguious call because the -`Deconstruct` method for `Person` has two output fields, and the `Deconstruct` +`Deconstruct` method for `Person` has two output parameters, and the `Deconstruct` method for `Student` has three. ## Conclusion @@ -333,7 +339,7 @@ method for `Student` has three. The new language and library support for named tuples makes it much easier to work with designs that use data structures that store multiple fields but do not define behavior, as classes and structs do. It's -easy and concise to use tuples for those types. You get all the benefits +easy and concise to use tuples for those types. You get all the benefits of static type checking, without needing to author types using the more verbose `class` or `struct` syntax. Even so, they are most useful for utility methods that are `private`, or `internal`. Create user defined types, either diff --git a/docs/toc.md b/docs/toc.md index 80fbc91519dc9..cc0120119d055 100644 --- a/docs/toc.md +++ b/docs/toc.md @@ -258,7 +258,7 @@ ### [Basic Types](csharp/basic-types.md) ### [Classes](csharp/classes.md) ### [Structs](csharp/structs.md) -### [Tuples and unnamed types](csharp/tuples.md) +### [Tuples](csharp/tuples.md) ### [🔧 Interfaces](csharp/interfaces.md) ### [Methods and Lambda Expressions](csharp/methods-lambda-expressions.md) #### [Methods](csharp/methods.md) diff --git a/samples/snippets/csharp/new-in-7/new-in-7/Program.cs b/samples/snippets/csharp/new-in-7/new-in-7/Program.cs index 65c24036dc11a..b5ee466548f7a 100644 --- a/samples/snippets/csharp/new-in-7/new-in-7/Program.cs +++ b/samples/snippets/csharp/new-in-7/new-in-7/Program.cs @@ -124,7 +124,7 @@ private static void TupleMethod() } #region 08_TupleReturningMethod - private static (int max, int min) Range(IEnumerable numbers) + private static (int Max, int Min) Range(IEnumerable numbers) { int min = int.MaxValue; int max = int.MinValue; @@ -145,15 +145,15 @@ private static void TupleDeclarations() #endregion #region 05_NamedTuple - (string alpha, string beta) namedLetters = ("a", "b"); + (string Alpha, string Beta) namedLetters = ("a", "b"); #endregion #region 06_ImplicitNamedTuple - var alphabetStart = (alpha: "a", beta: "b"); + var alphabetStart = (Alpha: "a", Beta: "b"); #endregion #region 07_NamedTupleConflict - (string first, string second) firstLetters = (alpha: "a", beta: "b"); + (string First, string Second) firstLetters = (Alpha: "a", Beta: "b"); #endregion } diff --git a/samples/snippets/csharp/tuples/tuples/Person.cs b/samples/snippets/csharp/tuples/tuples/Person.cs index 815f866618b0a..4cc19d260ec84 100644 --- a/samples/snippets/csharp/tuples/tuples/Person.cs +++ b/samples/snippets/csharp/tuples/tuples/Person.cs @@ -9,7 +9,7 @@ namespace tuples #region 12_TypeWithDeconstructMethod public class Person { - public string FirstName { get;} + public string FirstName { get; } public string LastName { get; } public Person(string first, string last) @@ -17,6 +17,7 @@ public Person(string first, string last) FirstName = first; LastName = last; } + public void Deconstruct(out string firstName, out string lastName) { firstName = FirstName; diff --git a/samples/snippets/csharp/tuples/tuples/Program.cs b/samples/snippets/csharp/tuples/tuples/Program.cs index fb86b061ee88a..4a7383c8c3df3 100644 --- a/samples/snippets/csharp/tuples/tuples/Program.cs +++ b/samples/snippets/csharp/tuples/tuples/Program.cs @@ -22,13 +22,17 @@ static void Main(string[] args) Console.WriteLine(StatisticsVersionThree.StandardDeviation(sample)); Console.WriteLine(StatisticsVersionFour.StandardDeviation(sample)); - var p2 = new Person("Bill", "Wagner"); - var (first, last) = p2; + #region 12A_DeconstructType + var p = new Person("Althea", "Goodwin"); + var (first, last) = p; + #endregion Console.WriteLine(first); Console.WriteLine(last); - var s1 = new Student("Bill", "Wagner", 4.5); + #region 13A_DeconstructExtension + var s1 = new Student("Cary", "Totten", 4.5); var (fName, lName, gpa) = s1; + #endregion var (f, l) = s1; @@ -42,14 +46,12 @@ static void Main(string[] args) private static void AssignmentStatements() { #region 03_VariableCreation - // The 'arity' and 'shape' of all these - // tuples are compatible. The only - // difference is the field names being - // used. + // The 'arity' and 'shape' of all these tuples are compatible. + // The only difference is the field names being used. var unnamed = (42, "The meaning of life"); var anonymous = (16, "a perfect square"); - var named = (answer: 42, message: "The meaning of life"); - var differentNamed = (secretConstant: 42, label: "The meaning of life"); + var named = (Answer: 42, Message: "The meaning of life"); + var differentNamed = (SecretConstant: 42, Label: "The meaning of life"); #endregion #region 04_VariableAssignment @@ -60,17 +62,16 @@ private static void AssignmentStatements() named = unnamed; // 'named' still has fields that can be referred to // as 'answer', and 'message': - Console.WriteLine($"{named.answer}, {named.message}"); + Console.WriteLine($"{named.Answer}, {named.Message}"); // unnamed to unnamed: anonymous = unnamed; // named tuples. named = differentNamed; - // The field names are not assigned. - // 'named' still has fields that can be referred to - // as 'answer' and 'message': - Console.WriteLine($"{named.answer}, {named.message}"); + // The field names are not assigned. 'named' still has + // fields that can be referred to as 'answer' and 'message': + Console.WriteLine($"{named.Answer}, {named.Message}"); #endregion } @@ -88,17 +89,17 @@ private static void InitializationStatements() private static double VersionThree(IEnumerable sequence) { - var computation = (sum: 0.0, sumOfSquares: 0.0, items: 0); + var computation = (Sum: 0.0, SumOfSquares: 0.0, Items: 0); foreach (var item in sequence) { - computation.items++; - computation.sum += item; - computation.sumOfSquares += item * item; + computation.Items++; + computation.Sum += item; + computation.SumOfSquares += item * item; } - var variance = computation.sumOfSquares - computation.sum * computation.sum / computation.items; - return Math.Sqrt(variance / computation.items); + var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Items; + return Math.Sqrt(variance / computation.Items); } private static double VersionFour(IEnumerable sequence) @@ -110,7 +111,7 @@ private static double VersionFour(IEnumerable sequence) } // Remove names, and it can be an unnamed tuple. - private static (double sum, double sumOfSquares, int items) ComputeCoreStats(IEnumerable sequence) + private static (int items, double sum, double sumOfSquares) ComputeCoreStats(IEnumerable sequence) { double total = 0; double sumOfSquares = 0; @@ -122,7 +123,7 @@ private static (double sum, double sumOfSquares, int items) ComputeCoreStats(IEn total += item; sumOfSquares += item * item; } - return (total, sumOfSquares, items); + return (items, total, sumOfSquares); } } } diff --git a/samples/snippets/csharp/tuples/tuples/ProjectionSample.cs b/samples/snippets/csharp/tuples/tuples/ProjectionSample.cs index 863afb84b01de..c0b6329ec98c1 100644 --- a/samples/snippets/csharp/tuples/tuples/ProjectionSample.cs +++ b/samples/snippets/csharp/tuples/tuples/ProjectionSample.cs @@ -13,8 +13,7 @@ public class ToDoItem public bool IsDone { get; set; } public DateTime DueDate { get; set; } public string Title { get; set; } - public string Notes { get; set; } - + public string Notes { get; set; } } #endregion diff --git a/samples/snippets/csharp/tuples/tuples/Statistics.cs b/samples/snippets/csharp/tuples/tuples/Statistics.cs index 7be15fe112609..9d02f81a0c90a 100644 --- a/samples/snippets/csharp/tuples/tuples/Statistics.cs +++ b/samples/snippets/csharp/tuples/tuples/Statistics.cs @@ -55,17 +55,17 @@ public static class StatisticsVersionThree #region 07_TupleVersion public static double StandardDeviation(IEnumerable sequence) { - var computation = (sum: 0.0, sumOfSquares: 0.0, count: 0); + var computation = (Count: 0, Sum: 0.0, SumOfSquares: 0.0); foreach (var item in sequence) { - computation.count++; - computation.sum += item; - computation.sumOfSquares += item * item; + computation.Count++; + computation.Sum += item; + computation.SumOfSquares += item * item; } - var variance = computation.sumOfSquares - computation.sum * computation.sum / computation.count; - return Math.Sqrt(variance / computation.count); + var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count; + return Math.Sqrt(variance / computation.Count); } #endregion } @@ -75,15 +75,15 @@ public static class StatisticsVersionFour #region 08_TupleMethodVersion public static double StandardDeviation(IEnumerable sequence) { - (double Sum, double SumOfSquares, int Count) computation = ComputeSumsAnSumOfdSquares(sequence); + (int Count, double Sum, double SumOfSquares) computation = ComputeSumsAnSumOfSquares(sequence); var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count; return Math.Sqrt(variance / computation.Count); } - private static (double Sum, double SumOfSquares, int Count) ComputeSumsAnSumOfdSquares(IEnumerable sequence) + private static (int Count, double Sum, double SumOfSquares) ComputeSumsAnSumOfSquares(IEnumerable sequence) { - var computation = (sum: 0.0, sumOfSquares: 0.0, count: 0); + var computation = (count: 0, sum: 0.0, sumOfSquares: 0.0); foreach (var item in sequence) { @@ -108,7 +108,7 @@ public static double StandardDeviation(IEnumerable sequence) return Math.Sqrt(variance / computation.Count); } - private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSquares(IEnumerable sequence) + private static (int Count, double Sum, double SumOfSquares) ComputeSumAndSumOfSquares(IEnumerable sequence) { double sum = 0; double sumOfSquares = 0; @@ -121,7 +121,7 @@ private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSq sumOfSquares += item * item; } - return (sum, sumOfSquares, count); + return (count, sum, sumOfSquares); } #endregion } @@ -132,14 +132,14 @@ public static class StatisticsVersionSix #region 10_Deconstruct public static double StandardDeviation(IEnumerable sequence) { - (double sum, double sumOfSquares, int count) = ComputeSumAndSumOfSquares(sequence); + (int count, double sum, double sumOfSquares) = ComputeSumAndSumOfSquares(sequence); var variance = sumOfSquares - sum * sum / count; return Math.Sqrt(variance / count); } #endregion - private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSquares(IEnumerable sequence) + private static (int Count, double Sum, double SumOfSquares) ComputeSumAndSumOfSquares(IEnumerable sequence) { double sum = 0; double sumOfSquares = 0; @@ -152,7 +152,7 @@ private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSq sumOfSquares += item * item; } - return (sum, sumOfSquares, count); + return (count, sum, sumOfSquares); } } @@ -168,7 +168,7 @@ public static double StandardDeviation(IEnumerable sequence) } #endregion - private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSquares(IEnumerable sequence) + private static (int Count, double Sum, double SumOfSquares) ComputeSumAndSumOfSquares(IEnumerable sequence) { double sum = 0; double sumOfSquares = 0; @@ -181,7 +181,7 @@ private static (double Sum, double SumOfSquares, int Count) ComputeSumAndSumOfSq sumOfSquares += item * item; } - return (sum, sumOfSquares, count); + return (count, sum, sumOfSquares); } } }