15

Collection Interfaces with Standard Query Operators

Through a set of extension methods and lambda expressions, the programming API called Language Integrated Query (LINQ) provides a far superior API for working with collections.1 In fact, in earlier editions of this book, the chapter on collections came immediately after the chapter on generics and just before the one on delegates. However, lambda expressions were so fundamental to LINQ that it is no longer possible to cover collections without first covering delegates (the basis of lambda expressions). Now that you have a solid foundation in lambda expressions from the preceding two chapters, we can delve into the details of collections—a topic that spans three chapters. In this chapter, the focus begins with standard query operators—a means of leveraging LINQ via direct invocation of extension methods.

After introducing collection initializers, this chapter covers the various collection interfaces and explores how they relate to one another. This is the basis for understanding collections, so you should cover the material with diligence. The section on collection interfaces includes coverage of the IEnumerable<T> extension methods2 that implement the standard query operators.

There are two categories of collection-related classes and interfaces: those that support generics and those that don’t. This chapter primarily discusses the generic collection interfaces. You should use collection classes that don’t support generics only when you are writing components that need to interoperate with earlier versions of the runtime. This is because everything that was available in the nongeneric form has a generic replacement that is strongly typed. Although the concepts still apply to both forms, we do not explicitly discuss the nongeneric versions.3

The chapter concludes with an in-depth discussion of anonymous types—topics that we covered only briefly in a few Advanced Topic sections in Chapter 3. The interesting thing about anonymous types is that they have been eclipsed by tuples4—a topic we discuss further at the end of the chapter.

Collection Initializers

A collection initializer allows programmers to construct a collection with an initial set of members at instantiation time in a manner similar to array declaration. Before collection initialization was available, elements had to be explicitly added to a collection after the collection was instantiated—using something like System.Collections.Generic.ICollection<T>’s Add() method. With collection initialization, the Add() calls are generated by the C# compiler rather than explicitly coded by the developer. Listing 15.1 shows how to initialize the collection using a collection initializer.

Listing 15.1: Collection Initialization
using System;
using System.Collections.Generic;
 
public class Program
{
    public static void Main()
    {
        List<string> sevenWorldBlunders;
        sevenWorldBlunders = new List<string>()
        {
            // Quotes from Gandhi
            "Wealth without work",
            "Pleasure without conscience",
            "Knowledge without character",
            "Commerce without morality",
            "Science without humanity",
            "Worship without sacrifice",
            "Politics without principle"
        };
 
        Print(sevenWorldBlunders);
    }
 
    private static void Print<T>(IEnumerable<T> items)
    {
        foreach(T item in items)
        {
            Console.WriteLine(item);
        }
    }
}

The syntax is similar not only to the array initialization but also to an object initializer with the curly braces following the constructor. If no parameters are passed in the constructor, the parentheses following the data type are optional (as they are with object initializers).

A few basic requirements are needed for a collection initializer to compile successfully. Ideally, the collection type to which a collection initializer is applied would be of a type that implements System.Collections.Generic.ICollection<T>. This ensures that the collection includes an Add() method that the compiler-generated code can invoke. However, a relaxed version of the requirement also exists that simply demands one or more Add() methods exist either as an extension method5 or as an instance method on a type that implements IEnumerable—even if the collection doesn’t implement ICollection<T>. The Add() methods need to take parameters that are compatible with the values specified in the collection initializer.

For dictionaries, the collection initializer syntax is slightly more complex, because each element in the dictionary requires both the key and the value. This syntax is shown in Listing 15.2.

Listing 15.2: Initializing a Dictionary<> with a Collection Initializer
using System;
using System.Collections.Generic;
// ...
#if !PRECSHARP6
        Dictionary<string, ConsoleColor> colorMap = new()
            {
                ["Error"] = ConsoleColor.Red,
                ["Warning"] = ConsoleColor.Yellow,
                ["Information"] = ConsoleColor.Green,
                ["Verbose"] = ConsoleColor.White
            };
#else
        Dictionary<string, ConsoleColor> colorMap =
            new Dictionary<string, ConsoleColor>
            {
                {"Error", ConsoleColor.Red },
                {"Warning", ConsoleColor.Yellow },
                {"Information", ConsoleColor.Green },
                {"Verbose", ConsoleColor.White}
            };
#endif

Listing 15.2 includes two different versions of the initialization. The first demonstrates a new syntax,6 which expresses the intent of a name/value pair by allowing the assignment operator to express which value is associated with which key. The second syntax7 pairs the name and the value together using curly brackets.

Allowing initializers on collections that don’t support ICollection<T> was important for two reasons. First, most collections (types that implement IEnumerable<T>) do not also implement ICollection<T>, which significantly reduces the usefulness of collection initializers. Second, matching on the method name and signature compatibility with the collection initializer items enables greater diversity in the items initialized into the collection. For example, the initializer now can support new DataStore(){ a, {b, c}} as long as there is one Add() method whose signature is compatible with a and a second Add() method whose signature is compatible with b, c.

________________________________________

1. Significant features added in C# 3.0 in collections attributable to LINQ.
2. Added in C# 3.0.
3. In fact, .NET Standards and .NET Core don’t even include the nongeneric collections.
4. Starting in C# 7.0.
5. Starting in C# 6.0.
6. Introduced in C# 6.0.
7. Works in C# 6.0 and later.
{{ snackbarMessage }}
;