C#中的访问者模式——5个版本

目录

介绍

我们试图解决的问题

访问者模式

C#中的访问者模式——第 1 版——经典访问者

C#中的访问者模式——第 2 版——动态访问者

此解决方案的局限性

C#中的访问者模式——第 3 版——反射访问者

此解决方案的局限性

C#中的访问者模式——第 4 版——反射扩展访问者

此解决方案的局限性

C# 中的访问者模式——第 5 版——泛型访问者

此解决方案的局限性

结论


介绍

这是一篇描述C#访问者模式的教程文章。目标受众是中级C#程序员及以上。

访客模式是23GoF模式中最复杂的模式之一。在C#中,它有多个版本。在这里,我们将用五个版本来描述它:

  1. C#中的访问者模式——第 1 版——经典访问者
  2. C#中的访问者模式——第 2 版——动态访问者
  3. C#中的访问者模式——第 3 版——反射访问者
  4. C#中的访问者模式——第 4 版——反射扩展访问者
  5. C#中的访问者模式——第 5 版——泛型访问者

我们试图解决的问题

首先,让我们尝试了解我们试图用这种模式解决什么问题,以及经典OO设计的局限性是什么。让我们看一下图1-1和代码1-1中的经典OO设计。

public abstract class Element
{
    public int Attribute1 = 0;
    public int Attribute2 = 0;

    abstract public void V1();

    abstract public void V2();

    abstract public void V3();
}

public class ElementA : Element
{
    public ElementA()
    {
    }

    public int Attribute3 = 0;

    public override void V1()
    {
    }

    public override void V2()
    {
    }

    public override void V3()
    {
    }
}

public class ElementB : Element
{
    public ElementB()
    {
    }

    public int Attribute3 = 0;

    public override void V1()
    {
    }

    public override void V2()
    {
    }

    public override void V3()
    {
    }
}

我们在这个解决方案中看到的问题,或者更好地说,限制是:

  • 在这种方法中,数据和算法(方法V1V2等)是耦合的​​。有时尝试将它们分开可能很有用
  • 在不改变现有类结构的情况下,添加新操作(例如V4)并不容易。这与开/关原则相反。希望能够在不改变类结构的情况下添加新的操作(方法)。
  • 在同一个地方有不同的方法(例如,V1V2),它们可以解决完全不同和不相关的功能/问题。例如,V1可以关注生成.pdf,而V2可以关注生成html。这与关注点分离的原则背道而驰。

访问者模式

访问者模式通过将数据和操作划分为单独的类来解决上述问题/限制。数据保存在Element/Elements类中,而操作保存在Visitor/Visitors类中,每个特定的Visitor都可以解决不同的问题。通过创建新Visitor类可以很容易地实现对, Elements的操作的扩展。

此模式的关键部分是设计解决方案,使Visitor对象能够在Element上执行操。 我们说Visitor访问Element是对Element进行操作。

如果我们查看类图Picture1-1,我们会看到对于对象 ElementA,我们有方法V1,所以操作调用看起来像:

ElementA elementa=new ElementA();
elementa.V1();

在访问者模式中,使用V1()方法执行的操作将封装在对象Visitor1中,使用V2()方法执行的操作将封装在object Visitor2中,等等。相同的操作调用现在看起来像:

ElementA elementa=new ElementA();
Visitor1 visitor1=new Visitor1();
visitor1.visit(elementa);

情况并没有到此结束。问题是我们将有几个ElementVisitor对象,我们经常通过基类/接口来处理这些对象。然后,出现了调度适当方法的问题。Dispatch是找出调用哪个具体方法的问题。

C#与大多数OO语言一样,以虚函数调用的形式支持单次调度。也就是所谓的动态绑定。根据所讨论对象的类型,在运行时动态地,C#将从虚拟方法表中调用适当的虚拟函数。

但有时,这还不够,需要多次调度。多重分派是根据多个对象的运行时类型找到调用哪个具体方法的问题。

C#中的访问者模式——第 1 版——经典访问者

访问者模式的经典访问者版本最常见于文学作品中。在经典版本的访问者模式中,模式基于C#双重调度机制。此解决方案中使用的双重调度机制基于C#的两个特性:

  1. 基于对象类型动态绑定具体虚方法的能力
  2. 根据参数类型将重载方法解析为具体方法的能力

以下是示例代码的类图的样子:

这是这个的代码:

public abstract class Element
{
    public abstract void Accept(IVisitor visitor);
}

public class ElementA : Element
{
    public int Id = 0;

    public ElementA(int Id)
    {
        this.Id = Id;
    }

    public override void Accept(IVisitor visitor)     //(2)
    {
        visitor.Visit(this);
    }
}

public class ElementB : Element
{
    public int Id = 0;

    public ElementB(int Id)
    {
        this.Id = Id;
    }
    public override void Accept(IVisitor visitor)
    {
        visitor.Visit(this);
    }
}

public interface IVisitor
{
    void Visit(ElementA ElemA);
    void Visit(ElementB ElemB);
}

public class Visitor1 : IVisitor    //(3)
{
    public virtual void Visit(ElementA ElemA)  //(4)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }
    public virtual void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

public class Visitor2 : IVisitor
{
    public virtual void Visit(ElementA ElemA)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }
    public virtual void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

class Client
{
    static void Main(string[] args)
    {
        //--single element, explicit call-------------------------
        ElementA element0 = new ElementA(0);
        Visitor1 vis0 = new Visitor1();

        vis0.Visit(element0);  //(0) works

        //--single element, base class call-----------------------
        Element element = new ElementA(1);
        IVisitor visitor = new Visitor1();

        //visitor.Visit(element);   //(5) will not compile

        element.Accept(visitor);  //(1)

        //--collection of elements-----------------
        List<IVisitor> listVis = new List<IVisitor>();
        listVis.Add(new Visitor1());
        listVis.Add(new Visitor2());

        List<Element> list = new List<Element>();
        list.Add(new ElementA(2));
        list.Add(new ElementB(3));
        list.Add(new ElementA(4));
        list.Add(new ElementB(5));

        foreach (IVisitor vis in listVis)
            foreach (Element elem in list)
            {
                elem.Accept(vis);
            }

        Console.ReadLine();
    }
}

以下是示例执行的结果:

请注意,在(0)中,当Visitor通过显式类调用时,一切正常。我们说Visitor访问Element是对Element进行操作。

但是,在(5)中,当我们尝试通过基类/接口调用visitor时,我们无法编译。编译器无法解析调用哪个方法。这就是为什么我们需要使用双重调度来正确解决调用哪个具体方法的所有这些魔法。

在(1)中,我们有适当的调用。正在发生的事情是:

  1. (1)中,我们动态绑定到(2)
  2. (2)中,我们动态绑定到(3)
  3. (2)中,我们对(4)有重载决议

因为在(2)中,我们有双重分辨率,这就是它被称为双重调度的原因。

此解决方案的局限性。作为任何解决方案,这将有一些限制/不需要的副作用:

  • 类层次结构ElementsVisitor之间有很强的循环依赖性。 如果需要经常更新层次结构,这可能是一个问题。
  • 请注意,在(4)中,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1 中,方法V1()可以对类Elementprivate成员进行操作。在C++中,这可以通过使用friend范式来解决,但在C#中并非如此。

C#中的访问者模式——第 2 版——动态访问者

访问者模式的动态访问者版本基于对动态调度的C#支持。这就是语言动态调度的能力,即在运行时做出具体的调用决策。我们将变量转换为dynamic,这样,将调度决策推迟到运行时。我们再次有双重分派,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。

以下是示例代码的类图的样子:

这是这个的代码:

public abstract class AElement
{
}

public class ElementA : AElement
{
    public int Id = 0;

    public ElementA(int Id)
    {
        this.Id = Id;
    }
}

public class ElementB : AElement
{
    public int Id = 0;

    public ElementB(int Id)
    {
        this.Id = Id;
    }
}

public interface IVisitor
{
    void Visit(ElementA ElemA);
    void Visit(ElementB ElemB);
}

public class Visitor1 : IVisitor    //(2)
{
    public virtual void Visit(ElementA ElemA)  //(3)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }
    public virtual void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

public class Visitor2 : IVisitor
{
    public virtual void Visit(ElementA ElemA)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }
    public virtual void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

class Client
{
    static void Main(string[] args)
    {
        //--single element-------------------------
        AElement element = new ElementA(1);
        IVisitor visitor = new Visitor1();

        visitor.Visit((dynamic)element); //(1)

        //--collection of elements-----------------
        List<IVisitor> listVis = new List<IVisitor>();
        listVis.Add(new Visitor1());
        listVis.Add(new Visitor2());

        List<AElement> list = new List<AElement>();
        list.Add(new ElementA(2));
        list.Add(new ElementB(3));
        list.Add(new ElementA(4));
        list.Add(new ElementB(5));

        foreach (IVisitor vis in listVis)
            foreach (AElement elem in list)
            {
                vis.Visit((dynamic)elem);
            }

        Console.ReadLine();
    }
}

以下是示例执行的结果:

(1)中,我们有新的调用。由于动态对象如何工作的性质,解决方案被推迟到运行时。然后,我们首先基于Visitor(2)的类型进行动态绑定,然后根据在运行时动态发现的Element类型动态解析到(3)

此解决方案的局限性

作为任何解决方案,这有一些限制/不需要的副作用:

  • 类层次结构ElementsVisitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
  • 请注意,Visitor要访问Element的数据属性ID ,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
  • dynamic对象的使用给我们带来了性能影响。

C#中的访问者模式——第 3 版——反射访问者

访问者模式的反射访问者版本基于使用C#反射技术在运行时发现对象类型并执行基于发现的类型的显式方法分派。我们再次有双重分派,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。

以下是示例代码的类图的样子:

这是这个的代码:

public abstract class AElement
{
}

public class ElementA : AElement
{
    public int Id = 0;

    public ElementA(int Id)
    {
        this.Id = Id;
    }
}

public class ElementB : AElement
{
    public int Id = 0;

    public ElementB(int Id)
    {
        this.Id = Id;
    }
}

public abstract class AVisitor
{
    public void Visit(AElement Elem)  //(2)
    {
        if (Elem is ElementA)
        {
            Visit((ElementA)Elem);
        };
        if (Elem is ElementB)
        {
            Visit((ElementB)Elem);
        };
    }
    public abstract void Visit(ElementA ElemA);
    public abstract void Visit(ElementB ElemB);
}

public class Visitor1 : AVisitor
{
    public override void Visit(ElementA ElemA)  //(3)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }
    public override void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

public class Visitor2 : AVisitor
{
    public override void Visit(ElementA ElemA)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }
    public override void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

class Client
{
    static void Main(string[] args)
    {
        //--single element-------------------------
        AElement element = new ElementA(1);
        AVisitor visitor = new Visitor1();

        visitor.Visit(element); //(1)

        //--collection of elements-----------------
        List<AVisitor> listVis = new List<AVisitor>();
        listVis.Add(new Visitor1());
        listVis.Add(new Visitor2());

        List<AElement> list = new List<AElement>();
        list.Add(new ElementA(2));
        list.Add(new ElementB(3));
        list.Add(new ElementA(4));
        list.Add(new ElementB(5));

        foreach (AVisitor vis in listVis)
            foreach (AElement elem in list)
            {
                vis.Visit(elem);
            }

        Console.ReadLine();
    }
}

以下是示例执行的结果:

(1)中,我们有新的调用。即使在编译时,它也被解析为方法(2)。然后在运行时,使用反射,解析参数类型并将调用传递给(3)

此解决方案的局限性

作为任何解决方案,这有一些限制/不需要的副作用:

  • 类层次结构ElementsVisitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
  • 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
  • 请注意,在(2)中,每个继承自AElement的类都被明确提及并检查类型。缺少某些类型可能是实现的问题。一种可能的解决方案是通过使用反射来发现程序集中的所有类型并自动调度到所有继承自AElement。但是,我们不会在这里这样做。

C#中的访问者模式——第 4 版——反射扩展访问者

访问者模式的Reflective-Extension访问者版本基于:1)使用C#反射技术在运行时发现对象类型,并根据发现的类型执行显式方法分派;2)扩展方法的使用。此版本与“Reflective Visitor”版本非常相似,但由于在其他文献中提到过,我们在此也将其列为单独的变体。我们再次有双重分派,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。

以下是示例代码的类图的样子:

这是此代码:

public abstract class AElement
{
}

public class ElementA : AElement
{
    public int Id = 0;

    public ElementA(int Id)
    {
        this.Id = Id;
    }
}

public class ElementB : AElement
{
    public int Id = 0;

    public ElementB(int Id)
    {
        this.Id = Id;
    }
}

public abstract class AVisitor
{
    public abstract void Visit(ElementA ElemA);
    public abstract void Visit(ElementB ElemB);
}

public static class AVisitorExtensions
{
    public static void Visit<T>(this T vis, AElement Elem)
        where T : AVisitor               //(2)
    {
        if (Elem is ElementA)
        {
            vis.Visit((ElementA)Elem);    //(3)
        };
        if (Elem is ElementB)
        {
            vis.Visit((ElementB)Elem);
        };
    }
}

public class Visitor1 : AVisitor
{
    public override void Visit(ElementA ElemA)  //(4)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }
    public override void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

public class Visitor2 : AVisitor
{
    public override void Visit(ElementA ElemA)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }
    public override void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

class Client
{
    static void Main(string[] args)
    {
        //--single element-------------------------
        AElement element = new ElementA(1);
        AVisitor visitor = new Visitor1();

        visitor.Visit(element);      //(1)

        //--collection of elements-----------------
        List<AVisitor> listVis = new List<AVisitor>();
        listVis.Add(new Visitor1());
        listVis.Add(new Visitor2());

        List<AElement> list = new List<AElement>();
        list.Add(new ElementA(2));
        list.Add(new ElementB(3));
        list.Add(new ElementA(4));
        list.Add(new ElementB(5));

        foreach (AVisitor vis in listVis)
            foreach (AElement elem in list)
            {
                vis.Visit(elem);
            }

        Console.ReadLine();
    }
}

以下是示例执行的结果:

(1)中,我们有新的调用。即使在编译时,它也被解析为方法(2)。然后在运行时,使用反射,参数类型在(3)中解析,调用传递给(4)。

此解决方案的局限性

作为任何解决方案,这有一些限制/不需要的副作用:

  • 类层次结构ElementsVisitor之间有很强的循环依赖性。如果需要经常更新层次结构,这可能是一个问题。
  • 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。
  • 请注意,在(2)中,每个继承自AElement的类都被明确提及并检查类型。缺少某些类型可能是实现的问题。一种可能的解决方案是使用反射发现程序集中的所有类型,并自动分派给所有继承自AElement。但是,我们不会在这里这样做。

C# 中的访问者模式——第 5 版——泛型访问者

访问者模式的泛型访问者版本类似于反射访问者模式,因为它依赖于1)反射在运行时动态发现类型;2) C#泛型来指定接口Visitor实现。我们再次有双重分派,因为我们是根据两个对象的类型分派到具体方法,只是使用的语言机制不同。

以下是示例代码的类图的样子:

这是此的代码:

public abstract class Element
{
    public abstract void Accept(IVisitor visitor);
}

public class ElementA : Element
{
    public int Id = 0;

    public ElementA(int Id)
    {
        this.Id = Id;
    }

    public override void Accept(IVisitor visitor)     //(2)
    {
        if (visitor is IVisitor<ElementA>)
        {
            ((IVisitor<ElementA>)visitor).Visit(this);
        }
    }
}

public class ElementB : Element
{
    public int Id = 0;

    public ElementB(int Id)
    {
        this.Id = Id;
    }

    public override void Accept(IVisitor visitor)     
    {
        if (visitor is IVisitor<ElementB>)
        {
            ((IVisitor<ElementB>)visitor).Visit(this);
        }
    }
}

public interface IVisitor { }; // marker interface

public interface IVisitor<TVisitable>
{
    void Visit(TVisitable obj);
}

public class Visitor1 : IVisitor,
            IVisitor<ElementA>, IVisitor<ElementB>
{
    public void Visit(ElementA ElemA)   //(3)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }

    public void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

public class Visitor2 : IVisitor,
            IVisitor<ElementA>, IVisitor<ElementB>
{
    public void Visit(ElementA ElemA)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemA.GetType().Name, ElemA.Id, this.GetType().Name);
    }

    public void Visit(ElementB ElemB)
    {
        Console.WriteLine("{0} with Id={1} visited by {2}",
            ElemB.GetType().Name, ElemB.Id, this.GetType().Name);
    }
}

class Client
{
    static void Main(string[] args)
    {
        //--single element, base class call-----------------------
        Element element = new ElementA(1);
        IVisitor visitor = new Visitor1();

        element.Accept(visitor);  //(1)

        //--collection of elements-----------------
        List<IVisitor> listVis = new List<IVisitor>();
        listVis.Add(new Visitor1());
        listVis.Add(new Visitor2());

        List<Element> list = new List<Element>();
        list.Add(new ElementA(2));
        list.Add(new ElementB(3));
        list.Add(new ElementA(4));
        list.Add(new ElementB(5));

        foreach (IVisitor vis in listVis)
            foreach (Element elem in list)
            {
                elem.Accept(vis);
            }

        Console.ReadLine();
    }
}

以下是示例执行的结果:

(1)中,我们有一个新的调用。在运行时,它动态绑定到(2)。然后在(2)中,我们使用反射将其显式解析为(3)

此解决方案的局限性

作为任何解决方案,这有一些限制/不需要的副作用:

  • 请注意,Visitor要访问Element的数据属性Id,该属性必须是public。这有点打破了封装原则。例如,在我们的第一个解决方案类图Picture1-1中,方法V1()可以对Element类的private成员进行操作。

结论

首先,我们讨论了我们的动机和我们试图解决的问题。我们正在努力实现的目标很重要,因为解决问题的方法可能不止一种。

然后我们展示了经典访客,这是由GoF提出并在文献中经常提到的版本。我认为由于创建它时语言(C++Smalltalk)的限制,这被提议为唯一和最终的解决方案。

现代OO语言,如C#,具有动态对象反射等新功能,可以通过不同的方式实现相同的目标。这在访问者模式的其他四个版本中得到了展示。如果您愿意,可以将它们视为启用现代C#”的访问者模式的替代版本。

https://www.codeproject.com/Articles/5326263/Visitor-Pattern-in-Csharp-5-Versions

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
原型模式是一种创建型设计模式,其提供了一种复制已有对象的方法来生成新对象的能力,而不必通过实例化的方式来创建对象。原型模式是通过克隆(浅复制或深复制)已有对象来创建新对象的,从而可以避免对象创建时的复杂过程。 在C#,可以通过实现ICloneable接口来实现原型模式。ICloneable接口定义了Clone方法,该方法用于复制当前对象并返回一个新对象。需要注意的是,Clone方法返回的是Object类型,需要进行强制类型转换才能得到复制后的对象。 以下是一个简单的示例代码: ```csharp public class Person : ICloneable { public string Name { get; set; } public int Age { get; set; } public object Clone() { return MemberwiseClone(); } } // 使用示例 var person1 = new Person { Name = "Tom", Age = 20 }; var person2 = (Person)person1.Clone(); person2.Name = "Jerry"; Console.WriteLine(person1.Name); // 输出 "Tom" Console.WriteLine(person2.Name); // 输出 "Jerry" ``` 在上面的示例代码,实现了一个Person类,并实现了ICloneable接口的Clone方法来实现原型模式。复制对象时,使用MemberwiseClone方法进行浅复制,即只复制值类型的字段和引用类型字段的引用,而不复制引用类型字段所引用的对象。在使用示例,首先创建一个Person对象person1,然后通过Clone方法复制一个新的对象person2,修改person2的Name属性后,输出person1和person2的Name属性,可以看到person1的Name属性并没有改变,说明person2是一个全新的对象。 需要注意的是,如果要实现深复制,即复制引用类型字段所引用的对象,需要在Clone方法手动将引用类型字段复制一份。另外,使用原型模式时,需要注意复制后的对象和原对象之间的关系,如果复制后的对象修改了原对象的状态,可能会对系统产生意想不到的影响。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值