别再让面试官问你单例(暨6种实现方式让你堵住面试官的嘴)

2024-08-29 23:38

本文主要是介绍别再让面试官问你单例(暨6种实现方式让你堵住面试官的嘴),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

引子

经常从Recruiter那里得到抱怨:“汤姆,为什么面试者每次回去的时候都感觉良好,而你却说此人达不到Senior级别?”

我都是微笑着说:“感觉不一定都是对的哦。”

Recruiter:“那你就不能问点别的么?为什么每次面试者都说你问的是单例?”

我只能解释:“单例挺好的,可以问出很多基础知识哦。”

Recruiter:“大叔,单例我都懂了,不就是程序运行的时候只能有一个实例么?我打电话招人的时候经常都帮你问过了呢!做开发的没几个不懂!”

我Faint。。。

为避免引起误会加注:问这个题目的目的不是仅仅为了单例,而是考察相关的基础知识,比如静态构造函数,私有构造函数,锁,延时创建对象, readonly/const等区别,不会仅以单例论英雄,之所以面试者以为感觉良好,主要是给出其中1-2个单例实现以后就觉得通过了面试,其实没有察觉到面试官所要考察的真正内容。

本文目的

写文本的目的,不是说从单例有多重要,多牛逼啥的。其实更多地是建议博客园的兄弟在面试的时候以另外一个角度来看到面试官的问题,做到主动出击,也就是说当人问你一个问题的时候,绝对不要想着他问的只是问题的表面,可能还隐藏着很多陷阱(因为面试官通常不会有太多时间面试,一般第一次约见都是60-90分钟,所以不太可能问太多问题,只能问几个问题,然后再根据这些问题延时出各种问题),所以在你回答问题的时候,尽量要避开这些陷阱,比如单例里我们经常谈到加锁和线程的问题,如果你对多线程不熟悉,防止陷在里面,那可以赶紧主动说出双锁这种实现方式,然后回头一转话题说:”其实单例要考察我们的东西有很多,比如私有构造函数,静态构造函数,静态字段,readonly和const的区别等等“,其实一般说了这么多以后,面试官基本上不会再在单例上揪住不放了,可能只是象征性问一下构造函数的区别而已,因为这时候他已经知道你基本上了解相关的内容了。当然,如果你想欲擒故纵,就是想让面试官在这个问题上再多问你半小时了,那也可以牵着面试官的鼻子走哦,不过,这个我想一般不太可能吧。

注:这周如果有时间,我会将我去年的一次被面试经历写下来与大家分享的(被印度佬面了将近7个小时,其实就是一道字符串的题目以及延伸,再加一些闲聊)。

下面就列举一下,在面试过程中得到的不同单例版本吧,大家也可以参考一下:

版本1:Recruiter都懂

using System;public sealed class Singleton
{private static Singleton instance;private Singleton() {}public static Singleton Instance{get {if (instance == null){instance = new Singleton();}return instance;}}
}

这个版本的主要问题,就是线程安全的问题,当2个请求同时方式这个类的实例的时候,可以会在同一时间点上都创建一个实例,虽然一般不会出异常错误,但是起码不是我们谈论的只保证一个实例了。

版本2

public sealed class Singleton
{// 在静态私有字段上声明单例
   private static readonly Singleton instance = new Singleton();// 私有构造函数,确保用户在外部不能实例化新的实例
   private Singleton(){}// 只读属性返回静态字段
   public static Singleton Instance{get {return instance; }}
}

标记类为sealed是好的,可以防止被集成,然后在子类实例化,使用在静态私有字段上通过new的形式,来保证在该类第一次被调用的时候创建实例,是不错的方式,但有一点需要注意的是,C#其实并不保证实例创建的时机,因为C#规范只是在IL里标记该静态字段是BeforeFieldInit,也就是说静态字段可能在第一次被使用的时候创建,也可能你没使用了,它也帮你创建了,也就是周期更早,我们不能确定到底是什么创建的实例。

版本3

    public sealed class Singleton{// 依然是静态自动hold实例
        private static volatile Singleton instance = null;// Lock对象,线程安全所用
        private static object syncRoot = new Object();private Singleton() { }public static Singleton Instance{get{if (instance == null){lock (syncRoot){if (instance == null)instance = new Singleton();}}return instance;}}}

使用volatile来修饰,是个不错的注意,确保instance在被访问之前被赋值实例,一般情况都是用这种方式来实现单例。

版本4

    public class Singleton{// 因为下面声明了静态构造函数,所以在第一次访问该类之前,new Singleton()语句不会执行
        private static readonly Singleton _instance = new Singleton();public static Singleton Instance{get { return _instance; }}private Singleton(){}// 声明静态构造函数就是为了删除IL里的BeforeFieldInit标记// 以去北欧静态自动在使用之前被初始化
        static Singleton(){}}

这种方式,其实是很不错的,因为他确实保证了是个延迟初始化的单例(通过加静态构造函数),但是该静态构造函数里没有东西哦,所以能有时候会引起误解,尤其是在code review或者代码优化的时候,不熟悉的人可能直接帮你删除了这段代码,那就又回到了版本2了哦,所以还是需要注意的,不过如果你在这个时机正好有代码需要执行的话,那也不错。

版本5

    public sealed class Singleton{private Singleton(){}public static Singleton Instance { get { return Nested._instance; } }private class Nested{static Nested(){}internal static readonly Singleton _instance = new Singleton();}}

这其实是根据版本4的一个变异版本,就不多说了

版本6

    public class Singleton{// 因为构造函数是私有的,所以需要使用lambda
        private static readonly Lazy<Singleton> _instance = new Lazy<Singleton>(() => new Singleton());// new Lazy<Singleton>(() => new Singleton(), LazyThreadSafetyMode.ExecutionAndPublication);
private Singleton(){}public static Singleton Instance{get{return _instance.Value;}}}

其实,一般Lazy的默认构造器只能调用传入泛型类型T的public构造函数的,但是在本例,因为代码是在我们的Singleton内部,所以调用私有的构造函数是没问题的,大家可能还有一个疑虑就是这种方式除了能做到Lazy初始化,做到线程安全么?大家看一下上面一层被注释掉的代码,多了一个LazyThreadSafetyMode.ExecutionAndPublication参数,意思是设置线程安全,但由于Lazy<T>默认的设置就是线程安全,所以不设置也是有效的。

 

所以说,面试的时候,如果能够把上述6个版本的各种实现原理概况说2分钟的话,我估计面试官一般情况不会再在单例上揪住不放了,而且如果你说的基本上都没问题的话,这个时候面试官通常已经开始对你有好感了,起码我是这样的,因为起码你已经将C#的一些基础知识在这个单例问题上体现了不少,不是么?

延伸

当然,如果你这个时候,回答不出那么多的话,我也不会在这个问题上纠缠那么多,免得制造紧张的气氛影响后面的讨论,通常情况下我会及时地切换到另外一个话题上(比如,你简历上写的最强的Skill以便让你重新恢复信心),如果你问答的都不错的话,其实我还想再问一小点,就一小点:如何实现泛型版本的单例?

这个可是加分项哦,之前,有人给了一个如下的代码:

    public class Singleton<T> where T : new(){private static readonly Lazy<T> _instance= new Lazy<T>(() => new T());public static T Instance{get { return _instance.Value; }}}

曾经这个版本,我在给自己问这个问题的时候,也考虑过,但其实它是有问题的,就是那个new T(),这样用的话,就默认了T有public的构造函数了,对吧?也就是说,T是有可能存在多个实例的,因为压根就不满足我们的先决条件:那就是你的类不能在外部实例化。

我们来帖一段老外给出的代码:

    public abstract class Singleton{private static readonly Lazy<T> _instance= new Lazy<T>(() =>{var ctors = typeof(T).GetConstructors(BindingFlags.Instance| BindingFlags.NonPublic| BindingFlags.Public);if (ctors.Count() != 1)throw new InvalidOperationException(String.Format("Type {0} must have exactly one constructor.", typeof(T)));var ctor = ctors.SingleOrDefault(c => c.GetParameters().Count() == 0 && c.IsPrivate);if (ctor == null)throw new InvalidOperationException(String.Format("The constructor for {0} must be private and take no parameters.", typeof(T)));return (T)ctor.Invoke(null);});public static T Instance{get { return _instance.Value; }}}

从上到下,我们来看看是如何实现的:

  1. 声明抽象类,以便不能直接使用,必须继承该类才能用
  2. 使用Lazy<T>作为_instance,T就是我们要实现单例的继承类
  3. Lazy类的构造函数有一个参数(Func类型),也就是和我们的版本6一样
  4. 根据微软的文档和单例特性,单例类的构造函数必须是私有的,所以这里要加相应的验证
  5. 一旦验证通过,就invoke这个私有的无参构造函数,不用担心他的效率,因为他只执行一次!
  6. Instance属性返回唯一的一个T的实例

我们来实现一个单例类:

    class MySingleton : Singleton<MySingleton>{int _counter;public int Counter{get { return _counter; }}private MySingleton(){_counter = 0;}public void IncrementCounter(){++_counter;}}

这个例子,在一般情况下没问题,但是并行计算的时候还是有问题的,因为上述的泛型版本的代码使用的Lazy<T>能确保我们在创建单例实例的时候是线程安全的,但是不意味着单例本身是线程安全的,我们来做个例子看看:

        static void Main(string[] args){Parallel.For(0, 100, i =>{for (int j = 0; j < 1000; ++j)MySingleton.Instance.IncrementCounter();});Console.WriteLine("Counter={0}.", MySingleton.Instance.Counter);Console.ReadLine();}

通常Counter的结果是小于100000的,因为单例里的IncrementCounter方法的代码本身不在线程安全的保护之内,所以如果我们想得到准确的100000这个数字的话,我们需要改一下MySingleton的IncrementCounter方法代码:

        public void IncrementCounter(){Interlocked.Increment(ref _counter);}

这样,结果就完美了,至此关于单例的问题就差不多就到这儿了,我们来总结一下单例的优缺点吧。

总结

单例的优点:
1.保证了所有的对象访问的都是同一个实例
2.由于类是由自己类控制实例化的,所以有相应的伸缩性

单例的缺点:
1.额外的系统开销,因为每次使用类的实例的时候,都要检查实例是否存在,可以通过静态实例该解决。
2.无法销毁对象,单例模式的特性决定了只有他自己才能销毁对象实例,但是一般情况下我们都没做这个事情。

这篇关于别再让面试官问你单例(暨6种实现方式让你堵住面试官的嘴)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1119179

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

在JS中的设计模式的单例模式、策略模式、代理模式、原型模式浅讲

1. 单例模式(Singleton Pattern) 确保一个类只有一个实例,并提供一个全局访问点。 示例代码: class Singleton {constructor() {if (Singleton.instance) {return Singleton.instance;}Singleton.instance = this;this.data = [];}addData(value)

内核启动时减少log的方式

内核引导选项 内核引导选项大体上可以分为两类:一类与设备无关、另一类与设备有关。与设备有关的引导选项多如牛毛,需要你自己阅读内核中的相应驱动程序源码以获取其能够接受的引导选项。比如,如果你想知道可以向 AHA1542 SCSI 驱动程序传递哪些引导选项,那么就查看 drivers/scsi/aha1542.c 文件,一般在前面 100 行注释里就可以找到所接受的引导选项说明。大多数选项是通过"_

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

用命令行的方式启动.netcore webapi

用命令行的方式启动.netcore web项目 进入指定的项目文件夹,比如我发布后的代码放在下面文件夹中 在此地址栏中输入“cmd”,打开命令提示符,进入到发布代码目录 命令行启动.netcore项目的命令为:  dotnet 项目启动文件.dll --urls="http://*:对外端口" --ip="本机ip" --port=项目内部端口 例: dotnet Imagine.M