建议36:使用FCL中的委托声明
我们应该习惯在代码中使用这类委托来代替自己的委托声明。
处理Action、Func和Predicate外,FCL中还有用于表示特殊含义的委托声明。如
用于表示注册事件方法的委托声明:
public delegate void EventHandler(object sender, EventArgs e); public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
表示线程方法的委托声明:
public delegate void ThreadStart(); public delegate void ParameterizedThreadStart(object obj);
表示异步回调的委托声明:
public delegate void AsyncCallback(IAsyncResult ar);
建议38:小心闭包中的陷阱
static void Main(string[] args) { List<Action> lists = new List<Action>(); for (int i = 0; i < 5; i++) { Action t = () => { Console.WriteLine(i.ToString()); }; lists.Add(t); } foreach (Action t in lists) { t(); } }
输出为:
5
5
5
5
5
如果匿名方法(lambda表达式)引用了某个局部变量,编译器就会自动将该引用提升到闭包对象中,即将for循环中的变量 i 修改成了引用闭包对象的公共变量 i 。这样,即使代码执行离开了原局部变量 i 的作用域(如for循环),包含该闭包对象的作用域还存在。理解了这一点,就理解了代码的输出了。
static void Main(string[] args) { List<Action> lists = new List<Action>(); for (int i = 0; i < 5; i++) { int temp = i; Action t = () => { Console.WriteLine(temp.ToString()); }; lists.Add(t); } foreach (Action t in lists) { t(); } }
建议91 属性和字段的区别
属性实质是方法,可以做更多的精细控制,可以让属性支持线程安全 ,而让属性支持线程安全可以让类型自身去实现,而让字段支持线程安全只能靠调用者本身去实现了。
建议 95 避免在构造方法中调用虚成员
static void Main() { American american = new American(); Console.ReadKey(); } class Person { public Person() { InitSkin(); } protected virtual void InitSkin() { //省略 } } class American : Person { Race Race; public American() : base() { Race = new Race() { Name = "White" }; } protected override void InitSkin() { Console.WriteLine(Race.Name); } } class Race { public string Name { get; set; } }
建议100 静态方法和实例方法没有本质区别
比如类型Person,我们都知道new Person() 会产生一个对象,这个对象叫做“实例对象”,它在运行时会加载到GC Heap上。而“类型对象”是指代表Person类型本身的那个对象,这个对象在第一次使用类型时被加载到Loader Heap上。类型对象包括其自身的指针、自身的同步索引快、静态字段,以及一个方法表。在这个方法表中,无论是静态方法还是实例方法都会被存储起来,当然,存储的是方法的记录项,方法本身是在调用时由运行时编译的。类型对象和实例对象在内存中的分布如下:
建议101:使用扩展方法,向现有类型“添加”方法
扩展方法除了让调用着可以像调用类型自身的方法一样去调用扩展方法外,它还有一些其他的主要优点:
- 可以扩展密封类型;
- 可以扩展第三方程序集中的类型;
- 扩展方法可以避免不必要的深度继承体系。
扩展方法还有一些必须遵循的要求:
- 扩展方法必须在静态类中,而且该类不能是一个嵌套类;
- 扩展方法必须是静态的;
- 扩展方法的第一个参数必须是要扩展的类型,而且必须加上this关键字;
- 不支持扩展属性、事件。
- 建议102:区分接口和抽象类的应用场合
接口可以包含方法、属性、索引器、事件的签名,但不能有实现,抽象类则可以。
接口在增加新方法后,所有的继承者都必须重构,否则编译不通过,而抽象类则不需要。
这些区别导致两者的应用场景不同:
如果对象存在多个功能相近且关系紧密的版本,则使用抽象类。
如果关系不紧密,但若干功能拥有共同的声明,则使用接口。
抽象类适合于提供丰富功能的场合,接口则更倾向于提供单一的一组功能。
从某种角度来看,抽象类比接口更具备代码的重用性。子类无须编写代码即可具备一个共性的行为。
采用抽象类的另一个好处是,如果为为基类增加一个方法,则继承该基类的所有子类自然就会具备这个额外的方法,而接口却不能。如果接口增加一个方法,必须修改所有的子类。所以,接口一旦设计出来就应该是不变的。抽象类则可以随着版本的升级增加一些功能。
接口的作用更倾向于说明类型具有某个或者某种功能。接口只负责声明,而抽象基类往往还要负责实现。
接口的职责必须单一,在接口中的方法应该尽可能的简练。
抽象类解决的是 is a 而接口解决的是 can a
-
它们仅包含静态成员。
-
它们不能被实例化。
-
它们是密封的。
-
它们不能包含实例构造函数
static class SampleClass { private static FileStream fileStream; static SampleClass() { try { fileStream = File.Open(@"c:\temp.txt", FileMode.Open); } catch (Exception ex) { Console.WriteLine(ex.Message); //异常处理 } } public static void SampleMethod() { } }
建议104:用多态代替条件语句
先,设计一个抽象类Commander:
abstract class Commander { public abstract void Execute(); }
所有的Start或者Stop之类的命令,全部继承自这个抽象类:
class StartCommander : Commander { public override void Execute() { //启动 } } class StopCommander : Commander { public override void Execute() { //停止 } }
在使用多态后,调用方法的代码应该如下所示:
static void Main(string[] args) { Commander commander = new StartCommander(); Drive(commander); commander = new StopCommander(); Drive(commander); } static void Drive(Commander commander) { commander.Execute(); }
可见,代码简洁了不少,而且,可扩展性增强了。即使未来还需要增加命令,扩展相应的子类就可以了。而且我们关闭了修改,即对Drive方法,即使增加再多的命令,也不需要对其进行修改。