百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

面试 | .NET基础知识快速通关(3)

bigegpt 2024-08-23 11:55 3 浏览


此系列文章为我在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 "我隐藏了基类";
    }
}

以上代码的运行结果如下图所示:

我们可以看到:当通过基类的引用去调用对象内的方法时,重写仍然能够找到定义在对象真正类型中的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();
    }
}

运行结果如下图所示:

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#中,当一个类型被构造时,它的构造顺序是这样的:

执行变量的初始化表达式 → 执行父类的构造方法(需要的话)→ 调用类型自己的构造方法

我们可以通过以下代码示例来看看上面的构造顺序是如何体现的:

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》

相关推荐

悠悠万事,吃饭为大(悠悠万事吃饭为大,什么意思)

新媒体编辑:杜岷赵蕾初审:程秀娟审核:汤小俊审签:周星...

高铁扒门事件升级版!婚宴上‘冲喜’老人团:我们抢的是社会资源

凌晨两点改方案时,突然收到婚庆团队发来的视频——胶东某酒店宴会厅,三个穿大红棉袄的中年妇女跟敢死队似的往前冲,眼瞅着就要扑到新娘的高额钻石项链上。要不是门口小伙及时阻拦,这婚礼造型团队熬了三个月的方案...

微服务架构实战:商家管理后台与sso设计,SSO客户端设计

SSO客户端设计下面通过模块merchant-security对SSO客户端安全认证部分的实现进行封装,以便各个接入SSO的客户端应用进行引用。安全认证的项目管理配置SSO客户端安全认证的项目管理使...

还在为 Spring Boot 配置类加载机制困惑?一文为你彻底解惑

在当今微服务架构盛行、项目复杂度不断攀升的开发环境下,SpringBoot作为Java后端开发的主流框架,无疑是我们手中的得力武器。然而,当我们在享受其自动配置带来的便捷时,是否曾被配置类加载...

Seata源码—6.Seata AT模式的数据源代理二

大纲1.Seata的Resource资源接口源码2.Seata数据源连接池代理的实现源码3.Client向Server发起注册RM的源码4.Client向Server注册RM时的交互源码5.数据源连接...

30分钟了解K8S(30分钟了解微积分)

微服务演进方向o面向分布式设计(Distribution):容器、微服务、API驱动的开发;o面向配置设计(Configuration):一个镜像,多个环境配置;o面向韧性设计(Resista...

SpringBoot条件化配置(@Conditional)全面解析与实战指南

一、条件化配置基础概念1.1什么是条件化配置条件化配置是Spring框架提供的一种基于特定条件来决定是否注册Bean或加载配置的机制。在SpringBoot中,这一机制通过@Conditional...

一招解决所有依赖冲突(克服依赖)

背景介绍最近遇到了这样一个问题,我们有一个jar包common-tool,作为基础工具包,被各个项目在引用。突然某一天发现日志很多报错。一看是NoSuchMethodError,意思是Dis...

你读过Mybatis的源码?说说它用到了几种设计模式

学习设计模式时,很多人都有类似的困扰——明明概念背得滚瓜烂熟,一到写代码就完全想不起来怎么用。就像学了一堆游泳技巧,却从没下过水实践,很难真正掌握。其实理解一个知识点,就像看立体模型,单角度观察总...

golang对接阿里云私有Bucket上传图片、授权访问图片

1、为什么要设置私有bucket公共读写:互联网上任何用户都可以对该Bucket内的文件进行访问,并且向该Bucket写入数据。这有可能造成您数据的外泄以及费用激增,若被人恶意写入违法信息还可...

spring中的资源的加载(spring加载原理)

最近在网上看到有人问@ContextConfiguration("classpath:/bean.xml")中除了classpath这种还有其他的写法么,看他的意思是想从本地文件...

Android资源使用(android资源文件)

Android资源管理机制在Android的开发中,需要使用到各式各样的资源,这些资源往往是一些静态资源,比如位图,颜色,布局定义,用户界面使用到的字符串,动画等。这些资源统统放在项目的res/独立子...

如何深度理解mybatis?(如何深度理解康乐服务质量管理的5个维度)

深度自定义mybatis回顾mybatis的操作的核心步骤编写核心类SqlSessionFacotryBuild进行解析配置文件深度分析解析SqlSessionFacotryBuild干的核心工作编写...

@Autowired与@Resource原理知识点详解

springIOCAOP的不多做赘述了,说下IOC:SpringIOC解决的是对象管理和对象依赖的问题,IOC容器可以理解为一个对象工厂,我们都把该对象交给工厂,工厂管理这些对象的创建以及依赖关系...

java的redis连接工具篇(java redis client)

在Java里,有不少用于连接Redis的工具,下面为你介绍一些主流的工具及其特点:JedisJedis是Redis官方推荐的Java连接工具,它提供了全面的Redis命令支持,且...