第四章 迭代器

第四章 迭代器
原著:Microsoft Corporation
原文:http://msdn.microsoft.com/vcsharp/team/language/default.aspx (SpecificationVer2.doc)
翻译:lover_P
出处:http://www.csdn.net/Develop/article/26/26043.shtm


--------------------------------------------------------------------------------

[内容]

4.1 迭代器块
4.1.1 枚举器接口
4.1.2 可枚举接口
4.1.3 生成的类型
4.1.4 this访问
4.2 Enumerator对象
4.2.1 MoveNext()方法
4.2.2 Current属性
4.2.3 Dispose()方法
4.3 Enumerable对象
4.3.1 GetEnumerator()方法
4.4 yield语句
4.4.1 有限赋值
4.5 实例
 

4.1 迭代器块
    一个迭代器块(iterator block)是一个能够产生有序的值序列的块。迭代器块和普通语句块的区别就是其中出现的一个或多个yield语句。

yield return语句产生迭代的下一个值。
yield break语句表示迭代完成。
    只要相应的函数成员的返回值类型是一个枚举器接口(见4.1.1)或是一个可枚举接口(见4.1.2),就可以将一个迭代器块用作方法体、运算符体或访问器体。

    迭代器块并不是C#语法中的独立元素。它们受多种因素的制约,并且对函数成员声明的语义有很大影响,但在语法上它们只是块(block)。

    当一个函数成员用一个迭代器块来实现时,如果函数成员的形式参数列表指定了ref或out参数,则会引起编译错误。

    如果在迭代器块中出现了return语句,则会引起编译错误(但yield return语句是允许的)。

    如果迭代器块包含不安全上下文,则会引起编译错误。一个迭代器块必须定义在一个安全的上下文中,即使它的声明嵌套在一个不安全上下文中。

4.1.1 枚举器接口
    枚举器(enumerator)接口包括非泛型接口System.Collections.IEnumerator和泛型接口System.Collections.Generic.IEnumerator<T>的所有实例化。在这一章中,这些接口将分别称作IEnumerator和IEnumerator<T>。

4.1.2 可枚举接口
    可枚举(enumerable)接口包括非泛型接口System.Collections.IEnumerable和泛型接口System.Collections.Generic.IEnumerable<T>。在这一章中,这些接口分别称作IEnumerable和IEnumerable<T>。

4.1.3 生成类型
    一个迭代器块能够产生一个有序的值序列,其中所有的制具有相同的类型。这个类型称为迭代器块的生成类型(yield type)。

用于实现一个返回IEnumerator或IEnumerable的函数成员的迭代器块的生成类型为object。
用于实现一个返回IEnumerator<T>或IEnumerable<T>的函数成员的迭代器块的生成类型为T。
4.1.4 this访问
    在一个类的一个实例成员中的迭代器块里,表达式this是一个值。这个值的类型就是出现这种用法的类,并且它是对被调用的方法所在的对象的一个引用。

    在一个结构的一个实例成员中的迭代器块里,表达式this是一个变量。这个变量的类型就是出现这种用法的结构,这个变量存贮了对被调用成员所在结构的一个拷贝。结构的实例成员中的迭代器块里的this变量和以该结构为类型的值变量完全一样。

4.2 Enumerator对象
    如果一个函数成员使用了迭代器块来返回一个枚举器接口类型,对该函数成员的调用不会立即执行迭代器块中的代码,而是建立并返回一个枚举器对象。这个对象封装了迭代器块中指定的代码,而对迭代器中指定的代码的执行发生在调用该枚举器对象的MoveNext()方法时。一个枚举器对象具有如下特征:

它实现了IEnumerator和IEnumerator<T>,这里T是迭代器块的生成类型。
它实现了System.IDisposable。
它用传递给函数成员的参数值(如果有的话)和实例值进行初始化。
它有四个可能的状态:before、running、suspended和after,其初始状态为before。
    典型的枚举器对象是由编译器自动生成的封装了迭代器块中的代码并实现了枚举器接口的枚举器类的实例,但其他的实现也是允许的。如果一个枚举器类是由编译器自动生成的,则该类是直接或间接地嵌套在包含了函数成员的类中的,具有私有的可访问性,并且具有一个由编译器保留使用的名字。

    一个枚举器对象可以实现上面所述之外的其它接口。

    后面的几节详细地描述了由一个枚举器对象所实现的IEnumerable和IEnumerable<T>接口中的MoveNext()、Current和Dispose()成员的确切行为。

    注意,枚举器对象不支持IEnumerator.Reset()方法。调用该方法会抛出System.NotSupporteException异常。

4.2.1 MoveNext()方法
    枚举器对象的MoveNext()方法封装了迭代器块的代码。对MoveNext()方法的调用执行了迭代器块中的代码,并为枚举器对象的Current属性设置一个适当的值。MoveNext()方法完成的确切动作取决于调用MoveNext()方法是枚举器对象的状态:

如果枚举器对象的状态为before,调用MoveNext()方法:
将状态设置为running。
将迭代器块对象的参数(包括this)初始化为枚举器对象初始化时所保存的变量值和实例值。
从执行迭代器块的开始执行,直到被中断(将在下面讨论)。
如果枚举器对象的状态为running,则调用MoveNext()方法的结果未指定。
如果枚举器对象的状态为suspended,调用MoveNext()方法:
将状态设置为running。
将所有局部变量和参数(包括this)恢复为迭代器块执行过程中最后一次挂起时所保存的值。注意这些变量所引用的对象的内容在上一次MoveNext()后可能会发生改变。
从上一次执行中断所在的yield return语句继续执行迭代器块,直到执行再次被中断(将在下面讨论)。
如果枚举器对象的状态为after,调用MoveNext()方法将返回false。
    当MoveNext()方法执行迭代器块时,执行过程会通过四种途径中断:yield return语句、yield break语句、遇到迭代器块的结尾以及迭代器块中抛出了异常并被传播到块外。

当遇到yield return语句(见4.4)时:
对语句中给定的表达式进行求值,隐式转换为生成类型,并赋给枚举器对象的Current属性。
挂起迭代器体的执行过程。保存所有局部变量和参数(包括this)的值,以及这个yield return语句的位置。如果该yield return语句位于一个或多个try块中,则与之相关联的finally块在此时还不会被执行。
将枚举器对象的状态设置为suspended。
向MoveNext()方法的调用者返回true,表示迭代已经成功地转移到下一个值上。
当遇到yield break语句(见4.4)时:
如果该yield break语句位于一个或多个try块中,则执行与之相关联的finally块。
将枚举器对象的状态设置为after。
向MoveNext()方法的调用者返回false,表示迭代完成。
当遇到迭代器快的结尾时:
将枚举器对象的状态设置为after。
向MoveNext()方法的调用者返回false,表示迭代完成。
当迭代器快抛出了一个异常,并传播到块外时:
迭代器块中适当的finally块将被执行。
将枚举器对象的状态设置为after。
将异常传播给MoveNext()方法的调用者。
4.2.2 Current属性
    一个枚举器对象的Current属性受迭代器块中的yield return语句的影响。

    当一个枚举器对象处于suspended状态时,Current属性的值由最后一次对MoveNext()方法的调用设置。当一个枚举器对象处于before、running或after状态时,访问Current属性的结果是未定义的。

For an iterator block with a yield type other than object, the result of accessing Current through the enumerator object’s IEnumerable implementation corresponds to accessing Current through the enumerator object’s IEnumerator<T> implementation and casting the result to object.

    如果一个迭代器块的生成类型不是object,通过枚举器对象实现的IEnumerable以及相应的IEnumerator<T>对Current的访问会将结果转换为object。

4.2.3 The Dispose method

4.2.3 Dispose()方法
The Dispose method is used to clean up the iteration by bringing the enumerator object to the after state.
" If the state of the enumerator object is before, invoking Dispose changes the state to after.
" If the state of the enumerator object is running, the result of invoking Dispose is unspecified.
" If the state of the enumerator object is suspended, invoking Dispose:
Changes the state to running.
Executes any finally blocks as if the last executed yield return statement were a yield break statement. If this causes an exception to be thrown and propagated out of the iterator body, the state of the enumerator object is set to after and the exception is propagated to the caller of the Dispose method.
Changes the state to after.
" If the state of the enumerator object is after, invoking Dispose has no affect.

    Dispose()方法通过将枚举器对象的状态设置为after来清除迭代器。

如果枚举器对象的状态为before,调用Dispose()方法将其状态设置为after。
如果枚举器对象的状态为running,调用Dispose()方法的结果是未定义的。
如果枚举器对象的状态为suspended,调用Dispose()方法:
将状态设置为running。
执行所有的finally块,好像yield return语句是yield break语句一样。如果这导致了异常被抛出并传播到迭代器块外,则将枚举器对象的状态设置为after并将异常传播给Dispose()方法的调用者。
将砖塔设置为after。
如果枚举器对象的状态为after,调用Dispose()方法没有任何效果。
4.3 Enumerable objects

4.3 Enumerable对象
When a function member returning an enumerable interface type is implemented using an iterator block, invoking the function member does not immediately execute the code in the iterator block. Instead, an enumerable object is created and returned. The enumerable object’s GetEnumerator method returns an enumerator object that encapsulates the code specified in the iterator block, and execution of the code in the iterator block occurs when the enumerator object’s MoveNext method is invoked. An enumerable object has the following characteristics:

    当一个返回一个可枚举接口类型的函数成员使用了迭代器块时,对该函数成员的调用不会立即执行迭代器块中的代码,而是建立并返回一个可枚举对象。该可枚举对象有一个GetEnumerator()方法,能够返回一个枚举器对象。该枚举器对象封装了迭代器块中指定的代码,当调用这个枚举器对象的MoveNext()方法时,会执行迭代器块中的代码。一个可枚举对象具有如下特征:

" It implements IEnumerable and IEnumerable<T>, where T is the yield type of the iterator block.
" It is initialized with a copy of the argument values (if any) and instance value passed to the function member.

它实现了IEnumerable或IEnumerable<T>,这里T是迭代器块的生成类型。
它用传递给函数成员的参数值(如果有的话)和实例值进行初始化。
An enumerable object is typically an instance of a compiler-generated enumerable class that encapsulates the code in the iterator block and implements the enumerable interfaces, but other methods of implementation are possible. If an enumerable class is generated by the compiler, that class will be nested, directly or indirectly, in the class containing the function member, it will have private accessibility, and it will have a name reserved for compiler use (§2.4.2).

    典型的可枚举对象是由编译器自动生成的封装了迭代器块中的代码并实现了可枚举接口的可枚举类的实例,但其他的实现也是允许的。如果一个可枚举类是由编译器自动生成的,则该类是直接或间接地嵌套在函数成员中的,具有私有的可访问性,并且具有一个由编译器保留使用的名字。

An enumerable object may implement more interfaces than those specified above. In particular, an enumerable object may also implement IEnumerator and IEnumerator<T>, enabling it to serve as both an enumerable and an enumerator. In that type of implementation, the first time an enumerable object’s GetEnumerator method is invoked, the enumerable object itself is returned. Subsequent invocations of the enumerable object’s GetEnumerator, if any, return a copy of the enumerable object. Thus, each returned enumerator has its own state and changes in one enumerator will not affect another.

    一个可枚举对象可以实现上述之外的其它接口。例如,一个可枚举对象还可以实现IEnumerator和IEnumerator<T>,使得它既是可枚举的又是一个枚举器。这种情况下,当可枚举对象的GetEnumerator()方法第一次被调用时,将返回可枚举对象本身。以后对可枚举对象的GetEnumerator()方法的调用(如果有的话),将返回可枚举对象的一个拷贝。因此,每个被返回的枚举器具有其自己的状态,并且一个枚举器和其它枚举器互不影响。

4.3.1 The GetEnumerator method

4.3.1 GetEnumerator()方法
An enumerable object provides an implementation of the GetEnumerator methods of the IEnumerable and IEnumerable<T> interfaces. The two GetEnumerator methods share a common implementation that acquires and returns an available enumerator object. The enumerator object is initialized with the argument values and instance value saved when the enumerable object was initialized, but otherwise the enumerator object functions as described in §22.2.

    一个可枚举对象提供了对IEnumerator和IEnumberator<T>接口的GetEnumerator()方法的实现。两个GetEnumerator()方法共享一个实现,能够获取并返回一个有效的枚举器对象。该枚举器对象使用可枚举对象被初始化时所保存的参数值和实例值进行初始化,该枚举器对象的功能如4.2节所描述。 

4.4 The yield statement

4.4 yield语句
The yield statement is used in an iterator block to yield a value to the enumerator object or to signal the end of the iteration.

    迭代器块中的yield语句用于生成一个值,或发出一个迭代完成的信号。

embedded-statement:
    ...
    yield-statement
yield-statement:
    yield   return   expression   ;
    yield   break   ;

内嵌语句:
    ...
    yield语句

yield语句:
    yield   return   表达式   ;
    yield   break   ;
 

To ensure compatibility with existing programs, yield is not a reserved word, and yield has special meaning only when it is used immediately before a return or break keyword. In other contexts, yield can be used as an identifier.

    为了保证和现有程序的兼容性,yield并不是一个保留字,只有当一个return语句紧随其后时,yield语句才有这特殊的意义。其它情况下,yield语句可以用作标识符。

The are several restrictions on where a yield statement can appear, as described in the following.

    yield语句的出现首很多限制,如下所描述:

" It is a compile-time error for a yield statement (of either form) to appear outside a method-body, operator-body or accessor-body
" It is a compile-time error for a yield statement (of either form) to appear inside an anonymous method.
" It is a compile-time error for a yield statement (of either form) to appear in the finally clause of a try statement.
" It is a compile-time error for a yield return statement to appear anywhere in a try statement that contains catch clauses.

如果一个yield语句出现在方法体、运算符体或访问器体之外,则会引起编译错误。
如果一个yield语句出现在匿名方法内部,则会引起编译错误。
如果一个yield语句出现在finally或一个try块内,则会引起编译错误。
如果一个yield语句出现在一个带有catch语句的try块内,则会引起编译错误。
The following example shows some valid and invalid uses of yield statements.

    下面的例子展示了一些yield语句的有效的和无效的用法。

delegate IEnumerable<int> D();

IEnumerator<int> GetEnumerator() {
    try {
        yield return 1;  // 正确
        yield break;     // 正确
    }
    finally {
        yield return 2;  // 错误,yield出现在finally块中E
        yield break;     // 错误,yield出现在finally块中
    }

    try {
        yield return 3;  // 错误,yield return语句出现在try...catch语句中
        yield break;     // 正确
    }
    catch {
        yield return 4;  // 错误,yield return语句出现在try...catch语句中
        yield break;     // 正确
    }

    D d = delegate {
        yield return 5;  // 错误,yield语句出现在匿名方法中
    };
}

int MyMethod() {
    yield return 1;      // 错误,迭代器块具有错误的返回值类型
}

An implicit conversion (§6.1) must exist from the type of the expression in the yield return statement to the yield type (§22.1.3) of the iterator block.

    从yield return语句中的表达式的类型到迭代器块的生成类型(见4.1.3)必存在一个隐式转换。

A yield return statement is executed as follows:

    yield return语句依照下面的步骤执行:

" The expression given in the statement is evaluated, implicitly converted to the yield type, and assigned to the Current property of the enumerator object.
" Execution of the iterator block is suspended. If the yield return statement is within one or more try blocks, the associated finally blocks are not executed at this time.
" The MoveNext method of the enumerator object returns true to its caller, indicating that the enumerator object successfully advanced to the next item.

对语句中给定的表达式进行求值,并隐式转换为生成类型,然后赋给枚举器对象的Current属性。
挂起对迭代器块的执行。如果该yield return语句位于一个或多个try块中,相应的finally块暂时不会被执行。
MoveNext()方法向其调用者返回true,表示枚举器对象成功地前进到下一个值上。
The next call to the enumerator object’s MoveNext method resumes execution of the iterator block from where it was last suspended.

    对枚举器对象的MoveNext()方法的下一次调用将从上一次挂起的地方恢复对迭代器块的执行。

A yield break statement is executed as follows:

    yield break语句依照下面的步骤执行:

" If the yield break statement is enclosed by one or more try blocks with associated finally blocks, control is initially transferred to the finally block of the innermost try statement. When and if control reaches the end point of a finally block, control is transferred to the finally block of the next enclosing try statement. This process is repeated until the finally blocks of all enclosing try statements have been executed.
" Control is returned to the caller of the iterator block. This is either the MoveNext method or Dispose method of the enumerator object.

如果yield break语句位于一个或多个带有finally块的try块中,控制将被转移到最里面的try块对应的finally块中。当控制流程遇到finally块的结尾(如果能够的话),控制将被转移到外一层try块对应的finally块中。这个过程持续到所有try语句对应的finally块都被执行完。
将控制返回给迭代器块的调用者。这可能从MoveNext()方法或Dispose()方法中返回。
Because a yield break statement unconditionally transfers control elsewhere, the end point of a yield break statement is never reachable.

    由于一个yield break语句无条件地将控制转移到其它地方,因此一个yield break的终点将永远不可达。

4.4.1 Definite assignment

4.4.1 明确赋值
For a yield return statement stmt of the form:

    对于下面形式的yield return语句:

yield return expr ;

" A variable v has the same definite assignment state at the beginning of expr as at the beginning of stmt.
" If a variable v is definitely assigned at the end of expr, it is definitely assigned at the end point of stmt; otherwise; it is not definitely assigned at the end point of stmt.

对于一个变量v,在expr的开始处和语句的开始处有同样的明确赋值。
如果一个变量v在expr的结束处被明确赋值,则它是在语句的结尾被明确赋值的;否则,它未在语句的结尾被明确赋值。

4.5 Implementation example

4.5 实例
This section describes a possible implementation of iterators in terms of standard C# constructs. The implementation described here is based on the same principles used by the Microsoft C# compiler, but it is by no means a mandated implementation or the only one possible.

    这一节将描述标准C#结构中的迭代器可能的实现。这里描述的实现是基于和Microsoft C#编译器相同的原则的,但决不是唯一可能的实现。

The following Stack<T> class implements its GetEnumerator method using an iterator. The iterator enumerates the elements of the stack in top to bottom order.

    下面的Stack<T>类使用一个迭代器实现了它的GetEnumerator()方法。该迭代器按照从顶至底的顺序枚举了堆栈中的所有元素。

using System;
using System.Collections;
using System.Collections.Generic;

class Stack<T> : IEnumerable<T> {
    T[] items;
    int count;

    public void Push(T item) {
        if (items == null) {
            items = new T[4];
        }
        else if (items.Length == count) {
            T[] newItems = new T[count * 2];
            Array.Copy(items, 0, newItems, 0, count);
            items = newItems;
        }
        items[count++] = item;
    }

    public T Pop() {
        T result = items[--count];
        items[count] = T.default;
        return result;
    }

    public IEnumerator<T> GetEnumerator() {
        for(int i = count - 1; i >= 0; --i) yield items[i];
    }
}

The GetEnumerator method can be translated into an instantiation of a compiler-generated enumerator class that encapsulates the code in the iterator block, as shown in the following.

    GetEnumerator()方法可以转换为编译器自动生成的枚举器类的实例,它封装了迭代器块中指定的代码,如下所示:

class Stack<T> : IEnumerable<T> {
    ...
    public IEnumerator<T> GetEnumerator() {
        return new __Enumerator1(this);
    }

    class __Enumerator1 : IEnumerator<T>, IEnumerator {
        int __state;
        T __current;
        Stack<T> __this;
        int i;

        public __Enumerator1(Stack<T> __this) {
            this.__this = __this;
        }

        public T Current {
            get { return __current; }
        }

        object IEnumerator.Current {
            get { return __current; }
        }

        public bool MoveNext() {
            switch (__state) {
                case 1: goto __state1;
                case 2: goto __state2;
            }

            i = __this.count - 1;

__loop:
            if(i < 0) goto __state2;
            __current = __this.items[i];
            __state = 1;
            return true;

__state1:
            --i;
            goto __loop;

__state2:
            __state = 2;
            return false;
        }

        public void Dispose() {
            __state = 2;
        }

        void IEnumerator.Reset() {
            throw new NotSupportedException();
        }
    }
}

In the preceding translation, the code in the iterator block is turned into a state machine and placed in the MoveNext method of the enumerator class. Furthermore, the local variable i is turned into a field in the enumerator object so it can continue to exist across invocations of MoveNext.

    上面的转换中,迭代器块中的代码被转换为状态机并放在枚举器类的MoveNext()方法中。此外,局部变量i被转换为枚举器对象的域,因此在对MoveNext()方法的调用过程中它将一直存在。

The following example prints a simple multiplication table of the integers 1 through 10. The FromTo method in the example returns an enumerable object and is implemented using an iterator.

    下面的例子打印了整数1至10的一个简单的乘法表。例子中的FromTo()方法返回了一个用迭代器实现的可枚举对象。

using System;
using System.Collections.Generic;

class Test {
    static IEnumerable<int> FromTo(int from, int to) {
        while(from <= to) yield return from++;
    }

    static void Main() {
        IEnumerable<int> e = FromTo(1, 10);

        foreach(int x in e) {
            foreach(int y in e) {
                Console.Write("{0,3} ", x * y);
            }
            Console.WriteLine();
        }
    }
}

The FromTo method can be translated into an instantiation of a compiler-generated enumerable class that encapsulates the code in the iterator block, as shown in the following.

    FromTo()方法可以被转换为由编译器自动生成的可枚举类的实例,它封装了迭代器块中的代码,如下所示:

using System;
using System.Threading;
using System.Collections;
using System.Collections.Generic;

class Test {
    ...
    static IEnumerable<int> FromTo(int from, int to) {
        return new __Enumerable1(from, to);
    }

    class __Enumerable1 : IEnumerable<int>, IEnumerable, IEnumerator<int>, IEnumerator {
        int __state;
        int __current;
        int __from;
        int from;
        int to;
        int i;

        public __Enumerable1(int __from, int to) {
            this.__from = __from;
            this.to = to;
        }

        public IEnumerator<int> GetEnumerator() {
            __Enumerable1 result = this;
            if(Interlocked.CompareExchange(ref __state, 1, 0) != 0) {
                result = new __Enumerable1(__from, to);
                result.__state = 1;
            }
            result.from = result.__from;
            return result;
        }

        IEnumerator IEnumerable.GetEnumerator() {
            return (IEnumerator)GetEnumerator();
        }

        public int Current {
            get { return __current; }
        }

        object IEnumerator.Current {
            get { return __current; }
        }

        public bool MoveNext() {
            switch (__state) {
            case 1:
                if(from > to) goto case 2;
                __current = from++;
                __state = 1;
                return true;

            case 2:
                __state = 2;
                return false;

            default:
                throw new InvalidOperationException();
            }
        }

        public void Dispose() {
            __state = 2;
        }

        void IEnumerator.Reset() {
            throw new NotSupportedException();
        }
    }
}

The enumerable class implements both the enumerable interfaces and the enumerator interfaces, enabling it to serve as both an enumerable and an enumerator. The first time the GetEnumerator method is invoked, the enumerable object itself is returned. Subsequent invocations of the enumerable object’s GetEnumerator, if any, return a copy of the enumerable object. Thus, each returned enumerator has its own state and changes in one enumerator will not affect another. The Interlocked.CompareExchange method is used to ensure thread-safe operation.

    这个可枚举类同时实现了可枚举接口和枚举器接口,因此它既是可枚举的又是一个枚举器。当GetEnumerator()方法第一次被调用时,将返回可枚举对象本身。以后对GetEnumerator()方法的调用(如果有的话),将返回可枚举对象的一个拷贝。因此返回的每一个枚举器具有其自己的状态,一个枚举器的改变不会影响到其它的枚举器。Interlocked.CoompareExchange()方法可以用于确保线程安全。

The from and to parameters are turned into fields in the enumerable class. Because from is modified in the iterator block, an additional __from field is introduced to hold the initial value given to from in each enumerator.
The MoveNext method throws an InvalidOperationException if it is called when __state is 0. This protects against use of the enumerable object as an enumerator object without first calling GetEnumerator.

    from和to参数被转换为可枚举类的域。因为迭代器块改变了from,因此引入了一个附加的__from域来保存每个枚举器中的from的初始值。

    当__state是0时,MoveNext()方法将跑出一个InvalidOperationException异常。这将保证不会发生没有首先调用GetEnumerator()方法而直接将可枚举对象用作枚举器。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值