• 面向对象23种设计模式系列(四)- 迭代器模式


    迭代器模式(Iterator Pattern)

      1、迭代器模式是设计模式中行为型模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式。简单来说,迭代器模式使得你能够获取到序列中的所有元素而不用关心是其类型是array,list,linked list或者是其他什么序列结构。这一点使得能够非常高效的构建数据处理通道(data pipeline)--即数据能够进入处理通道,进行一系列的变换,或者过滤,然后得到结果。事实上,这正是Linq的核心模式。

      2、在.NET中,迭代器模式被IEnumerator和IEnumerable及其对应的泛型接口所封装。如果一个类实现了IEnumerable接口,那么就能够被迭代;调用GetEnumerator方法将返回IEnumerator接口的实现,它就是迭代器本身。迭代器类似数据库中的游标,他是数据序列中的一个位置记录。迭代器只能向前移动,同一数据序列中可以有多个迭代器同时对数据进行操作。

      3、含有yield的函数说明它是一个生成器,而不是普通的函数。当程序运行到yield这一行时,该函数会返回值,并保存当前域的所有变量状态;等到该函数下一次被调用时,会从上一次中断的地方开始执行,一直遇到下一个yield,程序返回值,并在此保存当前状态; 如此反复,直到函数正常执行完成。

      4、yield是语法糖,编译时由编译器生成Iterrator的代码,包括MoveNext、Current、Reset等。

    一、迭代器模式的实现原理

    首先我们先来看个例子:

    /// <summary>
    /// 食物
    /// </summary>
    public class Food
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int Price { get; set; }
    }
    
    /// <summary>
    /// 肯德基菜单
    /// </summary>
    public class KFCMenu
    {
        private Food[] _foodList = new Food[3];
        public KFCMenu()
        {
            this._foodList[0] = new Food()
            {
                Id = 1,
                Name = "汉堡包",
                Price = 15
            };
            this._foodList[1] = new Food()
            {
                Id = 2,
                Name = "可乐",
                Price = 10
            };
            this._foodList[2] = new Food()
            {
                Id = 3,
                Name = "薯条",
                Price = 8
            };
        }
    
        public Food[] GetFoods()
        {
            return this._foodList;
        }
    }
    
    /// <summary>
    /// 麦当劳菜单
    /// </summary>
    public class MacDonaldMenu
    {
        private List<Food> _foodList = new List<Food>();
        public MacDonaldMenu()
        {
            this._foodList.Add(new Food()
            {
                Id = 1,
                Name = "鸡肉卷",
                Price = 15
            });
            this._foodList.Add(new Food()
            {
                Id = 2,
                Name = "红豆派",
                Price = 10
            });
            this._foodList.Add(new Food()
            {
                Id = 3,
                Name = "薯条",
                Price = 9
            });
        }
    
        public List<Food> GetFoods()
        {
            return this._foodList;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            {
                KFCMenu kfcMenu = new KFCMenu();
                Food[] foodCollection = kfcMenu.GetFoods();
                for (int i = 0; i < foodCollection.Length; i++)
                {
                    Console.WriteLine("KFC: Id={0} Name={1} Price={2}", foodCollection[i].Id, foodCollection[i].Name, foodCollection[i].Price);
                }
            }
    
            {
                MacDonaldMenu macDonaldMenu = new MacDonaldMenu();
                List<Food> foodCollection = macDonaldMenu.GetFoods();
                for (int i = 0; i < foodCollection.Count(); i++)
                {
                    Console.WriteLine("MacDonald: Id={0} Name={1} Price={2}", foodCollection[i].Id, foodCollection[i].Name, foodCollection[i].Price);
                }
            }
    
            Console.ReadKey();
        }
    }

    从上面的例子可以发现肯德基菜单和麦当劳菜单差不多,但是呢一个是数组存放一个是集合存放,这就导致了它们两者的访问方式不太一样。

    那么从某种角度上看我们当然希望它们两者能有一个统一的访问方式

    class Program
    {
        static void Main(string[] args)
        {
            {
                KFCMenu kfcMenu = new KFCMenu();
                Food[] foodCollection = kfcMenu.GetFoods();
                for (int i = 0; i < foodCollection.Length; i++)
                {
                    Console.WriteLine("KFC: Id={0} Name={1} Price={2}", foodCollection[i].Id, foodCollection[i].Name, foodCollection[i].Price);
                }
    
                foreach (var item in foodCollection)
                {
                    Console.WriteLine("KFC: Id={0} Name={1} Price={2}", item.Id, item.Name, item.Price);
                }
            }
    
            {
                MacDonaldMenu macDonaldMenu = new MacDonaldMenu();
                List<Food> foodCollection = macDonaldMenu.GetFoods();
                for (int i = 0; i < foodCollection.Count(); i++)
                {
                    Console.WriteLine("MacDonald: Id={0} Name={1} Price={2}", foodCollection[i].Id, foodCollection[i].Name, foodCollection[i].Price);
                }
    
                foreach (var item in foodCollection)
                {
                    Console.WriteLine("MacDonald: Id={0} Name={1} Price={2}", item.Id, item.Name, item.Price);
                }
            }
    
            Console.ReadKey();
        }
    }

    可以发现使用foreach后它们两者的访问方式就统一了。那么这个foreach是怎么设计出来的呢?其实这就用到了迭代器,迭代器可以为不同的数据结构提供一个通用的访问方式

    下面我们直接通过代码来看下迭代器的实现原理:

    /// <summary>
    /// 迭代器抽象类(模拟IEnumerator)
    /// </summary>
    public interface IIterator<T>
    {
        /// <summary>
        /// 当前的对象
        /// </summary>
        T Current { get; }
    
        /// <summary>
        /// 移动到下一个对象,是否存在。
        /// </summary>
        /// <returns></returns>
        bool MoveNext();
    
        /// <summary>
        /// 重置
        /// </summary>
        void Reset();
    }
    
    /// <summary>
    /// 抽象聚合类(模拟IEnumerable)
    /// </summary>
    public interface IAggregate<T>
    {
        IIterator<T> GetEnumerator();
    }
    /// <summary>
    /// 迭代器具体类
    /// 肯德基菜单迭代器
    /// </summary>
    public class KFCMenuIterator : IIterator<Food>
    {
        private Food[] _foodList = null;
        public KFCMenuIterator(KFCMenu kfcMenu)
        {
            this._foodList = kfcMenu.GetFoods();
        }
    
        private int _currentIndex = -1;
        public Food Current
        {
            get
            {
                return this._foodList[_currentIndex];
            }
        }
    
        public bool MoveNext()
        {
            return this._foodList.Length > ++this._currentIndex; //此处判断方式是.Length
        }
    
        public void Reset()
        {
            this._currentIndex = -1;
        }
    }
    
    /// <summary>
    /// 迭代器具体类
    /// 麦当劳菜单迭代器
    /// </summary>
    public class MacDonaldIterator : IIterator<Food>
    {
        private List<Food> _foodList = null;
        public MacDonaldIterator(MacDonaldMenu macDonaldMenu)
        {
            this._foodList = macDonaldMenu.GetFoods();
        }
    
        private int _currentIndex = -1;
        public Food Current
        {
            get
            {
                return this._foodList[_currentIndex];
            }
        }
    
        public bool MoveNext()
        {
            return this._foodList.Count > ++this._currentIndex; //此处判断方式是.Count
        }
    
        public void Reset()
        {
            this._currentIndex = -1;
        }
    }
    /// <summary>
    /// 肯德基菜单
    /// 实现IAggregate
    /// </summary>
    public class KFCMenu : IAggregate<Food>
    {
        private Food[] _foodList = new Food[3];
        public KFCMenu()
        {
            this._foodList[0] = new Food()
            {
                Id = 1,
                Name = "汉堡包",
                Price = 15
            };
            this._foodList[1] = new Food()
            {
                Id = 2,
                Name = "可乐",
                Price = 10
            };
            this._foodList[2] = new Food()
            {
                Id = 3,
                Name = "薯条",
                Price = 8
            };
        }
    
        public Food[] GetFoods()
        {
            return this._foodList;
        }
    
        public IIterator<Food> GetEnumerator()
        {
            return new KFCMenuIterator(this);
        }
    }
    
    /// <summary>
    /// 麦当劳菜单
    /// 实现IAggregate
    /// </summary>
    public class MacDonaldMenu : IAggregate<Food>
    {
        private List<Food> _foodList = new List<Food>();
        public MacDonaldMenu()
        {
            this._foodList.Add(new Food()
            {
                Id = 1,
                Name = "鸡肉卷",
                Price = 15
            });
            this._foodList.Add(new Food()
            {
                Id = 2,
                Name = "红豆派",
                Price = 10
            });
            this._foodList.Add(new Food()
            {
                Id = 3,
                Name = "薯条",
                Price = 9
            });
        }
    
        public List<Food> GetFoods()
        {
            return this._foodList;
        }
    
        public IIterator<Food> GetEnumerator()
        {
            return new MacDonaldIterator(this);
        }
    }

    使用如下(红色字体部分):

    using System;
    using System.Collections.Generic;
    using System.Linq;
    
    using IteratorPattern.Iterator;
    using IteratorPattern.Menu;
    
    namespace IteratorPattern
    {
        /// <summary>
        /// 迭代器模式(yield return)
        ///     1、迭代器模式是设计模式中行为型模式(behavioral pattern)的一个例子,他是一种简化对象间通讯的模式,也是一种非常容易理解和使用的模式。
        ///        简单来说,迭代器模式使得你能够获取到序列中的所有元素而不用关心是其类型是array,list,linked list或者是其他什么序列结构。
        ///        这一点使得能够非常高效的构建数据处理通道(data pipeline)--即数据能够进入处理通道,进行一系列的变换,或者过滤,然后得到结果。
        ///        事实上,这正是LINQ的核心模式。Linq to object的延迟查询,按需获取。
        ///     2、在.NET中,迭代器模式被IEnumerator和IEnumerable及其对应的泛型接口所封装。如果一个类实现了IEnumerable接口,那么就能够被迭代;
        ///        调用GetEnumerator方法将返回IEnumerator接口的实现,它就是迭代器本身。迭代器类似数据库中的游标,他是数据序列中的一个位置记录。
        ///        迭代器只能向前移动,同一数据序列中可以有多个迭代器同时对数据进行操作。
        ///     3、含有yield的函数说明它是一个生成器,而不是普通的函数。当程序运行到yield这一行时,该函数会返回值,并保存当前域的所有变量状态;
        ///        等到该函数下一次被调用时,会从上一次中断的地方开始执行,一直遇到下一个yield,程序返回值,并在此保存当前状态; 如此反复,直到函数正常执行完成。
        ///     4、yield是语法糖,编译时由编译器生成Iterrator的代码,包括MoveNext、Current、Reset等。
        /// </summary>
        class Program
        {
            static void Main(string[] args)
            {
                {
                    KFCMenu kfcMenu = new KFCMenu();
                    Food[] foodCollection = kfcMenu.GetFoods();
                    for (int i = 0; i < foodCollection.Length; i++)
                    {
                        Console.WriteLine("KFC: Id={0} Name={1} Price={2}", foodCollection[i].Id, foodCollection[i].Name, foodCollection[i].Price);
                    }
    
                    foreach (var item in foodCollection)
                    {
                        Console.WriteLine("KFC: Id={0} Name={1} Price={2}", item.Id, item.Name, item.Price);
                    }
    
                    IIterator<Food> foodIterator = kfcMenu.GetEnumerator();
                    while (foodIterator.MoveNext())
                    {
                        Food food = foodIterator.Current;
                        Console.WriteLine("KFC: Id={0} Name={1} Price={2}", food.Id, food.Name, food.Price);
                    }
                }
    
                {
                    MacDonaldMenu macDonaldMenu = new MacDonaldMenu();
                    List<Food> foodCollection = macDonaldMenu.GetFoods();
                    for (int i = 0; i < foodCollection.Count(); i++)
                    {
                        Console.WriteLine("MacDonald: Id={0} Name={1} Price={2}", foodCollection[i].Id, foodCollection[i].Name, foodCollection[i].Price);
                    }
    
                    foreach (var item in foodCollection)
                    {
                        Console.WriteLine("MacDonald: Id={0} Name={1} Price={2}", item.Id, item.Name, item.Price);
                    }
    
                    IIterator<Food> foodIterator = macDonaldMenu.GetEnumerator();
                    while (foodIterator.MoveNext())
                    {
                        Food food = foodIterator.Current;
                        Console.WriteLine("MacDonald: Id={0} Name={1} Price={2}", food.Id, food.Name, food.Price);
                    }
                }
    
                Console.ReadKey();
            }
        }
    }

    可以发现使用迭代器模式后我们做到了两者访问方式的统一。

    在C# 1.0中我们经常使用foreach来遍历一个集合中的元素,然而一个类型要能够使用foreach关键字来对其进行遍历必须实现IEnumerable或IEnumerable<T>接口

    之所以必须要实现IEnumerable这个接口,是因为foreach是迭代语句,要使用foreach就必须要有一个迭代器才行。

    IEnumerable接口中就有IEnumerator GetEnumerator()方法是返回迭代器的,实现了IEnumerable接口就必须实现GetEnumerator()这个方法来返回迭代器,有了迭代器自然就可以使用foreach语句了。

    在C# 1.0中要实现一个迭代器就必须实现IEnumerator接口中的bool MoveNext()和void Reset()方法。

    而在C# 2.0中提供了yield关键字来简化迭代器的实现,这样在C# 2.0中如果我们要自定义一个迭代器就容易多了。

    二、在C#1.0中实现迭代器

    在C# 1.0 中实现一个迭代器必须实现IEnumerator接口,下面代码演示了传统方式来实现一个自定义的迭代器:

    using System;
    
    namespace IteratorPattern.IteratorImpl
    {
        /// <summary>
        /// 朋友类
        /// </summary>
        public class Friend
        {
            private string _name;
            public string Name { get => _name; set => _name = value; }
    
            public Friend(string name)
            {
                this._name = name;
            }
        }
    }
    using System.Collections;
    
    namespace IteratorPattern.IteratorImpl.Demo1
    {
        /// <summary>
        /// 自定义迭代器,必须实现IEnumerator接口
        /// </summary>
        public class FriendIterator : IEnumerator
        {
            private readonly Friends _friends;
            private int _index;
            private Friend _current;
            internal FriendIterator(Friends friends)
            {
                this._friends = friends;
                _index = 0;
            }
    
            #region 实现IEnumerator接口中的方法
    
            public object Current
            {
                get
                {
                    return this._current;
                }
            }
    
            public bool MoveNext()
            {
                if (_index + 1 > _friends.Count)
                {
                    return false;
                }
                else
                {
                    this._current = _friends[_index];
                    _index++;
                    return true;
                }
            }
    
            public void Reset()
            {
                _index = 0;
            }
    
            #endregion 实现IEnumerator接口中的方法
        }
    }
    using System.Collections;
    
    namespace IteratorPattern.IteratorImpl.Demo1
    {
        /// <summary>
        /// 朋友集合
        /// </summary>
        public class Friends : IEnumerable
        {
            private Friend[] _arrFriend;
            public Friends()
            {
                _arrFriend = new Friend[]
                {
                    new Friend("张三"),
                    new Friend("李四"),
                    new Friend("王五")
                };
            }
    
            /// <summary>
            /// 索引器
            /// </summary>
            public Friend this[int index]
            {
                get { return _arrFriend[index]; }
            }
    
            public int Count
            {
                get { return _arrFriend.Length; }
            }
    
            /// <summary>
            /// 实现IEnumerable接口方法
            /// </summary>
            public IEnumerator GetEnumerator()
            {
                return new FriendIterator(this);
            }
        }
    }

    使用foreach方式遍历如下所示:

    //在C#1.0中实现迭代器
    {
        Console.WriteLine("在C#1.0中实现迭代器");
        var friendCollection = new IteratorImpl.Demo1.Friends();
        foreach (Friend item in friendCollection)
        {
            Console.WriteLine(item.Name);
        }
    }

    运行结果如下:

    三、在C#2.0中实现迭代器

    在C# 1.0 中要实现一个迭代器需要实现IEnumerator接口,这样就必须实现IEnumerator接口中的MoveNext、Reset方法和Current属性。而在C# 2.0 中通过yield return语句简化了迭代器的实现。

    下面来看看C# 2.0中简化迭代器的写法:

    using System;
    
    namespace IteratorPattern.IteratorImpl
    {
        /// <summary>
        /// 朋友类
        /// </summary>
        public class Friend
        {
            private string _name;
            public string Name { get => _name; set => _name = value; }
    
            public Friend(string name)
            {
                this._name = name;
            }
        }
    }
    using System.Collections;
    
    namespace IteratorPattern.IteratorImpl.Demo2
    {
        /// <summary>
        /// 朋友集合
        /// </summary>
        public class Friends : IEnumerable
        {
            private Friend[] _arrFriend;
            public Friends()
            {
                _arrFriend = new Friend[]
                {
                    new Friend("张三"),
                    new Friend("李四"),
                    new Friend("王五")
                };
            }
    
            /// <summary>
            /// 索引器
            /// </summary>
            public Friend this[int index]
            {
                get { return _arrFriend[index]; }
            }
    
            public int Count
            {
                get { return _arrFriend.Length; }
            }
    
            /// <summary>
            /// C# 2.0中简化迭代器的实现
            /// </summary>
            public IEnumerator GetEnumerator()
            {
                for (int index = 0; index < _arrFriend.Length; index++)
                {
                    // 这样就不需要额外定义一个FriendIterator迭代器来实现IEnumerator
                    // 在C# 2.0中只需要使用下面语句就可以实现一个迭代器
                    yield return _arrFriend[index];
                }
            }
        }
    }

    使用foreach方式遍历如下所示:

    //在C#2.0中实现迭代器
    {
        Console.WriteLine("在C#2.0中实现迭代器");
        var friendCollection = new IteratorImpl.Demo2.Friends();
        foreach (Friend item in friendCollection)
        {
            Console.WriteLine(item.Name);
        }
    }

    运行结果如下:

    在上面代码中有一个yield return语句,这个语句的作用就是告诉编译器GetEnumerator方法不是一个普通的方法,而是一个实现迭代器的方法。

    当编译器看到yield return语句时,编译器就知道需要实现一个迭代器,所以编译器生成中间代码时为我们生成了一个IEnumerator接口的对象,大家可以通过反编译工具进行查看。

    yield return语句其实是C#中提供的一个语法糖,简化我们实现迭代器的代码,把具体实现复杂迭代器的过程交给编译器帮我们去完成。

    四、迭代器的执行过程

    为了让大家更好的理解迭代器,下面列出迭代器的执行流程:

    五、迭代器的延迟计算

    从第四部分迭代器的执行过程中可以知道迭代器是延迟计算的,因为迭代的主体在MoveNext()中实现(在MoveNext()方法中访问了集合中的当前位置的元素)。

    foreach中每次遍历执行到in的时候才会调用MoveNext()方法,所以迭代器可以延迟计算,下面通过一个示例来演示迭代器的延迟计算:

    using System;
    using System.Collections.Generic;
    
    namespace IteratorPattern
    {
        /// <summary>
        /// yield是语法糖,编译时由编译器生成Iterrator的代码
        /// </summary>
        public class YieldDemo
        {
            /// <summary>
            /// 含有迭代器的
            /// </summary>
            public static IEnumerable<int> WithIterator()
            {
                for (int i = 0; i < 5; i++)
                {
                    Console.WriteLine($"在WithIterator方法中的,当前i的值为:{i}");
                    if (i > 1)
                    {
                        yield return i;
                    }
                }
            }
    
            /// <summary>
            /// 不包含迭代器的
            /// </summary>
            public static IEnumerable<int> WithoutIterator()
            {
                List<int> list = new List<int>();
                for (int i = 0; i < 5; i++)
                {
                    Console.WriteLine($"在WithoutIterator方法中的,当前i的值为:{i}");
                    if (i > 1)
                    {
                        list.Add(i);
                    }
                }
    
                return list;
            }
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            //迭代器的延迟计算
            {
                // 测试一
                YieldDemo.WithIterator();
    
                // 测试二
                YieldDemo.WithoutIterator();
    
                // 测试三
                foreach (var item in YieldDemo.WithIterator()) //按需获取,要一个拿一个
                {
                    Console.WriteLine($"在Main函数的输出语句中,当前i的值为:{item}");
                    if (item >= 3)
                    {
                        break;
                    }
                }
    
                // 测试四
                foreach (var item in YieldDemo.WithoutIterator()) //先全部获取,然后一起返回
                {
                    Console.WriteLine($"在Main函数的输出语句中,当前i的值为:{item}");
                    if (item >= 3)
                    {
                        break;
                    }
                }
            }
    
            Console.ReadKey();
        }
    }

    运行测试一结果如下:

    运行测试一的代码时会发现控制台中什么都不输出,这是为什么呢?下面我们通过反编译工具来看下原因:

    PS:此Demo的目标框架最好是Framework版本的,只有这样才方便通过反编译工具查看原理。

    从反编译的结果中我们就可以看出测试一什么都不输出的原因了,那是因为WithIterator方法中含有yield关键字,编译器遇到yield return语句就会帮我们生成一个迭代器类。

    从而当我们在测试一的代码中调用YieldDemo.WithIterator()时,对于编译器而言其实就是实例化了一个YieldDemo.<WithIterator>d__0的对象而已,所以运行测试一的代码时控制台中什么都不输出。

    运行测试二结果如下:

    运行测试二结果就如我们期望的那样输出,这里就不多解释了。

    运行测试三结果如下:

    运行测试四结果如下:

    对比测试三和测试四的结果可以发现迭代器是可以做到延迟计算、按需获取的。

    六、关于迭代器模式的一些小扩展

    using System;
    using System.Collections.Generic;
    
    namespace IteratorPattern.Show
    {
        public static class ExtendMethod
        {
            public static IEnumerable<T> TianYaWhere<T>(this IEnumerable<T> source, Func<T, bool> func)
            {
                if (source == null)
                {
                    throw new Exception("source is null");
                }
    
                if (func == null)
                {
                    throw new Exception("func is null");
                }
    
                foreach (var item in source)
                {
                    if (func.Invoke(item))
                    {
                        yield return item;
                    }
                }
            }
        }
    }
    using System;
    using System.Collections;
    using System.Collections.Generic;
    
    namespace IteratorPattern.Show
    {
        public static class LinqExtend
        {
            public static IEnumerable<TSource> TianYaWhere<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
            {
                if (source == null)
                {
                    throw new Exception("source");
                }
    
                if (predicate == null)
                {
                    throw new Exception("predicate");
                }
    
                return new EnumeratorIterator<TSource>(source, predicate);
            }
        }
    
        public class EnumeratorIterator<TSource> : IEnumerable<TSource>
        {
            private IEnumerable<TSource> _source;
            private Func<TSource, bool> _predicate;
            public EnumeratorIterator(IEnumerable<TSource> source, Func<TSource, bool> predicate)
            {
                this._source = source;
                this._predicate = predicate;
            }
    
            public IEnumerator<TSource> GetEnumerator()
            {
                foreach (var item in this._source)
                {
                    if (_predicate(item))
                    {
                        yield return item;
                    }
                }
            }
    
            IEnumerator IEnumerable.GetEnumerator()
            {
                foreach (var item in this._source)
                {
                    if (_predicate(item))
                    {
                        yield return item;
                    }
                }
            }
        }
    }

    至此本文就全部介绍完了,如果觉得对您有所启发请记得点个赞哦!!!

    本文部分内容参考博文:https://www.cnblogs.com/zhili/archive/2012/12/02/Interator.html

    Demo源码:

    链接:https://pan.baidu.com/s/1FqAvYAZhrKuCLzuJcTZ5KA 
    提取码:fx1g

    此文由博主精心撰写转载请保留此原文链接:https://www.cnblogs.com/xyh9039/p/13894175.html

    版权声明:如有雷同纯属巧合,如有侵权请及时联系本人修改,谢谢!!!

  • 相关阅读:
    Java实现 蓝桥杯 历届试题 网络寻路
    Joda-Time 简介
    Eclipse自动生成返回值对象的快捷键是什么?
    eclipse中使用Maven管理java工程设置jdk版本为jdk1.8
    Windows10系统下,彻底删除卸载MySQL
    win10 安装 mysql解压版安装步骤
    使用MySQL Workbench建立数据库,建立新的表,向表中添加数据
    ubuntu安装mysql可视化工具MySQL-workbench及简单操作
    MySQL Linux压缩版安装方法
    【Linux】MySQL解压版安装及允许远程访问
  • 原文地址:https://www.cnblogs.com/xyh9039/p/13894175.html
Copyright © 2020-2023  润新知