Essential C# 3.0 FOR NET FRAMEWORK 3.5 PHẦN 6 potx

87 1K 0
Essential C# 3.0 FOR NET FRAMEWORK 3.5 PHẦN 6 potx

Đ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

Chapter 10: Exception Handling 390 The code for throwing any exception is simply to prefix the exception instance with the keyword throw The type of exception used is obviously the type that best describes the circumstances surrounding the error that caused the exception For example, consider the TextNumberParser.Parse() method in Listing 10.1 Listing 10.1: Throwing an Exception public sealed class TextNumberParser { public static int Parse(string textDigit) { string[] digitTexts = { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" }; int result = Array.IndexOf( digitTexts, textDigit.ToLower()); if (result < 0) { throw new ArgumentException( "The argument did not represent a digit", "textDigit"); } return result; } } Instead of throwing System.Exception, it is more appropriate to throw ArgumentException because the type itself indicates what went wrong and includes special parameters for identifying which parameter was at fault Two similar exceptions are ArgumentNullException, and NullReferenceException ArgumentNullException should be thrown for the inappropriate passing of null arguments This is a special case of an invalid parameter exception that would more generally (when it wasn’t null) be thrown as an ArgumentException or an ArgumentOutOfRangeException NullReferenceException is generally something that only the underlying runtime will throw with an attempt to dereference a null value—to call a member on an object whose value is null Instead of causing a NullReferenceException, programmers should check parameters for null before Catching Exceptions accessing them and then throw an ArgumentNullException, which can provide more contextual information such as the parameter name Catching Exceptions Throwing a particular exception type enables the type itself to identify the problem It is not necessary, in other words, to catch the exception and use a switch statement on the exception message to determine what action to take in light of the exception Instead, C# allows for multiple catch blocks, each targeting a specific exception type, as Listing 10.2 shows Listing 10.2: Catching Different Exception Types public sealed class Program { public static void Main() { try { // throw new ApplicationException( "Arbitrary exception"); // } catch (NullReferenceException exception) { // Handle NullReferenceException } catch (ArgumentException exception) { // Handle ArgumentException } catch (ApplicationException exception) { // Handle ApplicationException } catch (SystemException exception) { // Handle SystemException } catch (Exception exception) { // Handle Exception } } } 391 392 Chapter 10: Exception Handling Listing 10.2 has five catch blocks, each handling a different type of exception When an exception occurs, the execution will jump to the catch block with the exception type that most closely matches The closeness of a match is determined by the inheritance chain For example, even though the exception thrown is of type System.Exception, this “is a” relationship occurs through inheritance because System.ApplicationException derives from System.Exception Since ApplicationException most closely matches the exception thrown, catch(ApplicationException ) will catch the exception instead of the catch(Exception ) block Catch blocks must appear in order, from most specific to most general, to avoid a compile error For example, moving the catch(Exception ) block before any of the other exceptions will result in a compile error, since all prior exceptions derive from System.Exception at some point in their inheritance chain Language Contrast: Java—Exception Specifiers C# has no equivalent for Java’s exception specifiers With exception specifiers, the Java compiler is able to verify that all possible exceptions thrown within a function (or a function’s call hierarchy) are either caught or declared as possibly rethrown The C# team considered this option and concluded that the maintenance burden that it imposed was not worth the perceived benefit Therefore, it is not necessary to maintain a list of all possible exceptions throughout a particular call stack, but neither is it possible to easily determine the possible exceptions (As it turns out, this wasn’t possible for Java either Calling virtual methods or using late binding, such as reflection, made it impossible to fully resolve at compile time what exceptions a method could possibly throw.) General Catch Block C# requires that any object that code throws must derive from System.Exception However, this requirement is not universal to all languages C/C++, for example, allows any object type to be thrown, General Catch Block including managed exceptions that don’t derive from System.Exception In C# 1.0, the result was that method calls (into other assemblies) could potentially throw exceptions that would not be caught with a catch(System.Exception) block If a different language throws a string, for example, the exception could go unhandled To avoid this, C# includes a catch block that takes no parameters The term for such a catch block is general catch block, and Listing 10.3 includes one Listing 10.3: Catching Different Exception Types public sealed class Program { public static void Main() { try { // throw new ApplicationException( "Arbitrary exception"); // } catch (NullReferenceException exception) { // Handle NullReferenceException } catch (ArgumentException exception) { // Handle ArgumentException } catch (ApplicationException exception) { // Handle ApplicationException } catch (SystemException exception) { // Handle SystemException } catch (Exception exception) { // Handle Exception } catch { // Any unhandled exception } } } 393 394 Chapter 10: Exception Handling The general catch block will catch all exceptions, regardless of whether they derive from System.Exception, assuming an earlier catch block does not catch them The disadvantage of such a block is simply that there is no exception instance to access and, therefore, no way to know the appropriate course of action It wouldn’t even be possible to recognize the unlikely case where such an exception is innocuous The best course of action is to handle the exception with some cleanup code before shutting down the application The catch block could save any volatile data, for example, before shutting down the application or rethrowing the exception The behavior in C# 2.0 varies slightly from the earlier C# behavior In C# 2.0, all exceptions, whether deriving from System.Exception or not, will propagate into C# assemblies as derived from System.Exception The result is that System.Exception catch blocks will catch all exceptions not caught by earlier blocks and a general catch block following a System.Exception catch block will never be invoked Furthermore, general catch blocks following a System.Exception catch block will result in a compiler warning ADVANCED TOPIC Empty Catch Block Internals The CIL code corresponding to an empty catch block is, in fact, a catch(object) block This means that regardless of the type thrown, the empty catch block will catch it Interestingly, it is not possible to explicitly declare a catch(object) exception block within C# code Therefore, there is no means of catching a non-System.Exception-derived exception and having an exception instance to scrutinize Ironically, unmanaged exceptions from languages such as C++ generally result in System.Runtime.InteropServices.SEHException type exceptions, which derive from the System.Exception type Therefore, not only can the unmanaged type exceptions be caught using a general catch block, but the non-System.Exception-managed types that are thrown can be caught as well—for instance, types such as string Guidelines for Exception Handling Guidelines for Exception Handling Exception handling provides much-needed structure to the error-handling mechanisms that preceded it However, it can still make for some unwieldy results if used haphazardly The following guidelines offer some best practices for exception handling • Catch only the exceptions you can handle Generally it is possible to handle some types of exceptions but not others For example, opening a file for exclusive read-write access may throw a System.IO.IOException because the file is already in use In catching this type of exception, the code can report to the user that the file is in use and allow the user the option of canceling the operation or retrying it Only exceptions for which there is a known action should be caught Other exception types should be left for callers higher in the stack • Don’t hide (bury) exceptions you don’t fully handle New programmers are often tempted to catch all exceptions and then continue executing instead of reporting an unhandled exception to the user However, this may result in a critical system problem going undetected Unless code takes explicit action to handle an exception or explicitly determines certain exceptions to be innocuous, catch blocks should rethrow exceptions instead of catching them and hiding them from the caller Predominantly, catch(System.Exception) and general catch blocks should occur higher in the call stack, unless the block ends by rethrowing the exception • Use System.Exception and general catch blocks rarely Virtually all exceptions derive from System.Exception However, the best way to handle some System.Exceptions is to allow them to go unhandled or to gracefully shut down the application sooner rather than later Exceptions such as System.OutOfMemoryException are nonrecoverable, for example, and the best course of action is to shut down the application Such catch blocks should appear only to run cleanup or emergency code (such as saving any volatile data) before 395 396 Chapter 10: Exception Handling shutting down the application or rethrowing the exception with throw; • Avoid exception reporting or logging lower in the call stack Often, programmers are tempted to log exceptions or report exceptions to the user at the soonest possible location in the call stack However, these locations are seldom able to handle the exception fully and they resort to rethrowing the exception Such catch blocks should not log the exception or report it to a user while in the bowels of the call stack If the exception is logged and rethrown, the callers higher in the call stack may the same, resulting in duplicate log entries of the exception Worse, displaying the exception to the user may not be appropriate for the type of application (Using System.Console.WriteLine() in a Windows application will never be seen by the user, for example, and displaying a dialog in an unattended command-line process may go unnoticed and freeze the application.) Logging- and exception-related user interfaces should be reserved for high up in the call stack • Use throw; rather than throw inside a catch block It is possible to rethrow an exception inside a catch block For example, the implementation of catch(ArgumentNullException exception) could include a call to throw exception However, rethrowing the exception like this will reset the stack trace to the location of the rethrown call, instead of reusing the original throw point location Therefore, unless you are rethrowing with a different exception type or intentionally hiding the original call stack, use throw; to allow the same exception to propagate up the call stack • Use caution when rethrowing different exceptions From inside a catch block, rethrowing a different exception will not only reset the throw point, it will hide the original exception To preserve the original exception, set the new exception’s InnerException property, generally assignable via the constructor Rethrowing a different exception should be reserved for situations where: a Changing the exception type clarifies the problem For example, in a call to Logon(User user), rethrowing a different exception type is perhaps more appropriate than propagating Defining Custom Exceptions System.IO.IOException when the file with the user list is inacces- sible b Private data is part of the original exception In the preceding scenario, if the file path is included in the original System.IO.IOException, thereby exposing private security information about the system, the exception should be wrapped This assumes, of course, that InnerException is not set with the original exception c The exception type is too specific for the caller to handle appropriately For example, instead of throwing an exception specific to a particular database system, a more generic exception is used so that database-specific code higher in the call stack can be avoided Defining Custom Exceptions Once throwing an exception becomes the best course of action, it is preferable to use framework exceptions because they are well established and understood Instead of throwing a custom invalid argument exception, for example, it is preferable to use the System.ArgumentException type However, if the developers using a particular API will take special action—the exception-handling logic will vary to handle a custom exception type, for instance—it is appropriate to define a custom exception For example, if a mapping API receives an address for which the ZIP Code is invalid, instead of throwing System.ArgumentException, it may be better to throw a custom InvalidAddressException The key is whether the caller is likely to write a specific InvalidAddressException catch block with special handling rather than just a generic System.ArgumentException catch block Defining a custom exception simply involves deriving from System.Exception or some other exception type Listing 10.4 provides an example Listing 10.4: Creating a Custom Exception class DatabaseException : System.Exception { public DatabaseException( System.Data.SqlClient.SQLException exception) 397 Chapter 10: Exception Handling 398 { InnerException = exception; // } public DatabaseException( System.Data.OracleClient.OracleException exception) { InnerException = exception; // } public DatabaseException() { // } public DatabaseException(string message) { // } public DatabaseException( string message, Exception innerException) { InnerException = innerException; // } } This custom exception might be created to wrap proprietary database exceptions Since Oracle and SQL Server (for example) each throw different exceptions for similar errors, an application could define a custom exception that standardizes the database-specific exceptions into a common exception wrapper that the application can handle in a standard manner That way, whether the application was using an Oracle or a SQL Server backend database, the same catch block could be used to handle the error higher up the stack The only requirement for a custom exception is that it derives from System.Exception or one of its descendents However, there are several more good practices for custom exceptions • All exceptions should use the “Exception” suffix This way, their pur- pose is easily established from the name Defining Custom Exceptions • Generally, all exceptions should include constructors that take no parameters, a string parameter, and a parameter set of a string and an inner exception Furthermore, since exceptions are usually constructed within the same statement in which they are thrown, any additional exception data should also be allowed as part of the constructor (The obvious exception to creating all these constructors is if certain data is required and a constructor circumvents the requirements.) • The inheritance chain should be kept relatively shallow (with fewer than approximately five levels) The inner exception serves an important purpose when rethrowing an exception that is different from the one that was caught For example, if a System.Data.SqlClient.SqlException is thrown by a database call but is caught within the data access layer to be rethrown as a DatabaseException, then the DatabaseException constructor that takes the SqlException (or inner exception) will save the original SqlException in the InnerException property That way, when requiring additional details about the original exception, developers can retrieve the exception from the InnerException property (e.g., exception.InnerException) ADVANCED TOPIC Serializable Exceptions Serializable objects are objects that the runtime can persist into a stream— a filestream, for example—and then reinstantiate out of the stream In the case of exceptions, this may be necessary for certain distributed communication technologies To support serialization, exception declarations should include the System.SerializableAttribute attribute or they should implement ISerializable Furthermore, they must include a constructor that takes System.Runtime.Serialization.SerializationInfo and System.Runtime.Serialization.StreamingContext Listing 10.5 shows an example of using System.SerializableAttribute Listing 10.5: Defining a Serializable Exception // Supporting serialization via an attribute [Serializable] 399 462 Chapter 12: Delegates and Lambda Expressions Listing 12.15: Omitting Parameter Types from Statement Lambdas // BubbleSort(items, (first, second) => { return first < second; } ); // In general, statement lambdas not need parameter types as long as the compiler can infer the types or can implicitly convert them to the requisite expected types In cases where inference is not possible, the data type is required, although even when it is not required, you can specify the data type explicitly to increase readability; once the statement lambda includes one type, all types are required In general, C# requires a lambda expression to have parentheses around the parameter list regardless of whether the data type is specified Even parameterless statement lambdas, representing delegates that have no input parameters, are coded using empty parentheses (see Listing 12.16) Listing 12.16: Parameterless Statement Lambdas using System; // Func getUserInput = () => { string input; { input = Console.ReadLine(); } while(input.Trim().Length==0); return input; }; // The exception to the parenthesis rule is that if the compiler can infer the data type and there is only a single input parameter, the statement lambda does not require parentheses (see Listing 12.17) Lambda Expressions Listing 12.17: Statement Lambdas with a Single Input Parameter using System.Collections.Generic; using System.Diagnostics; using System.Linq; // IEnumerable processes = Process.GetProcesses().Where( process => { return process.WorkingSet64 > 1000000000; }); // (In Listing 12.17, Where() returns a query for processes that have a physical memory utilization greater than 1GB.) Note that back on Listing 12.16, the body of the statement lambda includes multiple statements inside the statement block (via curly braces) Although there can be any number of statements in a statement lambda, typically a statement lambda uses only two or three statements in its statement block In contrast, the body of an expression lambda does not even make up a full statement since there is no statement block Expression Lambdas Unlike a statement lambda, which includes a statement block and, therefore, zero or more statements, an expression lambda has only an expression, with no statement block Listing 12.18 is the same as Listing 12.14, except that it uses an expression lambda rather than a statement lambda Listing 12.18: Passing a Delegate with a Statement Lambda class DelegateSample { // static void Main(string[] args) { int i; int[] items = new int[5]; for (i=0; i first < second; ); for (i = 0; i < items.Length; i++) { Console.WriteLine(items[i]); } } } The difference between a statement and an expression lambda is that the statement lambda has a statement block on the right side of the lambda operator, whereas the expression lambda has only an expression (no return statement or curly braces, for example) Generally, you would read a lambda operator in an expression lambda in the same way you would a statement lambda: “go/goes to.” In addition, “becomes” is sometimes clearer In cases such as the BubbleSort() call, where the expression lambda specified is a predicate (returns a Boolean), it is frequently clearer to replace the lambda operator with “such that.” This changes the pronunciation of the statement lambda in Listing 12.18 to read “first and second such that first is less than second.” One of the most common places for a predicate to appear is in the call to System.Linq.Enumerable()’s Where() function In cases such as this, neither “such that” nor “goes to” is needed We would read names.Where(name => name.Contains(" ")) as “names where names dot Contains a space,” for example One pronunciation difference between the lambda operator in statement lambdas and in expression lambdas is that “such that” terminology applies more to expression lambdas than to statements lambda since the latter tend to be more complex The anonymous function does not have any intrinsic type associated with it, although implicit conversion is possible for any delegate type as long as the parameters and return type are compatible In other words, an anonymous method is no more a ComparisonHandler type than another delegate type such as LessThanHandler As a result, you cannot use the typeof() operator (see Chapter 17) on an anonymous method, and calling GetType() is possible only after assigning or casting the anonymous method to a delegate variable Table 12.1 contains additional lambda expression characteristics TABLE 12.1: Lambda Expression Notes and Examples Statement Example Lambda expressions themselves not have type In fact, there is no concept of a lambda expression in the CLR Therefore, there are no members to call directly from a lambda expression The operator on a lambda expression will not compile, eliminating even the option of calls to object methods // ERROR: Operator '.' cannot be applied to // operand of type 'lambda expression' Type type = ((int x) => x).ToString();; Given that a lambda expression does not have an intrinsic type, it cannot appear on the right of an is operator // ERROR: The first operand of an 'is' or 'as' // operator may not be a lambda expression or // anonymous method bool boolean = ((int x) => x) is Func; Although there is no type on the lambda expression on its own, once assigned or cast, the lambda expression takes on a type Therefore, it is common for developers to informally refer to the type of the lambda expression concerning type compatibility, for example // ERROR: Lambda expression is not compatible with // Func type Func expression = ((int x) => x); A lambda expression cannot be assigned to an implicitly typed local variable since the compiler does not know what type to make the variable given that lambda expressions not have type // ERROR: Cannot assign lambda expression to an // implicitly typed local variable var thing = (x => x); Continues 465 466 TABLE 12.1: Lambda Expression Notes and Examples (Continued) Statement Example C# does not allow jump statements (break, goto, continue) inside anonymous functions if the target is outside the lambda expression Similarly, you cannot target a jump statement from outside the lambda expression (or anonymous methods) into the lambda expression // ERROR: Control cannot leave the body of an // anonymous method or lambda expression string[] args; Func expression; switch(args[0]) { case "/File": expression = () => { if (!File.Exists(args[1])) { break; } // return args[1]; }; // } Variables introduced within a lambda expression are visible only within the scope of the lambda expression body // ERROR: The name 'first' does not // exist in the current context Func expression = (first, second) => first > second; first++; TABLE 12.1: Lambda Expression Notes and Examples (Continued) Statement Example The compiler’s flow analysis is unable to detect initialization of local variables in lambda expressions int number; Func expression = text => int.TryParse(text, out number); if (expression("1")) { // ERROR: Use of unassigned local variable System.Console.Write(number); } int number; Func isFortyTwo = x => 42 == (number = x); if (isFortyTwo(42)) { // ERROR: Use of unassigned local variable System.Console.Write(number); } 467 Chapter 12: Delegates and Lambda Expressions 468 ADVANCED TOPIC Lambda Expression and Anonymous Method Internals Lambda expressions (and anonymous methods) are not an intrinsic construct within the CLR Rather, the C# compiler generates the implementation at compile time Lambda expressions provide a language construct for an inline-declared delegate pattern The C# compiler, therefore, generates the implementation code for this pattern so that the compiler automatically writes the code instead of the developer writing it manually Given the earlier listings, therefore, the C# compiler generates CIL code that is similar to the C# code shown in Listing 12.19 Listing 12.19: C# Equivalent of CIL Generated by the Compiler for Lambda Expressions class DelegateSample { // static void Main(string[] args) { int i; int[] items = new int[5]; for (i=0; i person.Name.ToUpper() == "INIGO MONTOYA"); select * from Person where upper(Name) = 'INIGO MONTOYA'; Lambda Expressions Recognizing the original Where() call parameter as data, you can see that it is made up of the following: • The call to the Person property, Name • A call to a string method called ToUpper() • A constant value, “INIGO MONTOYA” • An equality operator, == The Where() method takes this data and converts it to the SQL where clause by iterating over the data and building a SQL query string However, SQL is just one example of what an expression tree may convert to Both a lambda expression for delegates and a lambda expression for an expression tree are compiled, and in both cases, the syntax of the expression is verified at compile time with full semantic analysis The difference, however, is that a lambda expression is compiled into a delegate in CIL, whereas an expression tree is compiled into a data structure of type System.Linq.Expressions.Expression As a result, when a lambda expression is an expression lambda, it may execute—it is CIL instructions for what the runtime should However, if the lambda expression is an expression tree, it is not a set of CIL instructions, but rather a data structure Although an expression tree includes a method that will compile it into a delegate constructor call, it is more likely that the expression tree (data) will be converted into a different format or set of instructions System.Linq.Enumerable versus System.Linq.Queryable Let us consider an example that highlights the difference between a delegate and an expression tree System.Linq.Enumerable and System.Linq.Queryable are very similar They each provide virtually identical extension methods to the collection interfaces they extend (IEnumerable and IQueryable, respectively) Consider, for example, the Where() method from Listing 12.22 Given a collection that supports IEnumerable, a call to Where() could be as follows: persons.Where( person => person.Name.ToUpper() == "INIGO MONTOYA"); 473 474 Chapter 12: Delegates and Lambda Expressions Conceptually, the Enumerable extension method signature is defined on IEnumerable as follows: public IEnumerable Where( Func predicate); However, the equivalent Queryable extension on the IQueryable method call is identical, even though the conceptual Where() method signature (shown) is not: public IQueryable Where( Expression predicate); The calling code for the argument is identical because the lambda expression itself does not have type until it is assigned/cast Enumerable’s Where() implementation takes the lambda expression and converts it to a delegate that the Where() method’s implementation calls In contrast, when calling Queryable’s Where(), the lambda expression is converted to an expression tree so that the compiler converts the lambda expression into data The object implementing IQueryable receives the expression data and manipulates it As suggested before, the expression tree received by Where() may be converted into a SQL query that is passed to a database Examining an Expression Tree Capitalizing on the fact that lambda expressions don’t have intrinsic type, assigning a lambda expression to a System.Linq.Expressions.Expression creates an expression tree rather than a delegate In Listing 12.23, we create an expression tree for the Func (Recall that Func is functionally equivalent to the ComparisonHandler delegate.) Notice that just the simple act of writing an expression to the console, Console.WriteLine(expression) where expression is of type Expression, will result in a call to expression’s ToString() method) However, this doesn’t cause the expression to be evaluated or even to write out the fully qualified name of Func (as would happen if we used a delegate instance) Rather, displaying the expression writes out the data (in this case, the expression code) corresponding to the value of the expression tree Lambda Expressions Listing 12.23: Examining an Expression Tree using System; using System.Linq.Expressions; class Program { static void Main() { Expression expression; expression = (x, y) => x > y; Console.WriteLine(" -{0} -", expression); PrintNode(expression.Body, 0); Console.WriteLine(); Console.WriteLine(); expression = (x, y) => x * y > x + y; Console.WriteLine(" -{0} -", expression); PrintNode(expression.Body, 0); Console.WriteLine(); Console.WriteLine(); } public static void PrintNode(Expression expression, int indent) { if (expression is BinaryExpression) PrintNode(expression as BinaryExpression, indent); else PrintSingle(expression, indent); } private static void PrintNode(BinaryExpression expression, int indent) { PrintNode(expression.Left, indent + 1); PrintSingle(expression, indent); PrintNode(expression.Right, indent + 1); } private static void PrintSingle( Expression expression, int indent) { Console.WriteLine("{0," + indent * + "}{1}", "", NodeToString(expression)); } private static string NodeToString(Expression expression) { switch (expression.NodeType) { case ExpressionType.Multiply: return "*"; 475 Chapter 12: Delegates and Lambda Expressions 476 case ExpressionType.Add: return "+"; case ExpressionType.Divide: return "/"; case ExpressionType.Subtract: return "-"; case ExpressionType.GreaterThan: return ">"; case ExpressionType.LessThan: return "

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

Mục lục

  • Guidelines for Exception Handling

  • Introducing Generic Types

    • Using a Generic Class

    • Defining a Simple Generic Class

    • Type Parameter Naming Guidelines

    • Generic Interfaces and Structs

    • Defining a Constructor and a Finalizer

    • Specifying a Default Value

    • Type Compatibility between Generic Classes with Type-Compatible Type Parameters

    • Generic Internals

      • Instantiating Generics Based on Value Types

      • Instantiating Generics Based on Reference Types

      • 12 Delegates and Lambda Expressions

        • Introducing Delegates

          • Defining the Scenario

          • Defining a Delegate Type

          • System-Defined Delegates: Func<>

Tài liệu cùng người dùng

Tài liệu liên quan