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

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

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

422 CHAPTER 13 C# 2008 LANGUAGE FEATURES

this will result in a compiler error. When you are defining an automatic property, it must support both read and write functionality.

Interacting with Automatic Properties

Because the compiler will define the private backing field at compile time, the class defining automatic properties will always need to use property syntax to get and set the underlying value. This is important to note because many programmers make direct use of the private fields within a class definition, which is not possible in this case. For example, if the Car type were to override ToString(), you would need to implement this method using the property name:

class Car

{

public string PetName { get; set; }

public override string ToString()

{

//No access to the private member in the defining

//class. Must use properties!

return string.Format("PetName = {0}", PetName);

}

}

When you are using an object defined with automatic properties, you will be able to assign and obtain the values using the expected property syntax:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Automatic Properties *****");

Car c = new Car(); c.PetName = "Frank";

Console.WriteLine("Your car is named {0}? That's odd...", c.PetName);

Console.ReadLine();

}

Restricting Access on Automatic Properties

Recall that a “normal” .NET property can be constructed in such a way that the get and set logic is assigned a unique access modifier. For example, it is possible to define a public get scope and a more restrictive protected scope as follows:

//Anyone can get the PetName value, but

//only the defining type and the children can set it. public int PetName

{

get { return carName; }

protected set { carName = value; }

}

This same possibility is allowed using automatic property syntax as follows:

public string PetName { get; protected set; }

Of course, with this update, the previous Main() method would now generate a compiler error when attempting to assign the value of the PetName property:

CHAPTER 13 C# 2008 LANGUAGE FEATURES

423

static void Main(string[] args)

{

...

//Error! Setting the PetName is only possible

//from within the Car type or by a child type! c.PetName = "Frank";

//Getting the value is still OK.

Console.WriteLine("Your car is named {0}? That's odd...", c.PetName);

Console.ReadLine();

}

Regarding Automatic Properties and Default Values

When you use automatic properties to encapsulate numerical or Boolean data, you are able to use the autogenerated type properties straightaway within your code base, as the hidden backing fields will be assigned a safe default value that can be used directly. However, be very aware that if you use automatic property syntax to wrap a reference type, the hidden private reference type will also be set to a default value of null:

class Garage

{

//The hidden int backing field is set to zero! public int NumberOfCars { get; set; }

//The hidden Car backing field is set to null! public Car MyAuto { get; set; }

}

Given C#’s default values for field data, you would be able to print out the value of NumberOfCars as is (as it is automatically assigned the value of zero), but if you directly invoke MyAuto, you will receive a null reference exception:

static void Main(string[] args)

{

...

Garage g = new Garage();

//OK, prints defualt value of zero.

Console.WriteLine("Number of Cars: {0}", g.NumberOfCars);

//Runtime error! Backing field is currently null!

Console.WriteLine(g.MyAuto.PetName);

Console.ReadLine();

}

Given that the private backing fields are created at compile time, you will be unable to make use of C# field initialization syntax to allocate the reference type directly with the new keyword. Therefore, this work will need to be done with type constructors to ensure the object comes to life in a safe manner. For example:

class Garage

{

// The hidden backing field is set to zero! public int NumberOfCars { get; set; }

424CHAPTER 13 C# 2008 LANGUAGE FEATURES

//The hidden backing field is set to null! public Car MyAuto { get; set; }

//Must use constructors to override default

//values assigned to hidden backing fields. public Garage()

{

MyAuto = new Car(); NumberOfCars = 1;

}

public Garage(Car car, int number)

{

MyAuto = car; NumberOfCars = number;

}

}

As you most likely agree, this is a very nice extension to the C# programming language, as you can define a number of properties for a class using a streamlined syntax. Be aware of course that if you are building a property that requires additional code beyond getting and setting the underlying private field (such as data validation logic, writing to an event log, communicating with a database, etc.), you will be required to define a “normal” .NET property type by hand. C# 2008 automatic properties never do more than provide simple encapsulation for an underlying data type.

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

Understanding Extension Methods

The next C# 2008 language feature we will examine is the use of extension methods. As you know, once a type is defined and compiled into a .NET assembly, its definition is, more or less, final. The only way to add new members, update members, or remove members is to recode and recompile the code base into an updated assembly (or take more drastic measures, such as using the System.Reflection.Emit namespace to dynamically reshape a compiled type in memory).

Under C# 2008, it is now possible to define extension methods. In a nutshell, extension methods allow existing compiled types (specifically, classes, structures, or interface implementations) as well as types currently being compiled (such as types in a project that contains extension methods) to gain new functionality without needing to directly update the type being extended.

This technique can be quite helpful when you need to inject new functionality into types for which you do not have an existing code base. It can also be quite helpful when you need to force a type to support a set of members (in the interest of polymorphism), but cannot modify the original type declaration. Using extension methods, you can add functionality to precompiled types while providing the illusion these methods were there all along.

Note Understand that extension methods do not literally change the compiled code base! This technique only adds members to a type within the context of the current application.

When you define extension methods, the first restriction is that they must be defined within a static class (see Chapter 5), and therefore each extension method must also be declared with the

CHAPTER 13 C# 2008 LANGUAGE FEATURES

425

static keyword. The second point is that all extension methods are marked as such by using the this keyword as a modifier on the first (and only the first) parameter of the method in question. The third point is that every extension method can be called either from the correct instance in memory or statically via the defining static class! Sound strange? Let’s look at a full example to clarify matters.

Defining Extension Methods

Create a new Console Application named ExtensionMethods. Now, assume you are authoring a utility class named MyExtensions that defines two extension methods. The first method allows any object in the .NET base class libraries to have a brand-new method named

DisplayDefiningAssembly() that makes use of types in the System.Reflection namespace to display the assembly of the specified type.

The second extension method, named ReverseDigits(), allows any System.Int32 to obtain a new version of itself where the value is reversed digit by digit. For example, if an integer with the value 1234 called ReverseDigits(), the integer returned is set to the value 4321. Consider the following class implementation:

static class MyExtensions

{

//This method allows any object to display the assembly

//it is defined in.

public static void DisplayDefiningAssembly(this object obj)

{

Console.WriteLine("{0} lives here:\n\t->{1}\n", obj.GetType().Name, Assembly.GetAssembly(obj.GetType()));

}

//This method allows any integer to reverse its digits.

//For example, 56 would return 65.

public static int ReverseDigits(this int i)

{

//Translate int into a string, and then

//get all the characters.

char[] digits = i.ToString().ToCharArray();

//Now reverse items in the array. Array.Reverse(digits);

//Put back into string.

string newDigits = new string(digits);

// Finally, return the modified string back as an int. return int.Parse(newDigits);

}

}

Again, note how the first parameter of each extension method has been qualified with the this keyword, before defining the parameter type. It is always the case that the first parameter of an extension method represents the type being extended. Given that DisplayDefiningAssembly() has been prototyped to extend System.Object, any type in any assembly now has this new member. However, ReverseDigits() has been prototyped to only extend integer types, and therefore if anything other than an integer attempts to invoke this method, you will receive a compile-time error.

Understand that a given extension method could have multiple parameters, but only the first parameter can be qualified with this. For example, here is an overloaded extension method defined in another utility class, named simply TesterUtilClass:

426CHAPTER 13 C# 2008 LANGUAGE FEATURES

static class TesterUtilClass

{

//Every Int32 now has a Foo() method...

public static void Foo(this int i)

{ Console.WriteLine("{0} called the Foo() method.", i); }

//...which has been overloaded to take a string!

public static void Foo(this int i, string msg)

{ Console.WriteLine("{0} called Foo() and told me: {1}", i, msg); }

}

Invoking Extension Methods on an Instance Level

Now that we have these extension methods, look at how all objects (which of course means everything in the .NET base class libraries) have a new method named DisplayDefiningAssembly(), while System.Int32 types (and only integers) have methods named ReverseDigits() and Foo():

static void Main(string[] args)

{

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

//The int has assumed a new identity! int myInt = 12345678; myInt.DisplayDefiningAssembly();

//So has the DataSet!

System.Data.DataSet d = new System.Data.DataSet(); d.DisplayDefiningAssembly();

// And the SoundPlayer!

System.Media.SoundPlayer sp = new System.Media.SoundPlayer(); sp.DisplayDefiningAssembly();

// Use new integer functionality.

Console.WriteLine("Value of myInt: {0}", myInt); Console.WriteLine("Reversed digits of myInt: {0}", myInt.ReverseDigits()); myInt.Foo();

myInt.Foo("Ints that Foo? Who would have thought it!");

bool b2 = true;

//Error! Booleans don't have the Foo() method!

//b2.Foo();

Console.ReadLine();

}

Figure 13-2 shows the output.

CHAPTER 13 C# 2008 LANGUAGE FEATURES

427

Figure 13-2. Extension methods in action

Invoking Extension Methods Statically

Recall that the first parameter of an extension method is marked with the this keyword, followed by the type of item the method is applicable to. If we peek at what is happening behind the scenes (as verified by a tool such as ildasm.exe), we will find that the compiler simply calls the “normal” static method, passing in the variable calling the method as a parameter (e.g., it is the value of this). Consider the following C# code, which approximates the code substitution that took place:

private static void Main(string[] args)

{

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

int myInt = 12345678;

MyExtensions.DisplayDefiningAssembly(myInt);

DataSet d = new DataSet();

MyExtensions.DisplayDefiningAssembly(d);

SoundPlayer sp = new SoundPlayer();

MyExtensions.DisplayDefiningAssembly(sp);

Console.WriteLine("Value of myInt: {0}", myInt); Console.WriteLine("Reversed digits of myInt: {0}",

MyExtensions.ReverseDigits(myInt));

TesterUtilClass.Foo(myInt);

TesterUtilClass.Foo(myInt, "Ints that Foo? Who would have thought it!");

Console.ReadLine();

}

Given that calling an extension method from an object (thereby making it seem that the method is in fact an instance-level method) is just some smoke-and-mirrors effect provided by the compiler, you are always free to call extension methods as normal static methods using the expected C# syntax (as just shown).

428 CHAPTER 13 C# 2008 LANGUAGE FEATURES

The Scope of an Extension Method

As just explained, extension methods are essentially static methods that can be invoked from an instance of the extended type. Given this flavor of syntactic sugar, it is really important to point out that unlike a “normal” method, extension methods do not have direct access to the members of the type they are extending; said another way, extending is not inheriting. Consider the following simple Car type:

public class Car

{

public int Speed; public int SpeedUp()

{

return ++Speed;

}

}

If you were to build an extension method for the Car type named SlowDown(), you do not have direct access to the members of Car within the scope of the extension method as we are not performing an act of classical inheritance. Therefore, the following would result in a compiler error:

public static class CarExtensions

{

public static int SlowDown(this Car c)

{

// Error! This method is not deriving from Car! return --Speed;

}

}

The problem here is that the static SlowDown() extension method is attempting to access the Speed field of the Car type; however, because SlowDown() is a static member of the CarExtensions class, Speed does not exist in this context! What is permissible, however, is to make use of the this- qualified parameter to access all public members (and only the public members) of the type being extending. Thus, the following code compiles as expected:

public static class CarExtensions

{

public static int SlowDown(this Car c)

{

// OK!

return --c.Speed;

}

}

At this point, you could create a Car object and invoke the SpeedUp() and SlowDown() methods as follows:

static void UseCar()

{

Car c = new Car();

Console.WriteLine("Speed: {0}", c.SpeedUp()); Console.WriteLine("Speed: {0}", c.SlowDown());

}

CHAPTER 13 C# 2008 LANGUAGE FEATURES

429

Importing Types That Define Extension Methods

When you partition a set of static classes containing extension methods in a unique namespace, other namespaces in that assembly will make use of the standard C# using keyword to import not only the static classes themselves, but also each of the supported extension methods. This is important to remember, because if you do not explicitly import the correct namespace, the extension methods are not available for that C# code file.

In effect, although it can appear on the surface that extension methods are global in nature, they are in fact limited to the namespaces that define them or the namespaces that import them. Thus, if we wrap the definitions of our static classes (MyExtensions, TesterUtilClass, and

CarExtensions) into a namespace named MyExtensionMethods as follows:

namespace MyExtensionMethods

{

static class MyExtensions

{

...

}

static class TesterUtilClass

{

...

}

static class CarExtensions

{

...

}

}

other namespaces in the project would need to explicitly import the MyExtensionMethods namespace to gain the extension methods defined by these types. Therefore, the following is a compiler error:

// Here is our only using directive. using System;

namespace MyNewApp

{

class JustATest

{

void SomeMethod()

{

//Error! Need to import MyExtensionMethods

//namespace to extend int with Foo()!

int i = 0; i.Foo();

}

}

}

The IntelliSense of Extension Methods

Given the fact that extension methods are not literally defined on the type being extended, it is certainly possible to become confused when examining an existing code base. For example, assume you have imported a namespace that defined some number of extension methods authored by a

430CHAPTER 13 C# 2008 LANGUAGE FEATURES

teammate. As you are authoring your code, you might create a variable of the extended type, apply the dot operator, and find dozens of new methods that are not members of the original class definition!

Thankfully, Visual Studio’s IntelliSense mechanism marks all extension methods with a unique “downward arrow” icon (see Figure 13-3), which appears blue on your screen.

Figure 13-3. The IntelliSense of extension methods

Any method marked with this visual icon is a friendly reminder that the method is defined outside of the original class definition via an extension method.

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

Building and Using Extension Libraries

The previous example extended the functionality of various types (such as the System.Int32 type) for use by the current console application. However, I am sure you could imagine the usefulness of building a.NET code library that defines numerous extensions that can be referenced by multiple applications. As luck would have it, doing so is very straightforward.

To illustrate, create a new Class Library project (named MyExtensionsLibrary). Next, rename your initial C# code file to MyExtensions.cs, and copy the MyExtensions class definition in your new namespace:

namespace MyExtensionsLibrary

{

// Be sure to import System.Reflection. public static class MyExtensions

{

// Same implementation as before.

public static void DisplayDefiningAssembly(this object obj) {...}

CHAPTER 13 C# 2008 LANGUAGE FEATURES

431

// Same implementation as before.

public static int ReverseDigits(this int i) {...}

}

}

Note If you wish to export extension methods from a .NET code library, the defining type must be declared publically (recall the default access modifier for a type is internal).

At this point, you can compile your library and reference the MyExtensionsLibrary.dll assembly within new .NET projects. When you do so, the new functionality provided to System.Object and System.Int32 can be used by any application that references the library.

To test this out, add a new Console Application project (named MyExtensionsLibraryClient). Next, add a reference to the MyExtensionsLibrary.dll assembly. Within the initial code file, specify that you are using the MyExtensionsLibrary namespace, and author some simple code that invokes these new methods on a local integer:

using System;

// Import our custom namespace. using MyExtensionsLibrary;

namespace MyExtnesionsLibraryClient

{

class Program

{

static void Main(string[] args)

{

Console.WriteLine("***** Using Library with Extensions *****\n");

//This time, these extension methods

//have been defined within an external

//.NET class library.

int myInt = 987; myInt.DisplayDefiningAssembly(); Console.WriteLine("{0} is reversed to {1}",

myInt, myInt.ReverseDigits()); Console.ReadLine();

}

}

}

Microsoft recommends placing types that have extension methods in a dedicated assembly (within a dedicated namespace). The reason is simply to reduce cluttering of your programming environment. By way of example, if you were to author a core library for your company that every application was expected to make use of, and if the root namespace of that library defined 30 extension methods, the end result would be that all applications would now find these methods pop up in IntelliSense (even if they are not required).

Source Code The MyExtensionsLibrary and MyExtensionsLibraryClient projects can be found under the Chapter 13 subdirectory.