To support LINQ, an advanced API, only eight new language enhancements were made.9 Two of these enhancements were anonymous types and implicit local variables. Even so, anonymous types have essentially been eclipsed by the introduction of C# tuple syntax.10 In fact, with the sixth edition of this book, all the LINQ examples that previously leveraged anonymous types were updated to use tuples instead.
The remainder of the chapter covers the topic of anonymous types so that you can still make sense of the anonymous type language feature if you are working with code written prior to C# 7.0.
Anonymous types are data types that are declared by the compiler rather than through the explicit class definitions introduced in Chapter 6. As with anonymous functions, when the compiler sees an anonymous type, it does the work to make that class for you and then lets you use it as though you had declared it explicitly. Listing 15.26 shows such a declaration. The corresponding output is shown in Output 15.13.
Anonymous types are purely a C# feature, not a new kind of type in the runtime. When the compiler encounters the anonymous type syntax, it generates a CIL class with properties corresponding to the named values and data types in the anonymous type declaration.
Because an anonymous type has no name, it is not possible to declare a local variable as explicitly being of an anonymous type. Rather, the local variable’s type is replaced with var. However, by no means does this indicate that implicitly typed variables are untyped. On the contrary, they are fully typed to the data type of the value they are assigned. If an implicitly typed variable is assigned an anonymous type, the underlying CIL code for the local variable declaration is of the type generated by the compiler. Similarly, if the implicitly typed variable is assigned a string, its data type in the underlying CIL is a string. In fact, there is no difference in the resultant CIL code for implicitly typed variables whose assignment is not an anonymous type (such as string) and those that are declared with an explicit type. If the declaration statement is string text = "This is a test of the...", the resultant CIL code is identical to an implicitly typed declaration, var text = "This is a test of the...".
The compiler determines the data type of the implicitly typed variable from the expression assigned. In an explicitly typed local variable with an initializer (string s = "hello";), the compiler first determines the type of s from the declared type on the left-hand side, then analyzes the right-hand side and verifies that the expression on the right-hand side is assignable to that type. In an implicitly typed local variable, the process is in some sense reversed. First, the right-hand side is analyzed to determine its type, and then the var is logically replaced with that type.
Although C# does not include a name for the anonymous type, it is strongly typed as well. For example, the properties of the type are fully accessible. In Listing 15.26, patent1.Title and patent2.YearOfPublication are called within the Console.WriteLine statement. Any attempts to call nonexistent members will result in compile-time errors. Even IntelliSense in IDEs such as Visual Studio works with the anonymous type.
You should use implicitly typed variable declarations sparingly. Obviously, for anonymous types, it is not possible to specify the data type, and the use of var is required. However, if the data type is not an anonymous type, it is frequently preferable to use the explicit data type.
As is the case generally, you should focus on making the semantics of the code more readable while also using the compiler to verify that the resultant variable is of the type you expect. To accomplish this with implicitly typed local variables, use them only when the type assigned to the implicitly typed variable is entirely obvious. For example, in var items = new Dictionary<string, List<Account>>();, the resultant code is more succinct and readable. In contrast, when the type is not obvious, such as when a method return is assigned, developers should favor an explicit variable type declaration such as the following:
Dictionary<string, List<Account>> dictionary = GetAccounts();
Lastly, capitalizing on anonymous types, we could create an IEnumerable<T> collection where T is an anonymous type (see Listing 15.27 and Output 15.14).
The output of an anonymous type automatically shows the property names and their values as part of the generated ToString() method associated with the anonymous type.
Projection using the Select() method is very powerful. We already saw how to filter a collection vertically (reducing the number of items in the collection) using the Where() standard query operator. Now, by using the Select() standard query operator, we can also reduce the collection horizontally (making fewer columns) or transform the data entirely. By adding support of anonymous types, we can Select() an arbitrary “object” by extracting only those pieces of the original collection that are desirable for the current algorithm but without having to declare a class to contain them.
In Listing 15.26, member names for the anonymous types are explicitly identified using the assignment of the value to the name for patent1 and patent2 (e.g., Title = "Phonograph"). However, if the value assigned is a property or field call, the name may default to the name of the field or property rather than explicitly specifying the value. For example, patent3 is defined using a property named Title rather than an assignment to an explicit name. As Output 15.13 shows, the resultant property name is determined, by the compiler, to match the property from where the value was retrieved.
Both patent1 and patent2 have the same property names with the same data types. Therefore, the C# compiler generates only one data type for these two anonymous declarations. In contrast, patent3 forces the compiler to create a second anonymous type because the property name for the patent year is different from that in patent1 and patent2. Furthermore, if the order of the properties were switched between patent1 and patent2, these two anonymous types would not be type-compatible. In other words, the requirements for two anonymous types to be type-compatible within the same assembly are a match in property names, data types, and order of properties. If these criteria are met, the types are compatible, even if they appear in different methods or classes. Listing 15.28 demonstrates the type incompatibilities.
The first two compile-time errors assert that the types are not compatible, so they will not successfully convert from one to the other. The third compile-time error is caused by the reassignment of the Title property. Anonymous types are immutable, so it is a compile-time error to change a property on an anonymous type once it has been instantiated.
Although not shown in Listing 15.28, it is not possible to declare a method with an implicit data type parameter (var). Therefore, instances of anonymous types can be passed outside the method in which they are created in only two ways. First, if the method parameter is of type object, the anonymous type instance may be passed outside the method because the anonymous type will convert implicitly. A second way is to use method type inference, whereby the anonymous type instance is passed as a method type parameter that the compiler can successfully infer. Thus, calling void Method<T>(T parameter) using Function(patent1) would succeed, although the available operations on parameter within Function() are limited to those supported by object.
Although C# allows anonymous types such as the ones shown in Listing 15.26, it is generally not recommended that you define them in this way. Anonymous types provide critical functionality with support for projections,11 such as joining/associating collections, as we discuss later in the chapter. Nevertheless, you should generally reserve anonymous type definitions for circumstances where they are required, such as aggregation of data from multiple types.
At the time that anonymous types were introduced, they were a breakthrough that solved an important problem: declaring a temporary type on the fly without the ceremony of having to declare a full type. Even so, they have several drawbacks, as detailed earlier. Fortunately, tuples12 avoid these drawbacks and, in fact, essentially obviate the need for using anonymous types altogether. Specifically, tuples have the following advantages over anonymous types:
One way in which tuples differ from anonymous types is that anonymous types are reference types and tuples are value types. Whether this difference is advantageous to one approach or the other depends on the performance characteristics needed. If the tuple type is frequently copied and its memory footprint is more than 128 bits, a reference type is likely preferable. Otherwise, using a tuple will most likely be more performant—and a better choice to default to.
Even though Console.WriteLine()’s implementation is to call ToString(), notice in Listing 15.26 that the output from Console.WriteLine() is not the default ToString(), which writes out the fully qualified data type name. Rather, the output is a list of PropertyName = value pairs, one for each property on the anonymous type. This occurs because the compiler overrides ToString() in the anonymous type code generation, so as to format the ToString() output as shown. Similarly, the generated type includes overriding implementations for Equals() and GetHashCode().
The implementation of ToString() on its own is an important reason that variation in the order of properties causes a new data type to be generated. If two separate anonymous types, possibly in entirely separate types and even in separate namespaces, were unified and then the order of properties changed, changes in the order of properties on one implementation would have noticeable and possibly unacceptable effects on the other’s ToString() results. Furthermore, at execution time, it is possible to reflect back on a type and examine the members of a type—even to call one of these members dynamically (determining at runtime which member to call). A variation in the order of members of two seemingly identical types could then trigger unexpected results. To avoid this problem, the C# designers decided to generate two different types.
You cannot have a collection initializer for an anonymous type, since the collection initializer requires a constructor call, and it is impossible to name the constructor. The workaround is to define a method such as static List<T> CreateList<T>(T t) { return new List<T>(); }. Method type inference allows the type parameter to be implied rather than specified explicitly, so this workaround successfully allows for the creation of a collection of anonymous types.
Another approach to initializing a collection of anonymous types is to use an array initializer. As it is not possible to specify the data type in the constructor, array initialization syntax allows for anonymous array initializers using new[] (see Listing 15.29).
The resultant variable is an array of the anonymous type items, which must be homogeneous because it is an array.
________________________________________