分类: 日记

  • C#设计:单例模式

    单例模式在开发中经常会用到,单例模式是确保一个类只有一个实例,并且提供出一个供全局访问的点,例如开发中常用到的数据库操作实例,我们不可能每次操作数据库都要重新连接创建新的实例这样的效率太低了,通过单例模式可以只创建一次以后的每次访问都直接访问这个静态的单例即可。

    在 ASP.NET Core 提供了DI容器,可以很方便的创建单例服务等,那么怎么不依赖DI容器实现一个简单的单例模式?

    假设现在有一个类是 Drizzle,我们先用普通的方式去创建储存10个这样的实例:

    
    class Drizzle
    {
    
    }
    
    public static class Program
    {
        public static void Main()
        {
            var list = new List<Drizzle>();
            for (int i = 0; i < 10; i++)
            {
                list.Add(new Drizzle());
            }
    
            foreach (var item in list)
            {
                Console.WriteLine(item.GetHashCode());
            }
        }
    }
    27252167
    43942917
    59941933
    2606490
    23458411
    9799115
    21083178
    55530882
    30015890
    1707556

    由结果可以看出,这是分别创建了十个不同的实例。

    然后开始将Drizzle这个类改造成单例模式,这里主要说三种实现方式:

    • 饿汉模式
    • 内部静态类
    • 懒汉模式

    另外在本文的最后还会讲一些其他的C#特定的语法实现单例

    饿汉模式

    首先来看饿汉模式,怎么去实现他呢?

    1. 创建私有的构造函数,这样不能通过外部new出新的实例
    2. 创建私有的静态实例,并且实例化
    3. 创建公有的获取实例的属性或者接口

    按照上面的步骤我们可以把Drizzle类改成这样的:

    class Drizzle
    {
        // 静态私有实例
        private static readonly Drizzle _drizzle = new();
    
        // 构造函数私有化
        private Drizzle()
        {
        }
    
        // 获取实例的属性
        public static Drizzle Instance
        {
            get => _drizzle;
        }
    
        // 获取实例的方法
        public static Drizzle GetInstance() => Instance;
    }
    
    // Main中使用Drizzle.GetInstance()或者Drizzle.Instance替换new
    // 例如
    list.Add(Drizzle.GetInstance());
    
    27252167
    27252167
    27252167
    27252167
    27252167
    27252167
    27252167
    27252167
    27252167
    27252167
    

    这样就实现了饿汉单例模式啦!

    静态类模式

    然后来看看静态类模式如何实现

    1. 创建私有的构造函数,这样不能通过外部new出新的实例
    2. 创建私有的内部静态类
    3. 在静态类中创建类的实例
    4. 创建公有的获取实例的属性或者接口

    按照上面步骤我们可以将Drizzle改成:

    class Drizzle
    {
        // 创建私有静态类,并在内部实例化
        private static class CreateInstance
        {
            public static Drizzle Instance = new Drizzle();
        }
    
        // 获取实例的属性
        public static Drizzle Instance = CreateInstance.Instance;
    
        // 获取实例的方法
        public static Drizzle GetInstance() => Instance;
    }

    懒汉模式

    最后一个,懒汉模式,它的步骤和上面两种方案也有很多共同点,但是需要加锁来解决线程安全,实现步骤如下:

    1. 创建私有的构造函数,这样不能通过外部new出新的实例
    2. 创建私有的静态实例,但是不实例化
    3. 建立静态只读的锁对象
    4. 创建公有的获取实例的属性或者接口

    按照如上步骤,我们可以将Drizzle改造成:

    class Drizzle
    {
        // 创建私有静态类,暂不实例化
        private static Drizzle? _drizzle;
    
        // 创建锁
        private static readonly object _lock = new();
    
        // 获取实例的属性
        public static Drizzle Instance
        {
            get
            {
                if (_drizzle is null)
                {
                    lock (_lock)
                    {
                        _drizzle ??= new Drizzle();
                    }
                }
    
                return _drizzle;
            }
        }
    
        // 获取实例的方法
        public static Drizzle GetInstance() => Instance;
    }

    这样一个线程安全的懒汉单例模式就完成啦,那么为什么要对懒汉模式做线程安全的操作呢?

    首先我们对比饿汉和懒汉的区别,饿汉在创建静态实例的时候直接实例化,也就是程序运行的时候自动创建了这个唯一的单例,所以不会有线程上的不安全问题,而饿汉模式不会直接实例化,会在需要的时候进行实例化,但是如果在多线程的情况下,如果多个线程同时需要当前单例,但是单例还未实例化,这样会同时new出不同的实例,所以懒汉模式需要对获取实例的方法中加锁!

    C#的 Lazy

    public sealed class Singleton
    {
        private static readonly Lazy<Singleton> _lazy = new Lazy<Singleton>(() => new Singleton());
    
        public static Singleton Instance => _lazy.Value;
    
        private Singleton() { }
    }
    

    这里使用了 Lazy<T> 类来实现懒加载,并在静态构造函数中初始化了一个只读的静态变量 lazy,该变量的值为一个 Lazy<Singleton> 对象,该对象用于确保 Singleton 类只被实例化一次。Instance 属性返回 lazy.Value,该属性在第一次调用时会初始化 Singleton 对象。由于构造函数是私有的,因此只能通过 Instance 属性访问该对象。

    使用 sealed 关键字来防止其他类继承 Singleton 类。

    Lazy<T> 类本身就是为了解决多线程下的线程安全问题而设计的,它的默认行为是懒加载并且线程安全。当多个线程同时尝试访问 lazy 变量时,只有一个线程能够实例化 Singleton 对象,而其他线程会等待该对象被实例化后再访问 Instance 属性,确保了线程安全性。

    需要注意的是,如果要使用 .NET Core 中的 Lazy<T> 类来实现单例模式,需要在构造函数中传入一个 LazyThreadSafetyMode.ExecutionAndPublication 参数,以确保在多线程环境下实例化 Singleton 对象是线程安全的

  • Alist 多云盘管理

    最近发现一个更好用的文件储存管理系统:alist,之前介绍了:

    Cloudreve + Aria2 搭建私人云盘和离线下载 – 云烟成雨 (shiyu.dev)

    相对于前面的cloudreve,alist支持更多的储存,包括主流的云盘系统,而cloudreve主要支持服务器和对象储存,同样的alist也支持服务器离线下载,而且可以把一个云盘的内容复制到另一个云盘,非常方便。

  • 又是应付甲方需求的一天

    去年暑假开发的项目今天突然来了一堆新需求,好久没写python了,重新配好了环境,今晚加班了嘞

  • Iframe 跨域高度自适应

    在页面中嵌入iframe子页面是很常用的方法,例如本站中的友情链接和朋友圈是嵌入的moments.shiyu.dev的页面,但是嵌入后的高度自适应问题就比较麻烦。

    一开始想直接在iframe加载完成后读取子页面的doc中body的高度,结果发现有跨域问题,无法获得子页面的具体信息,后面在网上寻找各种解决方案,有很多是没有用的,甚至直接没考虑跨域的,最后找到一种方便而且使用的方法:子页面发送消息给父页面,父页面监听消息。

    现在假设父页面(A.html)的代码如下:

    <iframe id="friends-frame" loading="lazy" src="https://moments.shiyu.dev/friends" name="moments" width="100%" onload="friendsFinish()" style="border: none;">
    </iframe>
    
    <script>
        const friendsFinish = function () {
            window.addEventListener('message', function (event) {
                var iframe = document.getElementById("friends-frame")
                iframe.style.height = event.data + 100 + "px";
            })
        }
    </script>

    父页面存在一个iframe标签加载https://moments.shiyu.dev/friends 这个子页面,而且父子页面不在同一个域上,所以父页面在加载完成iframe后创建事件监听,监听子页面发来的消息(高度)。

    子页面(B.html)的代码如下:

    <div>
      ********
    </div>
    <script>
    function friendsOnload(target) {
        let h = document.body.scrollHeight;
        parent.postMessage(h, target);
    }
    </script>

    B页面加载后执行friendsOnload函数,传入target(父页面的域) 发送当前页面的高度。

    这样就完成了不同域下iframe高度自适应啦。

    实例:链接&关于 – 云烟成雨 (shiyu.dev)