Index :html
(1)类型语法、内存管理和垃圾回收基础程序员
(2)面向对象的实现和异常的处理基础面试
(3)字符串、集合与流数据库
(4)委托、事件、反射与特性多线程
(5)多线程开发基础框架
(6)ADO.NET与数据库开发基础less
(7)WebService的开发与应用基础编辑器
在C#中申明一个类型时,只支持单继承(即继承一个父类),但支持实现多个接口(Java也是如此)。像C++可能会支持同时继承自多个父类,但.NET的设计小组认为这样的机制会带来一些弊端,而且没有必要。ide
首先,看看多继承有啥好处?多继承的好处是更加贴近地设计类型。例如,当为一个图形编辑器设计带文本框的矩形类型时,最方便的方法多是这个类型既继承自文本框类型,又继承自矩形类型,这样它就天生地具备输入文本和绘画矩形的功能。But,自从C++使用多继承依赖,就一直存在一些弊端,其中最为严重的仍是所谓的“砖石继承”带来的问题,下图解释了砖石继承问题。性能
如上图所示,砖石继承问题根源在于最终的子类从不一样的父类中继承到了在它看来彻底不一样的两个成员,而事实上,这两个成员又来自同一个基类。鉴于此,在C#/Java中,多继承的机制已经被完全抛弃,取而代之的是单继承和多接口实现的机制。众所周知,接口并不作任何实际的工做,可是却制定了接口和规范,它定义了特定的类型都须要“作什么”,而把“怎么作”留给实现它的具体类型去考虑。也正是由于接口具备很大的灵活性和抽象性,所以它在面向对象的程序设计中更加出色地完成了抽象的工做。
在C#或其余面向对象语言中,重写、重载和隐藏的机制,是设计高可扩展性的面向对象程序的基础。
(1)重写和隐藏
重写(Override)是指子类用Override关键字从新实现定义在基类中的虚方法,而且在实际运行时根据对象类型来调用相应的方法。
隐藏则是指子类用new关键字从新实现定义在基类中的方法,但在实际运行时只能根据引用来调用相应的方法。
如下的代码说明了重写和隐藏的机制以及它们的区别:
public class Program { public static void Main(string[] args) { // 测试两者的功能 OverrideBase ob = new OverrideBase(); NewBase nb = new NewBase(); Console.WriteLine(ob.ToString() + ":" + ob.GetString()); Console.WriteLine(nb.ToString() + ":" + nb.GetString()); Console.WriteLine(); // 测试两者的区别 BaseClass obc = ob as BaseClass; BaseClass nbc = nb as BaseClass; Console.WriteLine(obc.ToString() + ":" + obc.GetString()); Console.WriteLine(nbc.ToString() + ":" + nbc.GetString()); Console.ReadKey(); } } // Base class public class BaseClass { public virtual string GetString() { return "我是基类"; } } // Override public class OverrideBase : BaseClass { public override string GetString() { return "我重写了基类"; } } // Hide public class NewBase : BaseClass { public new virtual string GetString() { return "我隐藏了基类"; } }
以上代码的运行结果以下图所示:
咱们能够看到:当经过基类的引用去调用对象内的方法时,重写仍然可以找到定义在对象真正类型中的GetString方法,而隐藏则只调用了基类中的GetString方法。
(2)重载
重载(Overload)是拥有相同名字和返回值的方法却拥有不一样的参数列表,它是实现多态的立项方案,在实际开发中也是应用得最为普遍的。常见的重载应用包括:构造方法、ToString()方法等等;
如下代码是一个简单的重载示例:
public class OverLoad { private string text = "我是一个字符串"; // 无参数版本 public string PrintText() { return this.text; } // 两个int参数的重载版本 public string PrintText(int start, int end) { return this.text.Substring(start, end - start); } // 一个char参数的重载版本 public string PrintText(char fill) { StringBuilder sb = new StringBuilder(); foreach (var c in text) { sb.Append(c); sb.Append(fill); } sb.Remove(sb.Length - 1, 1); return sb.ToString(); } } public class Program { public static void Main(string[] args) { OverLoad ol = new OverLoad(); // 传入不一样参数,PrintText的不一样重载版本被调用 Console.WriteLine(ol.PrintText()); Console.WriteLine(ol.PrintText(2,4)); Console.WriteLine(ol.PrintText('/')); Console.ReadKey(); } }
运行结果以下图所示:
在C#程序中,构造方法调用虚方法是一个须要避免的禁忌,这样作到底会致使什么异常?咱们不妨经过下面一段代码来看看:
// 基类 public class A { protected Ref my; public A() { my = new Ref(); // 构造方法 Console.WriteLine(ToString()); } // 虚方法 public override string ToString() { // 这里使用了内部成员my.str return my.str; } } // 子类 public class B : A { private Ref my2; public B() : base() { my2 = new Ref(); } // 重写虚方法 public override string ToString() { // 这里使用了内部成员my2.str return my2.str; } } // 一个简单的引用类型 public class Ref { public string str = "我是一个对象"; } public class Program { public static void Main(string[] args) { try { B b = new B(); } catch (Exception ex) { // 输出异常信息 Console.WriteLine(ex.GetType().ToString()); } Console.ReadKey(); } }
下面是运行结果,异常信息是空指针异常?
(1)要解释这个问题产生的缘由,咱们须要详细地了解一个带有基类的类型(事实上是System.Object,全部的内建类型都有基类)被构造时,全部构造方法被调用的顺序。
在C#中,当一个类型被构造时,它的构造顺序是这样的:
执行变量的初始化表达式 → 执行父类的构造方法(须要的话)→ 调用类型本身的构造方法
咱们能够经过如下代码示例来看看上面的构造顺序是如何体现的:
public class Program { public static void Main(string[] args) { // 构造了一个最底层的子类类型实例 C newObj = new C(); Console.ReadKey(); } } // 基类类型 public class Base { public Ref baseString = new Ref("Base 初始化表达式"); public Base() { Console.WriteLine("Base 构造方法"); } } // 继承基类 public class A : Base { public Ref aString = new Ref("A 初始化表达式"); public A() : base() { Console.WriteLine("A 构造方法"); } } // 继承A public class B : A { public Ref bString = new Ref("B 初始化表达式"); public B() : base() { Console.WriteLine("B 构造方法"); } } // 继承B public class C : B { public Ref cString = new Ref("C 初始化表达式"); public C() : base() { Console.WriteLine("C 构造方法"); } } // 一个简单的引用类型 public class Ref { public Ref(string str) { Console.WriteLine(str); } }
调试运行,能够看到派生顺序是:Base → A → B → C,也验证了刚刚咱们所提到的构造顺序。
上述代码的整个构造顺序以下图所示:
(2)了解完产生本问题的根本缘由,反观虚方法的概念,当一个虚方法被调用时,CLR老是根据对象的实际类型来找到应该被调用的方法定义。换句话说,当虚方法在基类的构造方法中被调用时,它的类型让然保持的是子类,子类的虚方法将被执行,可是这时子类的构造方法却尚未完成,任何对子类未构形成员的访问都将产生异常。
如何避免这类问题呢?其根本方法就在于:永远不要在非叶子类的构造方法中调用虚方法。
这是一个被问烂的问题,在C#中能够经过sealed关键字来申明一个不可被继承的类,C#将在编译阶段保证这一机制。可是,继承式OO思想中最重要的一环,可是否想过继承也存在一些问题呢?在设计一个会被继承的类型时,每每须要考虑再三,下面例举了常见的一些类型被继承时容易产生的问题:
(1)为了让派生类型能够顺利地序列化,非叶子类须要实现恰当的序列化方法;
(2)当非叶子类实现了ICloneable等接口时,意味着全部的子类都被迫须要实现接口中定义的方法;
(3)非叶子类的构造方法不能调用虚方法,并且更容易产生不能预计的问题;
鉴于以上问题,在某些时候没有派生须要的类型都应该被显式地添加sealed关键字,这是避免继承带来不可预计问题的最有效办法。
相信阅读本文的园友都已经养成了try-catch的习惯,但对于异常的捕捉和处理可能并不在乎。确实,直接捕捉全部异常的基类:Exception 使得程序方便易懂,但有时这样的捕捉对于业务处理没有任何帮助,对于特殊异常应该采用特殊处理可以更好地引导规划程序流程。
下面的代码演示了一个对于不一样异常进行处理的示例:
public class Program { public static void Main(string[] args) { Program p = new Program(); p.RiskWork(); Console.ReadKey(); } public void RiskWork() { try { // 一些可能会出现异常的代码 } catch (NullReferenceException ex) { HandleExpectedException(ex); } catch (ArgumentException ex) { HandleExpectedException(ex); } catch (FileNotFoundException ex) { HandlerError(ex); } catch (Exception ex) { HandleCrash(ex); } } // 这里处理预计可能会发生的,不属于错误范畴的异常 private void HandleExpectedException(Exception ex) { // 这里能够借助log4net写入日志 Console.WriteLine(ex.Message); } // 这里处理在系统出错时可能会发生的,比较严重的异常 private void HandlerError(Exception ex) { // 这里能够借助log4net写入日志 Console.WriteLine(ex.Message); // 严重的异常须要抛到上层处理 throw ex; } // 这里处理可能会致使系统崩溃时的异常 private void HandleCrash(Exception ex) { // 这里能够借助log4net写入日志 Console.WriteLine(ex.Message); // 关闭当前程序 System.Threading.Thread.CurrentThread.Abort(); } }
(1)如代码所示,针对特定的异常进行不一样的捕捉一般颇有意义,真正的系统每每要针对不一样异常进行复杂的处理。异常的分别处理是一种好的编码习惯,这要求程序员在编写代码的时候充分估计到全部可能出现异常的状况,固然,不管考虑得如何周到,最后都须要对异常的基类Exception进行捕捉,这样才能保证全部的异常都不会被随意地抛出。
(2)除此以外,除了在必要的时候写try-catch,不少园友更推荐使用框架层面提供的异常捕捉方案,以.NET为例:
WinForm,能够这样写:AppDomain.CurrentDomain.UnhandledException +=new UnhandledExceptionEventHandler(UnhandledExceptionFunction);
你们都知道,一般在编译程序时能够选择Bebug版本仍是Release版本,编译器将会根据”调试“和”发布“两个不一样的出发点去编译程序。在Debug版本中,全部Debug类的断言(Assert)语句都会获得保留,相反在Release版本中,则会被统统删除。这样的机制有助于咱们编写出方便调试同时又不影响正式发布的程序代码。
But,单纯的诊断和断言可能并不能彻底知足测试的需求,有时可能会须要大批的代码和方法去支持调试和测试,这个时候就须要用到Conditional特性。Conditional特性用于编写在某个特定版本中运行的方法,一般它编写一些在Debug版本中支持测试的方法。当版本不匹配时,编译器会把Conditional特性的方法内容置为空。
下面的一段代码演示了Conditional特性的使用:
//含有两个成员,生日和身份证 //身份证的第6位到第14位必须是生日 //身份证必须是18位 public class People { private DateTime _birthday; private String _id; public DateTime Birthday { set { _birthday = value; if (!Check()) throw new ArgumentException(); } get { Debug(); return _birthday; } } public String ID { set { _id = value; if (!Check()) throw new ArgumentException(); } get { Debug(); return _id; } } public People(String id, DateTime birthday) { _id = id; _birthday = birthday; Check(); Debug(); Console.WriteLine("People实例被构造了..."); } // 只但愿在DEBUG版本中出现 [Conditional("DEBUG")] protected void Debug() { Console.WriteLine(_birthday.ToString("yyyy-MM-dd")); Console.WriteLine(_id); } //检查是否符合业务逻辑 //在全部版本中都须要 protected bool Check() { if (_id.Length != 18 || _id.Substring(6, 8) != _birthday.ToString("yyyyMMdd")) return false; return true; } } public class Program { public static void Main(string[] args) { try { People p = new People("513001198811290215", new DateTime(1988, 11, 29)); p.ID = "513001198811290215"; } catch (ArgumentException ex) { Console.WriteLine(ex.GetType().ToString()); } Console.ReadKey(); } }
下图则展现了上述代码在Debug版本和Release版本中的输出结果:
①Debug版本:
②Release版本:
Conditional机制很简单,在编译的时候编译器会查看编译状态和Conditional特性的参数,若是二者匹配,则正常编译。不然,编译器将简单地移除方法内的全部内容。
咱们常常会面临一些类型转换的工做,其中有些是肯定能够转换的(好比将一个子类类型转为父类类型),而有些则是尝试性的(好比将基类引用的对象转换成子类)。当执行常识性转换时,咱们就应该作好捕捉异常的准备。
当一个不正确的类型转换发生时,会产生InvalidCastException异常,有时咱们会用try-catch块作一些尝试性的类型转换,这样的代码没有任何错误,可是性能却至关糟糕,为何呢?异常是一种耗费资源的机制,每当异常被抛出时,异常堆栈将会被创建,异常信息将被加载,而一般这些工做的成本相对较高,而且在尝试性类型转换时,这些信息都没有意义。
So,在.NET中提供了另一种语法来进行尝试性的类型转换,那就是关键字 is 和 as 所作的工做。
(1)is 只负责检查类型的兼容性,并返回结果:true 和 false。→ 进行类型判断
public static void Main(string[] args) { object o = new object(); // 执行类型兼容性检查 if(o is ISample) { // 执行类型转换 ISample sample = (ISample)o; sample.SampleShow(); } Console.ReadKey(); }
(2)as 不只负责检查兼容性还会进行类型转换,并返回结果,若是不兼容则返回 null 。→ 用于类型转型
public static void Main(string[] args) { object o = new object(); // 执行类型兼容性检查 ISample sample = o as ISample; if(sample != null) { sample.SampleShow(); } Console.ReadKey(); }
二者的共同之处都在于:不会抛出异常!综上比较,as 较 is 在执行效率上会好一些,在实际开发中应该量才而用,在只进行类型判断的应用场景时,应该多使用 is 而不是 as。
(1)朱毅,《进入IT企业必读的200个.NET面试题》
(2)张子阳,《.NET之美:.NET关键技术深刻解析》
(3)王涛,《你必须知道的.NET》