重构项目为taf_为所有人重构

为什么要重构?

重构是在不更改程序功能的情况下更改程序的结构。 重构是一项强大的技术,但需要谨慎执行。 主要危险是可能会无意中引入错误,尤其是在手工进行重构时。 这种危险导致对重构的普遍批评:为什么不破坏代码就修复代码?

您可能需要重构代码的原因有很多。 第一个是传说中的东西:古老产品的旧版本代码库被继承,或者以其他方式神秘地出现。 原来的开发团队已经消失了。 必须创建具有新功能的新版本,但是代码不再容易理解。 新的开发团队昼夜不停地工作,将其解密,映射,经过大量规划和设计后,将代码撕成碎片。 最终,他们根据新的愿景,将所有工作都重新进行了艰苦的努力。 这是在英勇的规模上重构的,很少有人活着讲述这个故事。

更为现实的情况是,将新要求引入到需要更改设计的项目中。 引入此需求是无关紧要的,这是由于原始计划中的疏忽大意还是由于使用了迭代方法(例如敏捷或测试驱动的开发)而故意在整个开发过程中引入了需求。 这是在较小的规模上进行重构的,它通常涉及更改类层次结构,可能是通过引入接口或抽象类,拆分类,重新排列类等等。

使用自动重构工具时,重构的最后一个原因只是作为生成代码的捷径-就像在不确定单词的拼写方式时使用拼写检查器为您键入单词。 重构的平常使用-例如,生成getter和setter方法-可以在您熟悉工具后节省大量时间。

Eclipse的重构工具并非旨在用于大规模的重构-很少有工具-但是它们对于在普通程序员一天中进行代码更改(无论是否涉及敏捷开发技术)都具有巨大的价值。 毕竟,任何可以自动化的复杂操作都是可以避免的乏味。 知道Eclipse提供了哪些重构工具,并了解它们的预期用途,将极大地提高您的生产率。

有两种重要的方法可以减少破坏代码的风险。 一种方法是对代码进行一套完整的单元测试:代码应在重构前后通过测试。 第二种方法是使用自动化工具(例如Eclipse的重构功能)来执行此重构。

全面测试和自动重构的结合特别强大,并将这种曾经神秘的艺术转变为有用的日常工具。 这种能够快速,安全地更改代码结构而又不改变其功能,以增加功能或改善其可维护性的能力会极大地影响您设计和开发代码的方式,无论是否将其纳入正式的敏捷方法中。

Eclipse中的重构类型

Eclipse的重构工具可以分为三大类(这是它们在“重构”菜单中的显示顺序):

  1. 更改代码的名称和物理组织,包括重命名字段,变量,类和接口,以及移动包和类
  2. 在类级别更改代码的逻辑组织,包括将匿名类转换为嵌套类,将嵌套类转换为顶级类,从具体类创建接口,以及将方法或字段从类移至子类或超类
  3. 更改类中的代码,包括将局部变量转换为类字段,将方法中的选定代码转换为单独的方法以及为字段生成getter和setter方法

几种重构并不完全适合这三个类别,尤其是“更改方法签名”,此处包含在第三类中。 除了这个例外,接下来的部分将按顺序讨论Eclipse的重构工具。

物理重组和重命名

您显然可以在没有特殊工具的情况下在文件系统中重命名或移动文件,但是对于Java源文件,这样做可能需要编辑许多文件才能更新importpackage语句。 同样,您可以使用文本编辑器的搜索和替换功能轻松地重命名类,方法和变量,但是您必须谨慎进行,因为不同的类可能具有同名的方法或变量。 检查项目中的所有文件以确保正确标识和更改每个实例可能很繁琐。

Eclipse的Rename and Move能够在整个项目中智能地进行这些更改,而无需用户干预,因为Eclipse可以从语义上理解代码,并且能够标识对特定方法,变量或类名的引用。 简化此任务有助于确保方法,变量和类名清楚地表明其意图。

查找名称不当或具有误导性的代码是很常见的,因为代码已更改为与最初计划的工作方式不同。 例如,可以通过使用URL类获取InputStream来扩展在文件中查找特定单词的程序,以与Web页面一起使用。 如果此输入流最初被称为file ,则应对其进行更改以反映其新的更一般的性质,也许改为sourceStream 。 开发人员通常无法进行这样的更改,因为它可能是一个混乱而乏味的过程。 当然,这会使代码使下一个必须使用它的开发人员感到困惑。

要重命名Java元素,只需在Package Explorer视图中单击它,或在Java源文件中选择它,然后选择Refactor> Rename 。 在对话框中,选择新名称,然后选择Eclipse是否也应更改对该名称的引用。 显示的确切字段取决于您选择的元素的类型。 例如,如果选择具有getter和setter方法的字段,则还可以更新这些方法的名称以反映新字段。 图1显示了一个简单的示例。

图1.重命名局部变量
重命名局部变量

像所有Eclipse重构一样,在指定执行重构所需的所有内容之后,可以按Preview在一个比较对话框中查看Eclipse建议进行的更改,该对话框使您可以分别否决或批准每个受影响的文件中的每个更改。 如果您对Eclipse正确执行更改的能力有信心,则可以按OK 。 显然,如果不确定重构将执行什么操作,则需要先预览,但是对于诸如“重命名”和“移动”之类的简单重构,通常不需要这样做。

Move的工作方式与重命名非常相似:您选择一个Java元素(通常是一个类),指定其新位置,并指定是否还应更新引用。 然后,您可以选择“ 预览”以检查更改,或者选择“ 确定”以立即进行重构,如图2所示。

图2.将一个类从一个包移动到另一个包
上课

在某些平台上(尤其是Windows),您还可以通过将类从一个包或文件夹中移动到另一包或文件夹中,只需将它们拖放到Package Explorer视图中即可。 所有参考将自动更新。

重新定义类关系

大量的Eclipse重构使您可以自动更改类关系。 这些重构通常不像Eclipse必须提供的其他类型的重构那样有用,而是有用的,因为它们执行相当复杂的任务。 当它们有用时,它们将非常有用。

促进匿名和嵌套类

两种重构(将匿名类转换为嵌套)和将嵌套类型转换为顶层是相似的,因为它们将类从当前范围移到范围范围内。

匿名类是一种语法简写,可让您实例化一个类,以在需要时实现抽象类或接口,而不必显式为其指定类名。 例如,当在用户界面中创建侦听器时,通常使用此方法。 在清单1中,假定Bag是在其他地方定义的接口,该接口声明两个方法get()set()

清单1. Bag类
public class BagExample
{
   void processMessage(String msg)
   {
      Bag bag = new Bag()
      {
         Object o;
         public Object get()
         {
            return o;
         }
         public void set(Object o)
         {
            this.o = o;
         }
      };
      bag.set(msg);
      MessagePipe pipe = new MessagePipe();
      pipe.send(bag);
   }
}

当匿名类太大而导致代码难以阅读时,您应该考虑使匿名类成为适当的类; 为了保留封装(换句话说,将其隐藏在不需要了解的外部类中),您应该将其设置为嵌套类,而不是顶级类。 您可以通过在匿名类内部单击并选择Refactor> Convert Anonymous Class to Nested来实现 。 在提示时输入类的名称,例如BagImpl ,然后选择PreviewOK 。 这将更改代码,如清单2所示。

清单2.重构Bag类
public class BagExample
{
   private final class BagImpl implements Bag
   {
      Object o;
      public Object get()
      {
         return o;
      }
      public void set(Object o)
      {
         this.o = o;
      }
   }
       
   void processMessage(String msg)
   {
     Bag bag = new BagImpl();
     bag.set(msg);
     MessagePipe pipe = new MessagePipe();
     pipe.send(bag);
   }
}

当您想使嵌套类可用于其他类时,将嵌套类型转换为顶层非常有用。 例如,您可能在类内使用值对象-例如上面的BagImpl类。 如果您以后决定该数据应在类之间共享,则此重构将从嵌套的类创建一个新的类文件。 您可以通过在源文件中突出显示类名(或在“大纲”视图中单击类名),然后选择“ 重构”>“将嵌套类型转换为顶级”来做到这一点。

此重构将要求您提供封闭实例的名称。 它可能会提供一些建议,例如example ,您可以接受。 稍后将清楚其含义。 按下OK之后 ,将更改封闭的BagExample类的代码,如清单3所示。

清单3.重构Bag类
public class BagExample
{
   void processMessage(String msg)
   {
      Bag bag = new BagImpl(this);
      bag.set(msg);
      MessagePipe pipe = new MessagePipe();
      pipe.send(bag);
   }
}

请注意,当嵌套一个类时,它可以访问外部类的成员。 为了保留此功能,重构会将封闭类BagExample的实例添加到以前嵌套的类中。 这是以前要求您提供名称的实例变量。 它还创建一个设置此实例变量的构造函数。 重构创建的新BagImpl类如清单4所示。

清单4. BagImpl类
final class BagImpl implements Bag
{
   private final BagExample example;
   /**
    * @paramBagExample
    */
  BagImpl(BagExample example)
   {
      this.example = example;
      // TODO Auto-generated constructor stub
   }
   Object o;
   public Object get()
   {
      return o;
   }
   public void set(Object o)
   {
      this.o = o;
   }
}

如果不需要保留对BagExample类的访问(如此处的情况),则可以安全地删除实例变量和构造函数,并将BagExample类中的代码更改为默认的无参数构造函数。

类层次结构内的移动成员

其他两个重构,下推和上拉,分别将类方法或字段从类移至其子类或超类。 假设您有一个抽象类Vehicle ,如清单5所示。

清单5.抽象的Vehicle类
public abstract class Vehicle
{
   protected int passengers;
   protected String motor;
   
   public int getPassengers()
   {
      return passengers;
   }
   public void setPassengers(int i)
   {
      passengers = i;
   }
   public String getMotor()
   {
      return motor;
   }
   public void setMotor(String string)
   {
      motor = string;
   }
}

你也有一个子类, Vehicle被称为Automobile如清单6所示。

清单6.汽车类
public class Automobile extends Vehicle
{
   private String make;
   private String model;
   public String getMake()
   {
      return make;
   }
   public String getModel()
   {
      return model;
   }
   public void setMake(String string)
   {
      make = string;
   }
   public void setModel(String string)
   {
      model = string;
   }
}

注意, Vehicle的属性之一是motor 。 如果您知道自己只会处理机动车辆,那么这很好,但是如果您想允许划艇之类的事情,您可能希望将motor属性从Vehicle类下推到Automobile类。 为此,请在“大纲”视图中选择motor ,然后选择“ 重构”>“下推”

Eclipse足够聪明,可以意识到您不能总是自己移动字段,并且提供了Add Required按钮,但这在Eclipse 2.1中并不总是能够正常工作。 您需要验证是否也下推了依赖此字段的所有方法。 在这种情况下,有两种方法,即getter和setter方法与motor磁场一起出现,如图3所示。

图3.添加所需的成员
添加所需的成员

按下OK后motor字段以及getMotor()setMotor()方法将移至Automobile类。 清单7显示了此重构后的Automobile类的外观。

清单7.重构汽车类
public class Automobile extends Vehicle
{
   private String make;
   private String model;
   protected String motor;
   public String getMake()
   {
      return make;
   }
   public String getModel()
   {
      return model;
   }
   public void setMake(String string)
   {
      make = string;
   }
   public void setModel(String string)
   {
      model = string;
   }
   public String getMotor()
   {
      return motor;
   }
   public void setMotor(String string)
   {
      motor = string;
   }
}

Pull Up重构几乎与Push down相同,除了它将类成员从类移到其超类而不是子类之外。 如果您以后改变主意并决定将motor移回Vehicle类,则可以使用此方法。 有关确保选择所有必需成员的警告同样适用。

Automobile类中具有motor意味着,如果您创建Vehicle其他子类(例如Bus ,则也需要将motor (及其相关方法)添加到Bus类中。 表示这种关系的一个方法是创建一个接口, Motorized ,其中AutomobileBus会实现,但RowBoat不会。

创建Motorized接口的最简单方法是在Automobile上使用提取接口重构。 为此,请在“大纲”视图中选择“ Automobile类,然后从菜单中选择“ 重构”>“提取接口 ”。 该对话框将允许您选择要包含在界面中的方法,如图4所示。

图4.提取电动接口
电动接口

选择OK之后 ,将创建一个接口,如清单8所示。

清单8.电动接口
public interface Motorized
{
   public abstract String getMotor();
   public abstract void setMotor(String string);
}

并且对Automobile的类声明进行了如下更改:

public class Automobile extends Vehicle implements Motorized

使用超类型

此类别中包括的最终重构是“尽可能使用超类型”。 考虑一个管理汽车库存的应用程序。 在整个过程中,它使用类型为Automobile对象。 如果您希望能够处理所有类型的车辆,则可以使用此重构将对Automobile的引用更改为对Vehicle的引用(请参见图5)。 如果使用instanceof运算符在代码中执行任何类型检查,则需要确定使用特定类型还是超类型是否合适,并检查第一个选项,即在'instanceof'表达式中适当使用选定的超类型。

图5.将汽车更改为超型汽车
超型

在Java语言中经常需要使用超类型,尤其是在使用Factory Method模式时。 通常,这是通过具有一个抽象类来实现的,该抽象类具有一个静态create()方法,该方法返回实现该抽象类的具体对象。 如果必须创建的具体对象的类型取决于客户端类不关心的实现细节,则这很有用。

在类中更改代码

重构的最大种类是重构类中的代码的重构。 其中,这些功能使您可以引入(或删除)中间变量,从旧变量的一部分创建新方法,以及为字段创建getter和setter方法。

提取和内联

有几个以单词Extract开头的重构:Extract Method,Extract Local Variable和Extract Constants。 如您所料,第一个提取方法将根据您选择的代码创建一个新方法。 例如,清单8中的类中的main()方法。它评估命令行选项,如果找到以-D开头的任何选项,则将它们作为名称/值对存储在Properties对象中。

清单8. main()
import java.util.Properties;
import java.util.StringTokenizer;
public class StartApp
{
   public static void main(String[] args)
   {
      Properties props = new Properties();
      for (int i= 0; i < args.length; i++)
      {
         if(args[i].startsWith("-D"))
         {
           String s = args[i].substring(2);
           StringTokenizer st = new StringTokenizer(s, "=");
            if(st.countTokens() == 2)
            {
              props.setProperty(st.nextToken(), st.nextToken());
            }
         }
      }
      //continue...
   }
}

在两种主要情况下,您可能想从方法中取出一些代码,然后将其放入另一个方法中。 第一种情况是该方法过长并且执行两个或多个逻辑上不同的操作。 (我们不知道main()方法还有什么作用,但是从我们在这里看到的证据来看,这并不是在这里提取方法的原因。)第二种情况是,如果有一个逻辑上截然不同的代码段可以通过其他方法重复使用。 例如,有时您会发现自己以几种不同的方法重复了几行代码。 在这种情况下,这是有可能的,但是您可能不会执行此重构,直到您真正需要重用此代码为止。

假设还有另一个地方需要解析名称/值对并将它们添加到Properties对象,则可以提取代码部分,其中包括StringTokenizer声明和if子句。 为此,突出显示此代码,然后从菜单中选择“ 重构”>“提取方法 ”。 系统将提示您输入方法名称; 输入addProperty ,然后确认该方法具有两个参数, Properties propStrings 。 清单9显示了Eclipse提取方法addProp()之后的类。

清单9.提取的addProp()
import java.util.Properties;
import java.util.StringTokenizer;
public class Extract
{
   public static void main(String[] args)
   {
      Properties props = new Properties();
      for (int i = 0; i < args.length; i++)
      {
         if (args[i].startsWith("-D"))
         {
            String s = args[i].substring(2);
            addProp(props, s);
         }
      }
   }
   private static void addProp(Properties props, String s)
   {
      StringTokenizer st = new StringTokenizer(s, "=");
      if (st.countTokens() == 2)
      {
         props.setProperty(st.nextToken(), st.nextToken());
      }
   }
}

提取局部变量重构采用直接使用的表达式,然后将其首先分配给局部变量。 然后,在以前的表达式所在的位置使用此变量。 例如,在上面的addProp()方法中,您可以突出显示对st.nextToken()的首次调用,然后选择Refactor> Extract Local Variable 。 系统将提示您提供一个变量。 输入key 。 注意,有一个选项可以用对新变量的引用替换所有出现的所选表达式。 这通常是适当的,但在nextToken()方法的情况下不适用,该方法(显然)每次调用时都会返回不同的值。 确保未选择此选项; 参见图6。

图6.不要替换所有出现的选定表达式
提取变量

接下来,对第二次调用st.nextToken()重复此重构,这次调用新的局部变量value 。 清单10显示了这两个重构之后的代码。

清单10.重构的代码
private static void addProp(Properties props, String s)
   {
     StringTokenizer st = new StringTokenizer(s, "=");
      if(st.countTokens() == 2)
      {
         String key = st.nextToken();
         String value = st.nextToken();
        props.setProperty(key, value);
      }
   }

以这种方式引入变量有几个好处。 首先,通过为表达式提供有意义的名称,它使代码正在执行的工作变得明确。 其次,它使调试代码变得更加容易,因为我们可以轻松地检查表达式返回的值。 最后,在一个表达式的多个实例可以用一个变量替换的情况下,这样做会更有效。

提取常量类似于提取局部变量,但是您必须选择一个静态的常量表达式,重构会将其转换为静态的最终常量。 这对于从代码中删除硬编码的数字和字符串很有用。 例如,在上面的代码中,我们使用-D"作为定义名称/值对的命令行选项。在代码中突出显示-D" ,选择Refactor> Extract Constant ,然后输入DEFINE作为常量的名称。 此重构将更改代码,如清单11所示。

清单11.重构代码
public class Extract
{
   private static final String DEFINE = "-D";
   public static void main(String[] args)
   {
      Properties props = new Properties();
      for (int i = 0; i < args.length; i++)
      {
         if (args[i].startsWith(DEFINE))
         {
            String s = args[i].substring(2);
            addProp(props, s);
         }
      }
   }
   // ...

对于每个Extract ...重构,都有一个对应的Inline ...重构执行相反的操作。 例如,如果您在上面的代码中突出显示变量s,请选择Refactor> Inline ... ,然后按OK ,Eclipse会在对addProp()的调用中直接使用表达式args[i].substring(2) addProp() ,如下所示:

if(args[i].startsWith(DEFINE))
         {
            addProp(props,args[i].substring(2));
         }

这可能比使用临时变量略有效率,并且通过使代码更简洁,根据您的观点,它要么更易于阅读,要么变得更加晦涩难懂。 但是,通常来说,这样的内联没有太多建议。

与可以使用内联表达式替换变量的方式相同,您还可以突出显示方法名称或静态最终常量。 从菜单中选择Refactor> Inline ... ,然后Eclipse将用方法代码替换方法调用,或者用常量值分别替换对常量的引用。

封装字段

通常,不建议公开对象的内部结构。 这就是为什么Vehicle类及其子类具有私有或受保护的字段,以及提供访问权限的公共setter和getter方法的原因。 这些方法可以两种不同的方式自动生成。

生成这些方法的一种方法是使用Source> Generate Getter and Setter 。 这将显示一个对话框,其中包含尚未使用的每个字段的建议的getter和setter方法。 但是,这不是重构,因为它不会更新对字段的引用以使用新方法。 必要时您需要自己做。 此选项可节省大量时间,但最适合在最初创建类或向类中添加新字段时使用,因为尚无其他代码引用这些字段,因此无需更改其他代码。

生成getter和setter方法的第二种方法是选择字段,然后从菜单中选择“ 重构”>“封装字段 ”。 该方法一次只为单个字段生成getter和setter,但是与Source> Generate Getter and Setter相比 ,它还将对该字段的引用更改为对新方法的调用。

例如,从新的,简单的Automobile类版本开始,如清单12所示。

清单12.简单的汽车类
public class Automobile extends Vehicle
{
   public String make;
   public String model;
}

接下来,创建一个实例化Automobile并直接访问make字段的类,如清单13所示。

清单13.实例化汽车
public class AutomobileTest
{
   public void race()
   {
      Automobilecar1 = new Automobile();
      car1.make= "Austin Healy";
      car1.model= "Sprite";
      // ...
   }
}

现在,通过突出显示字段名称并选择Refactor> Encapsulate Field来封装make字段。 在对话框中,输入getter和setter方法的名称-如您所料,默认情况下为getMake()setMake() 。 您还可以选择与该字段位于同一类中的方法将继续直接访问该字段,还是选择将这些引用更改为像所有其他类一样使用访问方法。 (有些人对某一种方式有强烈的偏爱,但是碰巧的是,在这种情况下选择什么都没关系,因为在Automobile中没有提及make字段)。 参见图7。

图7.封装一个字段
封装字段

按下OK之后Automobile类中的make字段将变为私有字段,并将具有getMake()setMake()方法,如清单14所示。

清单14.重构汽车类
public class Automobile extends Vehicle
{
   private String make;
   public String model;

   public void setMake(String make)
   {
      this.make = make;
   }

   public String getMake()
   {
      return make;
   }
}

AutomobileTest类也将更新为使用新的访问方法,如清单15所示。

清单15. AutomobileTest类
public class AutomobileTest
{
   public void race()
   {
      Automobilecar1 = new Automobile();
      car1.setMake("Austin Healy");
      car1.model= "Sprite";
      // ...
   }
}

更改方法签名

这里考虑的最终重构是最难使用的:更改方法签名。 这样做很明显-更改了方法的参数,可见性和返回类型。 这些更改对方法或调用该方法的代码的影响并不那么明显。 这里没有魔术。 如果更改导致重构方法出现问题-因为它留下未定义的变量或类型不匹配-重构操作将标记这些。 您可以选择接受重构,然后再纠正问题,或者取消重构。 如果重构在其他方法中引起问题,则这些问题将被忽略,并且重构后必须自己修复。

为了澄清这一点,请考虑清单16中的以下类和方法。

清单16. MethodSigExample类
public class MethodSigExample
{
   public int test(String s, int i)
   {
      int x = i + s.length();
      return x;
   }
}

上一个类中的方法test()由另一个类中的方法调用,如清单17所示。

清单17. callTest方法
public void callTest()
   {
     MethodSigExample eg = new MethodSigExample();
     int r = eg.test("hello", 10);
   }

在第一堂课中突出显示test ,然后选择“ 重构”>“更改方法签名” 。 将会出现图8中的对话框。

图8.更改方法签名选项
更改方法签名选项

第一种选择是更改方法的可见性。 在此示例中,将其更改为protected或private将阻止第二个类中的callTest()方法访问。 (如果它们在单独的软件包中,则更改对默认访问的权限也会导致此问题。)Eclipse在执行重构时不会标记此错误; 由您自己选择合适的值。

下一个选项是更改返回类型。 例如,将返回类型更改为float不会被标记为错误,因为test()方法的return语句中的int自动提升为float 。 尽管如此,这将导致第二类的callTest()出现问题,因为无法将float转换为int 。 您将需要将test()返回的返回值转换为int或将callTest()r类型更改为float

如果将第一个参数的类型从String更改为int则类似的考虑也适用。 这将在重构期间进行标记,因为它在重构的方法中引起问题: int没有length()方法。 但是,将其更改为StringBuffer不会被标记为问题,因为它确实具有length()方法。 当然,这将在callTest()方法中引起问题,因为它在调用test()时仍在传递String

如前所述,在这种重构导致错误的情况下,无论是否已标记,您都可以通过简单地逐案纠正错误来继续操作。 另一种方法是抢占错误。 如果要删除参数i ,因为它是不必要的,则可以从在重构方法中删除对其的引用开始。 删除参数将更加顺畅。

最后要说明的一件事是“默认值”选项。 仅在将参数添加到方法签名时使用。 当参数添加到调用方时,它用于提供一个值。 例如,如果我们添加一个类型为String的参数,名称为n ,默认值为world ,则callTest()方法中对test()的调用callTest()改为:

public void callTest()
   {
      MethodSigExample eg = new MethodSigExample();
      int r = eg.test("hello", 10, "world");
   }

摆脱这种关于“更改方法签名”重构的看似可怕的讨论的目的不是要有问题,而是要它是一种功能强大且节省时间的重构,通常需要进行周密的计划才能成功使用。

摘要

Eclipse的工具使重构变得容易,熟悉它们可以帮助您提高生产率。 迭代开发程序功能的敏捷开发方法依赖于重构,作为一种更改和扩展程序设计的技术。 但是,即使您没有使用需要重构的正式方法,Eclipse的重构工具也提供了节省时间的方式来进行常见类型的代码更改。 花一些时间来熟悉它们,以便您了解可以应用它们的情况,这是您宝贵的时间。


翻译自: https://www.ibm.com/developerworks/java/library/os-ecref/index.html

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值