C# 2012 说明指南(八)
二十二、异常
- 什么是异常?
- try 语句
- 异常类
- catch 子句
- 使用特定 catch 子句的示例
- 捕获条款部分
- 最后一块
- 寻找异常的处理程序
- 进一步搜索
- 抛出异常
- 抛出无异常对象
有哪些异常?
异常是程序中违反系统或应用约束的运行时错误,或者是正常运行期间不希望发生的情况。例如,当程序试图将一个数除以零或试图写入只读文件时。当这些发生时,系统捕捉到错误,引发异常。
如果程序没有提供处理异常的代码,系统将暂停程序。例如,下面的代码在试图除以零时会引发异常:
static void Main() { int x = 10, y = 0; x /= y; // Attempt to divide by zero--raises an exception }
运行此代码时,系统显示以下错误消息:
Unhandled Exception: System.DivideByZeroException: Attempted to divide by zero. at Exceptions_1.Program.Main() in C:\Progs\Exceptions\Program.cs:line 12
try 语句
try语句允许您指定代码块来防止异常,并在异常发生时提供代码来处理它们。try语句由三部分组成,如图图 22-1 所示。
tryBlock contains the code protected for exceptions.- The
catchclause part contains one or morecatchclauses. These are the code blocks that handle exceptions. They are also called exception handlers.finallyBlock contains the code to be executed under all circumstances, regardless of whether there is an exception.
图 22-1 。try 语句的结构
处理异常
前面的例子表明,试图除以零会导致异常。您可以通过将代码放在一个try块中并提供一个简单的catch子句来修改程序以处理该异常。当异常出现时,它在catch块中被捕获和处理。
` static void Main() { int x = 10;
try { int y = 0; x /= y; // Raises an exception } catch { ... // Code to handle the exception
Console.WriteLine("Handling all exceptions - Keep on Running"); } }`
该代码产生以下消息。请注意,除了输出消息之外,没有任何迹象表明发生了异常。
Handling all exceptions - Keep on Running
异常类
程序中可能出现许多不同类型的异常。BCL 定义了许多异常类,每个类代表一种特定的类型。发生这种情况时,CLR 会执行以下操作:
- It creates an exception object for the type.
- It looks for a suitable
catchclause to deal with it.
所有的异常类最终都是从System.Exception类派生出来的。图 22-2 显示了异常继承层次的一部分。
图 22-2 。异常层次结构
异常对象包含只读属性,其中包含导致异常的信息。表 22-1 显示了其中的一些特性。
catch 子句
catch子句处理异常。有三种形式,允许不同级别的处理。图 22-3 显示了这些表格。
图 22-3 。catch 子句的三种形式
general catch子句可以接受任何异常,但不能确定导致异常的异常类型。这仅允许对任何可能发生的异常进行一般的处理和清理。
特定的 catch子句形式将异常类的名称作为参数。它匹配指定类的异常或从它派生的异常类。
带有宾语形式的特定 catch 子句提供了关于异常的最多信息。它匹配指定类的异常,或从它派生的异常类。它通过将 CLR 创建的异常对象分配给异常变量*,为您提供了对该异常对象的引用。您可以在catch子句的块中访问异常变量的属性,以获得关于所引发的异常的特定信息。*
例如,下面的代码处理类型IndexOutOfRangeException的异常。当一个异常发生时,对实际异常对象的引用被传递到带有参数名e的代码中。三个WriteLine语句都从异常对象中读取一个字符串字段。
Exception type Exception variable ↓ ↓ catch ( IndexOutOfRangeException e ) { Accessing the exception variables <ins> ↓ </ins> Console.WriteLine( "Message: {0}", e.Message ); Console.WriteLine( "Source: {0}", e.Source ); Console.WriteLine( "Stack: {0}", e.StackTrace );
使用特定 catch 子句的例子
回到我们被零除的例子,下面的代码修改了前面的catch子句,专门处理DivideByZeroException类的异常。在前一个例子中,catch子句将处理在try块中引发的任何异常,而当前的例子将只处理那些DivideByZeroException类的异常。
int x = 10; try { int y = 0; x /= y; // Raises an exception } Exception type ↓ catch ( DivideByZeroException ) { ... Console.WriteLine("Handling an exception."); }
您可以进一步修改catch子句来使用一个异常变量。这允许您访问catch块中的异常对象。
int x = 10; try { int y = 0; x /= y; // Raises an exception } Exception type Exception variable ↓ ↓ catch ( DivideByZeroException e ) { Accessing the exception variables <ins> ↓ </ins> Console.WriteLine("Message: {0}", e.Message ); Console.WriteLine("Source: {0}", e.Source ); Console.WriteLine("Stack: {0}", e.StackTrace ); }
在我的计算机上,该代码产生以下输出。在您的计算机上,第三行和第四行中的文件路径会有所不同,并且会与您的项目和解决方案目录的位置相匹配。
Message: Attempted to divide by zero. Source: Exceptions 1 Stack: at Exceptions_1.Program.Main() in C:\Progs\Exceptions 1\ Exceptions 1\Program.cs:line 14
军规条款部分
一个catch子句的目的是允许你以一种优雅的方式处理一个异常。如果您的catch子句采用带参数的形式,那么系统已经将异常变量设置为对异常对象的引用,您可以通过检查来确定异常的原因。如果异常是前一个异常的结果,您可以从变量的InnerException属性中获取对前一个异常的对象的引用。
catch子句部分可以包含多个catch子句。图 22-4 显示了catch条款部分的概要。
图 22-4 。try 语句的 catch 子句部分的结构
当出现异常时,系统按顺序搜索catch子句列表,执行第一个与异常对象类型匹配的catch子句。正因为如此,在安排catch条款的顺序时有两条重要的规则。它们是:
- Specific
catchclauses must be sorted from the most specific exception type, followed by the most general exception type. For example, if you declare an exception class derived fromNullReferenceException, then thecatchclause of your derived exception type should be listed before thecatchclause ofNullReferenceException.- If there is a general
catchclause, it must be the last, after all, the specificcatchclause. The generalcatchclause is not encouraged, because when your code should handle the error in a specific way, it will allow the program to continue to execute, thus hiding the error. It also leaves the program in an unknown state. Therefore, if possible, you should use one of the specificcatchclauses.
终于封锁了
如果一个程序的控制流进入一个有finally块的try语句,那么finally块总是被执行。图 22-5 显示了控制流程。
- If there is no exception in
tryblock, then at the end oftryblock, control skips anycatchclause and goes tofinallyblock.- If there is an exception in the
tryblock, execute the correspondingcatchclause in thecatchclause section, and then execute thefinallyblock.
图 22-5 。最终块的执行
在返回到调用代码之前,finally块总是会被执行,即使try块有一个return语句或者在catch块中抛出一个异常。例如,在下面的代码中,在特定条件下执行的try块中间有一个return语句。这不允许它绕过finally语句。
try { if (inVal < 10) { Console.Write("First Branch - "); return; } else Console.Write("Second Branch - "); } finally { Console.WriteLine("In finally statement"); }
当变量inVal的值为5时,该代码产生以下输出:
First Branch - In finally statement
寻找异常的处理程序
当程序引发异常时,系统会检查程序是否为它提供了处理程序。图 22-6 显示了控制流程。
- If an exception occurs in the
tryblock, the system will check whether there are anycatchclauses that can handle the exception.- If a suitable
catchclause is found, the following happens:
- The
catchclause is executed.- If there is a
finallyblock, execute.- Continue execution after
trystatement (that is, afterfinallyblock or after the lastcatchclause if there is nofinallyblock).
图 22-6 。当前 try 语句中的处理程序出现异常
进一步搜索
如果异常是在不受try语句保护的代码段中引发的,或者如果try语句没有匹配的异常处理程序,系统将不得不进一步寻找匹配的处理程序。它将通过在调用栈中依次搜索,来查看是否有一个带有匹配处理程序的封闭的try块。
图 22-7 说明了搜索过程。图的左边是代码的调用结构,右边是调用栈。图中显示Method2是从Method1的try块内部调用的。如果在Method2中的try块内发生异常,系统执行以下操作:
- First, check whether
Method2has an exception handler that can handle exceptions.
- If yes,
Method2processing, the program continues to execute.- If not, the system continues to call the stack down to
Method1to search for a suitable handler.- If
Method1has an appropriatecatchclause, the system will do the following:
- It will return to the top of the call stack-that is,
Method2.- It executes
finallyblock ofMethod2and popsMethod2off the stack.- Execute the
catchclause ofMethod1and itsfinallyblock.- If
Method1does not have a suitablecatchclause, the system continues to search the call stack downwards.
图 22-7 。向下搜索调用栈
通用算法
图 22-8 显示了处理异常的一般算法。
图 22-8 。处理异常的一般算法
向下搜索调用堆栈的示例
在下面的代码中,Main开始执行并调用方法A,方法A调用方法B。代码后和图 22-9 中的给出了过程的描述和图表。
` class Program { static void Main() { MyClass MCls = new MyClass(); try { MCls.A(); } catch (DivideByZeroException e) { Console.WriteLine("catch clause in Main()"); } finally { Console.WriteLine("finally clause in Main()"); } Console.WriteLine("After try statement in Main."); Console.WriteLine(" -- Keep running."); } }
class MyClass { public void A() { try { B(); } catch (System.NullReferenceException) { Console.WriteLine("catch clause in A()"); } finally { Console.WriteLine("finally clause in A()"); } }
void B() { int x = 10, y = 0; try { x /= y; } catch (System.IndexOutOfRangeException) { Console.WriteLine("catch clause in B()"); } finally { Console.WriteLine("finally clause in B()"); } } }`
这段代码产生以下输出:
finally clause in B() finally clause in A() catch clause in Main() finally clause in Main() After try statement in Main. -- Keep running.
MainCallA, T1 callsB, andDivideByZeroExceptionexception is encountered.- Check whether the
catchpart of the systemBhas a matchingcatchclause. Although it has one forIndexOutOfRangeException, it has no one forDivideByZeroException.- Then, the system moves down the call stack, checks the
catchpart ofA, and finds thatAalso has no matchingcatchclause.- The system continues to call down the stack, checks the
catchclause ofMain, and finds thatMaindoes have aDivideByZeroExceptioncatchclause.- Although the matching
catchclause is now located, it has not been executed. Instead, the system returns to the top of the stack, executes the T2 clause of T1, and pops T3 from the call stack.- Then the system moves to
A, executes itsfinallyclause, and pops upAfrom the call stack.- Finally, execute the matching
catchclause ofMain, and then execute itsfinallyclause. Then continue to execute after thetrystatement ofMainends.
图 22-9 。在堆栈中搜索异常处理程序
抛出异常
你可以通过使用throw语句让你的代码显式引发一个异常。throw语句的语法如下:
throw ExceptionObject;
例如,下面的代码定义了一个名为PrintArg的方法,它接受一个string参数并将其打印出来。在try块中,它首先检查以确保参数不是null。如果是,它创建一个ArgumentNullException实例并抛出它。异常实例在catch语句中被捕获,并输出错误消息。Main调用该方法两次:一次使用null参数,另一次使用有效参数。
class MyClass { public static void PrintArg(string arg) { try { if (arg == null) Supply name of null argument. { ↓ ArgumentNullException myEx = new ArgumentNullException("arg"); throw myEx; } Console.WriteLine(arg); } catch (ArgumentNullException e) { Console.WriteLine("Message: {0}", e.Message); } } } class Program { static void Main() { string s = null; MyClass.PrintArg(s); MyClass.PrintArg("Hi there!"); } }
该代码产生以下输出:
Message: Value cannot be null. Parameter name: arg Hi there!
投掷无异常物体
在catch块中,也可以在没有异常对象的情况下使用throw语句。
- The form throws the current exception again, and the system continues to look for additional handlers for it.
- This form can only be used in
catchstatements.
例如,以下代码从第一个catch子句中重新抛出异常:
` class MyClass { public static void PrintArg(string arg) { try { try { if (arg == null) Supply name of null argument. { ↓ ArgumentNullException myEx = new ArgumentNullException("arg"); throw myEx; } Console.WriteLine(arg); } catch (ArgumentNullException e) { Console.WriteLine("Inner Catch: {0}", e.Message); throw; } ↑ } Rethrow the exception, with no additional parameters. catch { Console.WriteLine("Outer Catch: Handling an Exception."); } } }
class Program { static void Main() { string s = null; MyClass.PrintArg(s); } }`
这段代码产生以下输出:
Inner Catch: Value cannot be null. Parameter name: arg Outer Catch: Handling an Exception.
二十三、预处理器指令
- 什么是预处理器指令?
- 一般规则
-
define 和#undef 指令
- 条件编译
- 条件编译构造
- 诊断指令
- 行号指令
- 地区指令
-
pragma 警告指令
什么是预处理器指令?
源代码指定了程序的定义。预处理器指令指示编译器如何处理源代码。例如,在某些情况下,您可能希望编译器忽略部分代码,而在其他情况下,您可能希望编译该代码。预处理器指令为您提供了这些选项和其他几个选项。
在 C 和 C++中,有一个实际的预处理器阶段,在这个阶段,预处理器检查源代码,并准备一个输出文本流,供随后的编译阶段处理。在 C# 中,没有真正的预处理器。“预处理器”指令由编译器处理。然而,这个术语仍然存在。
一般规则
预处理器指令的一些最重要的语法规则如下:
- The preprocessor instruction must be on a separate line from the C# code.
- Unlike C# statements, preprocessor instructions do not end with semicolons.
- Each line containing preprocessing instructions must start with the
#character.
#There can be a space before the character.- There can be a space between the
#character and the instruction.- End of line comments are allowed.
- Delimited comments are allowed in the preprocessor command line by instead of .
以下代码说明了这些规则:
` No semicolon ↓ #define PremiumVersion // OK
Space before ↓ #define BudgetVersion // OK # define MediumVersion // OK ↑ Space between Delimited comments are not allowed. ↓ #define PremiumVersion /* all bells & whistles */ End-of-line comments are fine. ↓ #define BudgetVersion // Stripped-down version`
表 23-1 列出了预处理器指令。
# define 和#undef 指令
编译符号是只有两种可能状态的标识符。要么是定义的,要么是未定义的。编译符号具有以下特征:**
*> * Can be any identifier other than true or false. This includes the C# keyword and identifier declared in the C# code-both can be used.
- It has no value. Unlike C and C++, it does not represent strings.
如表 23-1 所示:
#define
#undef
#define PremiumVersion #define EconomyVersion ... #undef PremiumVersion
在列出任何 C# 代码之前,#define和#undef指令只能用在源文件的顶部。C# 代码启动后,#define和#undef指令就不能再使用了。
` using System; // First line of C# code #define PremiumVersion // Error
namespace Eagle { #define PremiumVersion // Error ...`
编译符号的范围仅限于单个源文件。重新定义一个已经定义的符号是完全可以的——当然,只要是在任何 C# 代码之前。
` #define AValue #define BValue
#define AValue // Redefinition is fine.`
有条件编译
条件编译允许您根据是否定义了特定的编译符号,将源代码的一部分标记为编译或跳过。
有四个指令用于指定条件编译:
#if#else#elif#endif
一个条件是一个返回true或false的简单表达式。
- Conditions can be composed of a single compiled symbol or expressions of symbols and operators, as shown in Table 23-2 of . Subexpressions can be grouped by brackets.
- The words
trueandfalsecan also be used in conditional expressions.
以下是条件编译条件的示例:
` Expression ↓ #if !DemoVersion ... #endif Expression ↓ #if (LeftHanded && OemVersion) || FullVersion ... #endif
#if true // The following code segment will always be compiled. ... #endif`
条件编译构造
#if和#endif指令是条件编译结构的匹配分界。只要有一个#if指令,就必须有一个与之匹配的#endif。
图 23-1 说明了#if和#if...#else构造。
- If the conditional calculation result in the
#ifconstruction istrue, then compile the following code segment. Otherwise, it will be skipped.- In the
#if...#elseconstruction, if the condition evaluates totrue, then code segment 1 is compiled. Otherwise, compile code segment 2 .
图 23-1 。#if 和#else 结构
例如,下面的代码演示了一个简单的#if...#else构造。如果定义了符号RightHanded,则编译#if和#else之间的代码。否则,编译#else和#endif之间的代码。
... #if RightHanded // Code implementing right-handed functionality ... #else // Code implementing left-handed functionality ... #endif
图 23-2 说明了#if...#elif和#if...#elif...#else构造。
#if...#elif构造中的
- :
- If COND1 evaluates to
true, Codesection1 is compiled, and compilation continues after#endif.- Otherwise, if cond2 evaluates to
true, codesection2 is compiled, and compilation continues after#endif.- This continues until one condition evaluates to
trueor all conditions return tofalse. If this is the case, no code part in the construction will be compiled, and compilation will continue after#endif.- The
#if...#elif...#elseconstruction works in the same way, except that if there is no condition oftrue, the code segment after#elsewill be compiled, and the compilation will continue after#endif.
***图 23-2。*如果...#elif 构造(左)和#if...#elif...#else 构造(右)
下面的代码演示了#if...#elif...#else构造。根据定义的编译符号,包含程序版本描述的字符串被设置为不同的值。
` #define DemoVersionWithoutTimeLimit ... const int intExpireLength = 30; string strVersionDesc = null; int intExpireCount = 0;
#if DemoVersionWithTimeLimit intExpireCount = intExpireLength; strVersionDesc = "This version of Supergame Plus will expire in 30 days";
#elif DemoVersionWithoutTimeLimit strVersionDesc = "Demo Version of Supergame Plus";
#elif OEMVersion strVersionDesc = "Supergame Plus, distributed under license";
#else strVersionDesc = "The original Supergame Plus!!";
#endif
Console.WriteLine( strVersionDesc ); ...`
诊断指令
诊断指令产生用户定义的编译时警告和错误信息。
以下是诊断指令的语法。消息是字符串,但是请注意,与普通的 C# 字符串不同,它们不必用引号括起来。
` #warning Message
#error Message`
当编译器到达一个诊断指令时,它写出相关的消息。编译器会列出诊断指令消息以及编译器生成的任何警告和错误消息。
例如,下面的代码显示了一个#error指令和一个#warning指令。
- The
#errorinstruction is in the#ifstructure, so it will only be generated when the conditions of the#ifinstruction are met.- The
#warninginstruction is to remind the programmer to come back and clean up a piece of code.
`#define RightHanded #define LeftHanded
#if RightHanded && LeftHanded #error Can't build for both RightHanded and LeftHanded #endif
#warning Remember to come back and clean up this code!`
行号指令
行号指令可以做几件事,包括:
- Change the apparent line number of warning and error messages reported by the compiler.
- Change the apparent file name of the source file being compiled.
- Hide a series of lines from the interactive debugger
#line指令的语法如下:
` #line integer // Sets line number of next line to value of integer #line "filename" // Sets the apparent file name #line default // Restores real line number and file name
#line hidden // Hides the following code from stepping debugger #line // Stops hiding from debugger`
带有整数参数的#line指令使编译器认为该值是下一行代码的行号。基于该行号,继续对后续行进行编号。
- To change the apparent file name, use the file name in double quotation marks as the parameter. Double quotes are required.
- To return the real line number and the real file name, use
defaultas the parameter.- To hide a piece of code in the single-step debugging function of the interactive debugger, use
hiddenas the parameter. To stop hiding, use the command without parameters. So far, this feature is mainly used in ASP.NET and WPF to hide the code generated by the compiler.
以下代码显示了行号指令的示例:
` #line 226 x = y + z; // Now considered by the compiler to be line 226 ...
#line 330 "SourceFile.cs" // Changes the reported line number and file name var1 = var2 + var3; ...
#line default // Restores true line numbers and file name`
地区指令
region 指令允许您标记并有选择地命名一段代码。一个区域由一个#region指令和它下面的一个#endregion指令组成。区域的特征如下:
- A
#regioninstruction is placed on the previous line of the code segment you want to mark, and a #endregioninstruction is placed after the last line of code in the area.- The
#regioninstruction can follow an optional text string in the line after it. This string is used as the name of the region.- Other regions can be nested inside the region.
- Zones can be nested at any level.
- A
#endregioninstruction always matches the first mismatched#regioninstruction above it.
虽然编译器会忽略区域指令,但是源代码工具可以使用它们。例如,Visual Studio 允许您轻松隐藏或显示区域。
例如,下面的代码有一个名为Constructors的区域,它包含了类MyClass的两个构造函数。在 Visual Studio 中,当您不想在代码中看到该区域时,可以将其折叠成一行,然后当您需要处理它或添加另一个构造函数时,再将其展开。
` #region Constructors MyClass() { ... }
MyClass(string s) { ... } #endregion`
区域可以嵌套,如图图 23-3 所示。
图 23-3 。嵌套区域
# pragma 警告指令
#pragma warning指令允许您关闭和重新打开警告信息。
- To turn off warning messages, use the
disableform and separate the list of warning numbers you want to turn off with commas.- To reopen the warning message, use the
restoretable to list the warning numbers you want to reopen.
例如,下面的代码关闭两条警告消息:618 和 414。在代码的下面,它打开 618 的消息,但关闭 414 的消息。
Warning messages to turn off <ins> ↓ </ins> #pragma warning disable 618, 414 ... Messages for the listed warnings are off in this section of code. #pragma warning restore 618
如果使用不带警告编号列表的任一形式,该命令将应用于所有警告。例如,下面的代码关闭并恢复所有警告消息。
` #pragma warning disable ... All warning messages are turned off in this section of code.
#pragma warning restore ... All warning messages are turned back on in this section of code.`*
二十四、反射和属性
- 元数据和反射
- 类型类
- 获取类型对象
- 什么是属性?
- 应用属性
- 预定义、保留的属性
- 关于应用属性的更多信息
- 自定义属性
- 访问属性
元数据和反射
大多数程序都是用来处理数据的。它们读取、写入、操作和显示数据。(图形是数据的一种形式。)然而,对于某些类型的程序来说,它们操纵的数据不是数字、文本或图形,而是关于程序和程序类型的信息。
关于程序及其类的数据称为元数据,存储在程序的程序集中。* A program can view other assemblies or its own metadata at runtime. When a running program looks at its own metadata or the metadata of other programs, this is called reflection.
对象浏览器是显示元数据的程序的一个例子。它可以读取程序集并显示它们包含的类型,以及所有的特征和成员。
本章将介绍你的程序如何使用Type类反映数据,以及如何使用属性向你的类型添加元数据。
注意要使用反射,必须使用
System.Reflection名称空间。
铅字类
在本文中,我描述了如何声明和使用 C# 中可用的类型。这些包括预定义的类型(int、long、string等)、来自 BCL 的类型(Console、IEnumerable等),以及用户定义的类型(MyClass、MyDel等)。每种类型都有自己的成员和特征。
BCL 声明了一个名为Type的抽象类,它被设计用来包含一个类型的特征。使用这个类的对象允许你得到关于你的程序正在使用的类型的信息。
由于Type是一个抽象类,它不能有实际的实例。相反,在运行时,CLR 创建从包含类型信息的Type ( RuntimeType)派生的类的实例。当您访问这些实例之一时,CLR 返回一个引用,不是派生类型的引用,而是基类Type的引用。不过,为了简单起见,在本章的其余部分,我将把引用所指向的对象称为类型为Type的对象,尽管从技术上讲,它是 BCL 内部的一个派生类型的对象。
关于Type需要了解的重要事项如下:
- For each type used in the program, the CLR will create a
Typeobject containing information about that type.- Each type used in the program is associated with a separate
Typeobject.- No matter how many types of instances are created, only one
Typeobject is associated with all instances.
图 24-1 显示了一个带有两个MyClass对象和一个OtherClass对象的运行程序。注意,虽然有两个MyClass实例,但是只有一个Type对象表示它。
图 24-1 。CLR 为程序中使用的每个类型实例化类型的对象。
你几乎可以从类型的Type对象中获得任何你需要知道的信息。表 24-1 列出了这个类中一些更有用的成员。
获取类型对象
你可以通过使用GetType方法或者使用typeof操作符得到一个Type对象。类型object包含一个名为GetType的方法,该方法返回对实例的Type对象的引用。因为每个类型最终都是从object派生的,所以您可以对任何类型的对象调用GetType方法来获取其Type对象,如下所示:
Type t = myInstance.GetType();
下面的代码显示了基类和从它派生的类的声明。方法Main为每个类创建一个实例,并将引用放在一个名为bca的数组中,以便于处理。在外层的foreach循环中,代码获取Type对象并打印出类名。然后它获取该类的字段并将它们打印出来。图 24-2 说明了内存中的对象。
` using System; using System.Reflection; // Must use this namespace class BaseClass { public int BaseField = 0; }
class DerivedClass : BaseClass { public int DerivedField = 0; }
class Program { static void Main( ) { var bc = new BaseClass(); var dc = new DerivedClass();
BaseClass[] bca = new BaseClass[] { bc, dc };
foreach (var v in bca) { Type t = v.GetType(); // Get the type.
Console.WriteLine("Object type : {0}", t.Name);
FieldInfo[] fi = t.GetFields(); // Get the field information. foreach (var f in fi) Console.WriteLine(" Field : {0}", f.Name); Console.WriteLine(); } } }`
该代码产生以下输出:
`Object type : BaseClass Field : BaseField
Object type : DerivedClass Field : DerivedField Field : BaseField`
图 24-2 。基类和派生类对象以及它们的类型对象
你也可以使用typeof操作符来获得一个Type对象。只需提供类型名作为操作数,它就会返回一个对Type对象的引用,如下所示:
Type t = typeof( DerivedClass ); ↑ ↑ Operator Type you want the Type object for
下面的代码显示了一个使用typeof操作符的简单例子:
` using System; using System.Reflection; // Must use this namespace
namespace SimpleReflection { class BaseClass { public int MyFieldBase; }
class DerivedClass : BaseClass { public int MyFieldDerived; }
class Program { static void Main( ) { Type tbc = typeof(DerivedClass); // Get the type. Console.WriteLine("Result is {0}.", tbc.Name);
Console.WriteLine("It has the following fields:"); // Use the type. FieldInfo[] fi = tbc.GetFields(); foreach (var f in fi) Console.WriteLine(" {0}", f.Name); } } }`
该代码产生以下输出:
Result is DerivedClass. It has the following fields: MyFieldDerived MyFieldBase
什么是属性?
一个属性是一种语言结构,允许你添加元数据到程序的集合中。它是一种特殊类型的类,用于存储关于程序结构的信息。
- The program structure to which you apply attributes is called its target .
- Programs designed to retrieve and use metadata, such as object browsers, are called consumers of attributes.
- There are predefined attributes. You can also declare custom attributes.
图 24-3 给出了使用属性所涉及的组件的概述,并说明了关于它们的以下几点:
- You apply the attribute to the program construction in the source code.
- The compiler gets the source code, generates metadata from the attributes, and puts the metadata into the assembly.
- The consumer program can access the metadata of attributes and the metadata of other components of the program. Note that the compiler generates and uses attributes.
图 24-3 。创建和使用属性所涉及的组件
按照惯例,属性名使用 Pascal 大小写,并以后缀Attribute结尾。但是,在将属性应用于目标时,可以省略后缀。例如,对于属性SerializableAttribute和MyAttributeAttribute,在将它们应用到一个构造时,您可以使用简称Serializable和MyAttribute。
应用属性
我将从展示如何使用已经定义的属性开始,而不是从描述如何创建属性开始。这样,你就可以知道它们是如何有用的。
属性的目的是告诉编译器发出一组关于程序构造的元数据,并将其放入程序集中。您可以通过将属性应用于构造来实现这一点。
- Apply an attribute by placing a attribute segment before construction.
- The attribute part consists of square brackets and contains an attribute name and sometimes a parameter list.
例如,下面的代码显示了两个类的标题。前几行代码显示了应用于类MyClass的名为Serializable的属性。注意Serializable没有参数列表。第二个类声明有一个名为MyAttribute的属性,它有一个带两个string参数的参数列表。
` [ Serializable ] // Attribute public class MyClass { ...
[ MyAttribute("Simple class", "Version 3.57") ] // Attribute with parameters public class MyOtherClass { ...`
关于属性,需要了解的一些重要信息如下:
- Most attributes only apply to constructs that follow the attribute segment.
- A construction with an attribute is called modifying , or modifying , which has this attribute. These two terms are very common.
预定义的、保留的属性
在这一节中,我们将看看. NET 预定义和保留的几个属性。
过时的属性
在一个程序的生命周期中,它可能会经历许多不同的版本,可能会持续几年。在其生命周期的后期,您通常会编写一个新方法来取代执行类似功能的旧方法。出于多种原因,您可能希望保留所有调用旧的、现已过时的方法的旧代码,而让新代码调用新方法。
当这种情况发生时,您会希望您的团队成员,或者以后处理代码的程序员,使用新的方法而不是旧的方法。为了帮助警告他们不要使用旧方法,您可以使用Obsolete属性将旧方法标记为过时,并在编译代码时显示一条有用的警告消息。下面的代码显示了它的用法示例:
` class Program Apply attribute. { ↓
[Obsolete("Use method SuperPrintOut")] // Apply attribute to method. static void PrintOut(string str) { Console.WriteLine(str); } static void Main(string[] args) { PrintOut("Start of Main"); // Invoke obsolete method. } }`
注意方法Main调用PrintOut,即使它被标记为过时。尽管如此,代码编译和运行良好,并产生以下输出:
Start of Main
不过,在编译期间,编译器会产生下列 CS0618 警告讯息,通知您正在使用过时的建构:
'AttrObs.Program.PrintOut(string)' is obsolete: 'Use method SuperPrintOut'
属性的另一个重载接受类型为bool的第二个参数。此参数指定是否将用法标记为错误,而不仅仅是警告。以下代码指定应将它标记为错误:
Flag as an error. ↓ [ Obsolete("Use method SuperPrintOut", true) ] // Apply attribute to method. static void PrintOut(string str) { ...
条件属性
Conditional属性允许您指示编译器包含或排除特定方法的所有调用。要使用Conditional属性,将它应用到方法声明中,并将编译符号作为参数。
如果定义了编译符号,编译器将包含所有方法调用的代码,就像对任何普通方法一样。* If the compilation symbol is defined by instead of , the compiler will omit all the method calls of in the whole code.
定义方法本身的 CIL 代码总是包含在程序集中。只是插入或省略了调用。
例如,在下面的代码中,Conditional属性被应用于名为TraceMessage的方法的声明。该属性只有一个参数,在本例中是字符串DoTrace。
- When compiling code, the compiler will check whether the compilation symbol named
DoTraceis defined.- If
DoTraceis defined, the compiler places all calls to methodTraceMessagein the code as usual.- If
DoTracecompilation symbol is not defined, it will not output any code forTraceMessagecall.
Compilation symbol <ins> ↓ </ins> [Conditional( "DoTrace" )] static void TraceMessage(string str) { Console.WriteLine(str); }
条件属性的例子
下面的代码展示了使用Conditional属性的完整示例。
- Method
Maincontains two calls to methodTraceMessage.- The declaration of method
TraceMessageis decorated with the attributeConditional, whose parameter is the compilation symbolDoTrace. Therefore, ifDoTraceis defined, the compiler will contain all the codes that callTraceMessage.- Since the first line of code defines a compilation symbol named
DoTrace, the compiler will contain the code that callsTraceMessagetwice.
` #define DoTrace using System; using System.Diagnostics;
namespace AttributesConditional { class Program { [Conditional( "DoTrace" )] static void TraceMessage(string str) { Console.WriteLine(str); }
static void Main( ) { TraceMessage("Start of Main"); Console.WriteLine("Doing work in Main."); TraceMessage("End of Main"); } } }`
该代码产生以下输出:
Start of Main Doing work in Main. End of Main
如果您注释掉第一行,使得DoTrace没有被定义,编译器将不会为对TraceMessage的两次调用插入代码。这一次,当您运行该程序时,它会产生以下输出:
Doing work in Main.
来电者信息属性
调用者信息属性允许您访问有关文件路径、行号和调用成员名称的源代码信息。
- The three attribute names are
CallerFilePath,CallerLineNumberandCallerMemberNamerespectively.- These properties can only be used with optional parameters on methods.
下面的代码声明了一个名为MyTrace的方法,该方法在其三个可选参数上使用了三个调用者信息属性。如果使用这些参数的显式值调用该方法,将使用实际参数的值。然而,在下面显示的来自Main的调用中,没有提供显式的值,所以系统提供源代码的文件路径、调用方法的行的行号以及调用方法的成员的名称。
` using System; using System.Runtime.CompilerServices;
public static class Program { public static void MyTrace( string message, [CallerFilePath] string fileName = "", [CallerLineNumber] int lineNumber = 0, [CallerMemberName] string callingMember = "" ) { Console.WriteLine( "File: {0}", fileName ); Console.WriteLine( "Line: {0}", lineNumber ); Console.WriteLine( "Called From: {0}", callingMember ); Console.WriteLine( "Message: {0}", message ); }
public static void Main() { MyTrace( "Simple message" ); } }`
该代码产生以下输出:
File: c:\TestCallerInfo\TestCallerInfo\Program.cs Line: 19 Called From: Main Message: Simple message
DebuggerStepThrough 属性
很多时候,当你调试代码并一行一行地单步执行时,有些方法是你不想让调试器进入的;您只想让它执行方法,并单步执行方法调用后的行。DebuggerStepThrough属性指示调试器在不进入目标代码的情况下执行它。
在我自己的代码中,这是我经常发现的最有用的属性。有些方法很小,但显然是正确的,以至于在调试时不得不重复地一步一步来,这很烦人。但是,请小心使用该属性,因为您不想排除可能包含 bug 的代码。
关于DebuggerStepThrough需要了解的重要事项如下:
- This attribute is in the
System.Diagnosticsnamespace.- You can use this property on classes, structures, constructors, methods or accessors.
下面的代码展示了在访问器和方法上使用的属性。如果您在调试器中单步调试这段代码,您会发现调试器没有进入IncrementFields方法或X属性的set访问器。
` using System; using System.Diagnostics; // Required for this DebuggerStepThrough
class Program { int _x = 1; int X { get { return _x; } [DebuggerStepThrough] // Don’t step through the set accessor. set { _x = _x * 2; _x += value; } }
public int Y { get; set; }
public static void Main() { Program p = new Program();
p.IncrementFields(); p.X = 5; Console.WriteLine( "X = {0}, Y = {1}", p.X, p.Y ); } [DebuggerStepThrough] // Don’t step through this method. void IncrementFields() { X++; Y++; } }`
其他预定义属性
那个 .NET Framework 预定义了许多由编译器和 CLR 理解和解释的属性。表 24-2 列出了其中的一些。该表使用短名称,不带“属性”后缀。比如CLSCompliant的全称是CLSCompliantAttribute。
关于应用属性的更多信息
到目前为止显示的简单属性使用了应用于方法的单个属性。本节描述其他类型的属性用法。
多重属性
您可以将多个属性应用于单个构造。
- Multiple attributes can be listed in one of the following formats:
- Individual attribute parts of, one by one. Usually these are stacked on top of each other in separate rows.
- Individual attribute segments, separating attributes with commas.
- Attributes can be listed in any order.
例如,下面的代码显示了应用多个属性的两种方式。代码的各个部分是等效的。
` [ Serializable ] // Stacked [ MyAttribute("Simple class", "Version 3.57") ]
[ MyAttribute("Simple class", "Version 3.57"), Serializable ] // Comma separated ↑ ↑ Attribute Attribute`
其他类型的目标
除了类之外,您还可以将属性应用于其他程序结构,例如字段和属性。下面的声明显示了一个字段的一个属性和一个方法的多个属性:
` [MyAttribute("Holds a value", "Version 3.2")] // On a field public int MyField;
[Obsolete] // On a method [MyAttribute("Prints out a message.", "Version 3.6")] public void PrintOut() { ...`
您还可以显式标记属性,以应用于特定的目标构造。若要使用显式目标说明符,请将目标类型放在属性部分的开头,后跟一个冒号。例如,下面的代码用一个属性来修饰方法,并且将一个属性应用于返回值。
Explicit target specifier ↓ [method: MyAttribute("Prints out a message.", "Version 3.6")] [return: MyAttribute("This value represents ...", "Version 2.3")] public long ReturnSetting() { ...
C# 语言定义了十个标准属性目标,在表 24-3 中列出。大多数目标名称都是不言自明的,但是type涵盖了类、结构、委托、枚举和接口。typevar目标名指定了使用泛型的构造的类型参数。
全局属性
您还可以通过使用assembly和module目标名称,使用显式目标说明符在程序集和模块级别设置属性。(组件和模块在第二十一章中进行了解释。)关于程序集级属性的一些要点如下:
- Assembly-level attributes must be placed in outside any namespace scope, usually in
AssemblyInfo.csfile.AssembyInfo.csFiles usually contain metadata about companies, products and copyright information.
以下是来自一个AssemblyInfo.cs文件的行:
[assembly: AssemblyTitle("SuperWidget")] [assembly: AssemblyDescription("Implements the SuperWidget product.")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("McArthur Widgets, Inc.")] [assembly: AssemblyProduct("Super Widget Deluxe")] [assembly: AssemblyCopyright("Copyright © McArthur Widgets 2012")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")]
自定义属性
您可能已经注意到,应用属性的语法与您到目前为止看到的语法非常不同。由此,您可能会认为属性是一种完全不同类型的构造。他们不是——他们只是一种特殊的阶级。
关于属性类的一些要点如下:
- The user-defined attribute class is called custom attribute .
- All the attribute classes of are derived from the
System.Attributeclass.
声明自定义属性
声明一个属性类在很大程度上与声明任何其他类是一样的。但是,有几件事需要注意:
- To declare a custom attribute, do the following:
- Declare a class derived from
System.Attribute.- Give it a name ending with the suffix
Attribute.- For security reasons, it is generally recommended that you declare the attribute class as
sealed.
例如,下面的代码显示了属性MyAttributeAttribute声明的开始:
Attribute name <ins> ↓ </ins> public sealed class MyAttribute<ins>Attribute</ins> : <ins>System.Attribute</ins> { ↑ ↑ ... Suffix Base class
由于属性包含有关目标的信息,因此属性类的公共成员通常只包含以下内容:
- field
- attribute
- Constructor
使用属性构造函数
像其他类一样,属性也有构造函数。每个属性必须至少有一个公共构造函数。
- Like other classes, if you do not declare a constructor, the compiler will generate an implicit, public and parameterless constructor for you.
- Property constructors, like other constructors, can be overloaded.
- When declaring a constructor, you must use the full class name, including the suffix. When applies attribute, you can only use abbreviation.
例如,使用下面的构造函数,如果方法名不包含后缀,编译器将产生一条错误消息:
Suffix <ins> ↓ </ins> public MyAttributeAttribute(string desc, string ver) { Description = desc; VersionNumber = ver; }
指定构造函数
当您将属性应用于目标时,您正在指定应该使用哪个构造函数来创建属性的实例。属性应用中列出的参数是构造函数的实际参数。
例如,在下面的代码中,MyAttribute被应用于一个字段和一个方法。对于该字段,声明指定了一个带有单个string参数的构造函数。对于该方法,它指定了一个带有两个string参数的构造函数。
` [MyAttribute("Holds a value")] // Constructor with one string public int MyField;
[MyAttribute("Version 1.3", "Sal Martin")] // Constructor with two strings public void MyMethod() { ...`
关于属性构造函数的其他要点如下:
- When applying the attribute, the actual parameter of the constructor must be a constant expression, and its value can be determined at compile time.
- If you apply the property constructor without parameters, you can omit the brackets. For example, both classes in the following code use the parameterless constructor of the property
MyAttr. The meaning of these two forms is the same.
` [MyAttr] class SomeClass ...
[MyAttr()] class OtherClass ...`
使用构造函数
请注意,您从未显式调用构造函数。相反,只有当属性消费者访问属性时,才会创建属性的实例,并调用其构造函数*。这与其他类实例非常不同,其他类实例是在使用对象创建表达式的位置创建的。应用属性是一个声明性语句,它并不决定何时应该构造属性类的对象。*
图 24-4 比较了普通类的构造函数的使用和带属性的构造函数的使用。
命令式语句实际上是说,“在这里创建一个新的类对象。”* Declarative statement says, "This attribute is associated with this target, and if you need to construct an attribute, use this constructor."
图 24-4 。比较构造函数的使用
构造函数中的位置和命名参数
像常规类的方法和构造函数一样,属性构造函数也可以使用位置参数和命名参数。
下面的代码使用一个位置参数和两个命名参数演示了属性的应用:
下面的代码展示了属性类的声明,以及它在类MyClass上的应用。注意,构造函数声明只列出了一个形参。然而,通过使用命名参数,您可以给构造函数三个实际参数。这两个命名参数设置字段Ver和Reviewer的值。
` public sealed class MyAttributeAttribute : System.Attribute { public string Description; public string Ver; public string Reviewer;
public MyAttributeAttribute(string desc) // Single formal parameter { Description = desc; } } Three actual parameters ↓ [MyAttribute("An excellent class", Reviewer="Amy McArthur", Ver="7.15.33")] class MyClass { ... }`
注意和方法一样,如果构造函数需要任何位置参数,它们必须放在任何命名参数之前。
限制一个属性的使用
您已经看到了可以将属性应用于类。但是属性本身是类,并且有一个重要的预定义属性可以应用于您的定制属性:AttributeUsage属性。您可以使用它将属性的使用限制到一组特定的目标类型。
例如,如果您希望您的自定义属性MyAttribute只应用于方法,您可以使用下面形式的AttributeUsage:
Only to methods <ins> ↓ </ins> [ AttributeUsage( AttributeTarget.Method ) ] public sealed class MyAttributeAttribute : System.Attribute { ...
AttributeUsage有三个重要的公共属性,在表 24-4 中列出。该表显示了属性的名称及其含义。对于后两个属性,它还显示了它们的默认值。
【AttributeUsage 的构造函数
AttributeUsage的构造函数接受一个单一的位置参数,该参数指定属性允许哪些目标类型。它使用这个参数来设置它的ValidOn属性。可接受的目标类型是AttributeTarget枚举的成员。表 24-5 显示了AttributeTarget枚举成员的完整集合。
您可以使用按位 OR 运算符组合使用类型。例如,下面代码中声明的属性只能应用于方法和构造函数。
Targets <ins> ↓ </ins> [ AttributeUsage( AttributeTarget.Method | AttributeTarget.Constructor ) ] public sealed class MyAttributeAttribute : System.Attribute { ...
当您将AttributeUsage应用到一个属性声明时,构造函数将至少有一个必需的参数,它包含要存储在ValidOn中的目标类型。您还可以通过使用命名参数来设置Inherited和AllowMultiple属性。如果你不设置它们,它们将有它们的默认值,如表 24-4 所示。
例如,下一个代码块指定了关于MyAttribute的以下内容:
MyAttributemust only be applied to classes.MyAttributeis not inherited by a class derived from the class to which it is applied.- There cannot be multiple instances of
MyAttributeapplied to the same target.
[ AttributeUsage( AttributeTarget.Class, // Required, positional Inherited = false, // Optional, named AllowMultiple = false ) ] // Optional, named public sealed class MyAttributeAttribute : System.Attribute { ...
建议自定义属性的做法
在编写自定义属性时,强烈建议采用以下做法:
- The attribute class should represent a certain state of the target construct.
- If the property requires certain fields, it contains a constructor with position parameters to collect data and initialize optional fields with named parameters as needed.
- Do not implement public methods or other function members other than properties.
- To increase security, declare the attribute class as
sealed.- Use the
AttributeUsageattribute in the attribute declaration to explicitly specify the attribute target set.
以下代码阐释了这些准则:
` [AttributeUsage( AttributeTargets.Class )] public sealed class ReviewCommentAttribute : System.Attribute { public string Description { get; set; } public string VersionNumber { get; set; } public string ReviewerID { get; set; }
public ReviewCommentAttribute(string desc, string ver) { Description = desc; VersionNumber = ver; } }`
访问属性
在本章的开始,您看到了您可以使用类型的Type对象来访问关于类型的信息。您可以用同样的方式访问自定义属性。这里有两种Type方法特别有用:IsDefined和GetCustomAttributes。
使用 IsDefined 方法
您可以使用Type对象的IsDefined方法来确定特定的属性是否应用于特定的类。
例如,下面的代码声明了一个名为MyClass的属性化类,并通过访问在程序本身中声明和应用的属性来充当自己的属性消费者。在代码的顶部是属性ReviewComment和应用它的类MyClass的声明。该代码执行以下操作:
- First,
MainCreate an object of a class. Then, it retrieves the reference to theTypeobject by using theGetTypemethod inherited from its base classobject.- By referencing the
Typeobject, you can call theIsDefinedmethod to see if the attributeReviewCommentis applied to this class.
- The first parameter accepts a
Typeobject of the attribute you are checking.- The type of the second parameter is
bool, which specifies whether to search the inheritance tree ofMyClassto find the attribute.
` [AttributeUsage(AttributeTargets.Class)] public sealed class ReviewCommentAttribute : System.Attribute { ... }
[ReviewComment("Check it out", "2.4")] class MyClass { }
class Program { static void Main() { MyClass mc = new MyClass(); // Create an instance of the class. Type t = mc.GetType(); // Get the Type object from the instance. bool isDefined = // Check the Type for the attribute. t.IsDefined(typeof(ReviewCommentAttribute), false);
if( isDefined ) Console.WriteLine("ReviewComment is applied to type {0}", t.Name); } }`
该代码产生以下输出:
ReviewComment is applied to type MyClass
使用 GetCustomAttributes 方法
GetCustomAttributes方法返回一个应用于构造的属性数组。
- The returned actual object is an array of
object, which must be converted to the correct attribute type.- Boolean parameter specifies whether to search the inheritance tree to find the attribute.
object[] AttArr = t.GetCustomAttributes(false);- When the
GetCustomAttributesmethod is called, an instance of each attribute associated with the target is created.
下面的代码使用与前面的示例相同的属性和类声明。但是在这种情况下,它不仅仅决定一个属性是否被应用到类中。相反,它检索应用于该类的属性数组,并循环遍历它们,打印出它们的成员值。
` using System;
[AttributeUsage( AttributeTargets.Class )] public sealed class MyAttributeAttribute : System.Attribute { public string Description { get; set; } public string VersionNumber { get; set; } public string ReviewerID { get; set; }
public MyAttributeAttribute( string desc, string ver ) { Description = desc; VersionNumber = ver; } }
[MyAttribute( "Check it out", "2.4" )] class MyClass { }
class Program { static void Main() { Type t = typeof( MyClass ); object[] AttArr = t.GetCustomAttributes( false );
foreach ( Attribute a in AttArr ) { MyAttributeAttribute attr = a as MyAttributeAttribute; if ( null != attr ) { Console.WriteLine( "Description : {0}", attr.Description ); Console.WriteLine( "Version Number : {0}", attr.VersionNumber ); Console.WriteLine( "Reviewer ID : {0}", attr.ReviewerID ); } } } }`
该代码产生以下输出:
Description : Check it out Version Number : 2.4 Reviewer ID :
二十五、其他主题
- 概述
- 琴弦
- StringBuilder 类
- 将字符串解析为数据值
- 关于可空类型的更多信息
- 方法主
- 文档注释
- 嵌套类型
- 析构函数和处置模式
- 与 COM 互操作
概述
在这一章中,我将讨论一些在使用 C# 时很重要但不适合其他章节的主题。这些包括字符串处理、可空类型、Main方法、文档注释和嵌套类型。
字符串
0 和 1 对于内部计算来说很好,但是对于人类可读的输入和输出,我们需要字符串。BCL 提供了许多使字符串处理变得容易的类。
C# 预定义类型string表示 .NET 类System.String。关于字符串,需要了解的最重要的事情如下:
- String is an array of Unicode characters.
- Strings are immutable-they cannot be changed.
string类型有许多有用的字符串操作成员,包括那些允许您确定它们的长度、改变它们的大小写、连接字符串以及执行许多其他有用任务的成员。表 25-1 显示了一些最有用的成员。
表 25-1 中许多方法的名字听起来好像它们在改变字符串对象。实际上,他们不是改变字符串,而是返回新的副本。对于一个string,任何“改变”都会分配一个新的不可变字符串。
例如,下面的代码声明并初始化一个名为s的string。第一个WriteLine语句调用s上的ToUpper方法,返回一个全大写的字符串副本。最后一行打印出s的值,显示它没有改变。
` string s = "Hi there.";
Console.WriteLine("{0}", s.ToUpper()); // Print uppercase copy Console.WriteLine("{0}", s); // String is unchanged`
该代码产生以下输出:
HI THERE. Hi there.
在我自己的编码中,我发现表中列出的非常有用的方法之一是Split方法。它将一个string分割成一组子字符串,并以数组的形式返回它们。您向该方法传递一个分隔符数组,这些分隔符用于确定在何处拆分字符串,并且您可以指定它应该如何处理输出数组中的空元素。最初的string当然保持不变。
下面的代码展示了一个使用Split方法的例子。在本例中,分隔符集由空格字符和四个标点符号组成。
class Program { static void Main() { string s1 = "hi there! this, is: a string."; char[] delimiters = { ' ', '!', ',', ':', '.' }; string[] words = s1.Split( delimiters, StringSplitOptions.RemoveEmptyEntries ); Console.WriteLine( "Word Count: {0}\n\rThe Words...", words.Length ); foreach ( string s in words ) Console.WriteLine( " {0}", s ); } }
该代码产生以下输出:
Word Count: 6 The Words... hi there this is a string
StringBuilder 类
StringBuilder类帮助你动态有效地产生字符串,同时减少复制的数量。
- The
StringBuilderclass is a member of BCL, in the namespaceSystem.Text.- The
StringBuilderobject is a variable array of Unicode characters.
例如,下面的代码声明并初始化一个StringBuilder对象,并打印其结果字符串值。第四行通过替换内部字符数组的一部分来改变实际的对象。现在,当您通过隐式调用ToString打印其字符串值时,您可以看到,与string类型的对象不同,StringBuilder对象实际上发生了变化。
` using System; using System.Text;
class Program { static void Main() { StringBuilder sb = new StringBuilder( "Hi there." ); Console.WriteLine( "{0}", sb.ToString() ); // Print string.
sb.Replace( "Hi", "Hello" ); // Replace a substring. Console.WriteLine( "{0}", sb.ToString() ); // Print changed string. } }`
该代码产生以下输出:
Hi there. Hello there.
当基于给定的字符串创建一个StringBuilder对象时,该类分配一个比实际的当前字符串长度更长的缓冲区。只要对字符串所做的更改适合缓冲区,就不会分配新的内存。如果对字符串的更改需要比缓冲区中可用空间更多的空间,则会分配一个新的更大的缓冲区,并将字符复制到该缓冲区中。和原来的缓冲区一样,这个新的缓冲区也有额外的空间。
要获得与StringBuilder内容对应的string,只需调用它的ToString方法。
解析字符串为数据值
字符串是 Unicode 字符的数组。例如,字符串"25.873"有六个字符长,而不是是一个数字。虽然它看起来像一个数字,但你不能对它执行算术运算。“相加”两个字符串产生它们的连接。
- Parsing allows you to get a string whose represents a value of and convert it into an actual typed value.
- All predefined simple types have a static method named
Parse, which takes a string representing a value and converts it into the actual value of the type.- If the string cannot be parsed, the system throws an exception.
以下语句显示了使用Parse方法的语法示例。注意,Parse是static,所以您需要通过使用目标类型的名称来调用它。
double d1 = double.Parse("<ins>25.873</ins>"); ↑ ↑ Target type String to be converted
以下代码显示了将两个字符串解析为类型为double的值,然后将它们相加的示例:
` static void Main() { string s1 = "25.873"; string s2 = "36.240";
double d1 = double.Parse(s1); double d2 = double.Parse(s2);
double total = d1 + d2; Console.WriteLine("Total: {0}", total); }`
该代码产生以下输出:
Total: 62.113
注意关于
Parse的一个常见误解是,由于它对字符串进行操作,所以它被认为是string类的成员。不是的。Parse根本不是单一的方法,而是由目标类型实现的许多方法。
Parse方法的缺点是,如果它们不能成功地将字符串解析为目标类型,就会抛出异常。异常是开销很大的操作,如果可能的话,您应该尝试以编程方式避免它们。TryParse方法允许您这样做。关于TryParse需要知道的重要事情如下:
- Every built-in type with a
Parsemethod also has aTryParsemethod.- The
TryParsemethod takes two parameters and returns abool.
- The first parameter is the string you want to parse.
- The second is the
outparameter that refers to the target type variable.- If
TryParseis successful, the parsed value is assigned tooutparameter, andtrueis returned. Otherwise, return tofalse.
一般来说,你应该使用TryParse而不是Parse来避免可能抛出的异常。下面的代码展示了使用int.TryParse方法的两个例子:
` class Program { static void Main( ) { string parseResultSummary; string stringFirst = "28"; int intFirst; Input string Output variable ↓ ↓ bool success = int.TryParse( stringFirst, out intFirst );
parseResultSummary = success ? "was successfully parsed" : "was not successfully parsed"; Console.WriteLine( "String {0} {1}", stringFirst, parseResultSummary );
string stringSecond = "vt750"; int intSecond; Input string Output variable ↓ ↓ success = int.TryParse( stringSecond, out intSecond );
parseResultSummary = success ? "was successfully parsed" : "was not successfully parsed"; Console.WriteLine( "String {0} {1}", stringSecond, parseResultSummary ); } }`
该代码产生以下输出:
String 28 was successfully parsed String vt750 was not successfully parsed
关于可空类型的更多信息
在第三章中,你可以快速了解可空类型。正如您所记得的,可空类型允许您创建一个可以标记为有效或无效的值类型变量,实际上允许您将值类型变量设置为“null”我想在《??》第三章中介绍可空类型和其他内置类型,但是既然你对 C# 有了更多的了解,现在是时候介绍它们更复杂的方面了。
回顾一下,可空类型总是基于另一个已经声明的类型,称为底层类型。
- Nullable types can be created from any value type, including predefined simple types.
- Nullable types cannot be created from reference types or other nullable types. You didn't explicitly declare a nullable type in your code. Instead, you declare a nullable variable. The compiler implicitly creates nullable types for you.
要创建一个可空类型的变量,只需在变量声明中在基础类型名称的末尾添加一个问号。不幸的是,这种语法让你看起来对你的代码有很多疑问。(开个玩笑——不过有点丑。)
例如,下面的代码声明了一个可空的int类型的变量。注意后缀是附加在类型名称上的——而不是变量名。
Suffix ↓ <ins>int?</ins> myNInt = 28; ↑ The name of the nullable type includes the suffix.
有了这个声明语句,编译器负责生成可空类型和该类型的变量。图 25-1 显示了这种可空类型的结构。它包含以下内容:
- Bottom type
- Several important read-only properties of an instance of:
- The attribute
HasValuebelongs to the typebooland indicates whether the value is valid or not.- The attribute
Valueis the same as the underlying type. If the variable is valid, it returns the value of the variable.
***图 25-1。*一个可空类型在一个结构中包含一个底层类型的对象,有两个只读属性。
使用可空类型和使用任何其他类型的变量几乎是一样的。读取可空类型的变量会返回其值。但是,您必须确保变量不是null。试图读取null变量的值会产生异常。
- Like any variable, to retrieve its value, you only need to use its name.
- To check whether the nullable type has a value, you can compare it with
nullor check itsHasValueattribute.
int? myInt1 = 15; Compare to null. ↓ if ( myInt1 != null ) Console.WriteLine("{0}", myInt1); ↑ Use variable name.
15
您可以轻松地在可空类型和其对应的不可空类型之间进行转换。关于可空类型转换,您需要知道的重要事情如下:
在不可空类型和它的可空版本之间有一个隐含的 t 转换。也就是说,不需要任何造型。* There is a explicit transformation between the nullable type and its non-nullable version.
例如,下面几行显示了两个方向的转换。在第一行中,类型为int的文字被隐式转换为类型为int?的值,并用于初始化可空类型的变量。在第二行中,变量被显式转换为不可为空的版本。
int? myInt1 = 15; // Implicitly convert int to int? int regInt = (int) myInt1; // Explicitly convert int? to int
赋给可空类型
可以为可空类型的变量分配三种值:
- A value of the underlying type.
- A value of the same nullable type.
- A value
null
以下代码显示了三种类型赋值的示例:
` int? myI1, myI2, myI3;
myI1 = 28; // Value of underlying type myI2 = myI1; // Value of nullable type myI3 = null; // null
Console.WriteLine("myI1: {0}, myI2: {1}", myI1, myI2);`
该代码产生以下输出:
myI1: 28, myI2: 28
零合并算子
标准的算术和比较运算符也处理可空类型。还有一个特殊的操作符叫做零合并操作符,它返回一个非空值给一个表达式,以防一个可空的类型变量是null。
null 合并运算符由两个连续的问号组成,有两个操作数。
- The first operand is a variable of nullable type.
- The second is the non-nullable value of the underlying type.
- If the calculation result of the first operand (nullable operand) is
nullat runtime, the nonnullable operand is returned as the result of the expression.
` Null coalescing operator int? myI4 = null; ↓ Console.WriteLine("myI4: {0}", myI4 ?? -1);
myI4 = 10; Console.WriteLine("myI4: {0}", myI4 ?? -1);`
该代码产生以下输出:
myI4: -1 myI4: 10
当比较同一可空类型的两个值并且都是null时,相等比较运算符(==和!=)认为它们相等。例如,在下面的代码中,两个可空的int被设置为null。相等比较运算符声明它们相等。
` int? i1 = null, i2 = null; // Both are null.
if (i1 == i2) // Operator returns true. Console.WriteLine("Equal");`
该代码产生以下输出:
Equal
使用可空的用户自定义类型
到目前为止,您已经看到了预定义的简单类型的可空形式。您还可以创建用户定义值类型的可空形式。这些带来了使用简单类型时不会出现的额外问题。
主要问题是对封装的基础类型成员的访问。可空类型不直接公开基础类型的任何成员。例如,看看下面的代码及其在图 25-2 中的表示。代码声明了一个名为MyStruct的struct(这是一个值类型),带有两个public字段。
- Because the fields of
structare public, they can be easily accessed in any instance of this structure, as shown in the left figure.- However, the nullable version of
structonly exposes the underlying type throughValueattribute, while does not directly disclose any of its members . Although members are common tostruct, they are not common to nullable types, as shown on the right side of the figure.
` struct MyStruct // Declare a struct. { public int X; // Field public int Y; // Field public MyStruct(int xVal, int yVal) // Constructor { X = xVal; Y = yVal; } }
class Program {
static void Main()
{
MyStruct? mSNull = new MyStruct(5, 10);
...`
图 25-2 。结构成员的可访问性不同于可空类型的可访问性。
例如,下面的代码使用了这个struct并创建了struct和相应的可空类型的变量。在第三和第四行代码中,直接读取了struct变量的值。在第五行和第六行中,它们必须从 nullable 的Value属性返回的值中读取。
` MyStruct mSStruct = new MyStruct(6, 11); // Variable of struct MyStruct? mSNull = new MyStruct(5, 10); // Variable of nullable type Struct access ↓ Console.WriteLine("mSStruct.X: {0}", mSStruct.X); Console.WriteLine("mSStruct.Y: {0}", mSStruct.Y);
Console.WriteLine("mSNull.X: {0}", mSNull.Value.X); Console.WriteLine("mSNull.Y: {0}", mSNull.Value.Y); ↑ Nullable type access`
可空
可空类型是通过使用名为System.Nullable<T>的. NET 类型实现的,它使用 C# 泛型特性。C# 可空类型的问号语法只是创建类型为Nullable<T>的变量的快捷语法,其中T是底层类型。Nullable<T>获取底层类型,将其嵌入到一个结构中,并为该结构提供可空类型的属性、方法和构造函数。
您可以使用Nullable<T>的泛型语法或 C# 快捷语法。快捷语法更容易编写和理解,并且不容易出错。以下代码使用前面示例中声明的带有 struct MyStruct的Nullable<T>语法,创建一个名为mSNull的Nullable<MyStruct>类型的变量:
Nullable<MyStruct> mSNull = new Nullable<MyStruct>();
以下代码使用问号语法,但在语义上等效于Nullable<T>语法:
MyStruct? mSNull = new MyStruct();
法主
每个 C# 程序必须有一个入口点——一个必须被称为Main的方法。
在本文的示例代码中,我使用了一个没有参数也不返回值的版本Main。然而,有四种形式的Main可以作为程序的入口点。这些形式如下:
static void Main() {...}static void Main( string[] args) {...}static int Main() {...}static int Main( string[] args) {...}
当程序终止时,前两种形式不向执行环境返回值。后两种形式返回一个int值。返回值(如果使用的话)通常用于报告程序的成功或失败,其中 0 通常用于指示成功。
第二种和第四种形式允许您在程序启动时从命令行向程序传递实际参数,也称为参数。命令行参数的一些重要特征如下:
- There can be zero or more command line arguments. Even if there is no parameter, the
argsparameter is notnull. Instead, it is an array without elements.- Independent variables are separated by spaces or tabs.
- Each parameter is interpreted by the program as a string, but you don't need to enclose it in quotation marks on the command line.
例如,以下名为CommandLineArgs的程序接受命令行参数,并打印出提供的每个参数:
class Program { static void Main(string[] args) { foreach (string s in args) Console.WriteLine(s); } }
您可以从 Windows 的命令提示符程序中执行此程序。以下命令行使用五个参数执行程序CommandLineArgs:
<ins>CommandLineArgs</ins> <ins>Jon Peter Beth Julia Tammi</ins> ↑ ↑ Executable Arguments Name
这会产生以下输出:
Jon Peter Beth Julia Tammi
关于Main需要了解的其他重要事项如下:
Mainmust always be declared asstatic.Maincan be declared either in a class or in a structure.
一个程序只能包含四个可接受的入口点形式Main的一个声明。然而,您可以合法地声明其他名为Main的方法,只要它们没有四种入口点形式中的任何一种——但是这样做会引起混乱。
主通道的可达性
Main可以声明为public或private:
- If
Mainis declared asprivate, other assemblies cannot be accessed, and only the operating system can start the program.- If
Mainis declared aspublic, other assemblies can be executed.
然而,操作系统总是可以访问Main,而不管它声明的访问级别或者声明它的class或struct的访问级别。
默认情况下,当 Visual Studio 创建一个项目时,它会创建一个程序大纲,其中Main是隐式的private。如果你需要的话,你可以随时添加public修饰语。
文档注释
文档注释特性允许您以 XML 元素的形式包含程序的文档。我在第十九章中介绍了 XML。Visual Studio 甚至会帮助您插入元素,并从源文件中读取它们,然后将它们复制到一个单独的 XML 文件中。
图 25-3 给出了使用 XML 注释的概述。这包括以下步骤:
- You can use Visual Studio to generate source files with embedded XML. Visual Studio can automatically insert most important XML elements.
- Visual Studio reads XML from the source code file and copies the XML code into a new file.
- Another program called document compiler can get XML files and generate various types of document files from it.
图 25-3 。XML 注释过程
Visual Studio 的早期版本包含一个基本的文档编译器,但在 Visual Studio 2005 发布之前它已被移除。微软已经开发了一个新的文档编译器,叫做 Sandcastle .NET 框架文档。可以从[sandcastle.codeplex.com](http://sandcastle.codeplex.com)开始了解更多,免费下载。
插入文档注释
文档注释以三个连续的正斜杠开始。
- The first two slashes indicate to the compiler that this is an end-of-line comment and should be ignored in program parsing.
- The third diagonal line indicates that this is a document comment.
例如,在下面的代码中,前四行显示了关于类声明的文档注释。他们使用了<summary> XML 标签。在字段声明的上方是三行记录字段的代码—同样使用了<summary>标签。
/// <summary> ← Open XML tag for the class. /// This is class MyClass, which does the following wonderful things, using /// the following algorithm. ... Besides those, it does these additional /// amazing things. /// </summary> ← Close XML tag. class MyClass // Class declaration { /// <summary> ← Open XML tag for the field. /// Field1 is used to hold the value of ... /// </summary> ← Close XML tag. public int Field1 = 10; // Field declaration ...
当您在语言功能(如类或类成员)的声明上方键入三个斜杠时,Visual Studio 会自动插入每个 XML 元素。
例如,以下代码显示了类MyClass声明上方的两条斜线:
// class MyClass { ...
只要添加了第三个斜杠,Visual Studio 就会立即将注释扩展为下面的代码,而无需您做任何事情。然后,您可以在标记之间的文档注释行中键入任何内容。
/// <summary> Automatically inserted /// Automatically inserted /// </summary> Automatically inserted class MyClass { ...
使用其他 XML 标签
在前面的例子中,您看到了summary XML 标签的使用。C# 还可以识别许多其他的标签。表 25-2 列出了一些最重要的。
嵌套类型
类型通常直接在命名空间中声明。但是,您也可以在class或struct声明中声明类型。
The type declared in another type declaration is called nested type . Like all type declarations, nested types are templates for type instances.
The nested type is declared as a member of *surrounding type .
- The nesting type can be any type.
- The closure type can be
classorstruct.*
例如,下面的代码显示了类MyClass,以及一个名为MyCounter的嵌套类。
class MyClass // Enclosing class { class MyCounter // Nested class { ... } ... }
如果将一个类型声明为嵌套类型只是为了用作封闭类型的助手,那么这样做通常是有意义的。
不要被术语嵌套所迷惑。嵌套指的是声明的位置——而不是任何实例的内存位置。尽管嵌套类型的声明位于封闭类型的声明内部,但嵌套类型的对象不一定包含在封闭类型的对象中。嵌套类型的对象——如果创建了的话——位于内存中的任何位置,如果它们没有在另一个类型中声明的话。
例如,图 25-4 显示了MyClass和MyCounter类型的对象,如前面的代码所示。图中还显示了一个名为Counter的字段,在类MyClass中,它是对嵌套类的一个对象的引用,该对象位于堆中的其他地方。
图 25-4 。嵌套指的是声明的位置,而不是对象在内存中的位置。
嵌套类的例子
下面的代码将类MyClass和MyCounter充实成一个完整的程序。MyCounter实现一个整数计数器,从 0 开始,可以使用++操作符递增。当调用MyClass的构造函数时,它会创建嵌套类的一个实例,并将引用分配给该字段。图 25-5 说明了代码中对象的结构。
` class MyClass { class MyCounter // Nested class { public int Count { get; private set; }
public static MyCounter operator ++( MyCounter current ) { current.Count++; return current; } }
private MyCounter counter; // Field of nested class type
public MyClass() { counter = new MyCounter(); } // Constructor
public int Incr() { return ( counter++ ).Count; } // Increment method. public int GetValue() { return counter.Count; } // Get counter value. }
class Program { static void Main() { MyClass mc = new MyClass(); // Create object.
mc.Incr(); mc.Incr(); mc.Incr(); // Increment it. mc.Incr(); mc.Incr(); mc.Incr(); // Increment it.
Console.WriteLine( "Total: {0}", mc.GetValue() ); // Print its value. } }`
该代码产生以下输出:
Total: 6
图 25-5 。嵌套类及其封闭类的对象
可见性和嵌套类型
在第七章中,你学习了类和一般类型可以有public或internal的访问级别。然而,嵌套类型是不同的,因为它们有成员可访问性而不是类型可访问性。因此,以下内容适用于嵌套类型:
- Nested types declared in a class can have any one of five accessibility levels of class members, which are
public,protected,private,internalorprotected internal.- Nested types declared inside a structure can have any one of three accessibility levels of structure members
public,internalorprivate.
在这两种情况下,嵌套类型的默认访问级别是private,这意味着在封闭类型之外看不到它。
封闭类和嵌套类的成员之间的关系不那么简单,如图 25-6 中的所示。嵌套类型可以完全访问封闭类型的成员,而不管它们声明的可访问性如何,包括成员private和protected。
然而,这种关系是不对称的。尽管封闭类型的成员总是可以看到嵌套类型声明并创建其变量和实例,但他们没有对嵌套类型成员的完全访问权。相反,它们的访问仅限于嵌套类成员的声明访问,就像嵌套类型是一个单独的类型一样。也就是说,他们可以访问public和internal成员,但不能访问嵌套类型的private或protected成员。
图 25-6 。嵌套类型成员和封闭类型成员之间的可访问性
您可以将这种关系总结如下:
- Members of nested types always have full access to members of closed types.
- A closed type
- Members of can always access the nested type itself.
- Only declared the nested type
成员的访问权限
嵌套类型的可见性也会影响基成员的继承。如果封闭类是派生类,嵌套类型可以隐藏同名的基类成员。和往常一样,在嵌套类的声明中使用new修饰符来使隐藏显式。
嵌套类型中的this引用是指嵌套类型的对象——而不是封闭类型的对象。如果嵌套类型的对象需要访问封闭类型,它必须有一个对它的引用。您可以通过让封闭对象将它的this引用作为参数提供给嵌套类型的构造函数来授予它这种访问权限,如下面的代码所示:
` class SomeClass // Enclosing class { int Field1 = 15, Field2 = 20; // Fields of enclosing class MyNested mn = null; // Reference to nested class
public void PrintMyMembers() { mn.PrintOuterMembers(); // Call method in nested class. }
public SomeClass() // Constructor { mn = new MyNested(this); // Create instance of nested class. } ↑ Pass in the reference to the enclosing class. class MyNested // Nested class declaration { SomeClass sc = null; // Reference to enclosing class
public MyNested(SomeClass SC) // Constructor of the nested class { sc = SC; // Store reference to enclosing class. }
public void PrintOuterMembers() { Console.WriteLine("Field1: {0}", sc.Field1); // Enclosing field Console.WriteLine("Field2: {0}", sc.Field2); // Enclosing field } } // End of nested class }
class Program { static void Main( ) { SomeClass MySC = new SomeClass(); MySC.PrintMyMembers(); } }`
该代码产生以下输出:
Field1: 15 Field2: 20
析构函数和 Dispose 模式
在第六章中,我们看了构造函数,它创建并设置了一个使用的类对象。一个类还可以有一个析构函数,它可以在一个类的实例不再被引用后执行清理或释放非托管资源所需的操作。非托管资源是指使用 Win32 API 获得的文件句柄或非托管内存块。这些东西不是你用就能得到的 .NET 资源,所以如果您坚持使用 .NET 类,你不太可能必须写很多析构函数。
关于析构函数,需要知道的重要事情如下:
每个类只能有一个析构函数。* A constructor cannot have parameters.* A destructor cannot have an accessibility modifier.* The destructor has the same name as the class, but it is preceded by a tilduh character (pronounced TIL-duh).* The destructor only acts on the instance of the class; Therefore, there is no static destructor.* Destructor cannot be explicitly called in code . On the contrary, in the process of garbage collection, when the garbage collector analyzes your code and determines that there are no more paths in the code that may refer to the object, the system will call it.
例如,以下代码说明了名为Class1的类的析构函数的语法:
Class1 { ~Class1() // The destructor { *CleanupCode* } ... }
使用析构函数的一些重要准则如下:
- Don't implement destructors if you don't need them. They can be very expensive in terms of performance.
- The destructor should only release the external resources owned by the object.
- The destructor should not access other objects, because you can't assume that those objects have not been destructed.
注在 3.0 版本发布之前,析构函数有时被称为终结器。你有时可能仍然会在文献和 .NET API 方法名。
标准处置模式
与 C++析构函数不同,当实例超出范围时,不会立即调用 C# 析构函数。事实上,没有办法知道析构函数何时会被调用。此外,如前所述,您不能显式调用析构函数。你所知道的是,在对象从托管堆中移除之前,系统会在某个时候调用它。
如果您的代码包含需要尽快释放的非托管资源,您不应该将该任务留给析构函数,因为不能保证析构函数会很快运行。相反,您应该采用所谓的标准处置模式。
标准处置模式包括以下特征:
- Classes with unmanaged resources should implement the
IDisposableinterface, which consists of a method namedDispose.DisposeContains the cleanup code for releasing resources.- When your code runs out of resources and you want to release them, your program code should call the
Disposemethod. Note thatDisposeis called by your code , not the system. Your class should also implement a destructor that calls theDisposemethod in caseDisposehas not been called before.
这可能有点混乱,所以让我总结一下模式。您希望将所有清理代码放在一个名为Dispose的方法中,当您的代码处理完资源时会调用该方法。作为备份,万一没有调用Dispose,你的类析构函数应该调用Dispose。另一方面,如果调用了Dispose,那么你要告诉垃圾收集器不要调用析构函数,因为清理已经由Dispose处理了。
*你的析构函数和Dispose代码应该遵循以下准则:
- Write the logic of your destructor and
Disposemethod, so that if your code cannot callDisposefor some reason, your destructor will call it, thus releasing resources.- At the end of the
Disposemethod, it should be a call to theGC.SuppressFinalizemethod, which tells CLR not to call the destructor of this object, because the cleaning has been completed.- Implement the code in
Disposeso that it is safe for the method to be called many times. That is to say, if it is written in this way, if it has been called, then any subsequent calls will not do any extra work and will not throw an exception.
下面的代码显示了标准的 dispose 模式,如图 25-7 所示。关于代码的重要内容如下:
Disposemethod has two overloads:publicmethod andprotectedmethod.protectedOverload is an overload that contains actual cleanup code.- The
publicversion is the version that you will explicitly call from your code to perform cleanup. In turn, it calls theprotectedversion.- The destructor calls
protectedversion.- The
boolparameter of the protected version allows the method to know where it was called-destructor or elsewhere in the code. This is very important for it, because according to what it is, it will do something slightly different. You can find the details in the code below.
图 25-7 。标准处置模式
比较构造函数和析构函数
表 25-3 提供了构造函数和析构函数被调用的总结和比较。
与 COM 互操作
虽然本文没有涉及 COM 编程,但是 C# 4.0 有几个语言特性可以使 COM 编程变得更容易。其中之一是省略引用特性,当你不需要使用方法传回的值时,它允许你调用一个 COM 方法而不使用ref关键字。
例如,如果运行程序的计算机上安装了 Microsoft Word,则可以在自己的程序中使用 Word 的拼写检查功能。您将使用的方法是在Document类上的CheckSpelling方法,它在Microsoft.Office.Tools.Word名称空间中。这个方法有 12 个参数,都是ref参数。如果没有这个特性,您必须为每个参数提供引用变量,即使您不需要使用它们将数据传递给方法或从方法接收数据。省略ref关键字仅适用于 COM 方法——对于其他任何方法,您仍然会得到一个编译错误。
这段代码可能类似于下面所示的代码。请注意以下关于此代码的内容:
- The call on the fourth line only uses the second and third parameters, both of which are Boolean values. However, since this method requires
refparameter, you must create two variablesignoreCaseandalwaysSuggestof typeobjectto save these values.- The third line creates a variable
optionalof typeobjectfor the other ten parameters.
object ignoreCase = true; object alwaysSuggest = false; Objects to hold Boolean variables object optional = Missing.Value; <ins> ↓ </ins> <ins> ↓ </ins> tempDoc.CheckSpelling( ref optional, ref ignoreCase, ref alwaysSuggest, ref optional, ref optional, ref optional, ref optional, ref optional, ref optional, ref optional, ref optional, ref optional );
有了省略引用特性,我们可以很好地解决这个问题,因为我们不必对那些不需要输出的参数使用ref关键字,我们可以对我们关心的两个参数使用内联bool s。简化的代码如下所示:
bool bool object optional = Missing.Value; ↓ ↓ tempDoc.CheckSpelling( optional, true, false, optional, optional, optional, optional, optional, optional, optional, optional, optional );
但是我们也可以使用可选参数特性。同时使用这两个特性(省略 ref 和可选参数)会使最终表单比原始表单简单得多:
tempDoc.CheckSpelling( Missing.Value, true, false );
下面的代码在一个完整的程序中包含了这个方法。要编译这段代码,您需要在您的计算机上安装 Visual Studio Tools for Office(VSTO ),并且您必须在项目中添加对Microsoft.Office.Interop.Word程序集的引用。要运行编译后的代码,还必须在计算机上安装 Microsoft Word。
` using System; using System.Reflection; using Microsoft.Office.Interop.Word;
class Program { static void Main() { Console.WriteLine( "Enter a string to spell-check:" ); string stringToSpellCheck = Console.ReadLine();
string spellingResults; int errors = 0; if ( stringToSpellCheck.Length == 0 ) spellingResults = "No string to check"; else { Microsoft.Office.Interop.Word.Application app = new Microsoft.Office.Interop.Word.Application();
Console.WriteLine( "\nChecking the string for misspellings ..." ); app.Visible = false;
Microsoft.Office.Interop.Word._Document tempDoc = app.Documents.Add( );
tempDoc.Words.First.InsertBefore( stringToSpellCheck ); Microsoft.Office.Interop.Word.ProofreadingErrors spellErrorsColl = tempDoc.SpellingErrors; errors = spellErrorsColl.Count;
//1. Without using optional parameters //object ignoreCase = true; //object alwaysSuggest = false; //object optional = Missing.Value; //tempDoc.CheckSpelling( ref optional, ref ignoreCase, ref alwaysSuggest, // ref optional, ref optional, ref optional, ref optional, ref optional, // ref optional, ref optional, ref optional, ref optional );
//2. Using the "omit ref" feature object optional = Missing.Value; tempDoc.CheckSpelling( optional, true, false, optional, optional, optional, optional, optional, optional, optional, optional, optional );
//3. Using "omit ref" and optional parameters
//tempDoc.CheckSpelling( Missing.Value, true, false );
app.Quit(false); spellingResults = errors + " errors found"; }
Console.WriteLine( spellingResults ); Console.WriteLine( "\nPress to exit program." ); Console.ReadLine(); } }`
当您运行这段代码时,它会产生如图 25-8 中所示的控制台窗口,要求您输入想要通过拼写检查器运行的字符串。当它收到字符串时,它打开 Word 并对其运行拼写检查。这时,你会看到 Word 的拼写检查窗口出现,如图图 25-9 所示。
***图 25-8。*要求将字符串发送到 Word 拼写检查器的控制台窗口
图 25-9。 Word 的拼写检查器使用控制台程序的 COM 调用创建*