《Effective C#》读书笔记

本文档提供了C#编程中的各种最佳实践建议,涵盖了属性使用、类型选择、内存管理、异常处理等多个方面,帮助开发者编写高效、健壮的代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  1. 使用属性代替可访问的数据成员

    除了私有数据成员,其他的都应该使用属性


IL查看可以看到属性是使用方法实现的,所以说即使多个实例也不会增加额外的内存开销。

Binding只支持属性,不支持数据成员

更容易扩展

属性对多线程支持更容易

属性可以为虚属性、抽象属性或者接口的一部分(只能定义自动属性在接口中)

属性更容易控制可见性(访问修饰符或只读只写)

利用属性实现索引器

Public int this[int index]

{

    get

{ return _theValue[index]; }

Set

{ _theValue[index] = value; }

}

属性和数据成员在源代码层次上是兼容的,但是在二进制层次上是不兼容的,必须重新编译

使用属性并不比数据成员慢,因为JIT编译器进行了内联处理


 


 


  1. 运行时常量(readonly)优于编译时常量(const

    const效率高, readonly 更灵活, 具体问题具体分析


const 声明编译时常量, readonly声明运行时常量

const声明的编译时常量在编译时会把所有用到的地方替换成声明的值;而readonly只是在所有使用的地方编译成对readonly声明变量的引用

const效率高, readonly更灵活

const只限于数值、字符串等,而readonly可用于对象


 


  1. 操作符isas优于强制转型

    能用is/as就不要用强制转换,但是as不能用于自定义转换(implicit/explicit)和值类型。Is可以用于值类型,但也不能用于自定义转换。


尽可能使用as/is操作符代替强转,因为它更安全,效率高。但asis都不能用于用户自定义转换

用户自定义强转 implicit(隐式) explicit(显式)

public static implicit/explicit 目标类型(待转类型 XXX

{

           ……

           return 目标类型结果;

}

Example:

//class2 -> class1

public static implicit class1(class2 c2)

{

      class1 c1 = new class1();

      c1.value = c2.value;

      return c1;

}

使用时class1 c1 = c2;

 

//class -> int

public static explicit int(class c)

{     

      return c.value;

}

使用时 int I =  (int)c1;

 

使用自定义转换时,比如定义了从class2class1的自定义转换,object objclass2

Class1 c1 = (class2)obj;这样会转换,因为虽然用从class2class1的自定义转换,但是没有从objectclass1的转换。

先将obj转成class2,再强转就会成功。

as不能用于值类型,否则编译器会报错;is可以用于值类型

foreach里面用的是强制转换来执行迭代里使用的类型迭代对象之间的类型转换。

如果两个类型之间存在自定转换,则迭代会失败,因为迭代过程中用的是object

所以如果用foreach迭代一个对象,使用自定义转换的类型来迭代对象是不行的。

对象的GetType()可以获得对象的运行时类型                        


 


  1. 使用Conditional 特性代替#if条件编译

    Conditional 特性只能用于整个方法(必须返回void)或派生于Attribute的类,所以不能完全替代#if. 这块就有个印象conditional会控制函数调用是否生成在程序集里面。


使用Conditional 特性即在整个方法上(非局部)定义特性Conditional. Conditional的参数可以在工程的Properties->build->Conditional Compilation Symbols里定义,也可以在程序里#define定义。

[Conditional(“DEBUG”)]

Private void CheckState()

{

}

Conditional 特性在编译时并不影响该方法的定义生成在程序集中,只是影响该方法的调用,即如果没有定义Conditional的参数符号,则调用该函数的地方不会生成在程序集里面。

.Net3.0conditional 可有在返回值为void的方法和派生于Attribute的类


 


  1. 总是提供ToString()方法


我们创建的每一个类型都应该重写ObjectToString()方法。如果创建更复杂的类型,则应该实现IFormattable.ToString()方法。


 


  1. 明辨值类型和引用类型的使用场合

    如果确定现在及将来只存数据,那么用值类型(struct;如果确定有任何行为,则使用引用类型(class,无法确定的情况也用引用类型。


值类型不支持多态(即不支持继承),比较适合存储数据。引用类型支持多态,应该用于定义应用程序的行为。结构体用于存储数据,类用于定义行为。

值类型在内存管理方面具有更好的效率:较少的内存碎片,较少的垃圾,以及较少的间接访问。更重要的是值类型从方法或者属性中返回时使用的是复制的方式这避免了将内部数据结构的引用暴露给外界的危险。


 


  1. 将值类型尽可能实现为具有常量性和原子性的类型

    如果是值类型(主要指结构体),那么要尽可能保证他的整体一致,最好不要改值(常量性),要改值一起改(原子性),这么做是因为各成员之间可能有依赖关系,改一个可能导致其他的无效。可以考虑创建一个新的对象,而不是在原有对象上改。


如果值类型(struct)里面有引用类型成员,则要做防御性复制,防止外部修改其值。


 


  1. 确保0为值类型的有效状态

    枚举时要从0开始,因为创建变量时默认初始值都是0.


创建枚举时,要确保0为有效的状态。即不要让枚举值从非0开始,因为创建一个枚举对象时,初始值默认为0.

位枚举。通过[Flags] 特性实现。位枚举用来表示一组可组合的位标记(OR组合)。比如枚举值为3 则为Flat|Sunken. 如果没有Flags特性标记,则值为3,仅表示值为3.

[Flags]

Public enum Styles

{

None = 0,

Flag =1,

Sunken = 2,

Raised=4

}

判断值里面是否含有某标记if((v&Flat)==Flat)

判断值里面是否只包含某标记 if(v==Flat)

值类型里面如果含有引用类型成员,比如string,则应该用一个string属性包装下这个string, 让其返回一个空字符串,而非空引用。


 


  1. 理解几个相等判断之间的关系

    如果想比较两个引用类型的


  • 引用相等:调用Object.ReferenceEquals().

  • 内容相等:重写objectvirtual Equals(object right).


        如果想比较两个值类型相等, 则需重写Object virtual Equals(objectright)方法(因  为值类型基类ValueType.Equals()效率低)或者重定义Operator ==(因为系统默认的是通过反射来比较两个值类型,效率低).


Object4个比较相等函数

Public static bool ReferenceEquals(object left, object right) – 判断两个引用变量是否指向同一个对象(引用相等)。

Public static bool Equals(object left, object right)—本质上调用左参数重写objectvirtual Equals(object right)方法,如果左参数没有重写该方法,则等同于ReferenceEquals().

Public virtual bool Equals(object right) – 值类型必须重写以比较两个值类型是否相等,引用类型如果想比较内容相等,则必须重写。参见p/59.

Public static bool operator == myclass left, myclass right - 值类型为了效率需要重新定义,引用类型一般比较引用相等,不必重新定义。

 

引用相等---两个引用类型的变量指向同一个对象。

值相等 ---两个值类型的变量类型相同,而且包含同样的内容。

Object.ReferenceEquals() 对于值类型,永远返回false.


 


  1. 理解GetHashCode()方法的缺陷

          尽量避免重写objectvirtual GetHashCode(),但自定义类型用于Hash Table的键值(比如Dictionary)时,必须重写,所以尽量避免用自定义类型作为键值。


GetHashCode()函数只用在一个地方:为一个基于散列(hash)的集合定义键的散列值,典型的集合为HashtableDictionary容器。

虽然Object.GetHashCode()效率低,但是最好避免重写这个函数。

如果实现了自己的Operator ==,那么就必须实现GetHashCode()

Object.GetHashCode()方法的默认行为:对于所有引用类型,该方法会正常工作,但不一定会产生一个高效的分布(如果重写了Operator == 将会破坏GetHashCode(). 只有在结构类型的第一个字段是只读的情况下,ValueType.GetHashCode()才会正常工作。


 


  1. 优先采用foreach循环语句

    foreach  效率最高,优先使用foreach遍历


Foreach循环效率最高,且foreach语句编译器会产生最佳代码。

Int len = foo.Length;

for( int i = 0; i < len; i ++)

{

 Int j = foo[i];

}

这样写效率最低。因为CLR会在访问数组元素之前进行一个数组界限测试,这么写会执行两次界限测试。

Foreach中循环变量是只读的

Foreach可以简化多维数组遍历。

迭代类需实现

Public IEnumerator GetEnumerator()

{

    yield return xxx;

}

迭代方法需:

IEnumerable方法名()

{

    yield return xxx;

}


 


  1. 变量初始化器优于赋值语句

         这里的初始化器只声明并初始化,并不是new实例后边{}那个。

           声明变量的同时初始化它们,但如果初始化成0null,则不必初始化,因为系统默认初始化是这些值。目的是为了确保变量被初始化。


声明时初始化变量,编译器会把这些代码插入到构造函数的前面(注不是里面,严格说是插入到其基类构造函数的前面)

不用声明并初始化变量的三个情形:

a如果初始化为0或者null, 则系统会默认做。

b同一个对象不要多次初始化

c 初始化如果有try catch,则放到构造函数中。


 


  1. 使用静态构造器初始化静态类成员

    静态成员初始化两种方法:声明时初始化(用于简单情况);静态构造函数(复杂情况)


静态构造函数

a.      无访问修饰符, 无参, 一个类只能有一个。

b.      如果没有定义静态构造函数,但有静态字段, 则编译器会自动生成静态构造.

c.       静态构造在创建类第一个实例或者静态成员被引用时自动调用,但仅调用一次。

d.      静态成员声明时被初始化将会在静态函数之前执行(在基类静态之前)。

如果仅需要分配一个静态成员变量,则使用声明时并初始化;如果需要复杂的逻辑来执行静态成员初始化则创建静态构造。


 


  1. 利用构造器链

         即把共同的初始化代码放到公共的构造函数里,然后通过this访问。不要在构造里面调用另一个函数来完成初始化.


构造器使用this,是在本构造之前调用this那个构造

不要在构造里面调用另一个函数来完成初始化。这样会导致在构造函数里默认把成员都初始化一遍,又调那个函数再初始化一遍,实际初始化两遍。

只读变量只能在构造函数里初始化

构造第一个类型实例操作顺序:

a.      将所有静态变量设置为0

b.      执行静态变量声明时赋值那个赋值。

c.       执行基类的静态构造函数。

d.      执行当前类型的静态构造函数。

e.      将当前实例的所有变量设置为0.

f.        执行当前实例变量声明时赋值那个赋值。

g.      执行基类实例的构造函数。

h.      执行当前实例的构造函数。


 


  1. 利用usingtry/finally语句来清理资源

           如果使用一个非托管资源则用using来隐式调用Dispose();如果有多个非托管资源则用try/finally(推荐)using嵌套。


Using 编译后即为try/finally. using 会把(){}里面的代码放到try里,然后在finally里会调()里面对象的Dispose方法。

using( class1 c1 = new class1() )

{

     c1.XXX();

}

using 只能用于支持IDisposable接口的对象。如果using用于对象不支持IDisposable,编译器会报错。

如果不能确定对象是否支持IDisposable接口,则

using( xxx as IDisposable ).  如果对象不支持using(null)不会做任何事情

一个using 里面可以放多个对象,但必须是同一个对象类型

Using( class1 c1 = new class1(), class1 cc = new class1() )

{

}

如果是不同的类型,则最好用try/finally.

new class() 一定要放到using里面,否则构造函数一旦异常,则Dispose()永远都调不到。

有些类型即支持Dispose()也支持Close()来释放资源。

一般在Dispose()的实现里面会调GC.SuppressFinalized()函数,即不调析构。

Close()会调析构。


 


  1. 尽量减少内存垃圾

         把频繁使用的引用类型局部变量提升为成员变量

          把常用的引用类型实例定义成静态成员变量

          不要对string进行+=字符串拼接,用string.Format()StringBuilder


String类型在构造完之后,其内容不可能被改变。当改变该string内容时,比如= +=实际上都是new一个新的String, 而旧的则变成垃圾。

这也解释了string虽然为引用类型,但是把一个String赋给另一个string时并不是把对象引用传过去,而是把内容传过去。

String msg = string.Format( “ Hello, {0} Today is {1}”, thisUser.Name, DateTime.Now.ToString());

 

StringBuilder sb = new StringBuilder( “ Hello, ”);

Sb.Append(thisUser.Name);

Sb.Append( “. Today is ”);

Sb.Append( DateTime.Now.ToString());

String strFinal = sb.ToString();


 


  1. 尽量减少装箱与拆箱

    尽量减少封箱、拆箱,因为其耗费性能,效率低。


封箱:值类型的copy放入object

拆箱:Object里的值copy一个副本给值类型。

密切关注一个类型到object的隐式转换。如果可以避免的话,值类型不应该被替换为object类型。比如

Console.wrinteline(“{0} {1}”, 12, 13 ); => Console.wrinteline(“{0} {1}”, 12.ToString(), 13.ToString() );

值类型存入集合时会有封箱操作,但是泛型不会。

所以集合还是用泛型好!

值类型优先存在数组里或者泛型里。(泛型里不存在封箱拆箱)

改变集合或泛型里面值类型的值:

MyStruct m =(MyStruct) MyList[0]; //MyList ArrayList,需要拆箱

MyStruct m = MyList[0]; //MyList List< MyStruct >,不需要拆箱

m.name = “Lina”;

MyList[0]=m;


 


  1. 实现标准Dispose模式

         按照书上方式实现类及其派生类的IDisposable接口的Dispose()方法

         


除了实现Dispose()方法,还要在析构函数里释放非托管资源,以确保用户忘记调用Dispose()方法时,非托管资源也能释放,但是析构函数不一定啥时候调。

Finalize()即析构函数,编译器会把析构函数编译成Finalize()

Dispose()和析构函数里只应执行释放资源而无其它操作。因为其它操作可能‘唤醒‘即将释放的资源。


 


  1. 定义并实现接口优于继承类型

         使用接口还是类继承看具体问题。但接口用于参数和返回值确实很灵活

         


接口中可定义方法,属性(必须为自动属性),事件和索引器。实现了接口的类型必须实现接口中定义的所有元素提供具体的实现。

将接口用于参数和返回值适应面广,易于重用。也就是只要支持该接口都可以用。

用接口可以把不相关类型的相同功能提取出来

通过接口还可以避免接口类型的拆箱代价,也就是可以通过接口访问集合内部的值类型,改变其值,而不必拆箱。P108

显示实现接口函数时,该实现只能通过接口访问,而不能通过继承自接口的类来访问。

Class1: IMyInterface

{

Public void   IMyInterface.Foo()

{

}

}


 


  1. 明辨接口实现和虚方法重写

         明确派生类是隐藏(new)还是重写(override)基类继承自接口的方法。若隐藏则基类和接口都无法访问派生类的该方法;重写则会访问。

          使用指向的思想可以理解调哪个函数:

         MyDrivedClass d = new MyDrivedClass();

      IMyInterfaceim = d;

      MyBaseClass b= d;

     Im, b指向d的内存虚表,如果override, 则可以访问该函数;如果new则不能。


派生类不能重写(override)基类已经实现的接口成员, 前提是该成员不是virtual, 如果基类virtual 该函数,则派生类可以override

派生类如果不通过new来显示隐藏基类的方法也可以,只是有个warning


 


  1. 使用委托表达回调

         多播委托跟事件由多个事件处理程序订阅一样

          


多播委托最后一个处理函数返回值作为整个多播的返回值


 


  1. 使用事件定义外发接口

         EventHandlerList存储大量事件/委托

          


使用Eventlog类将消息写入系统日志

通过EventHandkerList集合,存储指定委托的列表。用于事件数量很多的情况


 


  1. 避免返回内部类对象的引用

         要避免返回内部对象的引用,即不要把内部类对象暴露给外界。若必须暴露则用接口(尽量减少更改的可能性)和包装器对象(即把该类包装一下,控制其访问权限)

  2. 声明式编程优于命令式编程

         声明式编程即特性Attribute

          命令式编程即普通c#编程

          这章主要介绍的是特性+反射。具体实践中用特性还是普通代码具体问题具体分析。

  3. 尽可能将类型实现为可序列化的类型

    如果对象可能被序列化,则类前边最好加上[Serializable]。若要支持版本兼容则需要实现ISerializable接口


添加[Serializable]则该对象就支持序列化。如果成员不支持序列化,则其上加上[NonSerialized].

IFormatter ifo = new BinaryFormatter();

Ifo.Serialize() – 序列化

Ifo.Deserialize(); - 反序列化

对于使用[NonSerialized]的成员,反序列化后会使用系统默认的0null.如果想初始别的值,则实现IDeserializationCallback接口的OnDeserialization()

通过实现ISerializable接口来实现自定义序列化,尤其是版本兼容(添加一个版本字段)。

实现ISerializable,需要实现两个函数:

a.      构造函数: MyClass( SerializationInfo info, StreamingContext cntxt )---反序列化时候调用(自动调用)

b.      GetObjectData(SerializationInfo info, StreamingContext cntxt ) ---在序列化时候调用(自动调用)

ISerializable接口使用的方法和存储与[Serializable]一样,所以刚开始创建类的时候用[Serializable],将来如果有必要扩展时,再添加对ISerializable接口的支持。

但要注意如果之前串行化用的是[Serializable],则实现ISerializable接口时,包括构造和GetObjectData()都要按照变量在类中声明的顺序进行,因为[Serializable]是按照这个顺序的,如果调整顺序会打破先前序列化创建的文件的兼容性。

多版本兼容,最好添加一个版本字段,上来就读这个版本号,然后根据不同版本好来读取不同的版本变量。

对于派生类也支持序列化则基类的构造函数MyClass( SerializationInfo info, StreamingContext cntxt )要为public/protected,基类的GetObjectData()要定义成virtual. 然后派生类实现自己的反序列化构造及重写GetObjectData(),并调用基类对应的方法即可。


 


  1. 使用IComparableIComparer接口实现排序关系

         IComparable(CompareTo()方法)---被排序的类需要继承IComparable并实现CompareTo(Object)方法,即需要访问类内部。集合通过调用Sort()方法来实现排序List.Sort().

          IComparer(Compare(object,object))---需要单独实现一个类,该类需继承IComparer,并实现Compare()方法,不需要访问被排序类的内部,只需访问其public属性进行排序。集合通过调用Sort(IComparer)来实现排序,即List.Sort(继承自IComparer那个类)


如果不能访问类内部,且要对public属性进行排序,则用IComparer接口


 


  1. 避免ICloneable接口

         尽量避免ICloneable, 因为会给派生类带来麻烦。如果必须支持这个接口,则参考p166实现基类/派生类的深度复制方法


引用类型成员仅string类型特殊。不必考虑其深度复制,使用“=”Memberwiseclone()都行。因为如果改string的值都会new一个新的string

如果非得支持ICloneable,则该类中有别的引用成员,比如另一个类,则在实现clone()时候需要调用那个类的clone()方法。


 


  1. 避免强制转换操作符

    尽量避免使用implicit/explicit进行隐式/显式的转换。因为转换后的对象实际上是个临时对象,所有对转换结果进行的操作都是作用在临时对象上,可能会导致bug.


一般的隐式/显式转换实现为

Public static implicit/explicit operator class1( class2 c2)

{

     class1 c1 = new class1();

 c1.XXX = c2.XXX;

 return c1;

}

//转换接口是newc1,而不是源对象c2.


 


  1. 只有当新版基类导致问题时才考虑使用new修饰符

         尽量避免隐藏基类(new),因为会导致混淆不清,除非新版基类增加的成员与子类的已存在成员冲突。

  2. 尽可能实现CLS兼容的程序集

         要实现跨语言互操作,则程序集要支持CLS。即在程序里(namespace前边)添加特性[assembly: CLSCompliant( true ) ]. CLS兼容程序集要保证程序集中所有公有和受保护成员与CLS兼容。


强命名程序集:即程序集包括名称,版本号,语言文化属性和公有密钥标记。

弱命名程序集:只包含名称。

只有强命名程序集才可以存储在GAC中(Global Assembly Cache

AssemblyInfo.cs里面包含程序集的版本号等信息

CLS – Common Language Subsystem. CLS规范是一个每门.NET语言都必须支持的操作子集。

创建与CLS兼容的类型要求公有接口和受保护接口只能包含与CLS兼容的类型。如果实现了一个与CLS不兼容的接口,要保证该类型与CLS兼容,则要显示实现该接口(实现时候加上接口名IMyInterface.Foo()),这样该接口就不是该类型公有接口的一部分了。


 


  1. 尽可能实现短小简洁的函数

         JIT编译器是以函数为单位进行编译的,编写短小的函数可避免不被调用的代码也被JIT编译了。

         


 

Design Time: C#代码=>C#编译器编译成CIL程序集(Common Intermediate Language通用中间语言)

Run Time:     JIT 编译器编译成机器码=>交给CLR托管运行

 

JIT对属性会做内联处理,其效率不低,但是我们没办法定义内联


 


  1. 尽可能实现小尺寸、高内聚的程序集

        创建合适的程序集,过大过小都不好


程序集大,不用的代码虽然不会被JIT编译,但是会被加载---占内存

程序集多,跨程序集边界时,有额外的安全检查


 


  1. 限制类型的可见性

         尽量不要暴露类对外的可见性,仅暴露那些该暴露的功能。

  2. 创建大粒度的Web API

          N/A

  3. 重写优于事件处理器

         如果能通过重写(override)基类虚函数处理系统事件,则不必自定义事件处理函数(即自订阅事件),因为前者效率高。


如果一个事件处理器抛出异常,那么事件链上的其他处理器将不会被调用。通过重写受保护的虚函数,我们的处理器会首先被调用。

使用重写比关联事件处理器高效的多。查看是否有事件处理器及在运行时迭代每一个处理器,将会比调用一个虚函数占用更多的执行时间。


 


  1. 合理使用.NET运行时诊断

    通过Trace, Debug, Eventlog类输出到output,文件和系统日志等。通过TraceSwitch来控制输出级别。


Trace – Relase + Debug

Debug – Debug

Eventlog – 系统日志

Trace/Debug

a.      WriteLine()

b.      Write()

c.       WriteLineIf()

d.      WriteIf()

通过TraceSwitch来控制输出级别例子p210-p211 a-d

Trace/DebugTxt:

TextWriterTraceListener tr2 = new TextWriterTraceListener(System.IO.File.CreateText(“out.txt”));

Debug.Listeners.Add(tr2);

Debug.WriteLine(“hello world”);

Debug.Flush();

FCL  - Framework Class Library


 


  1. 使用标准配置机制

    可用App.config(应用程序配置信息)生成的<AppName>.<ext>.config(需要跟应用程序安装在一起)存储配置信息,也可以将配置信息存于自定义XML里,并放在通过GetFolderPath()获得的ApplicationData目录下。


App.config vs2013自带, vs2008需要手动添加

·         GetFolderPath(SpecialFolder.CommonApplicationData)

返回C\Documents and Settings\All users\Application Data

·         GetFolderPath(SpecialFolders.ApplicationData)

返回C\Documents and Settings\<user name>\Application Data

·         GetFolderPath(SpecialFolders.LocalApplicationData)

返回C\Documents and Settings\<user name>\Local Settings\Application Data

 


 


  1. 定制和支持数据绑定

         除了WPF, WindwsForm 也支持数据绑定。比如textBox.DataBindings.Add()。通过Binding对象的Parse/Format事件,可对绑定数据加入校验,转换等控制。

     


Control类派生类都支持数据绑定

数据源不同,绑定管理器不同

.Net绑定默认会进行类型转换

textBox1.DataBindings.Add(Binding) 实际上是Add一个Binding对象。Binding有两个事件Parse(当控件的值发生变化时发生)Format(当控件属性绑定到数据时发生)。可定制这两个事件,添加数据校验,转换等操作,来取代默认的转换。


 


  1. 使用.NET验证

         使用Validating事件处理来对用户输入进行验证

      


只有当CauseValidation属性为true时,才会触发ValidatingValidated事件

Validating – 发生时值还没被写入控件。若取消之前的操作则设定e.Cancel = true;

Validated – 发生时值已经写入控件,能取出。


 


  1. 根据需要选择恰当的集合

    优先使用Array,效率高,无封箱拆箱。另外ArrayList, List<T>, Dictionary<k,v>,Queue<T>, Stack<T>, LinkedList<T>, SortedList<k,v>.


使用数组存储值类型,不存在封箱、拆箱等效率损失。数组效率最高。

使用泛型存储值类型,也不存在封箱、拆箱。

Queue<T>­­-先进先出。 Enqueue()进队, Dequeue()出队.

Stack<T>先进后出。Push(), Pop()

LinkedList<T> - 双向链表

SortedList<k,v> - 有序列表

创建自定义集合时,派生于CollectionBaseDictionaryBase


 


  1. DataSet优于自定义结构

         N/A

  2. 利用特性简化反射

         主要讲的是特性+反射

           简而言之,类及成员加上特性后,反射更容易找。

          


特性实际上声明的是我们在运行时候的意图。在一个元素上标记特性可以描述他的用途,以及降低在运行时查找该元素的难度。


 


  1. 避免过度使用反射

         通过反射可在运行时获得类的信息, 调用类的方法, 构造函数,访问属性及字段的值等(主要通过Type类来实现)。但反射过于复杂且有安全隐患,建议用接口取代之。


反射获得类的构造并创建类实例:

Type t = typeof(mytype);

ConstructorInfo ci = t.GetContractor( new Type[0]) //调用无参构造

Ci.Invoke(); //创建类实例

 

如果获得int型参数构造,则用

Type [] types = new Type[1];

types[0] = typeof(int);

ConstructorInfo ci = t.GetContractor( types);

 

如果不知道类的类型时(之前没有该类型的相关知识),这是在运行时创建类实例的唯一选择。

反射获得类的所有成员(t is Type)

·         所有成员: t.GetMembers()/GetMember() + MemberInfo

·         方法:t.GetMethods()/GetMethod() + MethodInfo,

·         属性: t.GetProperties()/GetProperty() + PropertyInfo

·         事件:t.GetEvents()/GetEvent() + EventInfo

·         字段: t.GetFields()/GetField()+FieldInfo

 


 


  1. 为应用程序创建特定的异常类

    定制异常类需要派生于ApplicationException,并实现4个构造函数(throw扔出不同异常)


Public ApplicationException(); // 默认构造

Public ApplicationException( string ); //使用一个message来创建异常对象

Public ApplicationException(string, Exception); //可将内部异常,比如第三方库异常封装在这里。

Public ApplicationException( SerializationInfo, StreamingContext); //从输入流中创建异常对象。

 

在具体实现时,对于不关心的构造器,可以将工作委派给基类(ApplicationException):

Public class MyApplicationException : ApplicationException

{

 Public MyApplicationException (): base()

 {

 }

}

 


 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值