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

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

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

442 CHAPTER 13 C# 2008 LANGUAGE FEATURES

// Reflect over what the compiler generated.

ReflectOverAnonymousType(myCar);

Console.ReadLine();

}

Check out the output shown in Figure 13-4.

Figure 13-4. Anonymous types are represented by a compiler-generated class type.

First of all, notice that in this example, the myCar object is of type <>f__AnonymousType0`3 (your name may differ). Remember that the assigned type name is completely determined by the compiler and is not directly accessible in your C# code base.

Perhaps most important, notice that each name/value pair defined using the object initialization syntax is mapped to an identically named read-only property and a corresponding private read-only backing field. The following C# code approximates the compiler-generated class used to represent the myCar object (which again can be verified using tools such as reflector.exe or ildasm.exe):

internal sealed class <>f__AnonymousType0<<Color>j__TPar, <Make>j__TPar, <CurrentSpeed>j__TPar>

{

// Read-only fields

private readonly <Color>j__TPar <Color>i__Field;

private readonly <CurrentSpeed>j__TPar <CurrentSpeed>i__Field; private readonly <Make>j__TPar <Make>i__Field;

// Default constructor

public <>f__AnonymousType0(<Color>j__TPar Color, <Make>j__TPar Make, <CurrentSpeed>j__TPar CurrentSpeed);

// Overridden methods

public override bool Equals(object value); public override int GetHashCode();

public override string ToString();

// Read-only properties

public <Color>j__TPar Color { get; }

public <CurrentSpeed>j__TPar CurrentSpeed { get; } public <Make>j__TPar Make { get; }

}

The Implementation of ToString() and GetHashCode()

All anonymous types automatically derive from System.Object and are provided with an overridden version of Equals(), GetHashCode(), and ToString(). The ToString() implementation simply builds a string from each name/value pair, for example:

CHAPTER 13 C# 2008 LANGUAGE FEATURES

443

public override string ToString()

{

StringBuilder builder = new StringBuilder(); builder.Append("{ Color = "); builder.Append(this.<Color>i__Field); builder.Append(", Make = "); builder.Append(this.<Make>i__Field); builder.Append(", CurrentSpeed = "); builder.Append(this.<CurrentSpeed>i__Field); builder.Append(" }");

return builder.ToString();

}

The GetHashCode() implementation computes a hash value using each anonymous type’s member variables as input to the System.Collections.Generic.EqualityComparer<T> type. Using this implementation of GetHashCode(), two anonymous types will yield the same hash value if (and only if) they have the same set of properties that have been assigned the same values. Given this implementation, anonymous types are well suited to be contained within a Hashtable container.

The Semantics of Equality for Anonymous Types

While the implementation of the overridden ToString() and GetHashCode() methods is fairly straightforward, you may be wondering how the Equals() method has been implemented. For example, if we were to define two “anonymous cars” variables that specify the same name/value pairs, would these two variables be considered equal or not? To see the results firsthand, update your Program type with the following new method:

static void EqualityTest()

{

// Make 2 anonymous classes with identical name/value pairs.

var firstCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed = 55 }; var secondCar = new { Color = "Bright Pink", Make = "Saab", CurrentSpeed = 55 };

//Are they considered equal when using Equals()? if (firstCar.Equals(secondCar))

Console.WriteLine("Same anonymous object!"); else

Console.WriteLine("Not the same anonymous object!");

//Are they considered equal when using ==?

if (firstCar == secondCar) Console.WriteLine("Same anonymous object!");

else

Console.WriteLine("Not the same anonymous object!");

// Are these objects the same underlying type?

if (firstCar.GetType().Name == secondCar.GetType().Name) Console.WriteLine("We are both the same type!");

else

Console.WriteLine("We are different types!");

// Show all the details.

Console.WriteLine();

ReflectOverAnonymousType(firstCar);

ReflectOverAnonymousType(secondCar);

}

444 CHAPTER 13 C# 2008 LANGUAGE FEATURES

Assuming you have called this method from within Main(), Figure 13-5 shows the (somewhat surprising) output.

Figure 13-5. The equality of anonymous types

When you run this test code, you will see that the first conditional test where you are calling Equals() returns true, and therefore the message “Same anonymous object!” prints out to the screen. This is because the compiler-generated Equals() method makes use of value-based semantics when testing for equality (e.g., checking the value of each field for the objects being compared).

However, the second conditional test (which makes use of the C# equality operator, ==) prints out “Not the same anonymous object!”, which may seem at first glance to be a bit counterintuitive. This is due to the fact that anonymous types do not receive overloaded versions of the C# equality operators (== and !=). Given this, when you test for equality of anonymous types using the C# equality operators (rather than the Equals() method), the references, not the values maintained by the objects, are being tested for equality. Recall from Chapter 12 that this is the default behavior for all class types until you overload the operators directly in your code (something that is not possible for anonymous types, as you don’t define the type!).

Last but not least, in our final conditional test (where we are examining the underlying type name), we find that the anonymous types are instances of the same compiler-generated class type (in this example, <>f__AnonymousType0`3), due to the fact that firstCar and secondCar have the same properties (Color, Make, and CurrentSpeed).

This illustrates an important but subtle point: the compiler will only generate a new class definition when an anonymous type contains unique names of the anonymous type. Thus, if you were to declare identical anonymous types (again, meaning the same names) within the same assembly, the compiler only generates a single anonymous type definition.

Anonymous Types Containing Anonymous Types

It is possible to create an anonymous type that is composed of additional anonymous types. For example, assume you wish to model a purchase order that consists of a timestamp, a price point, and the automobile purchased. Here is a new (slightly more sophisticated) anonymous type representing such an entity:

// Make an anonymous type that is composed of another. var purchaseItem = new {

TimeBought = DateTime.Now,

CHAPTER 13 C# 2008 LANGUAGE FEATURES

445

ItemBought = new {Color = "Red", Make = "Saab", CurrentSpeed = 55},

Price = 34.000};

ReflectOverAnonymousType(purchaseItem);

At this point, you should understand the syntax used to define anonymous types, but you may still be wondering exactly where (and when) to make use of this new language feature. To be blunt, the use of anonymous type declarations should be used sparingly, typically only when making use of the LINQ technology set (see Chapter 14). You would never want to abandon the use of strongly typed classes/structures simply for the sake of doing so, given anonymous types’ numerous limitations, which include the following:

You don’t control the name of the anonymous type.

Anonymous types always extend System.Object.

The fields and properties of an anonymous type are always read-only.

Anonymous types cannot support events, custom methods, custom operators, or custom overrides.

Anonymous types are always implicitly sealed.

Anonymous types are always created using the default constructor.

However, when programming with the LINQ technology set, you will find that in many cases this syntax can be very helpful when you wish to quickly model the overall shape of an entity rather than its functionality.

Source Code The AnonymousTypes project can be found under the Chapter 13 subdirectory.

Summary

C# 2008 provides a number of very interesting features that bring C# into the family of functional languages. This chapter walked you through each of the core updates, beginning with the notion of implicitly typed local variables. While the vast majority of your local variables will not need to be declared with the var keyword, as you will see in the next chapter doing so can greatly simplify your interactions with the LINQ family of technologies.

This chapter also described the role of automatic properties, partial methods, extension methods (which allow you to add new functionality to a compiled type), and the syntax of object initialization (which can be used to assign property values at the time of construction).

The chapter wrapped up by examining the use of anonymous types. This language feature allows you to define the “shape” of a type rather than its functionality. This can be very helpful when you need to model a type for limited usage within a program, given that a majority of the workload is offloaded to the compiler.

C H A P T E R 1 4

An Introduction to LINQ

The previous chapter introduced you to numerous C# 2008 programming constructs. As you have seen, features such as implicitly typed local variables, anonymous types, object initialization syntax, and lambda expressions (examined in Chapter 11) allow us to build very functional C# code. Recall that while many of these features can be used directly as is, their benefits are much more apparent when used within the context of the Language Integrated Query (LINQ) technology set.

This chapter will introduce you to the LINQ model and its role in the .NET platform. Here, you will come to learn the role of query operators and query expressions, which allow you to define statements that will interrogate a data source to yield the requested result set. Along the way, you will build numerous LINQ examples that interact with data contained within arrays as well as various collection types (both generic and nongeneric) and understand the assemblies and types that enable LINQ.

Note Chapter 24 will examine additional LINQ-centric APIs that allow you to interact with relational databases and XML documents.

Understanding the Role of LINQ

As software developers, it is hard to deny that the vast majority of our programming time is spent obtaining and manipulating data. When speaking of “data,” it is very easy to immediately envision information contained within relational databases. However, another popular location in which data exists is within XML documents (*.config files, locally persisted DataSets, in-memory data returned from XML web services, etc.).

Data can be found in numerous places beyond these two common homes for information. For instance, say you have a generic List<T> type containing 300 integers, and you want to obtain a subset that meets a given criterion (e.g., only the odd or even members in the container, only prime numbers, only nonrepeating numbers greater than 50, etc.). Or perhaps you are making use of the reflection APIs and need to obtain only metadata descriptions for each class deriving from a particular parent class within an array of Types. Indeed, data is everywhere.

Prior to .NET 3.5, interacting with a particular flavor of data required programmers to make use of diverse APIs. Consider, for example, Table 14-1, which illustrates several common APIs used to access various types of data.

447

448 CHAPTER 14 AN INTRODUCTION TO LINQ

Table 14-1. Ways to Manipulate Various Types of Data

The Data We Want

How to Obtain It

Relational data

System.Data.dll, System.Data.SqlClient.dll, etc.

XML document data

System.Xml.dll

Metadata tables

The System.Reflection namespace

Collections of objects

System.Array and the System.Collections/System.Collections.

 

Generic namespaces

 

 

Of course, nothing is wrong with these approaches to data manipulation. In fact, when programming with .NET 3.5/C# 2008, you can (and will) certainly make direct use of ADO.NET, the XML namespaces, reflection services, and the various collection types. However, the basic problem is that each of these APIs is an island unto itself, which offers very little in the way of integration. True, it is possible (for example) to save an ADO.NET DataSet as XML, and then manipulate it via the System.Xml namespaces, but nonetheless, data manipulation remains rather asymmetrical.

The LINQ API is an attempt to provide a consistent, symmetrical manner in which programmers can obtain and manipulate “data” in the broad sense of the term. Using LINQ, we are able to create directly within the C# programming language entities called query expressions. These query expressions are based on numerous query operators that have been intentionally designed to look and feel very similar (but not quite identical) to a SQL expression.

The twist, however, is that a query expression can be used to interact with numerous types of data—even data that has nothing to do with a relational database. Specifically, LINQ allows query expressions to manipulate any object that implements the IEnumerable<T> interface (directly or indirectly via extension methods), relational databases, DataSets, or XML documents in a consistent manner.

Note Strictly speaking, “LINQ” is the term used to describe this overall approach to data access. LINQ to Objects is LINQ over objects implementing IEnumerable<T>, LINQ to SQL is LINQ over relational data, LINQ to DataSet is a superset of LINQ to SQL, and LINQ to XML is LINQ over XML documents. In the future, you are sure to find other APIs that have been injected with LINQ functionality (in fact, there are already other LINQ-centric projects under development at Microsoft).

LINQ Expressions Are Strongly Typed and Extendable

It is also very important to point out that a LINQ query expression (unlike a traditional SQL statement) is strongly typed. Therefore, the C# compiler will keep us honest and make sure that these expressions are syntactically well formed. On a related note, query expressions have metadata representation within the assembly that makes use of them. Tools such as Visual Studio 2008 can use this metadata for useful features such as IntelliSense, autocompletion, and so forth.

Also, before we dig into the details of LINQ, one final point is that LINQ is designed to be an extendable technology. While this initial release of LINQ is targeted for relational databases/ DataSets, XML documents, and objects implementing IEnumerable<T>, third parties can incorporate new query operators (or redefine existing operators) using extension methods (see Chapter 13) to account for addition forms of data.

CHAPTER 14 AN INTRODUCTION TO LINQ

449

Note Before you continue reading over this chapter, I wholeheartedly recommend that you first feel comfortable with the material presented in Chapter 13 (which covered C# 2008 specific constructs). As you will see, LINQ programming makes use of several of the new C# features to simplify coding tasks.

The Core LINQ Assemblies

As mentioned in Chapter 2, the New Project dialog of Visual Studio 2008 now has the option of selecting which version of the .NET platform you wish to compile against, using the drop-down list box mounted on the upper-right corner. When you opt to compile against the .NET Framework 3.5, each of the project templates will automatically reference the key LINQ assemblies. For example, if you were to create a new .NET 3.5 Console Application, you will find the assemblies shown in Figure 14-1 visible within the Solution Explorer.

Figure 14-1. .NET 3.5 project types automatically reference key LINQ assemblies.

Table 14-2 documents the role of the core LINQ-specific assemblies.

Table 14-2. Core LINQ-centric Assemblies

Assembly

Meaning in Life

System.Core.dll

Defines the types that represent the core LINQ API. This

 

is the one assembly you must have access to.

System.Data.Linq.dll

Provides functionality for using LINQ with relational

 

databases (LINQ to SQL).

System.Data.DataSetExtensions.dll

Defines a handful of types to integrate ADO.NET types

 

into the LINQ programming paradigm (LINQ to DataSet).

System.Xml.Linq.dll

Provides functionality for using LINQ with XML

 

document data (LINQ to XML).

 

 

When you wish to do any sort of LINQ programming, you will at the very least need to import the System.Linq namespace (defined within System.Core.dll), which is typically accounted for by new Visual Studio 2008 project files; for example, here is the starting code for a new .NET 3.5 Console Application project:

450CHAPTER 14 AN INTRODUCTION TO LINQ

using System;

using System.Collections.Generic; using System.Linq;

using System.Text;

namespace MyConsoleApp

{

class Program

{

static void Main(string[] args)

{

}

}

}

A First Look at LINQ Query Expressions

To begin examining the LINQ programming model, let’s build simple query expressions to manipulate data contained within various arrays. Create a .NET 3.5 Console Application named LinqOverArray, and define a static helper method within the Program class named QueryOverStrings(). In this method, create a string array containing six or so items of your liking (here, I listed out a batch of video games I am currently attempting to finish).

static void QueryOverStrings()

{

// Assume we have an array of strings.

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

"Daxter", "System Shock 2"}; Console.ReadLine();

}

Now, update Main() to invoke QueryOverStrings():

static void Main(string[] args)

{

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

QueryOverStrings();

Console.ReadLine();

}

When you have any array of data, it is very common to extract a subset of items based on a given requirement. Maybe you want to obtain only the items with names that contain a number (e.g., System Shock 2 and Half Life 2: Episode 1), have more than some number of characters, or don’t have embedded spaces (e.g., Morrowind). While you could certainly perform such tasks using members of the System.Array type and a bit of elbow grease, LINQ query expressions can greatly simplify the process.

Going on the assumption that we wish to obtain a subset from the array that contains items with names consisting of more than six characters, we could build the following query expression:

static void QueryOverStrings()

{

// Assume we have an array of strings.

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

"Daxter", "System Shock 2"};

CHAPTER 14 AN INTRODUCTION TO LINQ

451

//Build a query expression to represent the items in the array

//that have more than 6 letters.

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

// Print out the results. foreach (string s in subset)

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

}

Notice that the query expression created here makes use of the from, in, where, orderby, and select LINQ query operators. We will dig into the formalities of query expression syntax in

just a bit, but even now you should be able to parse this statement as “Give me the items inside of currentVideoGames that have more than six characters, ordered alphabetically.” Here, each item that matches the search criteria has been given the name “g” (as in “game”); however, any valid C# variable name would do:

IEnumerable<string> subset = from game in currentVideoGames where game.Length > 6 orderby game select game;

Notice that the “result set” variable, subset, is represented by an object that implements the generic version of IEnumerable<T>, where T is of type System.String (after all, we are querying an array of strings). Once we obtain the result set, we then simply print out each item using a standard foreach construct.

Before we see the results of our query, assume the Program class defines an additional helper function named ReflectOverQueryResults() that will print out various details of the LINQ result set (note the parameter is a System.Object, to account for multiple types of result sets):

static void ReflectOverQueryResults(object resultSet)

{

Console.WriteLine("***** Info about your query *****");

Console.WriteLine("resultSet is of type: {0}", resultSet.GetType().Name); Console.WriteLine("resultSet location: {0}", resultSet.GetType().Assembly);

}

Assuming you have called this method within QueryOverStrings() directly after printing out the obtained subset, if you run the application, you will see the subset is really an instance of the generic OrderedEnumerable<TElement, TKey> type (represented in terms of CIL code as

OrderedEnumerable`2), which is an internal abstract type residing in the System.Core.dll assembly (see Figure 14-2).

Figure 14-2. The result of our LINQ query