编写一个函数方法输入参数为python input函数,startStr,endStr三个,返回值为一个字符串数组

网站防火墙
网站防火墙
您的请求过于频繁,已被网站管理员设置拦截!
可能原因:您对该页面的访问过于频繁
如何解决:
1)稍等一段时间重新访问;
2)如网站托管,请联系空间提供商;
3)普通网站访客,清理浏览器Cookie之后重新访问,或直接联系网站管理员;3. (Java)写一个方法,参数为字符串,实现对该字符串的反转,返回反转后的字符串。如abc,反转后为cba。_百度知道
3. (Java)写一个方法,参数为字符串,实现对该字符串的反转,返回反转后的字符串。如abc,反转后为cba。
我有更好的答案
完整代码如下:public class TestString {
public static String reverse(String s){
char []c=new char[s.length()];
for(int i=0;i&s.length();i++)
c[i]=s.charAt(s.length()-1-i);
return String.valueOf(c);
} public static void main(String[] args) {
String s=&abc&;
s=reverse(s);
System.out.println(s); }}运行结果;cba有什么疑问可追问或者hi我
采纳率:54%
可用递归实现//s:表示要操作的字符串,size:表示字符串大小public void
writeBackward(String s,int size){
if(size&0)
System.out.println(s.substring(size-1,size));
writeBackward(s,size-1);
首先给你介绍一个类和一个方法,如下:StringBulider类就像一个可变长度的字符串数组一样,内容和长度都可以被改变,既方便又节省开销。reverse()翻转StringBulider对象中的字符串。public class Main{ public static void main(String args[]) throws Exception{
String array=&海阔凭鱼跃,天高任鸟飞&;
StringBuilder sb=new StringBuilder(array);
sb.reverse();
System.out.println(sb);}}
import java.util.*;public class Main{ public static void main(String args[]) throws Exception{
Scanner in = new Scanner(System.in);
String str = in.nextLine();
//输入一个字符串
char c[] = new char[str.length()];
//定义一个字符数组
for(int i=0;i&str.length();i++){
c[i] = str.charAt(i);
for(int i=c.length-1;i&=0;i--)
//循环输出
System.out.print(c[i]);
//从倒数第二个字符,向前输出 }}
public String hlh(String str){String str1=&&;for(char a:str.toCharArray()){str1=c+str1;}return str1;}
public class a{public static void main{String str=&abc&;StringBuffer sb = new StringBuffer(str);sb.reverse();str=new String(sb);System.out.println(str);}}}这是最简单的办法还可以用toCharArray 转换成char数组然后在反转
其他4条回答
为您推荐:
其他类似问题
字符串的相关知识
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。用java 编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串。但是要保_百度知道
用java 编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串。但是要保
用java 编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串。但是要保证汉字不被截半个,如&我ABC&4,应该截为&我AB&,输入&我ABC汉DEF&,6,应该输出为&我ABC&而不是&我ABC+汉的半个&
我有更好的答案
package&com.&&&&/**&&*&10、&编写一个截取字符串的函数,输入为一个字符串和字节数,输出为按字节截取的字符串。&&*&但是要保证汉字不被截半个,如“我ABC”4,应该截为“我AB”,输入“我ABC汉DEF”,6,应该输出为“我ABC”而不是“我ABC+汉的半个”。&&*&&&*&@author&&&*/&&&&public&class&Test10&{&&&&&&&&public&static&void&main(String[]&args)&{&&&&&&&&&&String&srcStr1&=&&我ABC&;&&&&&&&&&&String&srcStr2&=&&我ABC汉DEF&;&&&&&&&&&&&&splitString(srcStr1,&4);&&&&&&&&&&splitString(srcStr2,&6);&&&&&&}&&&&&&&&public&static&void&splitString(String&src,&int&len)&{&&&&&&&&&&int&byteNum&=&0;&&&&&&&&&&&&if&(null&==&src)&{&&&&&&&&&&&&&&System.out.println(&The&source&String&is&null!&);&&&&&&&&&&&&&&&&&&&&&&&&}&&&&&&&&&&&&byteNum&=&src.length();&&&&&&&&&&byte&bt[]&=&src.getBytes();&//&将String转换成byte字节数组&&&&&&&&&&&&if&(len&&&byteNum)&{&&&&&&&&&&&&&&len&=&byteN&&&&&&&&&&}&&&&&&&&&&&&//&判断是否出现了截半,截半的话字节对于的ASC码是小于0的值&&&&&&&&&&if&(bt[len]&&&0)&{&&&&&&&&&&&&&&String&subStrx&=&new&String(bt,&0,&--len);&&&&&&&&&&&&&&System.out.println(&subStrx==&&+&subStrx);&&&&&&&&&&}&else&{&&&&&&&&&&&&&&String&subStrx&=&new&String(bt,&0,&len);&&&&&&&&&&&&&&System.out.println(&subStrx==&&+&subStrx);&&&&&&&&&&}&&&&&&}&&&&}ps:看比人博客上面的,具体的没操作过,
这个没有用
我要的是输入
不是直接赋值
采纳率:85%
这个方法有个前提
必须是GBK编码
因为只有GBK 汉子才是2个字节 UTF-8就是3个字节public static String buStr(String str,int l) {
byte b[] = str.getBytes();
byte[l+1];
for(int i=0;i&b.i++){
a[i]=b[i];
if((b[i] & 0 || b[i] &127)&&i==l){//截取末位的汉子
System.out.println(b[i]+&:我是非字符&);
a[l]=b[i];
return new String(a); }
  字符串操作优化  字符串对象  字符串对象或者其等价对象 (如 char 数组),在内存中总是占据最大的空间块,因此如何高效地处理字符串,是提高系统整体性能的关键。  String 对象可以认为是 char 数组的延伸和进一步封装,它主要由 3 部分组成:char 数组、偏移量和 String 的长度。char 数组表示 String 的内容,它是 String 对象所表示字符串的超集。String 的真实内容还需要由偏移量和长度在这个 char 数组中进行定位和截取。  String 有 3 个基本特点:  1. 不变性;  2. 针对常量池的优化;  3. 类的 final 定义。  不变性指的是 String 对象一旦生成,则不能再对它进行改变。String 的这个特性可以泛化成不变 (immutable) 模式,即一个对象的状态在对象被创建之后就不再发生变化。不变模式的主要作用在于当一个对象需要被多线程共享,并且访问频繁时,可以省略同步和锁等待的时间,从而大幅提高系统性能。  针对常量池的优化指的是当两个 String 对象拥有相同的值时,它们只引用常量池中的同一个拷贝,当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。  下面代码 str1、str2、str4 引用了相同的地址,但是 str3 却重新开辟了一块内存空间,虽然 str3 单独占用了堆空间,但是它所指向的实体和 str1 完全一样。代码如下清单 1 所示。  清单 1. 示例代码  输出如清单 2 所示。  清单 2. 输出结果  SubString 使用技巧  String 的 substring 方法源码在最后一行新建了一个 String 对象,new String(offset+beginIndex,endIndex-beginIndex,value);该行代码的目的是为了能高效且快速地共享 String 内的 char 数组对象。但在这种通过偏移量来截取字符串的方法中,String 的原生内容 value 数组被复制到新的子字符串中。设想,如果原始字符串很大,截取的字符长度却很短,那么截取的子字符串中包含了原生字符串的所有内容,并占据了相应的内存空间,而仅仅通过偏移量和长度来决定自己的实际取值。这种算法提高了速度却浪费了空间。  下面代码演示了使用 substring 方法在一个很大的 string 独享里面截取一段很小的字符串,如果采用 string 的 substring 方法会造成内存溢出,如果采用反复创建新的 string 方法可以确保正常运行。  清单 3.substring 方法演示  输出结果如清单 4 所示。  清单 4. 输出结果  ImprovedHugeStr 可以工作是因为它使用没有内存泄漏的 String 构造函数重新生成了 String 对象,使得由 substring() 方法返回的、存在内存泄漏问题的 String 对象失去所有的强引用,从而被垃圾回收器识别为垃圾对象进行回收,保证了系统内存的稳定。  String 的 split 方法支持传入正则表达式帮助处理字符串,但是简单的字符串分割时性能较差。  对比 split 方法和 StringTokenizer 类的处理字符串性能,代码如清单 5 所示。  切分字符串方式讨论  String 的 split 方法支持传入正则表达式帮助处理字符串,操作较为简单,但是缺点是它所依赖的算法在对简单的字符串分割时性能较差。清单 5 所示代码对比了 String 的 split 方法和调用 StringTokenizer 类来处理字符串时性能的差距。  清单 5.String 的 split 方法演示  输出如清单 6 所示:  清单 6. 运行输出结果  当一个 StringTokenizer 对象生成后,通过它的 nextToken() 方法便可以得到下一个分割的字符串,通过 hasMoreToken 方法可以知道是否有更多的字符串需要处理。对比发现 split 的耗时非常的长,采用 StringTokenizer 对象处理速度很快。我们尝试自己实现字符串分割算法,使用 substring 方法和 indexOf 方法组合而成的字符串分割算法可以帮助很快切分字符串并替换内容。  由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。  以上实例运行结果差异较大的原因是 split 算法对每一个字符进行了对比,这样当字符串较大时,需要把整个字符串读入内存,逐一查找,找到符合条件的字符,这样做较为耗时。而 StringTokenizer 类允许一个应用程序进入一个令牌(tokens),StringTokenizer 类的对象在内部已经标识化的字符串中维持了当前位置。一些操作使得在现有位置上的字符串提前得到处理。 一个令牌的值是由获得其曾经创建 StringTokenizer 类对象的字串所返回的。  清单 7.split 类源代码  split 借助于数据对象及字符查找算法完成了数据分割,适用于数据量较少场景。  合并字符串  由于 String 是不可变对象,因此,在需要对字符串进行修改操作时 (如字符串连接、替换),String 对象会生成新的对象,所以其性能相对较差。但是 JVM 会对代码进行彻底的优化,将多个连接操作的字符串在编译时合成一个单独的长字符串。针对超大的 String 对象,我们采用 String 对象连接、使用 concat 方法连接、使用 StringBuilder 类等多种方式,代码如清单 8 所示。  清单 8. 处理超大 String 对象的示例代码  虽然第一种方法编译器判断 String 的加法运行成 StringBuilder 实现,但是编译器没有做出足够聪明的判断,每次循环都生成了新的 StringBuilder 实例从而大大降低了系统性能。  StringBuffer 和 StringBuilder 都实现了 AbstractStringBuilder 抽象类,拥有几乎相同的对外借口,两者的最大不同在于 StringBuffer 对几乎所有的方法都做了同步,而 StringBuilder 并没有任何同步。由于方法同步需要消耗一定的系统资源,因此,StringBuilder 的效率也好于 StringBuffer。 但是,在多线程系统中,StringBuilder 无法保证线程安全,不能使用。代码如清单 10 所示。  清单 10.StringBuilderVSStringBuffer  StringBuilder 数据并没有按照预想的方式进行操作。StringBuilder 和 StringBuffer 的扩充策略是将原有的容量大小翻倍,以新的容量申请内存空间,建立新的 char 数组,然后将原数组中的内容复制到这个新的数组中。因此,对于大对象的扩容会涉及大量的内存复制操作。如果能够预先评估大小,会提高性能。  数据定义、运算逻辑优化  使用局部变量  调用方法时传递的参数以及在调用中创建的临时变量都保存在栈 (Stack) 里面,读写速度较快。其他变量,如静态变量、实例变量等,都在堆 (heap) 中创建,读写速度较慢。清单 12 所示代码演示了使用局部变量和静态变量的操作时间对比。  清单 12. 局部变量 VS 静态变量  以上两段代码的运行时间分别为 0ms 和 15ms。由此可见,局部变量的访问速度远远高于类的成员变量。  位运算代替乘除法  位运算是所有的运算中最为高效的。因此,可以尝试使用位运算代替部分算数运算,来提高系统的运行速度。最典型的就是对于整数的乘除运算优化。清单 14 所示代码是一段使用算数运算的实现。  清单 14. 算数运算  两段代码执行了完全相同的功能,在每次循环中,整数 1000 乘以 2,然后除以 2。第一个循环耗时 546ms,第二个循环耗时 63ms。  替换 switch  关键字 switch 语句用于多条件判断,switch 语句的功能类似于 if-else 语句,两者的性能差不多。但是 switch 语句有性能提升空间。清单 16 所示代码演示了 Switch 与 if-else 之间的对比。  清单 16.Switch 示例  运行输出如清单 17 所示。  清单 17. 运行结果  1  2    172  93    使用一个连续的数组代替 switch 语句,由于对数据的随机访问非常快,至少好于 switch 的分支判断,从上面例子可以看到比较的效率差距近乎 1 倍,switch 方法耗时 172ms,if-else 方法耗时 93ms。  一维数组代替二维数组  JDK 很多类库是采用数组方式实现的数据存储,比如 ArrayList、Vector 等,数组的优点是随机访问性能非常好。一维数组和二维数组的访问速度不一样,一维数组的访问速度要优于二维数组。在性能敏感的系统中要使用二维数组,尽量将二维数组转化为一维数组再进行处理,以提高系统的响应速度。  清单 18. 数组方式对比
1条折叠回答
为您推荐:
其他类似问题
换一换
回答问题,赢新手礼包
个人、企业类
违法有害信息,请在下方选择后提交
色情、暴力
我们会通过消息、邮箱等方式尽快将举报结果通知您。编写最快捷简单的技术分享博文!欢迎关注我的个人微信公众号:时光墓碑
关于JS截取字符串以及截取数组项的几种常见方法解析
js字符串数组截取操作常用方法综合解析。1.substr()
substr() 方法可在字符串中抽取从 start 下标开始的指定数目的字符。
语法:stringObject.substr(start,length)
start:必需。要抽取的子串的起始下标。必须是数值。如果是负数,那么该参数声明从字符串的尾部开始算起的位置。也就是说,0是指字符串中第一个字符,-1 指字符串中最后一个字符,-2 指倒数第二个字符,以此类推。
length:可选。子串中的字符数。必须是数值。如果省略了该参数,那么返回从 stringObject 的开始位置到结尾的字串。
返回值:返回一个新的字符串。
var str="Hello world!"
document.write(str.substr(3,7))
输出:lo worl
提示:空格也占字符位!
2.substring()
substring() 方法用于提取字符串中介于两个指定下标之间的字符。
语法:stringObject.substring(start,stop)
start:必需。一个非负的整数,规定要提取的子串的第一个字符在 stringObject 中的位置。
stop:可选。一个非负的整数,比要提取的子串的最后一个字符在 stringObject 中的位置多 1。如果省略该参数,那么返回的子串会一直到字符串的结尾。
返回值:返回一个新的字符串,该字符串值包含stringObject 的一个子字符串,其内容是从 start 处到 stop-1 处的所有字符,其长度为 stop 减 start。
var str="Hello world!"
document.write(str.substring(3,7));
输出:lo w
提示:substring()不接受负值参数。
slice() 方法可从已有的数组中返回选定的元素。
语法:arrayObject.slice(start,end)
start:必需。规定从何处开始选取。如果是负数,那么它规定从数组尾部开始算起的位置。也就是说,-1 指最后一个元素,-2 指倒数第二个元素,以此类推。
end:可选。规定从何处结束选取。该参数是数组片断结束处的数组下标。如果没有指定该参数,那么切分的数组包含从 start 到数组结束的所有元素。如果这个参数是负数,那么它规定的是从数组尾部开始算起的元素。
返回值:返回一个新的数组,包含从 start 到 end (不包括该元素)的 arrayObject 中的元素。
var arr = new Array(6)
arr[0] = "George"
arr[1] = "John"
arr[2] = "Thomas"
arr[3] = "James"
arr[4] = "Adrew"
arr[5] = "Martin"
document.write(arr + "&br /&")
document.write(arr.slice(2,4) + "&br /&")
document.write(arr)
George,John,Thomas,James,Adrew,Martin
Thomas,James
George,John,Thomas,James,Adrew,Martin
提示:该方法并不会修改数组,而是返回一个子数组。如果想删除数组中的一段元素,应该使用方法Array.splice()。
4.splice()
splice() 方法用于插入、删除或替换数组的元素。
语法:arrayObject.splice(index,howmany,element1,.....,elementX)
index :必需。规定从何处添加/删除元素。该参数是开始插入和(或)删除的数组元素的下标,必须是数字。
howmany:必需。规定应该删除多少元素。必须是数字,但可以是 "0"。如果未规定此参数,则删除从 index 开始到原数组结尾的所有元素。
element1:可选。规定要添加到数组的新元素。从 index 所指的下标处开始插入。
elementX:可选。可向数组添加若干元素。
返回值:如果从 arrayObject 中删除了元素,则返回的是含有被删除的元素的数组。
var arr = new Array(6)
arr[0] = "George"
arr[1] = "John"
arr[2] = "Thomas"
arr[3] = "James"
arr[4] = "Adrew"
arr[5] = "Martin"
document.write(arr + "&br /&")
arr.splice(2,1,"William")
document.write(arr)
George,John,Thomas,James,Adrew,Martin
George,John,William,James,Adrew,Martin
提示:splice() 方法可删除从 index 处开始的零个或多个元素,并且用参数列表中声明的一个或多个值来替换那些被删除的元素。
5.replace()
replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
语法:stringObject.replace(regexp/substr,replacement)
regexp/substr:必需。规定子字符串或要替换的模式的 RegExp 对象。请注意,如果该值是一个字符串,则将它作为要检索的直接量文本模式,而不是首先被转换为 RegExp 对象。
replacement:必需。一个字符串值。规定了替换文本或生成替换文本的函数。
返回值:一个新的字符串,是用 replacement 替换了 regexp 的第一次匹配或所有匹配之后得到的。
var str="Visit Microsoft!"
document.write(str.replace(/Microsoft/, "W3School"));
输出:Visit W3School!
提示:字符串 stringObject 的 replace() 方法执行的是查找并替换的操作。它将在 stringObject 中查找与 regexp 相匹配的子字符串,然后用 replacement 来替换这些子串。如果 regexp 具有全局标志 g,那么 replace() 方法将替换所有匹配的子串。否则,它只替换第一个匹配子串。
replacement 可以是字符串,也可以是函数。如果它是字符串,那么每个匹配都将由字符串替换。但是 replacement 中的 $ 字符具有特定的含义。如下表所示,它说明从模式匹配得到的字符串将用于替换。
$1、$2、...、$99
与 regexp 中的第 1 到第 99 个子表达式相匹配的文本。
与 regexp 相匹配的子串。
位于匹配子串左侧的文本。
位于匹配子串右侧的文本。
直接量符号。
注意:ECMAScript v3 规定,replace() 方法的参数 replacement 可以是函数而不是字符串。在这种情况下,每个匹配都调用该函数,它返回的字符串将作为替换文本使用。该函数的第一个参数是匹配模式的字符串。接下来的参数是与模式中的子表达式匹配的字符串,可以有 0 个或多个这样的参数。接下来的参数是一个整数,声明了匹配在 stringObject 中出现的位置。最后一个参数是 stringObject 本身。
有任何交流和指教,请联系:http://www.yinmu.me
没有更多推荐了,
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!第 1 章 类和泛型
第 1 章 类和泛型
本章的范例涵盖了 C# 语言的基础,主题包括类和结构,如何使用它们,它们有哪些不同,何时使用类以及何时使用结构。在此基础上,我们将构建具有各种固有功能(如可排序、可搜索、可处理和可克隆)的类。此外,我们将深入讨论联合类型、字段初始化、lambda、局部方法、单路和多路广播委托、闭包、函数对象等主题。本章也包含了解析命令行参数的范例,这是开发人员一直喜爱的主题。
在开始展示这些范例之前,让我们回顾一下关于类、结构、泛型的面向对象能力的关键信息。类比结构灵活得多。结构可以跟类一样实现接口,但与类不同的是,它们不能继承自类或结构。这种限制使得你无法创建结构层次关系,而这用类可以做到。通过抽象基类实现的多态性也是在结构中无法使用的,因为除了装箱成 Object、ValueType 和 Enum,结构无法从另一个类派生。
结构与其他的值类型一样,都是从 System.ValueType 隐式派生的。乍看之下,结构类似于类,但它们实际上有很大的差别。在设计应用程序时,知道何时使用结构优于使用类将对你有很大的帮助。不正确地使用结构可能会使代码性能低下、难以修改。
结构相对于引用类型有两个性能优势。首先,如果一个结构是在栈上分配的(即不包含在引用类型内),访问结构及其数据的速度要快于访问堆中引用类型的速度。
引用类型的对象必须要跟随它在堆上的引用以获取它们的数据。不过,这种性能优势相对于结构的第二个性能优势就相形见绌了:要清理在栈上为结构分配的内存,只需要在方法调用返回时修改栈指针所指向的地址即可。这个调用要远远快于垃圾回收器自动清理托管堆上分配的引用类型。然而垃圾回收器的成本是延后的,所以不会立刻被人注意到。
当以传值方式传入其他方法时,结构的性能就比不上类了。因为结构存在于栈上,当以传值方式传入一个方法时,结构及其数据必须复制到一个新的局部变量(方法用于接收结构的参数)中。这一复制过程相比将一个引用传入方法要花费更多的时间,除非结构的大小与机器的指针大小相同或更小一些;因此,在 32 位的机器上,传入一个 32 位大小的结构与传入一个引用(与指针大小相同)的成本是相同的。在类和结构之间选择时,要记得这一点。尽管创建、访问和销毁类对象可能需要更长时间,但并不能抵消将结构多次按值传入一个或多个方法产生的性能下降。保持较小的结构体可以减小按值传递时所产生的性能下降。
以下情况应使用类。
其同一性很重要。结构在按值传入方法时会被隐式复制。
有较大的内存占用。
其字段需要初始化。
需要从一个基类继承。
需要多态行为;也就是说,你需要实现一个抽象基类,并从此基类派生出多个相似的类。(注意,多态性也可以通过接口实现,但通常并不适合在一个值类型中实现接口。这是因为当结构转换为接口时,会因装箱操作而导致性能损失。)
以下情况应使用结构。
其行为方式类似于原语类型(int、long、byte 等)。
仅占用较小的内存。
调用一个需要将结构体以传值方式传入的 P/Invoke 方法。平台调用(Platform Invoke,P/Invoke)允许托管代码调用 DLL 内公开的非托管方法。许多时候,非托管 DLL 内的方法都需要传入一个结构参数。使用结构是执行此操作的一种高效方法,并且在需要按值传入时是唯一的途径。
需要降低垃圾回收对应用程序性能的影响。
其字段只需要被初始化为默认值。对于数值类型,这个值为 0;对于布尔类型,则为 false;对于引用类型,则为 null。注意在 C# 6.0 中,结构可以拥有默认构造函数并将字段初始化为非默认值。
不需要继承一个基类(除了 ValueType 之外,所有结构都继承它)。
不需要多态行为。
当把结构传递给需要一个对象参数的方法时,例如 Framework 类库(FCL)中的任何非泛型集合类型,它们也可能会引起性能降低。把一个结构(对此问题而言其实是任何简单类型)传入一个需要对象参数的方法中将会导致结构被装箱。装箱(boxing)是指将一个值类型包装在一个对象中。这种操作比较耗时,并且可能导致性能降低。
最后,将泛型功能加入进来就能够编写类型安全且高效的基于集合和模式的代码了。泛型提供相当强大的编程能力,但是要求你正确使用它。如果你考虑把 ArrayList、Queue、Stack 和 Hashtable 对象转换成其对应的泛型对象,可以阅读一下 1.9 节和 1.10 节中的范例。你将看到这种转换并非总是很简单,有一些原因可能导致你根本不想执行这种转换。
1.1 创建联合类型的结构
1.1.1 问题
你需要创建一种数据类型,其行为方式类似于 C++ 中的联合类型。联合类型主要用于互操作场景,其中非托管代码接受和 / 或返回一个联合类型;我们建议你不要在其他情况下使用它。
1.1.2 解决方案
使用一个结构,并用 StructLayout 特性标记它(在构造函数中指定 LayoutKind.Explicit 布局类型)。此外,利用 FieldOffset 特性标记结构中的每个字段。下面的结构定义了一个联合类型,其中可以存储一个带符号数值。
using System.Runtime.IteropS
[StructLayoutAttribute(LayoutKind.Explicit)]
struct SignedNumber
[FieldOffsetAttribute(0)]
public sbyte Num1;
[FieldOffsetAttribute(0)]
public short Num2;
[FieldOffsetAttribute(0)]
public int Num3;
[FieldOffsetAttribute(0)]
public long Num4;
[FieldOffsetAttribute(0)]
public float Num5;
[FieldOffsetAttribute(0)]
public double Num6;
下一个结构类似于 SignedNumber 结构,不同之处是除了带符号的数值之外,它还可以包含 String 类型。
[StructLayoutAttribute(LayoutKind.Explicit)]
struct SignedNumberWithText
[FieldOffsetAttribute(0)]
public sbyte Num1;
[FieldOffsetAttribute(0)]
public short Num2;
[FieldOffsetAttribute(0)]
public int Num3;
[FieldOffsetAttribute(0)]
public long Num4;
[FieldOffsetAttribute(0)]
public float Num5;
[FieldOffsetAttribute(0)]
public double Num6;
[FieldOffsetAttribute(16)]
public string Text1;
1.1.3 讨论
联合类型是一种在 C++ 代码中较为常见的结构类型;不过,有一种方式可以使用 C# 中的结构数据类型来复制其结构。联合(union)是一种结构,在内存中的特定位置为该结构接受多种类型。例如,SignedNumber 结构是使用 C# 结构创建的一个联合类型的结构。这种结构可以接受任何类型的带符号的数值类型(sbyte、int 和 long 等),但它只在结构中的同一个位置(同一偏移量)接受这种数字类型。
 由于 StructLayoutAttribute 可以同时应用于结构和类,在创建联合数据类型时也可以使用类。
注意 FieldOffsetAttribute 将值 0 传递给它的构造函数。这表明这个字段距离结构开始处的偏移量为 0 字节。可以将这个特性与 StructLayoutAttribute 结合使用,手动强制指定结构中的字段开始于什么位置(即每个字段在内存中相对于这个结构开始处的偏移量)。FieldOffsetAttribute 只能与设置为 LayoutKind.Explicit 的 StructLayoutAttribute 一起使用。此外,它不能用于结构内的静态成员。
联合类型可能会带来一些问题,因为几种类型实质上是相互叠加在一起的。最大的问题是如何从联合类型结构中提取正确的数据类型。思考一下,如果你选择在 SignedNumber 结构中存储 long 数值类型的值 long.MaxValue,会发生什么情况。随后,你可能会偶然尝试从这个结构中提取一个 byte 数据类型值。这样操作,你将会只取回这个 long 值中的第一字节。
另一个问题是在正确的偏移位置开始字段。SignedNumberWithText 联合类型在偏移量为 0 的位置叠加了大量带符号的数值数据类型。这个结构中的最后一个字段位于内存中距离这个结构开始处偏移量为 16 字节的位置。如果你意外地把字符串字段 Text1 覆盖在任何其他带符号的数值数据类型之上,在运行时将得到一个异常。基本规则是:允许你把一种值类型叠加在另一种值类型之上,但是不能把一种引用类型叠加于一种值类型之上。如果用以下特性标记 Text1 字段:
[FieldOffsetAttribute(14)]
就会在运行时引发下面这个异常(注意,编译器不会捕获这个问题)。
System.TypeLoadException: Could not load type 'SignedNumberWithText' from
assembly 'CSharpRecipes, Version=1.0.0.0, Culture=neutral,
PublicKeyToken=fe85c3941fbcc4c5' because it contains an object field at
offset 14 that is incorrectly aligned or overlapped by a non-object field.
在 C# 中使用复杂的联合类型时,必须保证正确的偏移量。
1.1.4 参考
MSDN 文档中的“StructLayoutAttribute 类”主题。
1.2 使类型可排序
1.2.1 问题
你有一种数据类型,它将存储为 List&T& 或 SortedList&K,V& 的元素。你想使用 List&T&.Sort 方法或者 SortedList&K,V& 的内部排序机制来自定义此数据类型在数组中的排序方式。此外,你可能需要在 SortedList 集合中使用这种类型。
1.2.2 解决方案
例 1-1 演示了如何实现 IComparable&T& 接口。例 1-1 中展示的 Square 类实现了这个接口,使得 List&T& 和 SortedList&K,V& 集合能够排序和查找这些 Square 对象。
例 1-1:通过实现 IComparable&T& 使类型可排序
public class Square : IComparable&Square&
public Square(){}
public Square(int height, int width)
this.Height =
this.Width =
public int Height { }
public int Width { }
public int CompareTo(object obj)
Square square = obj as S
if (square != null)
return CompareTo(square);
new ArgumentException(
"Both objects being compared must be of type Square.");
public override string ToString()=&
($"Height: {this.Height}
Width: {this.Width}");
public override bool Equals(object obj)
if (obj == null)
Square square = obj as S
if(square != null)
return this.Height == square.H
public override int GetHashCode()
return this.Height.GetHashCode() | this.Width.GetHashCode();
public static bool operator ==(Square x, Square y) =& x.Equals(y);
public static bool operator !=(Square x, Square y) =& !(x == y);
public static bool operator &(Square x, Square y) =& (x.CompareTo(y) & 0);
public static bool operator &(Square x, Square y) =& (x.CompareTo(y) & 0);
public int CompareTo(Square other)
long area1 = this.Height * this.W
long area2 = other.Height * other.W
if (area1 == area2)
else if (area1 & area2)
else if (area1 & area2)
return -1;
return -1;
1.2.3 讨论
通过在类(或结构)上实现 IComparable&T& 接口,就可以利用 List&T& 和 SortedList&K,V& 类的排序例程。排序算法内置在这些类中;你只需要通过在 IComparable&T&.CompareTo 方法中实现的代码告诉它们如何对你的类进行排序即可。
当调用 List&Square&.Sort 方法对 Square 对象的列表进行排序时,列表是通过 Square 对象的 IComparable&Square& 接口进行排序的。当把对象添加到 SortedList&K,V& 中时,SortedList&K,V& 类的 Add 方法使用这个接口对它们进行排序。
IComparer&T& 设计用于解决如下问题:允许基于不同环境中的不同标准对对象进行排序。这个接口还允许你对其他人编写的类型进行排序。如果你还想按高度对 Square 对象进行排序,就可以创建一个名为 CompareHeight 的新类,如例 1-2 中所示。它也实现了 IComparer&Square& 接口。
例 1-2:通过实现 IComparer 使类型可排序
public class CompareHeight : IComparer&Square&
public int Compare(object firstSquare, object secondSquare)
Square square1 = firstSquare as S
Square square2 = secondSquare as S
if (square1 == null || square2 == null)
throw (new ArgumentException("Both parameters must be of type Square."));
return Compare(firstSquare,secondSquare);
#region IComparer&Square& Members
public int Compare(Square x, Square y)
if (x.Height == y.Height)
else if (x.Height & y.Height)
else if (x.Height & y.Height)
return -1;
return -1;
#endregion
然后将这个类传入 Sort 方法的 IComparer 参数。现在你可以指定以不同的方式对 Square 对象进行排序。比较器中实现的比较方法必须保持一致并应用全局排序,从而使得比较函数声明两个数据项相等时绝对正确,而不是以下情况的结果:一个数据项不大于另一个数据项或者一个数据项不小于另一个数据项。
 为了获得最佳性能,需要保持 CompareTo 方法短小、高效,因为它将被 Sort 方法调用多次。例如,在对含有 4 个数据项的数组排序时,Compare 方法将被调用 10 次。
例 1-3 中展示的 TestSort 方法演示了如何对 List&Square& 和 SortedList&int,Square& 实例使用 Square 和 CompareHeight 类。
例 1-3:TestSort 方法
public static void TestSort()
List&Square& listOfSquares = new List&Square&(){
new Square(1,3),
new Square(4,3),
new Square(2,1),
new Square(6,1)};
// 测试List&String&
Console.WriteLine("List&String&");
Console.WriteLine("Original list");
foreach (Square square in listOfSquares)
Console.WriteLine(square.ToString());
Console.WriteLine();
IComparer&Square& heightCompare = new CompareHeight();
listOfSquares.Sort(heightCompare);
Console.WriteLine("Sorted list using IComparer&Square&=heightCompare");
foreach (Square square in listOfSquares)
Console.WriteLine(square.ToString());
Console.WriteLine();
Console.WriteLine("Sorted list using IComparable&Square&");
listOfSquares.Sort();
foreach (Square square in listOfSquares)
Console.WriteLine(square.ToString());
// 测试SortedList
var sortedListOfSquares = new SortedList&int,Square&(){
{ 0, new Square(1,3)},
{ 2, new Square(3,3)},
{ 1, new Square(2,1)},
{ 3, new Square(6,1)}};
Console.WriteLine();
Console.WriteLine();
Console.WriteLine("SortedList&Square&");
foreach (KeyValuePair&int,Square& kvp in sortedListOfSquares)
Console.WriteLine ($"{kvp.Key} : {kvp.Value}");
这些代码的输出如下所示。
List&String&
Original list
Height:1 Width:3
Height:4 Width:3
Height:2 Width:1
Height:6 Width:1
Sorted list using IComparer&Square&=heightCompare
Height:1 Width:3
Height:2 Width:1
Height:4 Width:3
Height:6 Width:1
Sorted list using IComparable&Square&
Height:2 Width:1
Height:1 Width:3
Height:6 Width:1
Height:4 Width:3
SortedList&Square&
0 : Height:1 Width:3
1 : Height:2 Width:1
2 : Height:3 Width:3
3 : Height:6 Width:1
1.2.4 参考
范例 1.3(即 1.3 节);MSDN 文档中的“IComparable&T& 接口”主题。
1.3 使类型可查找
1.3.1 问题
你有一种数据类型,它将存储为 List&T& 中的元素。你想使用 BinarySearch 方法,自定义你的数据类型在列表中的查找方式。
1.3.2 解决方案
使用 IComparable&T& 和 IComparer&T& 接口。范例 1.2(即 1.2 节)中的 Square 类实现了 IComparable&T& 接口,使得 List&T& 和 SortedList&K,V& 集合可以排序和查找 Square 对象的数组和集合。
1.3.3 讨论
通过在类(或结构)上实现 IComparable&T& 接口,就可以利用 List&T& 和 SortedList&K,V& 类的排序例程。排序算法内置在这些类中;你只需要通过在 IComparable&T&.CompareTo 方法中实现的代码告诉它们如何对你的类进行排序即可。
要实现 CompareTo 方法,请参考范例 1.2(即 1.2 节)。
List&T& 类提供了一个 BinarySearch 方法来查找该列表中的元素。列表中的元素会与传递给对象参数中的 BinarySearch 方法的某个对象进行比较。SortedList 类没有 BinarySearch 方法;作为替代,它拥有 ContainsKey 方法,用于对列表中包含的键值执行二分查找。SortedList 类的 ContainsValue 方法在查找值时执行线性查找。这种线性查找使用 SortedList 集合中的元素的 Equals 方法来执行其工作。Compare 和 CompareTo 方法对于 SortedList 类中执行的线性查找不起任何作用,但是它们确实会影响二分查找。
 为了使用 List&T& 类的 BinarySearch 方法执行准确的查找,首先必须使用 List&T& 的 Sort 方法对其进行排序。此外,如果把一个 IComparer&T& 接口传入给 BinarySearch 方法,还必须把相同的接口传递给 Sort 方法。否则,BinarySearch 方法也许无法找到你正在寻找的对象。
例 1-4 中的 TestSort 方法演示了如何对 List&Square& 和 SortedList&int,Square& 集合实例使用 Square 和 CompareHeight 类。
例 1-4:使类型可查找
public static void TestSearch()
List&Square& listOfSquares = new List&Square& {new Square(1,3),
new Square(4,3),
new Square(2,1),
new Square(6,1)};
IComparer&Square& heightCompare = new CompareHeight();
// 测试List&Square&
Console.WriteLine("List&Square&");
Console.WriteLine("Original list");
foreach (Square square in listOfSquares)
Console.WriteLine(square.ToString());
Console.WriteLine();
Console.WriteLine("Sorted list using IComparer&Square&=heightCompare");
listOfSquares.Sort(heightCompare);
foreach (Square square in listOfSquares)
Console.WriteLine(square.ToString());
Console.WriteLine();
Console.WriteLine("Search using IComparer&Square&=heightCompare");
int found = listOfSquares.BinarySearch(new Square(1,3), heightCompare);
Console.WriteLine($"Found (1,3): {found}");
Console.WriteLine();
Console.WriteLine("Sorted list using IComparable&Square&");
listOfSquares.Sort();
foreach (Square square in listOfSquares)
Console.WriteLine(square.ToString());
Console.WriteLine();
Console.WriteLine("Search using IComparable&Square&");
found = listOfSquares.BinarySearch(new Square(6,1)); // 使用 IComparable
Console.WriteLine($"Found (6,1): {found}");
// 测试SortedList&Square&
var sortedListOfSquares = new SortedList&int,Square&(){
{0, new Square(1,3)},
{2, new Square(4,3)},
{1, new Square(2,1)},
{4, new Square(6,1)}};
Console.WriteLine();
Console.WriteLine("SortedList&Square&");
foreach (KeyValuePair&int,Square& kvp in sortedListOfSquares)
Console.WriteLine ($"{kvp.Key} : {kvp.Value}");
Console.WriteLine();
bool foundItem = sortedListOfSquares.ContainsKey(2);
Console.WriteLine($"sortedListOfSquares.ContainsKey(2): {foundItem}");
// 不要使用IComparer和IComparable
// -- 使用未重写过的Equals方法实现线性查找
Console.WriteLine();
Square value = new Square(6,1);
foundItem = sortedListOfSquares.ContainsValue(value);
Console.WriteLine("sortedListOfSquares.ContainsValue " +
$"(new Square(6,1)): {foundItem}");
这段代码显示的结果如下所示。
List&Square&
Original list
Height:1 Width:3
Height:4 Width:3
Height:2 Width:1
Height:6 Width:1
Sorted list using IComparer&Square&=heightCompare
Height:1 Width:3
Height:2 Width:1
Height:4 Width:3
Height:6 Width:1
Search using IComparer&Square&=heightCompare
Found (1,3): 0
Sorted list using IComparable&Square&
Height:2 Width:1
Height:1 Width:3
Height:6 Width:1
Height:4 Width:3
Search using IComparable&Square&
Found (6,1): 2
SortedList&Square&
0 : Height:1 Width:3
1 : Height:2 Width:1
2 : Height:4 Width:3
4 : Height:6 Width:1
sortedListOfSquares.ContainsKey(2): True
sortedListOfSquares.ContainsValue(new Square(6,1)): True
1.3.4 参考
范例 1.2(即 1.2 节);MSDN 文档中的“IComparable&T& 接口”和“IComparer&T& 接口”主题。
1.4 从一个方法返回多个数据项
1.4.1 问题
在许多情况下,从一个方法返回一个值是不够的。你需要一种方式来从一个方法返回不止一个数据项。
1.4.2 解决方案
对充当返回参数的参数使用关键字 out。下面的方法接受一个 inputShape 参数,并通过该值计算 height、width 和 depth。
public void ReturnDimensions(int inputShape,
out int height,
out int width,
out int depth)
height = 0;
width = 0;
depth = 0;
// 通过inputShape值计算height、width和depth
这个方法以如下方式进行调用:
// 声明输出参数
// 调用方法并返回height、width和depth
Obj.ReturnDimensions(1, out height, out width, out depth);
另一个方法将返回一个包含所有返回值的类或结构。修改前一个方法,使其返回一个结构,而不是使用 out 参数:
public Dimensions ReturnDimensions(int inputShape)
// 默认构造函数自动将结构的成员初始化为0
Dimensions objDim = new Dimensions();
// 通过inputShape的值计算objDim.Height、objDim.Width、objDim.Depth……
return objD
其中 Dimensions 的定义如下所示。
public struct Dimensions
public int H
public int W
public int D
现在以如下方式调用这个方法。
// 调用方法并且返回height、width和depth
Dimensions objDim = obj.ReturnDimensions(1);
除了从此方法返回一个用户定义的类或结构,也可以用一个 Tuple 对象包含所有的返回值。修改前一个方法,使其返回一个 Tuple。
public Tuple&int, int, int& ReturnDimensionsAsTuple(int inputShape)
// 通过inputShape值计算objDim.Height、objDim.Width、objDim.Depth
// 例如{5, 10, 15}
// 创建一个包含计算出的值的Tuple
var objDim = Tuple.Create&int, int, int&(5, 10, 15);
return (objDim);
现在以如下方式调用这个方法。
// 调用方法并且返回height、width和depth
Tuple&int, int, int& objDim = obj.ReturnDimensions(1);
1.4.3 讨论
在方法签名中使用 out 关键字创建一个参数,指示这个参数将由该方法初始化并返回。当需要方法返回多个值时,这个技巧就很有用。一个方法最多只能有一个返回值,但是通过使用 out 关键字,可以把多个参数标记为一个返回值。
要设置一个 out 参数,需要用 out 关键字标记方法签名中的参数,如下所示。
public void ReturnDimensions(int inputShape,
out int height,
out int width,
out int depth)
要调用这个方法,还必须用 out 关键字标记调用方法的参数,如下所示。
obj.ReturnDimensions(1, out height, out width, out depth);
这个方法中的 out 参数不必初始化;只需声明它们并传入 ReturnDimensions 方法中即可。
不管在调用方法之前是否初始化过它们,在 ReturnDimensions 方法内使用它们之前都必须初始化。即使不通过 ReturnDimensions 方法内的每条路径使用它们,仍然必须初始化它们。这就是这个方法以如下三行代码开始的原因。
height = 0;
width = 0;
depth = 0;
你可能想知道为什么不能使用 ref 参数代替 out 参数,鉴于它们都允许一个方法改变像这样标记的参数的值。答案是,out 参数使代码有些自文档化。当遇到一个 out 参数时,你知道这个参数充当一个返回值。此外,在把 out 参数传入方法中之前,不需要做额外的工作来初始化它;而 ref 参数则需要这样做。
 在调用方法时不需要对 out 参数进行封送;相反,在方法把数据返回给调用者时对其封送一次。任何其他调用类型(按值调用或者使用 ref 关键字按引用调用)都要求在两个方向上对值进行封送。在封送场合下使用 out 关键字可以改进远程调用性能。
在仅有少量值需要返回时,out 参数是非常有用的;但是当你遇到需要返回 4 个、5 个、6 个甚至更多的值时,它就变得笨重了。另外一个返回多个值的选项是创建并返回用户定义的类或结构,或者使用 Tuple 打包需要由某个方法返回的所有值。
使用类或结构返回多个值的第一个选项非常直接。只需要像下面这样创建类型(在本例中是该类型是一个结构)即可。
public struct Dimensions
public int H
public int W
public int D
如 1.4.2 节所展示的,将需要的数据填充到这个数据结构的每个字段中,并且从方法中返回它。
与使用用户定义的对象相比,使用 Tuple 的第二个选项更加简洁。可以创建一个 Tuple,用于包含不同类型的任意数量的值。此外,Tuple 中保存的数据是不可变的;一旦通过构造函数或者静态的 Create 方法将数据添加到 Tuple 中,就无法再修改这些数据了。
Tuple 可以接受并包含 8 个独立的值。如里你需要 8 个以上的值,那么需要使用这个特别的 Tuple 类。
Tuple&T1, T2, T3, T4, T5, T6, T7, TRest& Class
当创建一个包含超过 8 个值的 Tuple 时,你无法使用静态的 Create 方法,而是必须使用 Tuple 类的构造函数。下面的代码展示了如何创建一个包含 10 个整数值的 Tuple。
var values = new Tuple&int, int, int, int, int, int, int, Tuple&int, int, int&& (
1, 2, 3, 4, 5, 6, 7, new Tuple&int, int, int& (8, 9, 10));
当然,你可以继续将更多的 Tuple 添加到每个内嵌的 Tuple 类的最后,以创建你需要的任何大小的 Tuple。
1.4.4 参考
MSDN 文档中的“Tuple 类”和“Tuple&T1, T2, T3, T4, T5, T6, T7, TRest& 类”主题。
1.5 解析命令行参数
1.5.1 问题
你需要应用程序以标准格式(在 1.5.3 节中介绍)接受一个或多个命令行参数。你需要访问和解析传递给应用程序的完整命令行。
1.5.2 解决方案
在例 1-5 中,结合使用以下类来帮你解析命令行参数:Argument、ArgumentDefinition 和 ArgumentSemanticAnalyzer。
例 1-5:Argument 类
using System.D
using System.L
using System.Collections.ObjectM
public sealed class Argument
public string Original { }
public string Switch { }
public ReadOnlyCollection&string& SubArguments { }
private List&string& subA
public Argument(string original)
Original =
Switch = string.E
subArguments = new List&string&();
SubArguments = new ReadOnlyCollection&string&(subArguments);
private void Parse()
if (string.IsNullOrEmpty(Original))
char[] switchChars = { '/', '-' };
if (!switchChars.Contains(Original[0]))
string switchString = Original.Substring(1);
string subArgsString = string.E
int colon = switchString.IndexOf(':');
if (colon &= 0)
subArgsString = switchString.Substring(colon + 1);
switchString = switchString.Substring(0, colon);
Switch = switchS
if (!string.IsNullOrEmpty(subArgsString))
subArguments.AddRange(subArgsString.Split(';'));
// 一组谓词,提供关于参数的有用信息
// 使用lambda表达式实现
public bool IsSimple =& SubArguments.Count == 0;
public bool IsSimpleSwitch =&
!string.IsNullOrEmpty(Switch) && SubArguments.Count == 0;
public bool IsCompoundSwitch =&
!string.IsNullOrEmpty(Switch) && SubArguments.Count == 1;
public bool IsComplexSwitch =&
!string.IsNullOrEmpty(Switch) && SubArguments.Count & 0;
public sealed class ArgumentDefinition
public string ArgumentSwitch { }
public string Syntax { }
public string Description { }
public Func&Argument, bool& Verifier { }
public ArgumentDefinition(string argumentSwitch,
string syntax,
string description,
Func&Argument, bool& verifier)
ArgumentSwitch = argumentSwitch.ToUpper();
Description =
Verifier =
public bool Verify(Argument arg) =& Verifier(arg);
public sealed class ArgumentSemanticAnalyzer
private List&ArgumentDefinition& argumentDefinitions =
new List&ArgumentDefinition&();
private Dictionary&string, Action&Argument&& argumentActions =
new Dictionary&string, Action&Argument&&();
public ReadOnlyCollection&Argument& UnrecognizedArguments { }
public ReadOnlyCollection&Argument& MalformedArguments { }
public ReadOnlyCollection&Argument& RepeatedArguments { }
public ReadOnlyCollection&ArgumentDefinition& ArgumentDefinitions =&
new ReadOnlyCollection&ArgumentDefinition&(argumentDefinitions);
public IEnumerable&string& DefinedSwitches =&
from argumentDefinition in argumentDefinitions
select argumentDefinition.ArgumentS
public void AddArgumentVerifier(ArgumentDefinition verifier) =&
argumentDefinitions.Add(verifier);
public void RemoveArgumentVerifier(ArgumentDefinition verifier)
var verifiersToRemove = from v in argumentDefinitions
where v.ArgumentSwitch == verifier.ArgumentSwitch
foreach (var v in verifiersToRemove)
argumentDefinitions.Remove(v);
public void AddArgumentAction(string argumentSwitch, Action&Argument& action) =&
argumentActions.Add(argumentSwitch, action);
public void RemoveArgumentAction(string argumentSwitch)
if (argumentActions.Keys.Contains(argumentSwitch))
argumentActions.Remove(argumentSwitch);
public bool VerifyArguments(IEnumerable&Argument& arguments)
// 没有任何参数进行验证,失败
if (!argumentDefinitions.Any())
// 确认是否存在任一未定义的参数
this.UnrecognizedArguments =
( from argument in arguments
where !DefinedSwitches.Contains(argument.Switch.ToUpper())
select argument).ToList().AsReadOnly();
if (this.UnrecognizedArguments.Any())
//检查开关与某个已知开关匹配但是检查格式是否正确
//的谓词为false的所有参数
this.MalformedArguments = ( from argument in arguments
join argumentDefinition in argumentDefinitions
on argument.Switch.ToUpper() equals
argumentDefinition.ArgumentSwitch
where !argumentDefinition.Verify(argument)
select argument).ToList().AsReadOnly();
if (this.MalformedArguments.Any())
//将所有参数按照开关进行分组,统计每个组的数量,
//并选出包含超过一个元素的所有组,
//然后我们获得一个包含这些数据项的只读列表
this.RepeatedArguments =
(from argumentGroup in
from argument in arguments
where !argument.IsSimple
group argument by argument.Switch.ToUpper()
where argumentGroup.Count() & 1
select argumentGroup).SelectMany(ag =& ag).ToList().AsReadOnly();
if (this.RepeatedArguments.Any())
public void EvaluateArguments(IEnumerable&Argument& arguments)
//此时只需应用每个动作:
foreach (Argument argument in arguments)
argumentActions[argument.Switch.ToUpper()](argument);
public string InvalidArgumentsDisplay()
StringBuilder builder = new StringBuilder();
builder.AppendFormat($"Invalid arguments: {Environment.NewLine}");
// 添加未识别的参数
FormatInvalidArguments(builder, this.UnrecognizedArguments,
"Unrecognized argument: {0}{1}");
// 添加格式不正式的参数
FormatInvalidArguments(builder, this.MalformedArguments,
"Malformed argument: {0}{1}");
// 对于重复的参数,我们想要将其分组以用于显示,
// 因此通过开关分组并且将其添加到正在构建的字符串
var argumentGroups = from argument in this.RepeatedArguments
group argument by argument.Switch.ToUpper() into ag
select new { Switch = ag.Key, Instances = ag};
foreach (var argumentGroup in argumentGroups)
builder.AppendFormat($"Repeated argument:
{argumentGroup.Switch}{Environment.NewLine}");
FormatInvalidArguments(builder, argumentGroup.Instances.ToList(),
"\t{0}{1}");
return builder.ToString();
private void FormatInvalidArguments(StringBuilder builder,
IEnumerable&Argument& invalidArguments, string errorFormat)
if (invalidArguments != null)
foreach (Argument argument in invalidArguments)
builder.AppendFormat(errorFormat,
argument.Original, Environment.NewLine);
如何使用这些类为应用程序处理命令行?方法如下所示。
public static void Main(string[] argumentStrings)
var arguments = (from argument in argumentStrings
select new Argument(argument)).ToArray();
Console.Write("Command line: ");
foreach (Argument a in arguments)
Console.Write($"{a.Original} ");
Console.WriteLine("");
ArgumentSemanticAnalyzer analyzer = new ArgumentSemanticAnalyzer();
analyzer.AddArgumentVerifier(
new ArgumentDefinition("output",
"/output:[path to output]",
"Specifies the location of the output file.",
x =& x.IsCompoundSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("trialMode",
"/trialmode",
"If this is specified it places the product into trial mode",
x =& x.IsSimpleSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("DEBUGOUTPUT",
"/debugoutput:[value1];[value2];[value3]",
"A listing of the files the debug output " +
"information will be written to",
x =& x.IsComplexSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("",
"[literal value]",
"A literal value",
x =& x.IsSimple));
if (!analyzer.VerifyArguments(arguments))
string invalidArguments = analyzer.InvalidArgumentsDisplay();
Console.WriteLine(invalidArguments);
ShowUsage(analyzer);
// 设置命令行解析结果的容器
string output = string.E
bool trialmode =
IEnumerable&string& debugOutput =
List&string& literals = new List&string&();
//我们想对每一个解析出的参数应用一个动作,
//因此将它们添加到分析器
analyzer.AddArgumentAction("OUTPUT", x =& { output = x.SubArguments[0]; });
analyzer.AddArgumentAction("TRIALMODE", x =& { trialmode = });
analyzer.AddArgumentAction("DEBUGOUTPUT", x =&
{ debugOutput = x.SubA
analyzer.AddArgumentAction("", x=&{literals.Add(x.Original);});
// 检查参数并运行动作
analyzer.EvaluateArguments(arguments);
// 显示结果
Console.WriteLine("");
Console.WriteLine($"OUTPUT: {output}");
Console.WriteLine($"TRIALMODE: {trialmode}");
if (debugOutput != null)
foreach (string item in debugOutput)
Console.WriteLine($"DEBUGOUTPUT: {item}");
foreach (string literal in literals)
Console.WriteLine($"LITERAL: {literal}");
public static void ShowUsage(ArgumentSemanticAnalyzer analyzer)
Console.WriteLine("Program.exe allows the following arguments:");
foreach (ArgumentDefinition definition in analyzer.ArgumentDefinitions)
Console.WriteLine($"\t{definition.ArgumentSwitch}:
({definition.Description}){Environment.NewLine}
\tSyntax: {definition.Syntax}");
1.5.3 讨论
在解析命令行参数之前,必须明确选用一种通用格式。本范例中使用的格式遵循用于
Visual C#.NET 语言编译器的命令行格式。使用的格式定义如下所示。
通过一个或多个空白字符分隔命令行参数。
每个参数可以以一个 - 或 / 字符开头,但不能同时以这两个字符开头。如果不以其中一个字符开头,就把参数视为一个字面量,比如文件名。
以 - 或 / 字符开头的参数可被划分为:以一个选项开关开头,后接一个冒号,再接一个或多个用 ; 字符分隔的参数。命令行参数 -sw:arg1;arg2;arg3 可被划分为一个选项开关(sw)和三个参数(arg1、arg2 和 arg3)。注意,在完整的参数中不应该有任何空格,否则运行时命令行解析器将把参数分拆为两个或更多的参数。
用双引号包裹住的字符串(如 "c:\test\file.log")会去除双引号。这是操作系统解释传入应用程序中的参数时的一项功能。
不会去除单引号。
要保留双引号,可在双引号字符前放置 \ 转义序列字符。
仅当 \ 字符后面接着双引号时,才将 \ 字符作为转义序列字符处理;在这种情况下,只会显示双引号。
^ 字符被运行时解析器作为特殊字符处理。
幸运的是,在应用程序接收各个解析出的参数之前,运行时命令行解析器可以处理其中大部分任务。
运行时命令行解析器把一个包含每个解析过的参数的 string[] 传递给应用程序的入口点。入口点可以采用以下形式之一。
public static void Main()
public static int Main()
public static void Main(string[] args)
public static int Main(string[] args)
前两种形式不接受参数,但是后两种形式接受解析过的命令行参数的数组。注意,静态属性 Environment.CommandLine 将返回一个字符串,其中包含完整的命令行;静态方法 Environment.GetCommandLineArgs 将返回一个字符串数组,其中包含解析过的命令行参数。
1.5.2 节介绍的三个类涉及命令行参数的各个阶段。
封装一个命令行参数并负责解析该参数。
ArgumentDefinition
定义一个对当行命令行有效的参数。
ArgumentSemanticAnalyzer
基于设置的 ArgumentDefinition 进行参数的验证和获取。
把以下命令行参数传入这个应用程序中:
MyApp c:\input\infile.txt -output:d:\outfile.txt -trialmode
将得到以下解析过的选项开关和参数。
Command line: c:\input\infile.txt -output:d:\outfile.txt -trialmode
OUTPUT: d:\outfile.txt
TRIALMODE: True
LITERAL: c:\input\infile.txt
如果你没有正确地输入命令行参数,比如忘记了向 -output 选项开关添加参数,得到的输出将如下所示。
Command line: c:\input\infile.txt -output: -trialmode
Invalid arguments:
Malformed argument: -output
Program.exe allows the following arguments:
OUTPUT: (Specifies the location of the output file.)
Syntax: /output:[path to output]
TRIALMODE: (If this is specified, it places the product into trial mode)
Syntax: /trialmode
DEBUGOUTPUT: (A listing of the files the debug output information will be
written to)
Syntax: /debugoutput:[value1];[value2];[value3]
: (A literal value)
Syntax: [literal value]
在这段代码中有几个值得指出的地方。
每个 Argument 实例都需要能确定它自身的某些事项。相应地,作为 Argument 的属性暴露了一组谓词,告诉我们这个 Argument 的一些有用信息。ArgumentSemanticAnalyzer 将使用这些属性来确定参数的特征。
public bool IsSimple =& SubArguments.Count == 0;
public bool IsSimpleSwitch =&
!string.IsNullOrEmpty(Switch) && SubArguments.Count == 0;
public bool IsCompoundSwitch =&
!string.IsNullOrEmpty(Switch) && SubArguments.Count == 1;
public bool IsComplexSwitch =&
!string.IsNullOrEmpty(Switch) && SubArguments.Count & 0;
 关于 lambda 表达式的更多信息请参考 4.0 节。范例 1.16(即 1.16 节)中也包含了使用 lambda 表达式来实现闭包的相关讨论。
这段代码有多处在 LINQ 查询的结果上调用了 ToArray 或 ToList 方法。
var arguments = (from argument in argumentStrings
select new Argument(argument)).ToArray();
这是由于查询结果是延迟执行的。这不仅意味着将以迟缓方式来计算结果,而且意味着每次访问结果时都要重新计算它们。使用 ToArray 或 ToList 方法会强制积极计算结果,生成一份不需要在每次使用时都重新计算的副本。查询逻辑并不知道正在操作的集合是否发生了变化,因此每次都必须重新计算结果,除非使用这些方法创建出一份“即时”副本。
为了验证这些参数是否正确,必须创建 ArgumentDefinition,并将每个可接受的参数类型与 ArgumentSemanticAnalyzer 相关联,代码如下所示。
ArgumentSemanticAnalyzer analyzer = new ArgumentSemanticAnalyzer();
analyzer.AddArgumentVerifier(
new ArgumentDefinition("output",
"/output:[path to output]",
"Specifies the location of the output file.",
x =& x.IsCompoundSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("trialMode",
"/trialmode",
"If this is specified it places the product into trial mode",
x =& x.IsSimpleSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("DEBUGOUTPUT",
"/debugoutput:[value1];[value2];[value3]",
"A listing of the files the debug output " +
"information will be written to",
x =& x.IsComplexSwitch));
analyzer.AddArgumentVerifier(
new ArgumentDefinition("",
"[literal value]",
"A literal value",
x =& x.IsSimple));
每个 ArgumentDefinition 都包含 4 个部分:参数选项开关、显示参数语法的字符串、参数说明以及用于验证参数的验证谓词。这些信息可以用于验证参数,如下所示。
//检查开关与某个已知开关匹配但是检查格式是否正确
//的谓词为false的所有参数
this.MalformedArguments = ( from argument in arguments
join argumentDefinition in argumentDefinitions
on argument.Switch.ToUpper() equals
argumentDefinition.ArgumentSwitch
where !argumentDefinition.Verify(argument)
select argument).ToList().AsReadOnly();
ArgumentDefinition 还允许为程序编写一个使用说明方法。
public static void ShowUsage(ArgumentSemanticAnalyzer analyzer)
Console.WriteLine("Program.exe allows the following arguments:");
foreach (ArgumentDefinition definition in analyzer.ArgumentDefinitions)
Console.WriteLine("\t{0}: ({1}){2}\tSyntax: {3}",
definition.ArgumentSwitch, definition.Description,
Environment.NewLine,definition.Syntax);
为了获取参数的值以便使用它们,需要从解析过的参数中提取信息。对于解决方案示例,我们需要以下信息。
// 设置命令行解析结果的容器
string output = string.E
bool trialmode =
IEnumerable&string& debugOutput =
List&string& literals = new List&string&();
如何填充这些值?对于每个参数,都需要一个与之关联的动作,以确定如何从 Argument 实例获得值。每个动作就是一个谓词,这使得这种方式非常强大,因为你在这里可以使用任何谓词。下面的代码说明如何定义这些 Argument 动作并将其与 ArgumentSemanticAnalyzer 相关联。
//对于每一个解析出的参数,我们想要对其应用一个动作,
//因此将它们添加到分析器
analyzer.AddArgumentAction("OUTPUT", x =& { output = x.SubArguments[0]; });
analyzer.AddArgumentAction("TRIALMODE", x =& { trialmode = });
analyzer.AddArgumentAction("DEBUGOUTPUT", x =&
{ debugOutput = x.SubA});
analyzer.AddArgumentAction("", x=&{literals.Add(x.Original);});
现在已经建立了所有的动作,就可以对 ArgumentSemanticAnalyzer 应用 EvaluateArguments 方法来获取值,代码如下所示。
// 检查参数并运行动作
analyzer.EvaluateArguments(arguments);
现在通过执行动作填充了值,并且可以利用这些值来运行程序,代码如下所示。
// 传入参数值并运行程序
Program program = new Program(output, trialmode, debugOutput, literals);
program.Run();
如果在验证参数时使用 LINQ 来查询未识别的、格式错误的或者重复的实参(argument),其中任何一项都会导致形参(parameter)无效。
public bool VerifyArguments(IEnumerable&Argument& arguments)
// 没有任何参数进行验证,失败
if (!argumentDefinitions.Any())
// 确认是否存在任一未定义的参数
this.UnrecognizedArguments =
( from argument in arguments
where !DefinedSwitches.Contains(argument.Switch.ToUpper())
select argument).ToList().AsReadOnly();
if (this.UnrecognizedArguments.Any())
//检查开关与某个已知开关匹配但是检查格式是否正确
//的谓词为false的所有参数
this.MalformedArguments = ( from argument in arguments
join argumentDefinition in argumentDefinitions
on argument.Switch.ToUpper() equals
argumentDefinition.ArgumentSwitch
where !argumentDefinition.Verify(argument)
select argument).ToList().AsReadOnly();
if (this.MalformedArguments.Any())
//将所有参数按照开关进行分组,统计每个组的数量,
//并选出包含超过一个元素的所有组,
//然后我们获得一个包含这些数据项的只读列表
this.RepeatedArguments =
(from argumentGroup in
from argument in arguments
where !argument.IsSimple
group argument by argument.Switch.ToUpper()
where argumentGroup.Count() & 1
select argumentGroup).SelectMany(ag =& ag).ToList().AsReadOnly();
if (this.RepeatedArguments.Any())
与 LINQ 出现之前通过多重嵌套循环、switch 语句、IndexOf 方法及其他机制实现同样功能的代码相比,上述使用 LINQ 的代码更加易于理解每一个验证阶段。每个查询都用问题领域的语言简洁地指出了它在尝试执行什么任务。
 LINQ 旨在帮助解决那些必须排序、查找、分组、筛选和投影数据的问题。请使用它!
1.5.4 参考
MSDN 文档中的“Main”和“命令行参数”主题。
1.6 在运行时初始化常量字段
1.6.1 问题
标记为 const 的字段只能在编译时初始化。你需要在运行时而不是在编译时将一个字段初始化为一个有效值。然后在应用程序剩余的生命期内,这个字段必须像一个常量字段那样工作。
1.6.2 解决方案
在代码中声明一个常量值时有两种选择。你可以使用 readonly 字段或 const 字段,每种方式都有其优缺点。不过,如果你需要在运行时初始化一个常量字段,就必须使用 readonly 字段。
public class Foo
public Foo() {}
public Foo(int constInitValue)
bar = constInitV
// 类的其他部分
使用 const 字段无法做到这一点。const 字段只能在编译时初始化。
public class Foo
// 这一行造成一个编译时错误
public Foo() {}
public Foo(int constInitValue)
bar = constInitV //这一行同样造成一个编译时错误
// 类的其他部分
1.6.3 讨论
readonly 字段只允许在运行时在构造函数中执行初始化,而 const 字段必须在编译时进行初始化。因此,为了让一个必须为常量的字段在运行时初始化,唯一的方式是实现一个 readonly 字段。
只有两种方式可用于初始化一个 readonly 字段。第一种方式是向字段自身添加一个初始化器,代码如下所示。
public readonly int bar = 100;
第二种方式是通过一个构造函数初始化 readonly 字段。1.6.2 节中的代码演示了这种方法。如果查看下面的类:
public class Foo
public const int y = 1;
public Foo() {}
public Foo(int roInitValue)
x = roInitV
// 类的其他部分
你会看到它被编译成下面的中间语言(intermediate language,IL):
.class auto ansi nested public beforefieldinit Foo
extends [mscorlib]System.Object
.field public static literal int32 y = int32(0x) //&&-- const field
.field public initonly int32 x
//&&-- readonly field
.method public hidebysig specialname rtspecialname
instance void
.ctor(int32 roInitValue) cil managed
// Code size
instance void [mscorlib]System.Object::.ctor()
int32 CSharpRecipes.ClassesAndGenerics/Foo::x
} // end of method Foo::.ctor
.method public hidebysig specialname rtspecialname
instance void
.ctor() cil managed
// Code size
instance void [mscorlib]System.Object::.ctor()
} // end of method Foo::.ctor
} // End of class Foo
注意 const 字段被编译成一个静态字段,readonly 字段被编译成一个实例字段。因此,只需要类名就可以访问一个 const 字段。
 对于使用 const 字段的一个常见争论是,它们并不像 readonly 字段那样支持版本化。如果重新构建一个定义了 const 字段的组件,并且该 const 字段的值在之后的版本中发生了改变,那么使用旧版本构建的任何其他组件都不会获得新的值。只要一个字段将来有可能发生改变,就不要把它声明为一个 const 字段。
下面的代码显示了如何使用一个 readonly 实例字段。
Foo obj1 = new Foo(100);
Console.WriteLine(obj1.bar);
1.6.4 参考
MSDN 文档中的“const”和“readonly”关键字。
1.7 构建可克隆的类
1.7.1 问题
你需要一种方法对可能引用其他类型的数据类型进行浅克隆操作、深克隆操作或者同时执行这两种操作,但是不应该使用 ICloneable 接口,因为它违反了 .NET Framework 设计准则。
1.7.2 解决方案
为了解决使用 ICloneable 的问题,创建另外两个接口 IShallowCopy&T& 和 IDeepCopy&T& 来建立一种复制模式,代码如下所示。
public interface IShallowCopy&T&
T ShallowCopy();
public interface IDeepCopy&T&
T DeepCopy();
浅复制意味着所复制对象的字段将引用与原始对象相同的对象。为了允许进行浅复制,可在类中实现 IShallowCopy&T& 接口,代码如下所示。
using System.C
using System.Collections.G
public class ShallowClone : IShallowCopy&ShallowClone&
public int Data = 1;
public List&string& ListData = new List&string&();
public object ObjData = new object();
public ShallowClone ShallowCopy() =& (ShallowClone)this.MemberwiseClone();
深复制(或称克隆)意味着所复制对象的字段将引用原始对象的字段的新副本。为了进行深复制,可在类中实现 IDeepCopy&T& 接口,代码如下所示。
using System.C
using System.Collections.G
using System.Runtime.Serialization.Formatters.B
using System.IO;
[Serializable]
public class DeepClone : IDeepCopy&DeepClone&
public int data = 1;
public List&string& ListData = new List&string&();
public object objData = new object();
public DeepClone DeepCopy()
BinaryFormatter BF = new BinaryFormatter();
MemoryStream memStream = new MemoryStream();
BF.Serialize(memStream, this);
memStream.Flush();
memStream.Position = 0;
return (DeepClone)BF.Deserialize(memStream);
要同时支持浅复制和深复制方法,可同时实现这两个接口,代码如下所示。
using System.C
using System.Collections.G
using System.Runtime.Serialization.Formatters.B
using System.IO;
[Serializable]
public class MultiClone : IShallowCopy&MultiClone&,
IDeepCopy&MultiClone&
public int data = 1;
public List&string& ListData = new List&string&();
public object objData = new object();
public MultiClone ShallowCopy() =& (MultiClone)this.MemberwiseClone();
public MultiClone DeepCopy()
BinaryFormatter BF = new BinaryFormatter();
MemoryStream memStream = new MemoryStream();
BF.Serialize(memStream, this);
memStream.Flush();
memStream.Position = 0;
return (MultiClone)BF.Deserialize(memStream);
1.7.3 讨论
.NET Framework 中包含一个名为 ICloneable 的接口,它最初被设计为在 .NET 中实现克隆的方法。设计建议现在不再在任何公开 API 中使用这个接口,因为它容易将自身导向不同的解释。此接口看起来如下所示。
public interface ICloneable
object Clone();
注意此接口只有一个方法 Clone,它返回一个对象。该克隆是对象的浅副本还是深副本呢?无法通过该接口得知这一点,因为实现可以选择任何一个方式。这就是不应该再使用它,而是引入 IShallowCopy&T& 和 IDeepCopy&T& 接口的原因。
克隆操作能够创建出类型实例的一个准确副本(克隆)。克隆可能采用两种形式之一:浅复制和深复制。浅复制相对容易一些,它对涉及复制的对象调用 ShallowCopy 方法。
在原始对象中,引用类型的字段像值类型的字段那样进行复制。例如,如果原始对象包含一个 StreamWriter 类型的字段,克隆的对象将指向原始对象的 StreamWriter 的同一个实例,并没有创建新对象。
 在执行克隆操作时无需处理静态字段。每个应用程序域中的每个类的每个静态字段只会保留一个内存位置。克隆出的对象与原始对象访问相同的静态字段。
对浅复制的支持是通过 Object 类的 MemberwiseClone 方法来实现的,Object 类充当了所有 .NET 类的基类。因此,下面的代码通过 Clone 方法允许创建和返回一个浅复制。
public ShallowClone ShallowCopy() =& (ShallowClone)this.MemberwiseClone();
克隆一个对象的另一种方式是创建深复制。就像浅复制那样,深复制将创建原始对象的一个副本。不同的是,深复制还会创建原始对象中每个引用类型的字段的单独副本。因此,如果原始对象包含一个 StreamWriter 类型的字段,复制的对象也会包含一个 StreamWriter 类型的字段,但是复制对象的 StreamWriter 字段将指向一个新的 StreamWriter 对象,而不是原始对象的 StreamWriter 对象。
.NET Framework 没有直接提供对深复制的支持,但是下面的代码提供了一种实现深复制的简单方式。
BinaryFormatter BF = new BinaryFormatter();
MemoryStream memStream = new MemoryStream();
BF.Serialize(memStream, this);
memStream.Flush();
memStream.Position = 0;
return (BF.Deserialize(memStream));
总而言之,这使用二进制序列化将原始对象序列化到一个内存流中,然后将其反序列化到一个新对象中,并将该对象返回给调用者。在调用 Deserialize 方法之前将内存流指针重新定位到流的开始处是十分重要的;否则就会引发一个异常,指示序列化的对象中不包含任何数据。
使用对象序列化执行深复制时,不必修改执行深复制的代码就能改下层的对象。如果你手动执行深复制,就必须对原始对象的每个实例字段创建新实例,并把新实例复制到克隆的对象。这是一件非常琐碎的事情。如果修改了原始对象的字段,你必须修改深复制的代码以反映出这些修改。使用序列化可以依靠序列化器动态查找和序列化对象中包含的所有字段。如果修改了对象,序列化器仍然不需要修改就可以进行深复制。
你可能想手动执行深复制的一个原因是,仅当对象中的一切都可序列化时,本范例中介绍的序列化技术才会正确工作。当然,手动复制有时也于事无补,因为有些对象天生就是不可复制的。假设你有一个网络管理应用,其中一个对象代表网络上的一台特定打印机。当你复制它时会指望它做什么呢?传真一份订购单以购买一台新的打印机吗?
深复制与生俱来的一个问题是在具有循环引用的嵌套数据结构上执行深复制。本范例使得处理循环引用成为可能,尽管这仍是一个难题。因此,事实上,如果你使用本范例中的方法,就无需避免循环引用。
1.7.4 参考
Krzysztof Cwalina 和 Brad Abrams 所著的《.NET 设计规范:约定、惯用法与模式(第 2 版)》;MSDN 文档中的“Object.MemberwiseClone 方法”主题。
1.8 确保对象的处置
1.8.1 问题
当一个对象的工作完成或者超出作用域时,你需要采用一种方式确保一些处理得到执行。
1.8.2 解决方案
使用 using 语句,代码如下所示。
using System.IO;
using(FileStream FS = new FileStream("Test.txt", FileMode.Create))
FS.WriteByte((byte)1);
FS.WriteByte((byte)2);
FS.WriteByte((byte)3);
SW.WriteLine("some text.");
1.8.3 讨论
using 语句非常易于使用,并且可以避免编写多余代码的麻烦。如果解决方案中没有使用 using 语句,将会如下所示。
FileStream FS = new FileStream("Test.txt", FileMode.Create);
FS.WriteByte((byte)1);
FS.WriteByte((byte)2);
FS.WriteByte((byte)3);
StreamWriter SW = new StreamWriter(FS);
SW.WriteLine("some text.");
if (SW != null)
((IDisposable)SW).Dispose();
if (FS != null)
((IDisposable)FS).Dispose();
关于 using 语句,需要注意以下几点。
存在一个 using 指令,如下所示。应将其与 using 语句区分开。这可能会使初次接触这一语言的开发人员混淆。
using System.IO;
using 语句的子句中定义的变量都必须具有相同的类型,并且必须具有一个初始化器。不过,因为可以在单个代码块前使用多个 using 语句,所以这并不是一个重大的限制。
using 子句中定义的变量在 using 语句体中被认为是只读的。这可以阻止开发人员在尝试处置变量最初引用的对象时无意中将变量转变成引用不同的对象,避免引发问题。
不应该在 using 块外声明变量,然后在 using 子句内初始化它。
下面的代码说明了最后一点。
FileStream FS;
using(FS = new FileStream("Test.txt", FileMode.Create))
FS.WriteByte((byte)1);
FS.WriteByte((byte)2);
FS.WriteByte((byte)3);
using(StreamWriter SW = new StreamWriter(FS))
SW.WriteLine("some text.");
对于这段示例代码来说,不会有任何问题。但是,要考虑到变量 FS 是可以在 using 块外使用的。实际上,可以将这段代码修改为下面这样。
FileStream FS;
using(FS = new FileStream("Test.txt", FileMode.Create))
FS.WriteByte((byte)1);
FS.WriteByte((byte)2);
FS.WriteByte((byte)3);
using(StreamWriter SW = new StreamWriter(FS))
SW.WriteLine("some text.");
FS.WriteByte((byte)4);
这段代码可以编译,但会在这个代码段的最后一行上引发一个 ObjectDisposedException,因为已经对 FS 对象调用了 Dispose 方法。对象此时还没有被垃圾回收,仍然以被处置过的状态存在于内存中。
1.8.4 参考
MSDN 文档中的“Cleaning Up Unmanaged Resources”“IDisposable 接口”“Using foreach with Collections”和“实现 Finalize 和 Dispose 以清理非托管资源”主题。
1.9 确定何时何处使用泛型
1.9.1 问题
你想在新项目中使用泛型类型或者把现有项目中的非泛型类型转换成它们对应的泛型类型。不过,你并不真正明白为什么要这样做,也不知道应该把哪些非泛型类型转换成泛型类型。
1.9.2 解决方案
在决定何时何处使用泛型类型时,需要考虑以下几点。
你的类型将包含或操作多种不同的未确定数据类型(例如,一种集合类型)吗?如果是,那么与创建非泛型类型相比,创建泛型类型会有几个好处。如果你的类型只操作某种特定的类型,那么可能不需要创建泛型类型。
如果你的类型在值类型上操作,那么将会发生装箱和拆箱操作。你应该考虑使用泛型来避免装箱和拆箱操作带来的性能损失。
与泛型关联的更强大的类型检查有助于更快地(即在编译时而不是在运行时)找出错误,从而缩短错误修正周期。
由于你编写了多个类来处理不同的数据类型(例如,使用一个专门的 ArrayList 只存储 StreamReader,并使用另一个 ArrayList 只存储 StreamWriter),这导致你的代码产生“代码膨胀”了吗?更容易的做法是,编写一次代码,使其适用于所操作的所有数据类型。
泛型可以使得代码更清晰。通过消除代码膨胀并强制对类型执行更强大的类型检查,可以使代码更易于阅读和理解。
1.9.3 讨论
在大多数情况下,你的代码将受益于使用泛型类型。泛型可以实现更高效的代码重用,更快速的性能,更强大的类型检查和更易于阅读的代码。
1.9.4 参考
MSDN 文档中的“泛型概述”和“泛型的优点”主题。
1.10 理解泛型类型
1.10.1 问题
你需要理解 .NET 类型如何适用于泛型,以及泛型 .NET 类型与常规 .NET 类型有着怎样的区别。
1.10.2 解决方案
可以用两个快速的试验来展示常规 .NET 类型和泛型 .NET 类型之间的区别。在深入代码之前,如果你对泛型不熟悉,可以先跳到具体解释泛型的 1.10.3 节,之后再回到本部分。
当定义一个常规 .NET 类型时,它看起来就像是例 1-6 中定义的 FixedSizeCollection 类型。
例 1-6:FixedSizeCollection(一种常规 .NET 类型)
public class FixedSizeCollection
/// &summary&
/// 构造函数,增加静态计数器的值
/// 设置数据项最大数量
/// &/summary&
/// &param name="maxItems"&&/param&
public FixedSizeCollection(int maxItems)
FixedSizeCollection.InstanceCount++;
this.Items = new object[maxItems];
/// &summary&
/// 将一个未知类型的数据项添加到类中
/// object可以包含任何类型
/// &/summary&
/// &param name="item"&要添加的数据项&/param&
/// &returns&新添加的数据项的索引&/returns&
public int AddItem(object item)
if (this.ItemCount & this.Items.Length)
this.Items[this.ItemCount] =
return this.ItemCount++;
throw new Exception("Item queue is full");
/// &summary&
/// 从类中获得一个数据项
/// &/summary&
/// &param name="index"&要获取的数据项的索引&/param&
/// &returns&object类型的一个数据项&/returns&
public object GetItem(int index)
if (index &= this.Items.Length &&
index &= 0)
throw new ArgumentOutOfRangeException(nameof(index));
return this.Items[index];
#region Properties
/// &summary&
/// 静态的实例计数器,用于标准类型
/// &/summary&
public static int InstanceCount { }
/// &summary&
/// 类中包含的数据项数量
/// &/summary&
public int ItemCount { }
/// &summary&
/// 类中包含的数据项
/// &/summary&
private object[] Items { }
#endregion // Properties
/// &summary&
/// 重写ToString以提供类的详细信息
/// &/summary&
/// &returns&包含类详细信息、格式化过的字符串&/returns&
public override string ToString() =&
$"There are {FixedSizeCollection.InstanceCount.ToString()}
instances of {this.GetType().ToString()} and this instance
contains {this.ItemCount} items...";
FixedSizeCollection 拥有一个静态整型属性

我要回帖

更多关于 sas input函数 的文章

 

随机推荐