1. Trang chủ
  2. » Ngoại Ngữ

C# in Depth what you need to master c2 and 3 phần 6 pdf

42 407 0

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

THÔNG TIN TÀI LIỆU

Thông tin cơ bản

Định dạng
Số trang 42
Dung lượng 396,79 KB

Nội dung

181Summary (without using a dedicated thread) until it had completed, calling the supplied dele- gate to handle the result. It would then call MoveNext again, and our method would continue. This time we kick off two requests in parallel, and the CCR to call another del- egate with the results of both operations when they’ve both finished. After that, Move- Next is called for a final time and we get to complete the request processing. Although it’s obviously more complicated than the synchronous version, it’s still all in one method, it will get executed in the order written, and the method itself can hold the state (in the local variables, which become state in the extra type generated by the compiler). It’s fully asynchronous, using as few threads as it can get away with. I haven’t shown any error handling, but that’s also available in a sensible fashion that forces you to think about the issue at appropriate places. It all takes a while to get your head around (at least unless you’ve seen continuation- passing style code before) but the potential benefits in terms of writing correct, scalable code are enormous—and it’s only feasible in such a neat way due to C# 2’s syntactic sugar around iterators and anonymous methods. The CCR hasn’t hit the mainstream at the time of writing, but it’s possible that it will become another normal part of the development toolkit 11 —and that other novel uses for iterator blocks will be thought up over time. As I said earlier, the point of the section is to open your mind to possible uses of the work that the compiler can do for you beyond just simple iteration. 6.5 Summary C# supports many patterns indirectly, in terms of it being feasible to implement them in C#. However, relatively few patterns are directly supported in terms of language fea- tures being specifically targeted at a particular pattern. In C# 1, the iterator pattern was directly supported from the point of view of the calling code, but not from the perspective of the collection being iterated over. Writing a correct implementation of IEnumerable was time-consuming and error-prone, without being interesting. In C# 2 the compiler does all the mundane work for you, building a state machine to cope with the “call-back” nature of iterators. It should be noted that iterator blocks have one aspect in common with the anony- mous methods we saw in chapter 5, even though the actual features are very different. In both cases, extra types may be generated, and a potentially complicated code trans- formation is applied to the original source. Compare this with C# 1 where most of the transformations for syntactic sugar ( lock , using , and foreach being the most obvious examples) were quite straightforward. We’ll see this trend toward smarter compilation continuing with almost every aspect of C# 3. As well as seeing a real-life example of the use of iterators, we’ve taken a look at how one particular library has used them in a fairly radical way that has little to do with what comes to mind when we think about iteration over a collection. It’s worth bearing in mind that different languages have also looked at this sort of problem 11 Some aspects of the CCR may also become available as part of the Parallel Extensions library described in chapter 13. 182 CHAPTER 6 Implementing iterators the easy way before—in computer science the term coroutine is applied to concepts of this nature. Different languages have historically supported them to a greater or lesser extent, with tricks being applicable to simulate them sometimes—for example, Simon Tatham has an excellent article 12 on how even C can express coroutines if you’re will- ing to bend coding standards somewhat. We’ve seen that C# 2 makes coroutines easy to write and use. Having seen some major and sometimes mind-warping language changes focused around a few key features, our next chapter is a change of pace. It describes a number of small changes that make C# 2 more pleasant to work with than its predecessor, learning from the little niggles of the past to produce a language that has fewer rough edges, more scope for dealing with awkward backward-compatibility cases, and a bet- ter story around working with generated code. Each feature is relatively straightfor- ward, but there are quite a few of them… 12 http://www.chiark.greenend.org.uk/~sgtatham/coroutines.html 183 Concluding C# 2: the final features So far we’ve looked at the four biggest new features of C# 2: generics, nullable types, delegate enhancements, and iterator blocks. Each of these addresses a fairly complex requirement, which is why we’ve gone into each of them in some depth. The remain- ing new features of C# 2 are knocking a few rough edges off C# 1. They’re little niggles that the language designers decided to correct—either areas where the language needed a bit of improvement for its own sake, or where the experience of working with code generation and native code could be made more pleasant. This chapter covers ■ Partial types ■ Static classes ■ Separate getter/setter property access ■ Namespace aliases ■ Pragma directives ■ Fixed-size buffers ■ Friend assemblies 184 CHAPTER 7 Concluding C# 2: the final features Over time, Microsoft has received a lot of feedback from the C# community (and its own developers, no doubt) about areas where C# hasn’t gleamed quite as brightly as it might. Although it’s impossible to please everyone—and in particular the value of each feature has to be weighed against the extra complexity it might bring to the lan- guage—several smaller changes made it into C# 2 along with the larger ones. None of the features in this chapter are particularly difficult, and we’ll go through them fairly quickly. Don’t underestimate how important they are, however—just because a topic can be explored in a few pages doesn’t mean it’s useless. You’re likely to use some of these features on a fairly frequent basis. Here’s a quick rundown of the features covered in this chapter, so you know what to expect: ■ Partial types—The ability to write the code for a type in multiple source files; particularly handy for types where part of the code is autogenerated and the rest is written manually. ■ Static classes—Tidying up utility classes so that the compiler can make it clearer when you’re trying to use them inappropriately. ■ Separate getter/setter property access—Finally, the ability to have a public getter and a private setter for properties! (That’s not the only combination available, but it’s the most common one.) ■ Namespace aliases—Ways out of sticky situations where type names aren’t unique. ■ Pragma directives—Compiler-specific instructions for actions such as suppressing specific warnings for a particular section of code. ■ Fixed-size buffers—More control over how structs handle arrays in unsafe code. ■ InternalsVisibleToAttribute (friend assemblies)—A feature spanning library, frame- work, and runtime, this allows selected assemblies more access when required. You may well be itching to get on to the sexy stuff from C# 3 by this point, and I don’t blame you. Nothing in this chapter is going to set the world on fire—but each of these features can make your life more pleasant, or dig you out of a hole in some cases. Hav- ing dampened your expectations somewhat, our first feature is actually pretty nifty. 7.1 Partial types The first change we’ll look at is due to the power struggle that was usually involved when using code generators with C# 1. For Windows Forms, the designer in Visual Stu- dio had to have its own regions of code that couldn’t be touched by developers, within the same file that developers had to edit for user interface functionality. This was clearly a brittle situation. In other cases, code generators create source that is compiled alongside manually written code. In C# 1, adding extra functionality involved deriving new classes from the autogenerated ones, which is ugly. There are plenty of other scenarios where having an unnecessary link in the inheritance chain can cause problems or reduce encapsulation. For instance, if two different parts of your code want to call each other, you need virtual methods for the parent type to call the child, and protected methods for the reverse sit- uation, where normally you’d just use two private nonvirtual methods. 185Partial types C# 2 allows more than one file to contribute to a type, and indeed IDEs can extend this notion so that some of the code that is used for a type may not even be visible as C# source code at all. Types built from multiple source files are called partial types. In this section we’ll also learn about partial methods, which are only relevant in par- tial types and allow a rich but efficient way of adding manually written hooks into autogenerated code. This is actually a C# 3 feature (this time based on feedback about C# 2), but it’s far more logical to discuss it when we examine partial types than to wait until the next part of the book. 7.1.1 Creating a type with multiple files Creating a partial type is a cinch—you just need to include the partial contextual keyword in the declaration for the type in each file it occurs in. A partial type can be declared within as many files as you like, although all the examples in this section use two. The compiler effectively combines all the source files together before compiling. This means that code in one file can call code in another and vice versa, as shown in figure 7.1—there’s no need for “forward references” or other tricks. You can’t write half of a member in one file and half of it in another—each individ- ual member has to be complete within its own file. 1 There are a few obvious restric- tions about the declarations of the type—the declarations have to be compatible. Any file can specify interfaces to be implemented (and they don’t have to be implemented in that file); any file can specify the base type; any file can specify a type parameter constraint. However, if multiple files specify a base type, those base types have to be the same, and if multiple files specify type parameter constraints, the constraints have to be identical. Listing 7.1 gives an example of the flexibility afforded (while not doing anything even remotely useful). 1 There’s an exception here: partial types can contain nested partial types spread across the same set of files. partial class Example { void FirstMethod() { SecondMethod(); } void ThirdMethod() { } } partial class Example { void SecondMethod() { ThirdMethod(); } } Example1.cs Example2.cs Figure 7.1 Code in partial types is able to “see” all of the members of the type, regardless of which file each member is in. 186 CHAPTER 7 Concluding C# 2: the final features // Example1.cs using System; partial class Example<TFirst,TSecond> : IEquatable<string> where TFirst : class { public bool Equals(string other)) { return false; } } // Example2.cs using System; partial class Example<TFirst,TSecond> : EventArgs, IDisposable { public void Dispose() { } } I stress that this listing is solely for the purpose of talking about what’s legal in a decla- ration—the types involved were only picked for convenience and familiarity. We can see that both declarations ( B and D ) contribute to the list of interfaces that must be implemented. In this example, each file implements the interfaces it declares, and that’s a common scenario, but it would be legal to move the implementation of IDisposable E to Example1.cs and the implementation of IEquatable<string> C to Example2.cs. Only B specified any type constraints, and only C specified a base class. If B specified a base class, it would have to be EventArgs , and if D specified any type constraints they’d have to be exactly as in B . In particular, we couldn’t spec- ify a type constraint for TSecond in D even though it’s not mentioned in B . Both types have to have the same access modifier, if any—we couldn’t make one declara- tion internal and the other public , for example. In “single file” types, initialization of member and static variables is guaranteed to occur in the order they appear in the file, but there’s no guaranteed order when mul- tiple files are involved. Relying on the order of declaration within the file is brittle to start with—it leaves your code wide open to subtle bugs if a developer decides to “harmlessly” move things around. So, it’s worth avoiding this situation where you can anyway, but particularly avoid it with partial types. Now that we know what we can and can’t do, let’s take a closer look at why we’d want to do it. 7.1.2 Uses of partial types As I mentioned earlier, partial types are primarily useful in conjunction with designers and other code generators. If a code generator has to modify a file that is “owned” by a developer, there’s always a risk of things going wrong. With the partial types model, a Listing 7.1 Demonstration of mixing declarations of a partial type Specifies interface and type parameter constraint B C Implements IEquatable<string> Specifies base class and interface D E Implements IDisposable 187Partial types code generator can own the file where it will work, and completely overwrite the whole file every time it wants to. Some code generators may even choose not to generate a C# file at all until the build is well under way. For instance, the Windows Presentation Foundation version of the Snippy application has Extensible Application Markup Language ( XAML) files that describe the user interface. When the project is built, each XAML file is converted into a C# file in the obj directory (the filenames end with “.g.cs” to show they’ve been generated) and compiled along with the partial class providing extra code for that type (typically event handlers and extra construction code). This completely prevents developers from tweaking the generated code, at least without going to extreme lengths of hacking the build file. I’ve been careful to use the phrase code generator instead of just designer because there are plenty of code generators around besides designers. For instance, in Visual Studio 2005 web service proxies are generated as partial classes, and you may well have your own tools that generate code based on other data sources. One reasonably common example of this is Object Relational Mapping ( ORM)—some ORM tools use database entity descriptions from a configuration file (or straight from the database) and generate partial classes representing those entities. This makes it very straightforward to add behavior to the type: overriding virtual methods of the base class, adding new members with business logic, and so forth. It’s a great way of letting the developer and the tool work together, rather than constantly squabbling about who’s in charge. One scenario that is occasionally useful is for one file to be generated containing multiple partial types, and then some of those types are enhanced in other files, one manually generated file per type. To return to the ORM example, the tool could gen- erate a single file containing all the entity definitions, and some of those entities could have extra code provided by the developer, using one file per entity. This keeps the number of automatically generated files low, but still provides good visibility of the manual code involved. Figure 7.2 shows how the uses of partial types for XAML and entities are similar, but with slightly different timing involved when it comes to creating the autogenerated C# code. A somewhat different use of partial types is as an aid to refactoring. Sometimes a type gets too big and assumes too many responsibilities. One first step to dividing the bloated type into smaller, more coherent ones can be to first split it into a partial type over two or more files. This can be done with no risk and in an experimental manner, moving methods between files until each file only addresses a particular concern. Although the next step of splitting the type up is still far from automatic at that stage, it should be a lot easier to see the end goal. When partial types first appeared in C# 2, no one knew exactly how they’d be used. One feature that was almost immediately requested was a way to provide optional “extra” code for generated methods to call. This need has been addressed by C# 3 with partial methods. 188 CHAPTER 7 Concluding C# 2: the final features 7.1.3 Partial methods—C# 3 only! Just to reiterate my previous explanation, I realize that the rest of this part of the book has just been dealing with C# 2 features—but partial methods don’t fit with any of the other C# 3 features and they do fit in very well when describing partial types. Apologies for any confusion this may cause. Back to the feature: sometimes we want to be able to specify behavior in a manually created file and use that behavior from an automatically generated file. For instance, in a class that has lots of automatically generated properties, we might want to be able to specify code to be executed as validation of a new value for some of those proper- ties. Another common scenario is for a code-generation tool to include construc- tors—manually written code often wants to hook into object construction to set default values, perform some logging, and so forth. In C# 2, these requirements could only be met either by using events that the man- ually generated code could subscribe to, or by making the automatically generated code assume that the handwritten code will include methods of a particular name— making the whole code fail to compile unless the relevant methods are provided. Alternatively, the generated code can provide a base class with virtual methods that do nothing by default. The manually generated code can then derive from the class and override some or all of the methods. All of these solutions are somewhat messy. C# 3’s partial methods effectively pro- vide optional hooks that have no cost whatsoever if they’re not implemented—any calls to the unimplemented partial methods are removed by the compiler. It’s easiest to understand this with an example. Listing 7.2 shows a partial type specified in two files, with the constructor in the automatically generated code calling two partial methods, one of which is implemented in the manually generated code. GuiPage.xaml.cs (Handwritten C#) GuiPage.xaml (XAML) GuiPage.g.cs (C#) GuiPage type (Part of an assembly) XAML to C# converter (Build time) Customer.cs (Handwritten C#) Schema/model (Database, XML, etc) GeneratedEntities.cs (C# - includes partial Customer class) Customer type (Part of an assembly) Code generator (Prebuild) Using XAML for declarative UI design Prebuilding partial classes for database entities C# compilation C# compilation Figure 7.2 Comparison between XAML precompilation and autogenerated entity classes 189Partial types // Generated.cs using System; partial class PartialMethodDemo { public PartialMethodDemo() { OnConstructorStart(); Console.WriteLine("Generated constructor"); OnConstructorEnd(); } partial void OnConstructorStart(); partial void OnConstructorEnd(); } // Handwritten.cs using System; partial class PartialMethodDemo { partial void OnConstructorEnd() { Console.WriteLine("Manual code"); } } As shown in listing 7.2, partial methods are declared just like abstract methods: by pro- viding the signature without any implementation but using the partial modifier. Similarly, the actual implementations just have the partial modifier but are other- wise like normal methods. Calling the parameterless constructor of PartialMethodDemo would result in “Gen- erated constructor” and then “Manual code” being printed out. Examining the IL for the constructor, you wouldn’t see a call to OnConstructorStart because it no longer exists—there’s no trace of it anywhere in the compiled type. Because the method may not exist, partial methods must have a return type of void and can’t take out parameters. They have to be private, but they can be static and/or generic. If the method isn’t implemented in one of the files, the whole state- ment calling it is removed, including any argument evaluations. If evaluating any of the arguments has a side effect that you want to occur whether or not the partial method is implemented, you should perform the evaluation separately. For instance, suppose you have the following code: LogEntity(LoadAndCache(id)); Here LogEntity is a partial method, and LoadAndCache loads an entity from the data- base and inserts it into the cache. You might want to use this instead: MyEntity entity = LoadAndCache(id); LogEntity(entity); That way, the entity is loaded and cached regardless of whether an implementation has been provided for LogEntity . Of course, if the entity can be loaded equally Listing 7.2 A partial method called from a constructor 190 CHAPTER 7 Concluding C# 2: the final features cheaply later on, and may not even be required, you should leave the statement in the first form and avoid an unnecessary load in some cases. To be honest, unless you’re writing your own code generators, you’re more likely to be implementing partial methods than declaring and calling them. If you’re only imple- menting them, you don’t need to worry about the argument evaluation side of things. In summary, partial methods in C# 3 allow generated code to interact with handwrit- ten code in a rich manner without any performance penalties for situations where the interaction is unnecessary. This is a natural continuation of the C# 2 partial types fea- ture, which enables a much more productive relationship between code-generation tools and developers. Our next feature is entirely different, and is just a way of telling the compiler more about the intended nature of a type so that it can perform more checking on both the type itself and any code using it. 7.2 Static classes Our second new feature is in some ways completely unnecessary—it just makes things tidier and a bit more elegant when you write utility classes. Everyone has utility classes. I haven’t seen a significant project in either Java or C# that didn’t have at least one class consisting solely of static methods. The classic exam- ple appearing in developer code is a type with string helper methods, doing anything from escaping, reversing, smart replacing—you name it. An example from the Frame- work is the System.Math class. The key features of a utility class are as follows: ■ All members are static (except a private constructor). ■ The class derives directly from object . ■ Typically there’s no state at all, unless there’s some caching or a singleton involved. ■ There are no visible constructors. ■ The class is sealed if the developer remembers to do so. The last two points are optional, and indeed if there are no visible constructors (including protected ones) then the class is effectively sealed anyway. Both of them help make the purpose of the class more obvious, however. Listing 7.3 gives an example of a C# 1 utility class—then we’ll see how C# 2 improves matters. using System; public sealed class StringHelper { private StringHelper() { } Listing 7.3 A typical C# 1 utility class Seals class to prevent derivation B Prevents instantiation from other code [...]... 3. 5.21022.8 Copyright (c) Microsoft Corporation All rights reserved Public key is 002400000480000094000000 060 200000024000052 534 131 0004000001 000100a5 137 2c81ccfb8fba9c5fb84180c4129e50f0facdce 932 cf31fe 563 d0fe3cb6b1d5129e2 832 60 60a3a 539 f287aaf59affc5aabc4d8f981 e1a82479ab795f410eab22e3 266 033 c 63 3 400 463 ee75 133 78bb4ef41fc 0cae5fb 039 86d 133 67 7c82a 865 b278c48d99dc251201b9c43edd7bedef d4b 530 6efd0dec7787ec6b 664 47 1c2. .. d4b 530 6efd0dec7787ec6b 664 47 1c2 Public key token is 64 7b9 933 0b7f792c The source code for the Source class would now need to have this as the attribute: [assembly:InternalsVisibleTo("FriendAssembly,PublicKey="+ "002400000480000094000000 060 200000024000052 534 131 0004000001"+ "000100a5 137 2c81ccfb8fba9c5fb84180c4129e50f0facdce 932 cf31fe"+ " 563 d0fe3cb6b1d5129e2 832 60 60a3a 539 f287aaf59affc5aabc4d8f981"+ "e1a82479ab795f410eab22e3 266 033 c 63 3 400 463 ee75 133 78bb4ef41fc"+... same information (plus the project it’s in) except that you don’t get the warning number (CS0 169 ) To find out the number, you need to either select the warning and bring up the help related to it, or look in the Output window, where the full text is shown We need the number in order to make the code compile without warnings, as shown in listing 7.10 Listing 7.10 Disabling (and restoring) warning CS0 169 ... #pragma warning disable 0 169 int x; #pragma warning restore 0 169 } Listing 7.10 is self-explanatory—the first pragma disables the particular warning we’re interested in, and the second one restores it It’s good practice to disable warnings for as short a space as you can, so that you don’t miss any warnings you genuinely ought to fix If you want to disable or restore multiple warnings in a single line, just... "e1a82479ab795f410eab22e3 266 033 c 63 3 400 463 ee75 133 78bb4ef41fc"+ "0cae5fb 039 86d 133 67 7c82a 865 b278c48d99dc251201b9c43edd7bedef"+ "d4b 530 6efd0dec7787ec6b 664 47 1c2" )] Unfortunately, you have to either have the public key on one line or use string concatenation—whitespace in the public key will cause a compilation failure It would be a lot more pleasant to look at if we really could specify the token instead of the whole... just from looking at the call Integer literals are another example where guessing the inferred type is harder than one might suppose How quickly can you work out the type of each of the variables declared here? var var var var var var a b c d e f = = = = = = 21474 8 36 47; 21474 8 36 48; 4294 967 295; 4294 967 2 96; 92 233 72 0 36 854775807; 92 233 72 0 36 854775808; The answers are int, uint, uint, long, long, and ulong,... that have been introduced Hopefully this will become apparent in the remainder of the book, but you re more likely to feel it gradually as you begin to see LINQ improving your own code I don’t wish to sound like a mindless and noncritical C# devotee, but I feel there’s something special in C# 3 With that brief burst of abstract admiration out of the way, let’s start looking at C# 3 in a more concrete... factory method of some sort And yet in C# 2 very few language features are geared toward making life easier when it comes to initialization If you can’t do what you want using constructor arguments, you re basically out of luck you need to create the object, then manually initialize it with property calls and the like This is particularly annoying when you want to create a whole bunch of objects in. .. another one that you may well never use—but at the same time, if you ever do, it’s likely to make your life somewhat simpler 7 .6 Fixed-size buffers in unsafe code When calling into native code with P/Invoke, it’s not particularly unusual to find yourself dealing with a structure that is defined to have a buffer of a particular length within it Prior to C# 2, such structures were difficult to handle directly,... [DllImport("kernel32.dll")] static extern IntPtr GetStdHandle(int nStdHandle); [DllImport("kernel32.dll")] static extern bool GetConsoleScreenBufferInfoEx (IntPtr handle, ref CONSOLE_SCREEN_BUFFER_INFOEX info); unsafe static void Main() { IntPtr handle = GetStdHandle(StdOutputHandle); CONSOLE_SCREEN_BUFFER_INFOEX info; info = new CONSOLE_SCREEN_BUFFER_INFOEX(); info.StructureSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX); . but C# 2 makes it explicit and actively prevents the type from being misused. First we’ll see what changes are needed to turn listing 7 .3 into a “proper” static class as defined in C# 2. As you. elsewhere. C# 2 introduces the :: syntax to do this, as shown in listing 7 .6. Listing 7.5 Using aliases to distinguish between different Button types 195Namespace aliases using System; using WinForms. them. If you re only imple- menting them, you don’t need to worry about the argument evaluation side of things. In summary, partial methods in C# 3 allow generated code to interact with handwrit- ten

Ngày đăng: 12/08/2014, 12:20

TỪ KHÓA LIÊN QUAN

w