Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

462 CHAPTER 14 AN INTRODUCTION TO LINQ

On the plus side, the anonymous method syntax does keep all the processing contained within a single method definition. Nevertheless, this method is functionally equivalent to the

QueryStringsWithEnumerableAndLambdas() and QueryStringsWithOperators() methods created in the previous sections.

Building Query Expressions Using the Enumerable Type and

Raw Delegates

Finally, if we want to build a query expression using the really verbose approach, we could avoid the use of lambdas/anonymous method syntax and directly create delegate targets for each Func<> type. Here is the final iteration of our query expression, modeled within a new class type named

VeryComplexQueryExpression:

class VeryComplexQueryExpression

{

public static void QueryStringsWithRawDelegates()

{

Console.WriteLine("***** Using Raw Delegates *****");

string[] currentVideoGames = {"Morrowind", "BioShock", "Half Life 2: Episode 1", "The Darkness",

"Daxter", "System Shock 2"};

//Build the necessary Func<> delegates using anonymous methods.

Func<string, bool> searchFilter = new Func<string, bool>(Filter); Func<string, string> itemToProcess = new Func<string,string>(ProcessItem);

//Pass the delegates into the methods of Enumerable.

var subset = currentVideoGames

.Where(searchFilter).OrderBy(itemToProcess).Select(itemToProcess);

// Print out the results. foreach (var game in subset)

Console.WriteLine("Item: {0}", game); Console.WriteLine();

}

// Delegate targets.

public static bool Filter(string s) {return s.Length > 6;} public static string ProcessItem(string s) { return s; }

}

We can test this iteration of our string processing logic by calling this method within Main() method of the Program class as follows:

VeryComplexQueryExpression.QueryStringsWithRawDelegates();

If you were to now run the application to test each possible approach, it should not be too surprising that the output is identical regardless of the path taken. Keep the following points in mind regarding how LINQ query expressions are represented under the covers:

Query expressions are created using various C# query operators.

Query operators are simply shorthand notations for invoking extension methods defined by the System.Linq.Enumerable type.

Many methods of Enumerable require delegates (Func<> in particular) as parameters.

CHAPTER 14 AN INTRODUCTION TO LINQ

463

Under C# 2008, any method requiring a delegate parameter can instead be passed a lambda expression.

Lambda expressions are simply anonymous methods in disguise (which greatly improve readability).

Anonymous methods are shorthand notations for allocating a raw delegate and manually building a delegate target method.

Whew! That might have been a bit deeper under the hood than you wish to have gone, but I hope this discussion has helped you understand what the user-friendly C# query operators are actually doing behind the scenes. Let’s now turn our attention to the operators themselves.

Source Code The LinqOverArrayUsingEnumerable project can be found under the Chapter 14 subdirectory.

Investigating the C# LINQ Query Operators

C# defines a good number of query operators out of the box. Table 14-3 documents some of the more commonly used query operators.

Note The .NET Framework 3.5 SDK documentation provides full details regarding each of the C# LINQ operators. Look up the topic “LINQ General Programming Guide” for more information.

Table 14-3. Various LINQ Query Operators

Query Operators

Meaning in Life

from, in

Used to define the backbone for any LINQ expression, which

 

allows you to extract a subset of data from a fitting container.

where

Used to define a restriction for which items to extract from a

 

container.

select

Used to select a sequence from the container.

join, on, equals, into

Performs joins based on specified key. Remember, these “joins”

 

do not need to have anything to do with data in a relational

 

database.

orderby, ascending, descending

Allows the resulting subset to be ordered in ascending or

 

descending order.

group, by

Yields a subset with data grouped by a specified value.

 

 

In addition to the partial list of operators shown in Table 14-3, the Enumerable type provides a set of methods that do not have a direct C# query operator shorthand notation, but are instead exposed as extension methods. These generic methods can be called to transform a result set in various manners (Reverse<>(), ToArray<>(), ToList<>(), etc.). Some are used to extract singletons from a result set, others perform various set operations (Distinct<>(), Union<>(), Intersect<>(), etc.), and still others aggregate results (Count<>(), Sum<>(), Min<>(), Max<>(), etc.).

464 CHAPTER 14 AN INTRODUCTION TO LINQ

Obtaining Counts Using Enumerable

Using these query operators (and auxiliary members of the System.Linq.Enumerable type), you are able to build very expressive query expressions in a strongly typed manner. To invoke the Enumerable extension methods, you typically wrap the LINQ expression within parentheses to cast the result to an IEnumerable<T>-compatible object to invoke the Enumerable extension method.

You have already done so during our examination of immediate execution; however, here is another example that allows you to discover the number of items returned by a LINQ query:

static void GetCount()

{

string[] currentVideoGames = {"Morrowind", "BioShock", "Half Life 2: Episode 1", "The Darkness",

"Daxter", "System Shock 2"};

// Get count from the query.

int numb = (from g in currentVideoGames where g.Length > 6 orderby g

select g).Count<string>();

// numb is the value 5.

Console.WriteLine("{0} items honor the LINQ query.", numb);

}

Building a New Test Project

To begin digging into more intricate LINQ queries, create a new Console Application named FunWithLinqExpressions. Next, define a trivial Car type, this time sporting a custom ToString() implementation to quickly view the object’s state:

class Car

{

public string PetName = string.Empty; public string Color = string.Empty; public int Speed;

public string Make = string.Empty;

public override string ToString()

{

return string.Format("Make={0}, Color={1}, Speed={2}, PetName={3}", Make, Color, Speed, PetName);

}

}

Now populate an array with the following Car objects within your Main() method:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Query Expressions *****\n");

// This array will be the basis of our testing...

Car[] myCars = new [] {

new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"}, new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"},

CHAPTER 14 AN INTRODUCTION TO LINQ

465

new Car{ PetName = "Hank", Color = "Tan", Speed = 0, Make = "Ford"}, new Car{ PetName = "Sven", Color = "White", Speed = 90, Make = "Ford"}, new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Zippy", Color = "Yellow", Speed = 55, Make = "VW"}, new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}

};

// We will call various methods here!

Console.ReadLine();

}

Basic Selection Syntax

Because LINQ query expressions are validated at compile time, you need to remember that the ordering of these operators is critical. In the simplest terms, every LINQ query expression is built using the from, in, and select operators:

var result = from item in container select item;

In this case, our query expression is doing nothing more than selecting every item in the container (similar to a Select * SQL statement). Consider the following:

static void BasicSelection(Car[] myCars)

{

// Get everything.

Console.WriteLine("All cars:");

var allCars = from c in myCars select c; foreach (var c in allCars)

{

Console.WriteLine(c.ToString());

}

}

Again, this query expression is not entirely useful, given that our subset is identical to that of the data in the incoming parameter. If we wish, we could use this incoming parameter to extract only the PetName values of each car using the following selection syntax:

// Now get only the names of the cars.

Console.WriteLine("Only PetNames:");

var names = from c in myCars select c.PetName;

foreach (var n in names)

{

Console.WriteLine("Name: {0}", n);

}

In this case, names is really an internal type that implements IEnumerable<string>, given that we are selecting only the values of the PetName property for each Car object. Again, using implicit typing via the var keyword, our coding task is simplified.

Now consider the following task. What if you’d like to obtain and display the makes of each vehicle? If you author the following query expression:

var makes = from c in myCars select c.Make;

you will end up with a number of redundant listings, as you will find BMW, Ford, and VW accounted for multiple times. You can use the Enumerable.Distinct<T>() method to eliminate such duplication:

466 CHAPTER 14 AN INTRODUCTION TO LINQ

var makes = (from c in myCars select c.Make).Distinct<string>();

When calling any extension method defined by Enumerable, you can do so either at the time you build the query expression (as shown in the previous example) or via an extension method on a compatible underlying array type. Thus, the following code yields identical output:

var makes = from c in myCars select c.Make; Console.WriteLine("Distinct makes:"); foreach (var m in makes.Distinct<string>())

{

Console.WriteLine("Make: {0}", m);

}

Figure 14-7 shows the result of calling BasicSelections().

Figure 14-7. Selecting basic data from the Car[] parameter

Obtaining Subsets of Data

To obtain a specific subset from a container, you can make use of the where operator. When doing so, the general template now becomes as follows:

var result = from item in container where Boolean expression select item;

Notice that the where operator expects an expression that resolves to a Boolean. For example, to extract from the Car[] parameter only the items that have “BMW” as the value assigned to the Make field, you could author the following code within a new method named GetSubsets():

static void GetSubsets(Car[] myCars)

{

// Now get only the BMWs.

var onlyBMWs = from c in myCars where c.Make == "BMW" select c;

CHAPTER 14 AN INTRODUCTION TO LINQ

467

foreach (Car c in onlyBMWs)

{

Console.WriteLine(c.ToString());

}

}

As seen earlier in this chapter, when you are building a where clause, it is permissible to make use of any valid C# operators to build complex expressions. For example, consider the following query that only extracts out the BMWs going at least 100 mph:

// Get BMWs going at least 100 mph. var onlyFastBMWs = from c in myCars

where c.Make == "BMW" && c.Speed >= 100 select c;

foreach (Car c in onlyFastBMWs)

{

Console.WriteLine("{0} is going {1} MPH", c.PetName, c.Speed);

}

Projecting New Data Types

It is also possible to project new forms of data from an existing data source. Let’s assume that you wish to take the incoming Car[] parameter and obtain a result set that accounts only for the make and color of each vehicle. To do so, you can define a select statement that dynamically yields new types via C# 2008 anonymous types. Recall from Chapter 13 that the compiler defines a read-only property and a read-only backing field for each specified name, and also is kind enough to override

ToString(), GetHashCode(), and Equals():

var makesColors = from c in myCars select new {c.Make, c.Color}; foreach (var o in makesColors)

{

// Could also use Make and Color properties directly. Console.WriteLine(o.ToString());

}

Figure 14-8 shows the output of each of these new queries.

Figure 14-8. Enumerating over subsets

468 CHAPTER 14 AN INTRODUCTION TO LINQ

Reversing Result Sets

You can reverse the items within a result set quite simply using the generic Reverse<T>() method of the Enumerable type. For example, the following method selects all items from the incoming Car[] parameter in reverse:

static void ReversedSelection(Car[] myCars)

{

// Get everything in reverse.

Console.WriteLine("All cars in reverse:");

var subset = (from c in myCars select c).Reverse<Car>(); foreach (Car c in subset)

{

Console.WriteLine("{0} is going {1} MPH", c.PetName, c.Speed);

}

}

Here, we called the Reverse<T>() method at the time we constructed our query. Again, as an alternative, we could invoke this method on the myCars array as follows:

static void ReversedSelection(Car[] myCars)

{

// Get everything in reverse.

Console.WriteLine("All cars in reverse:"); var subset = from c in myCars select c; foreach (Car c in subset.Reverse<Car>())

{

Console.WriteLine(c.ToString());

}

}

Sorting Expressions

As you have seen over this chapter’s initial examples, a query expression can take an orderby operator to sort items in the subset by a specific value. By default, the order will be ascending; thus, ordering by a string would be alphabetical, ordering by numerical data would be lowest to highest, and so forth. If you wish to view the results in a descending order, simply include the descending operator. Ponder the following method:

static void OrderedResults(Car[] myCars)

{

// Order all the cars by PetName.

var subset = from c in myCars orderby c.PetName select c;

Console.WriteLine("Ordered by PetName:"); foreach (Car c in subset)

{

Console.WriteLine(c.ToString());

}

//Now find the cars that are going less than 55 mph,

//and order by descending PetName

subset = from c in myCars

where c.Speed > 55 orderby c.PetName descending select c; Console.WriteLine("\nCars going faster than 55, ordered by PetName:"); foreach (Car c in subset)

{

CHAPTER 14 AN INTRODUCTION TO LINQ

469

Console.WriteLine(c.ToString());

}

}

Although ascending order is the default, you are able to make your intentions very clear by making use of the ascending operator:

var subset = from c in myCars

orderby c.PetName ascending select c;

Given these examples, you can now understand the format of a basic sorting query expression as follows:

var result = from item in container orderby value ascending/descending select item;

Finding Differences

The last LINQ query we will examine for the time being involves obtaining a result set that determines the differences between two IEnumerable<T> compatible containers. Consider the following method, which makes use of the Enumerable.Except() method to yield (in this example) a Yugo:

static void GetDiff()

{

List<string> myCars = new List<String> { "Yugo", "Aztec", "BMW"};

List<string> yourCars = new List<String> { "BMW", "Saab", "Aztec" };

var carDiff =(from c in myCars select c)

.Except(from c2 in yourCars select c2);

Console.WriteLine("Here is what you don't have, but I do:"); foreach (string s in carDiff)

Console.WriteLine(s); // Prints Yugo.

}

These examples should give you enough knowledge to feel comfortable with the process of building LINQ query expressions. Chapter 24 will explore the related topics of LINQ to ADO (which is a catch-all term describing LINQ to SQL and LINQ to DataSet) and LINQ to XML. However, before wrapping the current chapter, let’s examine the topic LINQ queries as method return values.

Source Code The FunWithLinqExpressions project can be found under the Chapter 14 subdirectory.

LINQ Queries: An Island unto Themselves?

You may have noticed that each of the LINQ queries seen over the course of this chapter were all defined within the scope of a local method. Moreover, to simplify our programming, the variable used to hold the result set was stored in an implicitly typed local variable (in fact, in the case of projections, this is mandatory). Recall from Chapter 13 that implicitly typed local variables cannot be used to define parameters, return values, or fields of a class type.

470 CHAPTER 14 AN INTRODUCTION TO LINQ

Given this point, you may wonder exactly how you could return a query result to an external caller. The answer is it depends. If you have a result set consisting of strongly typed data (such as an array of strings, a List<T> of Cars, or whatnot), you could abandon the use of the var keyword and using a proper IEnumerable<T> or IEnumerable type (again, as IEnumerable<T> extends IEnumerable). Consider the following example for a new .NET 3.5 Console Application named LinqRetValues:

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** LINQ Transformations *****\n");

IEnumerable<string> subset = GetStringSubset(); foreach (string item in subset)

{

Console.WriteLine(item);

}

Console.ReadLine();

}

static IEnumerable<string> GetStringSubset()

{

string[] currentVideoGames = {"Morrowind", "BioShock", "Half Life 2: Episode 1", "The Darkness",

"Daxter", "System Shock 2"};

// Note subset is an IEnumerable<string> compatible object.

IEnumerable<string> subset = from g in currentVideoGames where g.Length > 6

orderby g select g;

return subset;

}

}

This example works as expected, only because the return value of the GetStringSubset() and the LINQ query within this method has been strongly typed. If you used the var keyword to define the subset variable, it would be permissible to return the value only if the method is still prototyped to return IEnumerable<string> (and if the implicitly typed local variable is in fact compatible with the specified return type).

However, always remember that when you have a LINQ query that makes use of a projection, you have no way of knowing the underlying data type, as this is determined at compile time. In these cases, the var keyword is mandatory; therefore, the following code method would not compile:

// Error! Can't return a var data type! static var GetProjectedSubset()

{

Car[] myCars = new Car[] {

new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"}, new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"}, new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}

};

CHAPTER 14 AN INTRODUCTION TO LINQ

471

var makesColors = from c in myCars select new { c.Make, c.Color }; return makesColors; // Nope!

}

Given that return values cannot be implicitly typed, how can we return the makesColors object to an external caller?

Transforming Query Results to Array Types

When you wish to return projected data to a caller, one approach is to transform the query result into a standard CLR Array object using the ToArray<T>() extension method. Thus, if we were to update our query expression as follows:

// Return value is now an Array. static Array GetProjectedSubset()

{

Car[] myCars = new Car[]{

new Car{ PetName = "Henry", Color = "Silver", Speed = 100, Make = "BMW"}, new Car{ PetName = "Daisy", Color = "Tan", Speed = 90, Make = "BMW"}, new Car{ PetName = "Mary", Color = "Black", Speed = 55, Make = "VW"}, new Car{ PetName = "Clunker", Color = "Rust", Speed = 5, Make = "Yugo"}, new Car{ PetName = "Melvin", Color = "White", Speed = 43, Make = "Ford"}

};

var makesColors = from c in myCars select new { c.Make, c.Color };

//Map set of anonymous objects to an Array object.

//Here were are relying on type inference of the generic

//type parameter, as we don't know the type of type! return makesColors.ToArray();

}

we could invoke and process the data from Main() as follows:

Array objs = GetProjectedSubset(); foreach (object o in objs)

{

Console.WriteLine(o); // Calls ToString() on each anonymous object.

}

Note that we have to use a literal System.Array object and cannot make use of the C# array declaration syntax, given that we don’t know the underlying type of type! Also note that we are not specifying the type parameter to the generic ToArray<T>() method, as we (once again) don’t know the underlying data type until compile time (which is too late for our purposes).

The obvious problem is that we lose any strong typing, as each item in the Array object is assumed to be of type Object. Nevertheless, when you need to return a LINQ result set which is the result of a projection operation, transforming the data into an Array type (or another suitable container via other members of the Enumerable type) is mandatory.

Source Code The LinqRetValues project can be found under the Chapter 14 subdirectory.