C# 学习(二)基础语法

一、 基本语法

使用下面这段代码作为例子:

using System;
namespace RectangleApplication
{
    class Rectangle
    {
        // 成员变量
        double length;
        double width;
        public void Acceptdetails()
        {
            length = 4.5;    
            width = 3.5;
        }
        public double GetArea()
        {
            return length * width;
        }
        public void Display()
        {
            Console.WriteLine("Length: {0}", length);
            Console.WriteLine("Width: {0}", width);
            Console.WriteLine("Area: {0}", GetArea());
        }
    }
   
    class ExecuteRectangle
    {
        static void Main(string[] args)
        {
            Rectangle r = new Rectangle();
            r.Acceptdetails();
            r.Display();
            Console.ReadLine();
        }
    }
}

1.1 using 关键字

任何 C# 代码的开端,都是通过 using 引入本文件需要的命名空间, 就是导包

1.2 class 关键字

通过使用 class 来声明一个类

1.3 注释

注释可以分为单行注释和多行注释:

  • 单行注释
    通过 // 进行注释
  • 多行注释
    通过下面的方式:
/**
 * 这里是注释
 * /

1.4 实例化

我们通过 new Rectangle() 来实例化一个矩形类, 因此和 java 一样,都是通过 new 来实例化。

1.5 成员变量和成员函数

lengthwidth 是类 Rectangle 的成员变量。
GetAreaDisplay 是类 Rectangle 的成员函数。

1.6 标识符

标识符是分配给类型(类、接口、结构、记录、委托或枚举)、成员、变量或命名空间的名称。

在 C# 中,它们的命名必须遵循如下基本规则:

  • 必须以字母、下划线或 @ 开头,后面可以跟一系列的字母、数字( 0 - 9 )、下划线( _ )、@。
  • 第一个字符不能是数字。
  • 不能包含任何嵌入的空格或符号,比如 ? - +! # % ^ & * ( ) [ ] { } . ; : " ’ / \。
  • 不能是 C# 关键字,除非它们有一个 @ 前缀。 例如,@if 是有效的标识符,但 if 不是,因为 if 是关键字。
  • 必须区分大小写。大写字母和小写字母被认为是不同的字母。
  • 不能与C#的类库名称相同。

1.7 关键字

C# 保留着两种关键字:

  • 保留关键字
    静态的关键字,如 privateabstract 等这种修饰关键字
  • 上下文关键字
    在上下文中有特殊意义,如 getset,以后会学习到
    在这里插入图片描述

二、 数据类型

在 C# 中,有以下三种数据类型:

  • 值类型(Value types)
  • 引用类型(Reference types)
  • 指针类型(Pointer types)

2.1 值类型(也就是基本数据类型)

值类型可以直接进行值的分配,这些值都是从 System.ValueType 中派生出来的。这些值类型存储一个数据,这些数据有浮点数、整型、字符等。

下表为 C# 里的可用值类型:
在这里插入图片描述

我们可以通过 sizeOf 来打印这些类型在对应机器/平台上所占字节:

static void Main(string[] args)
{
    Console.WriteLine("Size of int: {0}", sizeof(int));
}

// 打印: Size of int: 4, 说明 int 在平台上占 4 个字节

2.2 变量引用

引用类型不包含实际的数据,只包含对变量的引用。它有三种: objectstringdynamic

a. object / Object

System.Object 类型是 C# 的最顶层基类,所以对象类型可以分配以任意的类型。 不过在分配之前,可能会有装箱拆箱的阶段。

  • 装箱
    将值类型(基本数据类型) 转化为对象类型
  • 拆箱
    将对象类型转化为值类型

如下面这段代码:

Object obj;   // 也可以声明成: object obj
obj = 100;    // 将 100 的 int 转化为一个对象

我们可以打印下 obj 最终的类型:

Console.WriteLine("type of obj: {0}", obj.GetType());

// 打印结果:type of obj: System.Int32

b. String

System.String 允许我们给其分配任意字符串数据,它是从 Object 派生出来的,其值可以通过两种形式进行分配:

  • 引号
String str = "runoob.com";
  • @修饰
@"runoob.com";

第二种的使用场景在对字符串进行转义时,会更加方便,例如下面代码:

string str = @"C:\Windows";

等价于:

string str = "C:\\Windows";

并且 @字符串中可以任意换行,换行符及缩进空格都计算在字符串长度之内:

string str = @"<script type=""text/javascript"">
    <!--
    -->
</script>";

c. dynamic

动态类型可以让我们直接存储任意值在该类型变量中,它会等到编译期才会去检查这个类型,同时我们也可以改变它:

static void Main(string[] args)
{
    dynamic str = "aa";
    Console.WriteLine("get str type {0}", str.GetType());
    str = 12;
    Console.WriteLine("get str type {0}", str.GetType());
}

// 打印:
get str type System.String
get str type System.Int32

看起来这是一个比较便利的赋值方式,但感觉会破坏代码的可读性。

2.3 指针引用

和 C、C++ 一样,C# 中的指针也是存储另一种类型的内存地址。

声明指针类型的语法:

type* identifier;

如:

char* cptr;   // 指向一个字符的内存位置
int* iptr;    // 指向一个整型的内存位置

三、 类型转化

C# 的类型分为 显示转化隐式转化

3.1 隐式类型转化

隐式转换是指:将一个较小范围的数据类型转换为较大范围的数据类型时,编译器会自动完成类型转换

这些转换是 C# 默认的安全方式进行的转换, 不会导致数据丢失。

例如:

  • 从小的整数类型转换为大的整数类型
  • 从派生类转换为基类

下面代码中,将一个 byte 类型(1个字节)的变量赋值给 int 类型(4个字节)的变量,编译器会自动将 byte 类型转换为 int 类型,不需要显示转换:

byte b = 10;
int i = b; // 隐式转换

3.2 显示类型转化(强制转化)

在代码上用显示的关键字将类型进行转化。

显式转换是指:将一个较大范围的数据类型转换为较小范围的数据类型时,或者将一个对象类型转换为另一个对象类型时,需要使用强制类型转换符号进行显示转换,强制转换会造成数据丢失

例如,将一个 int 类型的变量赋值给 byte 类型的变量,需要显示转换。

int i = 10;
byte b = (byte)i;   // 使用括号进行强转

下面是一个强转类的例子:

using System;

namespace TypeConversionApplication
{
    class ExplicitConversion
    {
        static void Main(string[] args)
        {
            double d = 5673.74;
            int i;

            // 强制转换 double 为 int
            i = (int)d;
            Console.WriteLine(i);
            Console.ReadKey();
           
        }
    }
}

// 将打印 5673

C# 内置了一些转化函数,我们可以通过函数去进行强转:
在这里插入图片描述
如下代码所示:

int i = 75;
float f = 53.005f;
double d = 2345.7652;
bool b = true;
Console.WriteLine(i.ToString());
Console.WriteLine(f.ToString());
Console.WriteLine(d.ToString());
Console.WriteLine(b.ToString());

// 打印:
75
53.005
2345.7652
True

四、 常量

常量是固定值,它可以是任意值类型数据(基本数据类型),还有枚举,例如常见的有:

  • 整数常量
    十进制、八进制、十六进制等整数数据
  • 浮点数常量
    整数、小数和指数所构成的浮点数
  • 字符常量
    单个字符,使用单引号 (‘x’) 进行修饰
  • 字符串常量
    多个字符构成,使用双引号 (“xxx”) 进行修饰
  • 。。 等等其他基本数据类型

通过 const 关键字来修饰一个常量字符:

const int c1 = 5;
const int c2 = c1 + 5;
const string c3 = "rikka";

五、 省略 运算符、判断、循环 章节

这些用法和 java 如出一辙…

六、C# 的封装权限

C# 也有 privatepublicprotectedinternal 来修饰类、函数、变量,但是比较特殊的是它还有一个 protected internal
在这里插入图片描述

  • public
    所有外部对象均可访问
  • internal
    同一个程序集(命名空间、包)的对象可以访问
  • protected
    只有本类和本类的子类可以访问
  • private
    仅本类可以访问
  • protected internal
    protectedinternal 的并集,即 本类、子类、命名空间内可访问。因此它是介于 publicinternal 之间的访问权限

七、方法(method)

C# 定义方法为 method 而不是 function,就说明其入参是可变的~

其定义方法的方式和 java 一致,但不同点是,它的参数传递除了可以传递一般的值,还可以传递引用参数输出参数

方式描述
值参数默认的方式,这种方式下将复制参数的实际值给函数,作为形式参数,实参和形参使用的是两个不同内存中的值。在这种情况下,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。
引用参数这种方式复制参数的内存位置的引用给形式参数。这意味着,当形参的值发生改变时,同时也改变实参的值
输出参数这种方式可以返回多个值。

a. 传递值参数

class NumberManipulator
{
    public void swap(int x, int y)
    {
        int temp;
     
        temp = x; /* 保存 x 的值 */
        x = y;    /* 把 y 赋值给 x */
        y = temp; /* 把 temp 赋值给 y */
    }
 
    static void Main(string[] args)
    {
        NumberManipulator n = new NumberManipulator();
        /* 局部变量定义 */
        int a = 100;
        int b = 200;
     
        Console.WriteLine("在交换之前,a 的值: {0}", a);
        Console.WriteLine("在交换之前,b 的值: {0}", b);
     
        /* 调用函数来交换值 */
        n.swap(a, b);
     
        Console.WriteLine("在交换之后,a 的值: {0}", a);
        Console.WriteLine("在交换之后,b 的值: {0}", b);
    }
}

// 打印输出:
在交换之前,a 的值:100
在交换之前,b 的值:200
在交换之后,a 的值:100
在交换之后,b 的值:200

b. 传递引用参数

给函数传递引用参数,意思是传递一个对变量的内存位置的引用,因此在函数体对它进行修改时,调用该函数的函数中,变量也会发生改变。

通过使用 ref 关键字来传入引用参数:

class NumberManipulator
{
    public void swap(ref int x, ref int y)
    {
        int temp;
        temp = x; /* 保存 x 的值 */
        x = y; /* 把 y 赋值给 x */
        y = temp; /* 把 temp 赋值给 y */
    }
    static void Main(string[] args)
    {
        NumberManipulator n = new NumberManipulator();
        /* 局部变量定义 */
        int a = 100;
        int b = 200;
        Console.WriteLine("在交换之前,a 的值: {0}", a);
        Console.WriteLine("在交换之前,b 的值: {0}", b);
        /* 调用函数来交换值 */
        n.swap(ref a, ref b);
        Console.WriteLine("在交换之后,a 的值: {0}", a);
        Console.WriteLine("在交换之后,b 的值: {0}", b);
    }
}

// 打印:
在交换之前,a 的值: 100
在交换之前,b 的值: 200
在交换之后,a 的值: 200
在交换之后,b 的值: 100

c. 传递输出参数

一个方法只能返回一个数据,而通过传递输出参数,则函数能输出多个参数,而且和传递引用参数一样,方法能够改变传入参数的实际值。
使用 out 关键字来传递输出参数:

public void getValue(out int x)
{
    int temp = 5;
    x = temp;   // 必须要处理 x,让它有个值,不然会报红
}
static void Main(string[] args)
{
    NumberManipulator n = new NumberManipulator();
    /* 局部变量定义 */
    int a = 100;
    Console.WriteLine("在方法调用之前,a 的值: {0}", a);
    /* 调用函数来获取值 */
    n.getValue(out a);
    Console.WriteLine("在方法调用之后,a 的值: {0}", a);
}

// 打印:
在方法调用之前,a 的值: 100
在方法调用之后,a 的值: 5

在举一个代码示例,我们可以不用输出参数进行初始化,直接定义后就传入,让函数体来计算出结果:

public void getValues(out int x, out int y )
{
    Console.WriteLine("请输入第一个值: ");
    x = Convert.ToInt32(Console.ReadLine());
    Console.WriteLine("请输入第二个值: ");
    y = Convert.ToInt32(Console.ReadLine());
}

static void Main(string[] args)
{
    NumberManipulator n = new NumberManipulator();
    /* 局部变量定义 */
    int a , b;
 
    /* 调用函数来获取值 */
    n.getValues(out a, out b);
    Console.WriteLine("在方法调用之后,a 的值: {0}", a);
    Console.WriteLine("在方法调用之后,b 的值: {0}", b);
}

// 打印:
请输入第一个值:
10
请输入第二个值:
200
在方法调用之后,a 的值: 10
在方法调用之后,b 的值: 200

八、可空类型

C# 没有 java 中那样的 @Nullable@NonNullable 注解来强制声明数据是否可空,但是它也有类似的能力来进行标识。

8.1 C# 的单问号(?) 和双问号(??)

  • 单问号
    ? 单问号用于对 intdoublebool 这种不可以直接赋值为 null 的基本数据类型,它可以让这个值可空:
int? i = 3;

// 等价于
Nullable<int> i = new Nullable<int>(3)

int i; //默认值0
int? ii; //默认值null
  • 双问号
    用于判断一个变量在为 null 的时候返回一个指定的值。
double? num1 = null;
double num2 = num1 ?? 5.3; // num1 为空时,赋值5.43,

8.2 单问号 - C# 可空类型(Nullable)

C# 提供了一个特殊的数据类型,nullable 类型(可空类型),通过加上单问号,可以表示其基础值类型正常范围内的值,再加上一个 null 值。

例如 bool? 可以提供 true、false 、null。

声明一个 nullable 类型(可空类型)的语法如下:

<data_type>? <variable_name> = null;

例如下面代码:

int? num1 = null;
int? num2 = 45;
double? num3 = new double?();
double? num4 = 3.14157;
bool? boolval = new bool?();
// 显示值
Console.WriteLine("显示可空类型的值: {0}, {1}, {2}, {3}", num1, num2, num3, num4);
Console.WriteLine("一个可空的布尔值: {0}", boolval);

// 打印:
显示可空类型的值: , 45, , 3.14157
一个可空的布尔值:

8.3 双问号 - Null 合并运算符 ??

?? 是一个二元运算符,又称合并运算符,类似于 Kotlin 的 Elvis (?:) 运算符。

double? num1 = null;
double? num2 = 3.14157;
double num3;
num3 = num1 ?? 5.34;      // num1 如果为空值则返回 5.34
Console.WriteLine("num3 的值: {0}", num3);
num3 = num2 ?? 5.34;
Console.WriteLine("num3 的值: {0}", num3);

因为 C# 也有和 java 一样的 bool ? a : b 的三目运算符,因此 ?? 其实是它的一个简化版本:

num3 = num1 ?? 5.34;
// 等价于
num3 = num1 != null ? num1.Value : 5.34d;    // IDE 遇到这个情况,会提示你优化成上面那样

九、数组

在 C# 中,我们通过 datatype[] arrayName; 来声明一个可变数组。例如:

// 声明一个大小为 10 的双浮点数组
double[] myDoubleArray = new double[10];

并且可以直接进行赋值:

double[] myDoubleArray = new double[10];
myDoubleArray[0] = 0.1;

myDoubleArray[10] = 0.2;  // 运行时报错,数组越界

我们还可以通过别的方式来初始化一个数组:

double[] balance = { 2340.0, 4523.69, 3421.0};    // 在花括号中定义初始值

int [] marks = new int[5]  { 99,  98, 92, 97, 95};   // 定义大小并初始化

int[] score = marks;   // 也可以将一个数组直接赋值给另一个数组

最后, C# 提供 foreach 方法来帮助我们高效访问数组中的元素:

int []  n = new int[10]; /* n 是一个带有 10 个整数的数组 */

/* 使用原始的 for 循环来初始化数组 n 中的元素 */        
for ( int i = 0; i < 10; i++ )
{
    n[i] = i + 100;
}

// 使用foreachx循环
foreach (int j in n )
{
    int i = j-100;
    Console.WriteLine("Element[{0}] = {1}", i, j);
}
// 打印:
Element[0] = 100
Element[1] = 101
Element[2] = 102
Element[3] = 103
...

当然了,除了这些,C# 还提供了一些“花样”的数组,下面让我们来学习它们。

9.1 多维数组

C# 可以支持 二维、三维… n维数组:

string [,] names;    // 二维字符串数组
int [ , , ] m;       // 三维整型数组

// 初始化一个二维数组
int [,] a = new int [3,4] {
 {0, 1, 2, 3} ,   /*  初始化索引号为 0 的行 */
 {4, 5, 6, 7} ,   /*  初始化索引号为 1 的行 */
 {8, 9, 10, 11}   /*  初始化索引号为 2 的行 */
};

9.2 交错数组

就像 Kotlin、Java 中 List<List<Int>>, 是数组的数组,也被称为交错数组。

在 C# 中使用连续多个中括号来声明,例如:

int[][] a;    // Kotlin 中的 List<List<Int>>
string[][][] b;   // Kotlin 中的 List<List<List<String>>>

9.3 传递参数数组(可变长入参)

有时,当声明一个方法时,我不能确定要传递给函数作为参数的参数数目。C# 参数数组解决了这个问题,参数数组通常用于传递未知数量的参数给函数。类似于 Kotlin 中的 vararg,在 C# 中则使用 params 关键字:

// 声明方式
public 返回类型 方法名称( params 类型名称[] 数组名称 )

class ParamArray
{
    public int AddElements(params int[] arr)    // 可以传递不确定数量的 int
    {
        int sum = 0;
        foreach (int i in arr)
        {
            sum += i;
        }
        return sum;
    }
}
 
class TestClass
{
    static void Main(string[] args)
    {
        ParamArray app = new ParamArray();
        int sum = app.AddElements(512, 720, 250, 567, 889);
        Console.WriteLine("总和是: {0}", sum);
        Console.ReadKey();
    }
}

十、结构体(struct)

就像 Kotlin 中的 data class 一样, C# 也有结构体 struct

struct Books    // 定义一个书本的结构体,可以声明与书相关的数据和函数
{
   public string title;
   public string author;
   public string subject;
   public int book_id;
}; 

Books myBook1 = new Books();

// 即使不用 new ,也可以被实例化
Books myBook2;
myBook2.book_id = 1;

但是它和普通 class 有很多不同点,先来看看其特点。

10.1 结构体的特点

  • 可带有方法、字段、索引、属性、运算符方法和事件。
  • 可定义构造函数,但不能定义析构函数(学习类时会讲)。但是,您不能为结构定义无参构造函数。无参构造函数(默认)是自动定义的,且不能被改变。
  • 与类不同,结构不能继承其他的结构或类。
  • 不能作为其他结构或类的基础结构。
  • 结构可实现一个或多个接口。
  • 结构成员不能指定为 abstractvirtualprotected
  • 当使用 new 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 new 操作符即可被实例化。
  • 如果不使用 new 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。建议还是用 new 来实例化

10.2 结构体与类的区别

  1. 结构体是值类型,它在栈中分配空间而类是引用类型,它在堆中分配空间,栈中保存的只是引用。
    因为栈的读写速度快于堆,因此结构体的性能高于类(在成员不是很多的情况下)
  2. 结构类型直接存储成员数据,让其他类的数据位于堆中,位于栈中的变量保存的是指向堆中数据对象的引用。
  3. 结构体中声明的字段无法赋予初值,类可以.

结构体和类的适用场合分析:

  • 当堆栈的空间很有限,且有大量的逻辑对象时,创建类要比创建结构体好一些;
  • 对于点、矩形和颜色这样的轻量对象,假如要声明一个含有许多个颜色对象的数组,则CLR需要为每个对象分配内存,在这种情况下,使用结构体的成本较低;
  • 在表现抽象和多级别的对象层次时,类是最好的选择,因为结构体不支持继承。
  • 大多数情况下,目标类型只是含有一些数据,或者以数据为主,选择结构体

十一、类

没有什么需要学习的,唯一要注意的是,和 C语言一样, C# 的类也有析构函数

析构函数是在对象被移除堆栈时调用的,因此适合用来做资源释放,无法被继承和重载。在 C# 中使用 ~ 来开启析构函数:

~Line() //析构函数
{
     Console.WriteLine("对象已删除");
}

11. 1 继承与重写

C# 中只能单继承,使用 : 关键字来继承:

abstract class Shape
{
    public void setWidth(int w)
    {
        width = w;
    }
    public void setHeight(int h)
    {
        height = h;
    }
    protected int width;
    protected int height;
    
	abstract public int Area();
}

// 派生类
class Rectangle: Shape
{
    public int getArea()
    {
        return (width * height);
    }
	
	public override int Area()
	{
    	Console.WriteLine("Rectangle 类的面积:");
    	return GetArea();
	}
}

11.2 虚方法

当有一个定义在类中的函数需要在继承类中实现时,可以使用虚方法 virtual

虚方法抽象类 组成了 C# 的多态。下面看下虚方法的运用:

public class Shape
{
    public int X { get; private set; }
    public int Y { get; private set; }
    public int Height { get; set; }
    public int Width { get; set; }
   
    // 虚方法
    public virtual void Draw()
    {
        Console.WriteLine("执行基类的画图任务");
    }
}

class Circle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个圆形");
        base.Draw();
    }
}
class Rectangle : Shape
{
    public override void Draw()
    {
        Console.WriteLine("画一个长方形");
        base.Draw();
    }
}

11.3 运算符重载

和 Kotlin 一样,可以去重载运算符,通过 operator 关键字,例如下面代码,将重载 Box 类的加法运算符:

public static Box operator+ (Box b, Box c)
{
   Box box = new Box();
   box.length = b.length + c.length;
   box.breadth = b.breadth + c.breadth;
   box.height = b.height + c.height;
   return box;
}

11.4 接口实现

通过使用 interface 来定义接口,而实现类在实现时,无需 override 关键字:

interface MyInterface1
{
    void myFunction1();
}
interface MyInterface2
{
    void myFunction2();
}
class MyClass : MyInterface1, MyInterface2   // 多实现
{
    void MyInterface1.myFunction1()
    {
    }
    void MyInterface2.myFunction2()
    {
    }
}

十二、 预处理指令

预处理器指令指导编译器在实际编译开始之前对信息进行预处理。

所有的预处理器指令都是以 # 开始。预处理器指令不是语句,所以它们不以分号(;)结束。

C# 编译器没有一个单独的预处理器,但是,指令被处理时就像是有一个单独的预处理器一样。在 C# 中,预处理器指令用于在条件编译中起作用。一个预处理器指令必须是该行上的唯一指令。

下表为 C# 中预处理指令列表:
在这里插入图片描述

那预处理指令的作用是什么呢?
实际上它在程序调试和运行上有重要的作用。

比如预处理器指令可以禁止编译器编译代码的某一部分,如果计划发布两个版本的代码,即基本版本和有更多功能的企业版本,就可以使用这些预处理器指令来控制。在编译软件的基本版本时,使用预处理器指令还可以禁止编译器编译于额外功能相关的代码。另外,在编写提供调试信息的代码时,也可以使用预处理器指令进行控制。总的来说和普通的控制语句(if等)功能类似,方便在于预处理器指令包含的未执行部分是不需要编译的。

12.1 #define 预处理器

#define 预处理器指令创建符号常量。

#define 允许定义一个符号,这样,通过使用符号作为传递给 #if 指令的表达式,表达式将返回 true。它的使用:

#define PI               // 定义 PI

using System;
namespace PreprocessorDAppl
{
   class Program
   {
      static void Main(string[] args)
      {
         #if (PI)                                  // 是否有定义 PI, 因为上面定义了,所以这条会被编译
            Console.WriteLine("PI is defined");
         #else                                     // 因为 PI 已定义,所以这下面的代码不会被编译,相当于做了编译优化
            Console.WriteLine("PI is not defined");
         #endif
      }
   }
}

// 执行: PI is defined

12.2 条件指令

可以使用 #if 指令来创建一个条件指令。条件指令用于测试符号是否为真。如果为真,编译器会执行 #if 和下一个指令之间的代码,例如下面代码中,在不同的环境下来实现不同的功能

#define DEBUG             // 这里可能定义在别的地方,如总配置文件中 Configer
#define VC_V10            // 这里可能定义在别的地方
using System;

public class TestClass
{
   public static void Main()
   {
      #if (DEBUG && !VC_V10)
         Console.WriteLine("DEBUG is defined");
      #elif (!DEBUG && VC_V10)
         Console.WriteLine("VC_V10 is defined");
      #elif (DEBUG && VC_V10)
         Console.WriteLine("DEBUG and VC_V10 are defined");
      #else
         Console.WriteLine("DEBUG and VC_V10 are not defined");
      #endif
   }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值