C#创建安全的栈(Stack)存储结构
C#创建安全的栈(Stack)存储结构
彭泽0902 发表于11个月前
C#创建安全的栈(Stack)存储结构
  • 发表于 11个月前
  • 阅读 0
  • 收藏 0
  • 点赞 0
  • 评论 0

腾讯云 十分钟定制你的第一个小程序>>>   

    在C#中,用于存储的结构较多,如:DataTable,DataSet,List,Dictionary,Stack等结构,各种结构采用的存储的方式存在差异,效率也必然各有优缺点。现在介绍一种后进先出的数据结构。

   谈到存储结构,我们在项目中使用的较多。对于Task存储结构,栈与队列是类似的结构,在使用的时候采用不同的方法。C#中栈(Stack)是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义;堆是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小。

    在C#中,栈通常保存着我们代码执行的步骤。C#中的引用类型存储在栈中,在程序运行的时候,每个线程(Thread)都会维护一个自己的专属线程堆栈。当一个方法被调用的时候,主线程开始在所属程序集的元数据中,查找被调用方法,然后通过JIT即时编译并把结果(一般是本地CPU指令)放在栈顶。CPU通过总线从栈顶取指令,驱动程序以执行下去。

    以上对栈这个数据结构进行了一个简单的介绍,现在看一下C#实现栈结构的底层方法:

/// <summary>
    /// 初始化 <see cref="T:System.Collections.Generic.Stack`1"/> 类的新实例,该实例为空并且具有默认初始容量。 /// </summary>
 [__DynamicallyInvokable] public Stack(); /// <summary>
    /// 初始化 <see cref="T:System.Collections.Generic.Stack`1"/> 类的新实例,该实例为空,具有指定的初始容量或默认的初始容量(其中较大的一个)。 /// </summary>
    /// <param name="capacity"><see cref="T:System.Collections.Generic.Stack`1"/> 可包含的初始元素数。</param><exception cref="T:System.ArgumentOutOfRangeException"><paramref name="capacity"/> is less than zero.</exception>
 [__DynamicallyInvokable] public Stack(int capacity); /// <summary>
    /// 初始化 <see cref="T:System.Collections.Generic.Stack`1"/> 类的新实例,该实例包含从指定集合复制的元素并且具有足够的容量来容纳所复制的元素。 /// </summary>
    /// <param name="collection">从中复制元素的集合。</param><exception cref="T:System.ArgumentNullException"><paramref name="collection"/> is null.</exception>
 [__DynamicallyInvokable] public Stack(IEnumerable<T> collection);

   以上是对stack的部分方法的介绍,由于在操作数据存储的同时,会考虑到线程的安全性。

   进程作为操作系统执行程序的基本单位,拥有应用程序的资源,进程包含线程,进程的资源被线程共享,线程不拥有资源。线程分为前台线程和后台线程,通过Thread类新建线程默认为前台线程。当所有前台线程关闭时,所有的后台线程也会被直接终止,不会抛出异常。

    接下来看一下ReaderWriterLockSlim类:

/// <summary>
  /// 表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问。 /// </summary>
 [__DynamicallyInvokable] [HostProtection(SecurityAction.LinkDemand, ExternalThreading = true, Synchronization = true)] [HostProtection(SecurityAction.LinkDemand, MayLeakOnAbort = true)] public class ReaderWriterLockSlim : IDisposable { /// <summary>
    /// 使用默认属性值初始化 <see cref="T:System.Threading.ReaderWriterLockSlim"/> 类的新实例。 /// </summary>
 [__DynamicallyInvokable] public ReaderWriterLockSlim(); /// <summary>
    /// 在指定锁定递归策略的情况下初始化 <see cref="T:System.Threading.ReaderWriterLockSlim"/> 类的新实例。 /// </summary>
    /// <param name="recursionPolicy">枚举值之一,用于指定锁定递归策略。</param>
 [__DynamicallyInvokable] public ReaderWriterLockSlim(LockRecursionPolicy recursionPolicy); /// <summary>
    /// 尝试进入读取模式锁定状态。 /// </summary>
    /// <exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已进入读取的模式。- 或 -当它已经包含写入锁时,当前线程可能不会获取读的锁定。- 或 -递归数将超出该计数器的容量。此限制是很大的应用程序应永远不会遇到它。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public void EnterReadLock(); /// <summary>
    /// 尝试进入读取模式锁定状态,可以选择超时时间。 /// </summary>
    /// 
    /// <returns>
    /// 如果调用线程已进入读取模式,则为 true;否则为 false。 /// </returns>
    /// <param name="timeout">等待的间隔;或为 -1 毫秒,表示无限期等待。</param><exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已进入该锁。- 或 -递归数将超出该计数器的容量。限制为应用程序应永远不会遇到它太大。</exception><exception cref="T:System.ArgumentOutOfRangeException"><paramref name="timeout"/> 为负数,但它不等于-1 毫秒为单位),这是唯一允许的值为负。- 或 -值 <paramref name="timeout"/> 大于 <see cref="F:System.Int32.MaxValue"/> 毫秒为单位)。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public bool TryEnterReadLock(TimeSpan timeout); /// <summary>
    /// 尝试进入读取模式锁定状态,可以选择整数超时时间。 /// </summary>
    /// 
    /// <returns>
    /// 如果调用线程已进入读取模式,则为 true;否则为 false。 /// </returns>
    /// <param name="millisecondsTimeout">等待的毫秒数,或为 -1 (<see cref="F:System.Threading.Timeout.Infinite"/>),表示无限期等待。</param><exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已进入该锁。- 或 -递归数将超出该计数器的容量。限制为应用程序应永远不会遇到它太大。</exception><exception cref="T:System.ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> 为负数,但它不是等于 <see cref="F:System.Threading.Timeout.Infinite"/> (-1),这是唯一允许的值为负。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public bool TryEnterReadLock(int millisecondsTimeout); /// <summary>
    /// 尝试进入写入模式锁定状态。 /// </summary>
    /// <exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已在任何模式下进入该锁。- 或 -当前线程已进入读取的模式,因此尝试进入锁定状态写模式,则会创建导致死锁的可能性。- 或 -递归数将超出该计数器的容量。限制为应用程序应永远不会遇到它太大。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public void EnterWriteLock(); /// <summary>
    /// 尝试进入写入模式锁定状态,可以选择超时时间。 /// </summary>
    /// 
    /// <returns>
    /// 如果调用线程已进入写入模式,则为 true;否则为 false。 /// </returns>
    /// <param name="timeout">等待的间隔;或为 -1 毫秒,表示无限期等待。</param><exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已进入该锁。- 或 -当前线程最初在读取模式中,输入该锁,因此尝试进入写入模式会创建导致死锁的可能性。- 或 -递归数将超出该计数器的容量。限制为应用程序应永远不会遇到它太大。</exception><exception cref="T:System.ArgumentOutOfRangeException"><paramref name="timeout"/> 为负数,但它不等于-1 毫秒为单位),这是唯一允许的值为负。- 或 -值 <paramref name="timeout"/> 大于 <see cref="F:System.Int32.MaxValue"/> 毫秒为单位)。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public bool TryEnterWriteLock(TimeSpan timeout); /// <summary>
    /// 尝试进入写入模式锁定状态,可以选择超时时间。 /// </summary>
    /// 
    /// <returns>
    /// 如果调用线程已进入写入模式,则为 true;否则为 false。 /// </returns>
    /// <param name="millisecondsTimeout">等待的毫秒数,或为 -1 (<see cref="F:System.Threading.Timeout.Infinite"/>),表示无限期等待。</param><exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已进入该锁。- 或 -当前线程最初在读取模式中,输入该锁,因此尝试进入写入模式会创建导致死锁的可能性。- 或 -递归数将超出该计数器的容量。限制为应用程序应永远不会遇到它太大。</exception><exception cref="T:System.ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> 为负数,但它不是等于 <see cref="F:System.Threading.Timeout.Infinite"/> (-1),这是唯一允许的值为负。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public bool TryEnterWriteLock(int millisecondsTimeout); /// <summary>
    /// 尝试进入可升级模式锁定状态。 /// </summary>
    /// <exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已在任何模式下进入该锁。- 或 -当前线程已进入读取的模式,因此尝试进入可升级模式将有死锁的可能性。- 或 -递归数将超出该计数器的容量。限制为应用程序应永远不会遇到它太大。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public void EnterUpgradeableReadLock(); /// <summary>
    /// 尝试进入可升级模式锁定状态,可以选择超时时间。 /// </summary>
    /// 
    /// <returns>
    /// 如果调用线程已进入可升级模式,则为 true;否则为 false。 /// </returns>
    /// <param name="timeout">等待的间隔;或为 -1 毫秒,表示无限期等待。</param><exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已进入该锁。- 或 -当前线程最初在读取模式中,输入该锁,因此尝试进入可升级模式会创建导致死锁的可能性。- 或 -递归数将超出该计数器的容量。限制为应用程序应永远不会遇到它太大。</exception><exception cref="T:System.ArgumentOutOfRangeException"><paramref name="timeout"/> 为负数,但它不等于-1 毫秒为单位),这是唯一允许的值为负。- 或 -值 <paramref name="timeout"/> 大于 <see cref="F:System.Int32.MaxValue"/> 毫秒为单位)。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public bool TryEnterUpgradeableReadLock(TimeSpan timeout); /// <summary>
    /// 尝试进入可升级模式锁定状态,可以选择超时时间。 /// </summary>
    /// 
    /// <returns>
    /// 如果调用线程已进入可升级模式,则为 true;否则为 false。 /// </returns>
    /// <param name="millisecondsTimeout">等待的毫秒数,或为 -1 (<see cref="F:System.Threading.Timeout.Infinite"/>),表示无限期等待。</param><exception cref="T:System.Threading.LockRecursionException"><see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"/> 属性是 <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"/> 和当前的线程已进入该锁。- 或 -当前线程最初在读取模式中,输入该锁,因此尝试进入可升级模式会创建导致死锁的可能性。- 或 -递归数将超出该计数器的容量。限制为应用程序应永远不会遇到它太大。</exception><exception cref="T:System.ArgumentOutOfRangeException"><paramref name="millisecondsTimeout"/> 为负数,但它不是等于 <see cref="F:System.Threading.Timeout.Infinite"/> (-1),这是唯一允许的值为负。</exception><exception cref="T:System.ObjectDisposedException"><see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象已被释放。</exception>
 [__DynamicallyInvokable] public bool TryEnterUpgradeableReadLock(int millisecondsTimeout); /// <summary>
    /// 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式。 /// </summary>
    /// <exception cref="T:System.Threading.SynchronizationLockException">在读取模式中,当前线程不已进入该锁。</exception>
 [__DynamicallyInvokable] public void ExitReadLock(); /// <summary>
    /// 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式。 /// </summary>
    /// <exception cref="T:System.Threading.SynchronizationLockException">当前线程不已进入写入模式的锁定。</exception>
 [__DynamicallyInvokable] public void ExitWriteLock(); /// <summary>
    /// 减少可升级模式的递归计数,并在生成的计数为 0(零)时退出可升级模式。 /// </summary>
    /// <exception cref="T:System.Threading.SynchronizationLockException">当前线程不已进入可升级模式的锁定。</exception>
 [__DynamicallyInvokable] public void ExitUpgradeableReadLock(); /// <summary>
    /// 释放 <see cref="T:System.Threading.ReaderWriterLockSlim"/> 类的当前实例所使用的所有资源。 /// </summary>
    /// <exception cref="T:System.Threading.SynchronizationLockException"><see cref="P:System.Threading.ReaderWriterLockSlim.WaitingReadCount"/> 是大于零。- 或 -<see cref="P:System.Threading.ReaderWriterLockSlim.WaitingUpgradeCount"/> 是大于零。- 或 -<see cref="P:System.Threading.ReaderWriterLockSlim.WaitingWriteCount"/> 是大于零。</exception><filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public void Dispose(); /// <summary>
    /// 获取一个值,该值指示当前线程是否已进入读取模式的锁定状态。 /// </summary>
    /// 
    /// <returns>
    /// 如果当前线程已进入读取模式,则为 true;否则为 false。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public bool IsReadLockHeld { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取一个值,该值指示当前线程是否已进入可升级模式的锁定状态。 /// </summary>
    /// 
    /// <returns>
    /// 如果当前线程已进入可升级模式,则为 true;否则为 false。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public bool IsUpgradeableReadLockHeld { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取一个值,该值指示当前线程是否已进入写入模式的锁定状态。 /// </summary>
    /// 
    /// <returns>
    /// 如果当前线程已进入写入模式,则为 true;否则为 false。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public bool IsWriteLockHeld { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取一个值,该值指示当前 <see cref="T:System.Threading.ReaderWriterLockSlim"/> 对象的递归策略。 /// </summary>
    /// 
    /// <returns>
    /// 枚举值之一,用于指定锁定递归策略。 /// </returns>
 [__DynamicallyInvokable] public LockRecursionPolicy RecursionPolicy { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取已进入读取模式锁定状态的独有线程的总数。 /// </summary>
    /// 
    /// <returns>
    /// 已进入读取模式锁定状态的独有线程的数量。 /// </returns>
 [__DynamicallyInvokable] public int CurrentReadCount { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取当前线程进入读取模式锁定状态的次数,用于指示递归。 /// </summary>
    /// 
    /// <returns>
    /// 如果当前线程未进入读取模式,则为 0(零);如果线程已进入读取模式但却不是以递归方式进入的,则为 1;或者如果线程已经以递归方式进入锁定模式 n - 1 次,则为 n。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public int RecursiveReadCount { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取当前线程进入可升级模式锁定状态的次数,用于指示递归。 /// </summary>
    /// 
    /// <returns>
    /// 如果当前线程没有进入可升级模式,则为 0;如果线程已进入可升级模式却不是以递归方式进入的,则为 1;或者如果线程已经以递归方式进入可升级模式 n - 1 次,则为 n。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public int RecursiveUpgradeCount { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取当前线程进入写入模式锁定状态的次数,用于指示递归。 /// </summary>
    /// 
    /// <returns>
    /// 如果当前线程没有进入写入模式,则为 0;如果线程已进入写入模式却不是以递归方式进入的,则为 1;或者如果线程已经以递归方式进入写入模式 n - 1 次,则为 n。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public int RecursiveWriteCount { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取等待进入读取模式锁定状态的线程总数。 /// </summary>
    /// 
    /// <returns>
    /// 等待进入读取模式的线程总数。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public int WaitingReadCount { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取等待进入可升级模式锁定状态的线程总数。 /// </summary>
    /// 
    /// <returns>
    /// 等待进入可升级模式的线程总数。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public int WaitingUpgradeCount { [__DynamicallyInvokable] get; } /// <summary>
    /// 获取等待进入写入模式锁定状态的线程总数。 /// </summary>
    /// 
    /// <returns>
    /// 等待进入写入模式的线程总数。 /// </returns>
    /// <filterpriority>2</filterpriority>
 [__DynamicallyInvokable] public int WaitingWriteCount { [__DynamicallyInvokable] get; } }

     以上是对Stack和线程的相关知识的浅述,现在介绍一下线程安全的Stack:

 

/// <summary>
    /// 表示对象的后进先出线程安全集合(栈结构) /// </summary>
    /// <typeparam name="T"></typeparam>
    public class TStack<T> : IEnumerable<T>, ICollection { /// <summary>
        /// 内部堆栈 /// </summary>
        private readonly Stack<T> _mStack; /// <summary>
        /// 锁访问堆栈(用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问。) /// </summary>
        private readonly ReaderWriterLockSlim _lockStack = new ReaderWriterLockSlim(); /// <summary>
        /// 仅用于SyncRoot属性 /// </summary>
        private readonly object _objSyncRoot = new object(); // Variables
        /// <summary>
        /// 初始化一个新的实例 <see cref="TStack{T}"/> class. /// </summary>
        public TStack() { _mStack = new Stack<T>(); } /// <summary>
        /// 初始化一个新的实例 <see cref="TStack{T}"/> class. /// </summary>
        /// <param name="col">
        /// 开始集合 /// </param>
        public TStack(IEnumerable<T> col) { _mStack = new Stack<T>(col); } // Init
        /// <summary>
        /// 获取枚举器 /// </summary>
        public IEnumerator<T> GetEnumerator() { Stack<T> localStack = null; // 初始化枚举器
            _lockStack.PerformUsingReadLock(() => { // 创建一个m_tlist副本
                localStack = new Stack<T>(_mStack); }); // 获取枚举器
            foreach (T item in localStack) yield return item; } /// <summary>
        /// 获取枚举器 /// </summary>
 IEnumerator IEnumerable.GetEnumerator() { Stack<T> localStack = null; // 初始化枚举器
            _lockStack.PerformUsingReadLock(() => { // 创建一个m_TList的副本
                localStack = new Stack<T>(_mStack); }); // 获取枚举器
            foreach (T item in localStack) yield return item; } /// <summary>
        /// 复制到一个数组 /// </summary>
        /// <param name="array"></param>
        /// <param name="index"></param>
        public void CopyTo(Array array, int index) { _lockStack.PerformUsingReadLock(() => _mStack.ToArray().CopyTo(array, index)); } /// <summary>
        ///堆栈中的项目数 /// </summary>
        public int Count { get { return _lockStack.PerformUsingReadLock(() => _mStack.Count); } } /// <summary>
        /// 总为真 /// </summary>
        public bool IsSynchronized { get { return true; } } /// <summary>
        ///同步根 /// </summary>
        public object SyncRoot { get { return _objSyncRoot; } } /// <summary>
        ///清除集合 /// </summary>
        public void Clear() { _lockStack.PerformUsingWriteLock(() => _mStack.Clear()); } // Clear
        /// <summary>
        ///如果项目在堆栈中,则为true /// </summary>
        /// <param name="item"></param>
        /// <returns></returns>
        public bool Contains(T item) { return _lockStack.PerformUsingReadLock(() => _mStack.Contains(item)); } // 包含
        /// <summary>
        /// 返回堆栈中的顶部项,而不从堆栈中删除它 /// </summary>
        /// <returns></returns>
        public T Peek() { return _lockStack.PerformUsingReadLock(() => _mStack.Peek()); } // Peek
        /// <summary>
        ///删除并返回堆栈中的顶部项目 /// </summary>
        /// <returns></returns>
        public T Pop() { return _lockStack.PerformUsingWriteLock(() => _mStack.Pop()); } // Pop
        /// <summary>
        /// 将一个项目插入堆栈 /// </summary>
        /// <param name="item"></param>
        public void Push(T item) { _lockStack.PerformUsingWriteLock(() => _mStack.Push(item)); } // Push
        /// <summary>
        ///将堆栈转换为数组 /// </summary>
        /// <returns></returns>
        public T[] ToArray() { return _lockStack.PerformUsingReadLock(() => _mStack.ToArray()); } // ToArray
        /// <summary>
        /// 将容量设置为堆栈中实际的元素数量 /// </summary>
        public void TrimExcess() { _lockStack.PerformUsingWriteLock(() => _mStack.TrimExcess()); } }

    以上的操作方法继承了IEnumerable<T>, ICollection两个接口。有兴趣的,可以对IEnumerable<T>, ICollection两个接口进行细致的了解。

 

共有 人打赏支持
粉丝 0
博文 44
码字总数 57771
×
彭泽0902
如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
* 金额(元)
¥1 ¥5 ¥10 ¥20 其他金额
打赏人
留言
* 支付类型
微信扫码支付
打赏金额:
已支付成功
打赏金额: