Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 59 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
59
Dung lượng
1,33 MB
Nội dung
CHAPTER 14 ■ EXTENSION METHODS 502 changed. C#, on the other hand, offers a hybrid environment in which you are free to implement functional programming if you choose. Also, those familiar with the Standard Template Library (STL) will get a familiar feeling from this style of programming. STL swept through the C++ programming community back in the early 1990s and encouraged a more functional programming thought process. Operation Chaining Using extension methods, operation chaining becomes a more natural process. Again, it’s nothing that you could not have done in the C# 2.0 days using plain static methods and anonymous methods. However, with the streamlined syntax, chaining actually removes the clutter and can trigger some innovative thinking. Let’s start with the example from the previous section, in which we took a list of integers and transformed them into a list of doubles. This time, we’ll look at how we can actually chain operations in a fluid way. Let’s suppose that after dividing the integers by 3, we want to then compute the square of the result. The following code shows how to do that: using System; using System.Linq; using System.Collections.Generic; public static class MyExtensions { public static IEnumerable<R> Transform<T, R>( this IEnumerable<T> input, Func<T, R> op ) { foreach( var item in input ) { yield return op( item ); } } } public class TransformExample { static IEnumerable<int> CreateInfiniteList() { int n = 0; while( true ) yield return n++; } static double DivideByThree( int n ) { return (double)n / 3; } static double Square( double r ) { return r * r; } static void Main() { var divideByThree = new Func<int, double>( DivideByThree ); var squareNumber = CHAPTER 14 ■ EXENTENSION METHODS 503 new Func<double, double>( Square ); var result = CreateInfiniteList(). Transform( divideByThree ). Transform( squareNumber ); var iter = result.GetEnumerator(); for( int i = 0; i < 25; ++i ) { iter.MoveNext(); Console.WriteLine( iter.Current ); } } } Isn’t that cool? In one statement of code, I took an infinite list of integers and applied a divisor followed by a squaring operation, and the end result is a lazy-evaluated IEnumerable<double> type that computes each element as needed. Functional programming is actually pretty useful when you look at it this way. Of course, you could chain as many operations as necessary. For example, you might want to append a rounding operation at the end. Or maybe you want to append a filtering operation so that only the results that match a certain criteria are considered. To do that, you could create a generic Filter<T> extension method, similar to Transform<T>, that takes a predicate delegate as a parameter and uses it to filter the items in the collection. At this point, I’m sure that you’re thinking of all the really useful extension methods you could create to manipulate data. You might be wondering if a host of these extension methods already exists. Check out the System.Linq.Enumerable class. This class provides a whole host of extension methods that are typically used with LINQ, which I cover in Chapter 16. All these extension methods operate on types of IEnumerable<T>. Also, the System.Linq.Queryable class provides the same extension methods for types that implement IQueryable<T>, which derives from IEnumerable<T>. Custom Iterators Chapter 9 covered iterators, which were added to the language in C# 2.0. I described some ways you could create custom iterators. Extension methods offer even more flexibility to create custom iterators for collections in a very expressive way. By default, every collection that implements IEnumerable or IEnumerable<T> has a forward iterator, so a custom iterator would be necessary to walk through a collection in a different way than its default iterator. Also, you will need to create a custom iterator for types that don’t support IEnumerable<T>, as I’ll show in the next section, “Borrowing from Functional Programming.” Let’s look at how you can use extension methods to implement custom iterators on types implementing IEnumerable<T>. For example, imagine a two-dimensional matrix implemented as a List<List<int>> type. When performing some operations on such matrices, it’s common to require an iterator that walks through the matrix in row-major fashion. What that means is that the iterator walks all the items of the first row, then the second row, and so on until it reaches the end of the last row. You could iterate through the matrix in row-major form as shown here: using System; using System.Collections.Generic; public class IteratorExample { static void Main() { CHAPTER 14 ■ EXTENSION METHODS 504 var matrix = new List<List<int>> { new List<int> { 1, 2, 3 }, new List<int> { 4, 5, 6 }, new List<int> { 7, 8, 9 } }; // One way of iterating the matrix. foreach( var list in matrix ) { foreach( var item in list ) { Console.Write( "{0}, ", item ); } } Console.WriteLine(); } } Yes, this code gets the job done, but it is not very reusable. Let’s see one way this can be redone using an extension method: using System; using System.Collections.Generic; public static class CustomIterators { public static IEnumerable<T> GetRowMajorIterator<T>( this List<List<T>> matrix ) { foreach( var row in matrix ) { foreach( var item in row ) { yield return item; } } } } public class IteratorExample { static void Main() { var matrix = new List<List<int>> { new List<int> { 1, 2, 3 }, new List<int> { 4, 5, 6 }, new List<int> { 7, 8, 9 } }; // A more elegant way to enumerate the items. foreach( var item in matrix.GetRowMajorIterator() ) { Console.Write( "{0}, ", item ); } Console.WriteLine(); } } CHAPTER 14 ■ EXENTENSION METHODS 505 In this version, I have externalized the iteration into the GetRowMajorIterator<T> extension method. At the same time, I made the extension method generic so it will accept two-dimensional nested lists that contain any type, thus making it a bit more reusable. Borrowing from Functional Programming You might have already noticed that many of the new features added in C# 3.0 facilitate a functional programming model. You’ve always been able to implement functional programming models in C#, but the new language features make it easier syntactically by making the language more expressive. Sometimes, the functional model facilitates easier solutions to various problems. Various languages are categorized as functional languages, and Lisp is one of them. If you’ve ever programmed using Lisp, you know that the list is one of the core constructs in that language. In C#, we can model such a list using the following interface definition at the core: public interface IList<T> { T Head { get; } IList<T> Tail { get; } } ■ Caution Although I have named this type IList<T> for this example, be sure not to confuse it with IList<T> in the System.Collections.Generic namespace. If one were to implement this type as written, it would be best to define it within one’s own namespace to avoid name conflict. After all, that is one of the benefits of using namespaces. The structure of this list is a bit different from the average linked list implementation. Notice that instead of one node containing a value and a pointer to the next node, it instead contains the value at the node and then a reference to the rest of the list. In fact, it’s rather recursive in nature. That’s no surprise because recursive techniques are part of the functional programming model. For example, if you were to represent a list on paper by writing values within parentheses, a traditional list might look like the following: (1 2 3 4 5 6) Whereas a list defined using the IList<T> interface above could look like this: (1 (2 (3 (4 (5 (6 (null null))))))) Each set of parentheses contains two items: the value of the node and then the remainder of the list within a nested set of parentheses. So, to represent a list with just one item in it, such as just the number 1, we could represent it this way: (1 (null null)) And of course, the empty list could be represented this way: (null null) CHAPTER 14 ■ EXTENSION METHODS 506 In the following example code, I create a custom list called MyList<T> that implements IList<T>. The way it is built here is not very efficient, and I’ll have more to say about that shortly. using System; using System.Collections.Generic; public interface IList<T> { T Head { get; } IList<T> Tail { get; } } public class MyList<T> : IList<T> { public static IList<T> CreateList( IEnumerable<T> items ) { IEnumerator<T> iter = items.GetEnumerator(); return CreateList( iter ); } public static IList<T> CreateList( IEnumerator<T> iter ) { if( !iter.MoveNext() ) { return new MyList<T>( default(T), null ); } return new MyList<T>( iter.Current, CreateList(iter) ); } private MyList( T head, IList<T> tail ) { this.head = head; this.tail = tail; } public T Head { get { return head; } } public IList<T> Tail { get { return tail; } } private T head; private IList<T> tail; } public static class CustomIterators { public static IEnumerable<T> LinkListIterator<T>( this IList<T> theList ) { CHAPTER 14 ■ EXENTENSION METHODS 507 for( var list = theList; list.Tail != null; list = list.Tail ) { yield return list.Head; } } } public class IteratorExample { static void Main() { var listInts = new List<int> { 1, 2, 3, 4 }; var linkList = MyList<int>.CreateList( listInts ); foreach( var item in linkList.LinkListIterator() ) { Console.Write( "{0}, ", item ); } Console.WriteLine(); } } First, notice in Main that I am initializing an instance of MyList<int> using a List<int>. The CreateList static method recursively populates MyList<int> using these values. Once CreateList is finished, we have an instance of MyList<int> that can be visualized as follows: (1 (2 (3 (4 (null null))))) You’re probably wondering why the list is not represented using the following: (1 (2 (3 (4 null)))) You could do that; however, you will find that it is not as easy to use either when composing the list or consuming it. Speaking of consuming the list, you can imagine that there are times when you need to iterate over one of these lists. In that case, you need a custom iterator, which I have highlighted in the example. The code in Main uses this iterator to send all the list items to the console. The output is as follows: 1, 2, 3, 4, In the example, notice that the LinkListIterator<T> method creates a forward iterator by making some assumptions about how to determine whether it has reached the end of the list and how to increment the cursor during iteration. That is, it starts at the head and assumed it has finished iterating once the current node’s tail is null. What if we externalized this information? For example, what if we wanted to allow the user to parameterize what it means to iterate, such as iterate forwards, backwards, circularly, and so on? How could we do that? If the idea of delegates pops into your mind, you’re right on track. Check out the following revised version of the iterator extension method and the Main method: public static class CustomIterators { CHAPTER 14 ■ EXTENSION METHODS 508 public static IEnumerable<T> GeneralIterator<T>( this IList<T> theList, Func<IList<T>, bool> finalState, Func<IList<T>, IList<T>> incrementer ) { while( !finalState(theList) ) { yield return theList.Head; theList = incrementer( theList ); } } } public class IteratorExample { static void Main() { var listInts = new List<int> { 1, 2, 3, 4 }; var linkList = MyList<int>.CreateList( listInts ); var iterator = linkList.GeneralIterator( delegate( IList<int> list ) { return list.Tail == null; }, delegate( IList<int> list ) { return list.Tail; } ); foreach( var item in iterator ) { Console.Write( "{0}, ", item ); } Console.WriteLine(); } } Notice that the GeneralIterator<T> method accepts two more delegates, one of which is then called upon to check whether the cursor is at the end of the list, and the other to increment the cursor. In the Main method, I am passing two delegates in the form of anonymous methods. Now the GeneralIterator<T> method can be used to iterate over every other item in the list simply by modifying the delegate passed in through the incrementer parameter. ■ Note Some of you might already be familiar with lambda expressions, which were introduced in C# 3.0. Indeed, when using lambda expressions, you can clean up this code considerably by using the lambda expression syntax to replace the previous anonymous delegates. I cover lambda expressions in Chapter 15. As a final extension method example for operations on the IList<T> type, consider how we could create an extension method to reverse the list and return a new IList<T>. There are several ways one could consider doing this, and some are much more efficient than others. However, I want to show you an example that uses a form of recursion. Consider the following Reverse<T> custom method implementation: CHAPTER 14 ■ EXENTENSION METHODS 509 public static class CustomIterators { public static IList<T> Reverse<T>( this IList<T> theList ) { var reverseList = new List<T>(); Func<IList<T>, List<T>> reverseFunc = null; reverseFunc = delegate(IList<T> list) { if( list != null ) { reverseFunc( list.Tail ); if( list.Tail != null ) { reverseList.Add( list.Head ); } } return reverseList; }; return MyList<T>.CreateList( reverseFunc(theList) ); } } If you’ve never encountered this style of coding, it can surely make your brain twist inside your head. The key to the work lies in the fact that there is a delegate defined that calls itself and captures variables along the way. 3 In the preceding code, the anonymous method is assigned to the reverseFunc variable. And as you can see, the anonymous method body calls reverseFunc, or more accurately, itself! In a way, the anonymous method captures itself! The trigger to get all the work done is in the last line of the Reverse<> method. It initiates the chain of recursive calls to the anonymous method and then passes the resulting List<T> to the CreateList method, thus creating the reversed list. Those who pay close attention to efficiency are likely pointing out the inefficiency of creating a temporary List<T> instance that is then passed to CreateList in Main. After all, if the original list is huge, creating a temporary list to just throw away moments later will put pressure on the garbage collected heap, among other things. For example, if the constructor to MyList<T> is made public, you can eliminate the temporary List<T> entirely and build the new MyList<T> using a captured variable as shown here: public static class CustomIterators { public static IList<T> Reverse<T>( this IList<T> theList ) { var reverseList = new MyList<T>(default(T), null); Func<IList<T>, MyList<T>> reverseFunc = null; reverseFunc = delegate(IList<T> list) { if( list.Tail != null ) { reverseList = new MyList<T>( list.Head, reverseList ); reverseFunc( list.Tail ); } return reverseList; 3 Computer science wonks like to call a delegate that captures variables a closure, which is a construct in which a function is packaged with an environment (such as variables). CHAPTER 14 ■ EXTENSION METHODS 510 }; return reverseFunc(theList); } } The previous Reverse<T> method first creates an anonymous function and stores it in the local variable reverseFunc. It then returns the results of calling the anonymous method to the caller of Reverse<T>. All the work of building the reversed list is encapsulated into the closure created by the anonymous method and the captured local variables reverseList and reverseFunc. reverseFunc simply calls itself recursively until it is finished building the reversed list into the reverseList captured variable. Those of you who are more familiar with functional programming are probably saying that the preceding Reverse<T> extension method can be cleaned up by eliminating the captured variable and using the stack instead. In this case, it’s more of a stylistic change, but I want to show it to you for completeness’ sake. Instead of having the captured variable reverseList, as in the previous implementation of Reverse<T>, I instead pass the reference to the list I am building as an argument to each recursion of the anonymous method reverseFunc. Why would you want to do this? By eliminating the captured variable reverseList, you eliminate the possibility that the reference could be inadvertently changed outside of the scope of the anonymous method. Therefore, my final example of the Reverse<T> method uses only the stack as a temporary storage location while building the new reversed list: public static class CustomIterators { public static IList<T> Reverse<T>( this IList<T> theList ) { Func<IList<T>, IList<T>, IList<T>> reverseFunc = null; reverseFunc = delegate(IList<T> list, IList<T> result) { if( list.Tail != null ) { return reverseFunc( list.Tail, new MyList<T>(list.Head, result) ); } return result; }; return reverseFunc(theList, new MyList<T>(default(T), null)); } } ■ Note This code uses the Func<> definition, which is a generic delegate that is defined in the System namespace. Using Func<> is a shortcut you can employ to avoid having to declare delegate types all over the place. You use the Func<> type parameter list to declare what the parameter types (if any) and return type of the delegate are. If the delegate you need has no return value, you can use the Action<> generic delegate type. The MyList<T> class used in the previous examples builds the linked list from the IEnumerable<T> type entirely before the MyList<T> object can be used. I used a List<T> as the seed data, but I could have used anything that implements IEnumerable<T> to fill the contents of MyList<T>. But what if CHAPTER 14 ■ EXENTENSION METHODS 511 IEnumerable<T> were an infinite iterator similar to the one created by CreateInfiniteList in the “Operation Chaining” section of this chapter? If you fed the result of CreateInfiniteList to MyList<T>.CreateList, you would have to kill the program forcefully or wait until your memory runs out as it tries to build the MyList<T>. If you are creating a library for general use that contains a type such as MyList<T>, which builds itself given some IEnumerable<T> type, you should do your best to accommodate all scenarios that could be thrown at you. The IEnumerable<T> given to you could take a very long time to calculate each item of the enumeration. For example, it could be enumerating over a database of live data in which database access is very costly. For an example of how to create the list in a lazy fashion, in which each node is created only when needed, check out Wes Dyer’s excellent blog, specifically the entry titled “Why all the love for lists?” 4 The technique of lazy evaluation while iterating is a fundamental feature of LINQ, which I cover in Chapter 16. The Visitor Pattern The Visitor pattern, as described in the seminal pattern book Design Patterns: Elements of Reusable Object-Oriented Software by the Gang of Four, 5 allows you to define a new operation on a group of classes without changing the classes. Extension methods present a handy option for implementing the Visitor pattern. For example, consider a collection of types that might or might not be related by inheritance, and imagine that you want to add functionality to validate instances of them at some point in your application. One option, although very unattractive, is to modify the public contract of all the types, introducing a Validate method on each of them. One might even jump to the conclusion that the easiest way to do it is to introduce a new base type that derives from System.Object, implements Validate as an abstract method, and then makes all the other types derive from the new type instead of System.Object. That would be nothing more than a maintenance nightmare in the end. By now, you should agree that an extension method or a collection of extension methods will do the trick nicely. Given a collection of unrelated types, you will probably implement a host of extension methods. But the beauty is that you don’t have to change the already defined types. In fact, if they’re not your types to begin with, you cannot change them anyway. Consider the following code: using System; using ValidatorExtensions; namespace ValidatorExtensions { public static class Validators { public static void Validate( this String str ) { // Do something to validate the String instance. Console.WriteLine( "String with \"" + str + "\" Validated." ); 4 You can find Wes Dyer’s blog titled “Yet Another Language Geek” at blogs.msdn.com/wesdyer/. 5 Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides (Boston, MA: Addison-Wesley Professional, 1995), is cited in the references at the end of this book. [...]... "\n{0}\t{1}\t{2}\t{3}\n", "Count", "Fibonacci".PadRight( 24) , "1/Fibonacci".PadRight( 24) , "Fibonacci Constant".PadRight( 24) ); for( ulong i = 1; i x * y can be rewritten as the following lambda with a statement block: (x, y) => { return x * y; } In form, lambdas with statement blocks are almost identical to anonymous methods But there is one major difference between lambdas with statement blocks and lambda expressions Lambdas with statement blocks can be converted only to delegate types, whereas lambda expressions... elegant means of defining potentially very complex functions that can even be built by assembling together other functions 523 CHAPTER 15 ■ LAMBDA EXPRESSIONS ■ Note In the previous code example, you likely noticed the implications of referencing the counter variable within the lambda expression After all, counter is actually a local variable within the scope of Main, yet within the scope of WriteStream . CHAPTER 14 ■ EXTENSION METHODS 5 04 var matrix = new List<List<int>> { new List<int> { 1, 2, 3 }, new List<int> { 4, 5, 6 }, new List<int> { 7, 8, 9 }. CHAPTER 14 ■ EXTENSION METHODS 5 14 void DoValidation(); } public class SupplyCabinet : IValidator { public void DoValidation() { Console.WriteLine( " Validating SupplyCabinet". SupplyCabinet supplies = new SupplyCabinet(); Employee hrLady = new Employee(); data.Validate(); // Force generic version supplies.Validate<SupplyCabinet>(); CHAPTER 14 ■ EXENTENSION