大家好,欢迎来到IT知识分享网。
此系列文章为我在2015年发布于博客园的.NET基础拾遗系列,它十分适合初中级.NET开发工程师在面试前进行一个系统的复习,因此我将其搬到公众号分享与你。
本文为第三篇,我们会对.NET的面向对象进行基础复习,全文会以Q/A的形式展现,即以面试题的形式来描述。
1 .NET中的类可以多继承吗?
在C#中申明一个类型时,只支持单继承(即继承一个父类),但支持实现多个接口(Java也是如此)。像C++可能会支持同时继承自多个父类,但.NET的设计小组认为这样的机制会带来一些弊端,并且没有必要。
首先,看看多继承有啥好处?
多继承的好处是更加贴近的设计类型。例如,当为一个图形编辑器设计带文本框的矩形类型时,最方便的方法可能是这个类型既继承自文本框类型,又继承自矩形类型,这样它就天生地具有输入文本和绘画矩形的功能。
But,自从C++使用多继承依赖,就一直存在一些弊端,其中最为严重的还是所谓的“砖石继承”带来的问题,下图解释了砖石继承问题。
如上图所示,砖石继承问题根源在于最终的子类从不同的父类中继承到了在它看来完全不同的两个成员,而事实上,这两个成员又来自同一个基类。鉴于此,在C#/Java中,多继承的机制已经被彻底抛弃,取而代之的是单继承和多接口实现的机制。
众所周知,接口并不做任何实际的工作,但是却制定了接口和规范,它定义了特定的类型都需要“做什么”,而把“怎么做”留给实现它的具体类型去考虑。也正是因为接口具有很大的灵活性和抽象性,因此它在面向对象的程序设计中更加出色地完成了抽象的工作。
2 了解.NET中的重写、重载和隐藏吗?
在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 "我隐藏了基类";
}
}
IT知识分享网
以上代码的运行结果如下图所示:
我们可以看到:当通过基类的引用去调用对象内的方法时,重写仍然能够找到定义在对象真正类型中的GetString方法,而隐藏则只调用了基类中的GetString方法。
(2)重载
重载(Overload)是拥有相同名字和返回值的方法却拥有不同的参数列表,它是实现多态的立项方案,在实际开发中也是应用得最为广泛的。常见的重载应用包括:构造方法、ToString()方法等等;
以下代码是一个简单的重载示例:
IT知识分享网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();
}
}
运行结果如下图所示:
3 能否在构造方法中调用虚方法?
在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#中,当一个类型被构造时,它的构造顺序是这样的:
执行变量的初始化表达式 → 执行父类的构造方法(需要的话)→ 调用类型自己的构造方法
我们可以通过以下代码示例来看看上面的构造顺序是如何体现的:
IT知识分享网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总是根据对象的实际类型来找到应该被调用的方法定义。
换句话说,当虚方法在基类的构造方法中被调用时,它的类型仍然保持的是子类,子类的虚方法将被执行,但是这时子类的构造方法却还没有完成,任何对子类未构造成员的访问都将产生异常。
如何避免这类问题呢?
其根本方法就在于:永远不要在非叶子类的构造方法中调用虚方法。
4 如何声明一个类使其不能被继承?
(1)快速回答
这是一个被问烂的问题,在C#中可以通过sealed关键字来申明一个不可被继承的类,C#将在编译阶段保证这一机制。
(2)拓展延伸
继承是面向对象思想中最重要的一环,但是否想过继承也存在一些问题呢?
在设计一个会被继承的类型时,往往需要考虑再三,下面例举了常见的一些类型被继承时容易产生的问题:
- 为了让派生类型可以顺利地序列化,非叶子类需要实现恰当的序列化方法;
- 当非叶子类实现了ICloneable等接口时,意味着所有的子类都被迫需要实现接口中定义的方法;
- 非叶子类的构造方法不能调用虚方法,而且更容易产生不能预计的问题;
鉴于以上问题,在某些时候没有派生需要的类型都应该被显式地添加sealed关键字,这是避免继承带来不可预计问题的最有效办法。
总结
本文总结复习了.NET的面向对象的实现相关的重要知识点,下一篇会总结.NET异常处理相关的重要知识点,欢迎继续关注!
参考资料(全是经典)
朱毅,《进入IT企业必读的200个.NET面试题》
张子阳,《.NET之美:.NET关键技术深入解析》
王涛,《你必须知道的.NET》
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/6729.html