Excerpt
0.前言此文主要介绍C#的基础知识,介绍方式主要是通过与C++语言进行类比,文章适合完全没接触C#语言但有C++编程基础的程序员快速了解C#的基础内容,也适合太久没使用C#语言的程序员快速重温相关知识。编写这篇文章的目的在于记录与分享,方便后续重温与掌握。最后,文章中可能会存在一些小问题,还望各位看官不吝指出。1.基础内容1.1数据类型C#含有三大数据类型:值类型、引用类型、指针类型。1.值类型定义: 值类型的变量直接存储数据。类型: 基础数据类型,如bool、char、int、float等;
0.前言
此文主要介绍C#的基础知识,介绍方式主要是通过与C++语言进行类比,文章适合完全没接触C#语言但有C++编程基础的程序员快速了解C#的基础内容,也适合太久没使用C#语言的程序员快速重温相关知识。编写这篇文章的目的在于记录与分享,方便后续重温与掌握。最后,文章中可能会存在一些小问题,还望各位看官不吝指出。
1.基础内容
1.1数据类型
C#含有三大数据类型:值类型、引用类型、指针类型。
1.值类型
(1)定义:值类型的变量直接存储数据。
(2)类型:基础数据类型,如bool、char、int、float等;结构类型,即struct类型;枚举类型,即enum类型。
(3)注意:值类型变量无法赋值为null,可以通过在值类型后面加上?为该变量增加可空类型,如 int? i = 3 等价于 Nullable i = 3。
2.引用类型
(1)定义:引用类型的变量不存储实际数据,而是存储对变量的引用。
(2)类型:class(类)、interface(接口)、delegate(委托)、object(通用对象)、string(字符串)。
(3)注意:string与其他引用类型不同,string在堆上的数据是不可修改的,即给string变量赋新值时,程序会在堆上创建数据并更新string变量的指向,同时保留原来的数据(数据由垃圾回收机制管理)。所以,如果要使用string的引用特性,需要使用ref关键字。
3.指针类型
(1)定义:指针类型的变量存储数据值的内存地址,与C++中的指针有相同功能。
(2)注意:C#为了类型安全,默认不支持指针,需要使用unsafe关键字开启不安全代码开发模式。
namespace CSharpLearn
{
class Person
{
public int m_age;
}
class Student : Person
{
public string m_school;
}
class BasicContent
{
public static void DataTypeDemo()
{
///值类型与引用类型的区别
int intValue1 = 0;
int intValue2 = intValue1;
intValue1 = 5;
Person person1 = new Person { m_age = 1 };
Person person2 = person1;
person1.m_age = 2;
string stringValue1 = "Hello World";
string stringValue2 = stringValue1;
ref string stringValue3 = ref stringValue1;
stringValue1 = "Good Morning";
Console.WriteLine("值类型与引用类型的区别:");
Console.WriteLine($"值类型:{intValue1}、{intValue2}");
Console.WriteLine("引用类型class:{0},{1}", person1.m_age, person2.m_age);
Console.WriteLine("引用类型string:{0},{1},{2}", stringValue1, stringValue2, stringValue3);
}
}
}
输出结果:
值类型:5、0
引用类型class:2、3
引用类型string:Good Morning、Hello World、Good Morning
1.2类型转换
C#类型转换包含隐式转换和显式转换,概念与C++相同。
C#提供了几种显式类型转换方法,常用的有:工具类System.Convert、内置函数、()运算符、as运算符。
1.工具类System.Convert
(1)定义:System.Convert提供一系列静态方法,实现将一个基础数据类型转换为另一个基础数据类型。如 ToInt32/ToDouble/ToString 。
(2)注意:如果转换失败,程序将会崩溃,需要使用 try catch 语句。
2.内置函数
(1)定义:基础数据类型内置 Parse()/TryParse()/ToString() 用于自身类型与string类型之间的互转。
(2)注意:调用 Parse() 进行转换,如果转换失败,程序将会崩溃;而调用 TryParse() 进行转换,如果转换失败,函数返回 false 。
3.()运算符
(1)定义:() 运算符是CSharp支持的传统强制类型转换方法,效果跟C++相同。
(2)注意:使用 () 符号进行类型转换,如果转换失败,程序将会崩溃。
4.as运算符
(1)定义:as 运算符将表达式结果显式转换为给定的引用或可以为null值的类型。
(2)注意:如果无法进行转换,as 运算符返回null,而不会引发异常;as不能用在没有继承关系的类型之间的转换,否则会出现编译错误;as 不能用于值类型数据,否则会出现编译错误。
namespace CSharpLearn
{
class Person
{
public int m_age;
}
class Student : Person
{
public string m_school;
}
class BasicContent
{
public static void DataTypeDemo()
{
///类型转换
string stringValue4 = "1.23";
string stringValue5 = "not number";
Convert.ToDouble(stringValue4);
Convert.ToDouble(stringValue5);//崩溃
double doubleValue1 = double.Parse(stringValue5);//崩溃
double doubleValue2 = 1.23;
int intValue3 = (int)doubleValue2;//显示类型转换,也可以不用(int),则使用的是隐式转换
//int intValue4 = doubleValue2 as int;//编译报错
Person person3 = new Student() { m_age = 1, m_school = "清华大学" };
Student student = person3 as Student;
Console.WriteLine("\n类型转换");
Console.WriteLine($"使用()结果:{intValue3};使用as结果:{student.m_school}");
}
}
}
1.3函数
C#没有全局变量和全局函数,所有变量与函数必须定义在类型中。
C#中的函数传递方式有两种:值传递、引用传递。概念相当于C++函数中的普通值类型参数和引用类型参数。
使用引用传递方式时形参类型前面和实参变量前面必须加上关键字 ref 或者 out ,两者的区别在于 ref 修饰的变量在传递给函数前必须先赋值,而 out 修饰的变量在传递给函数前没有赋值的要求,但是在函数返回之前必须先赋值。
namespace CSharpLearn
{
class BasicContent
{
public static void FunctionDemo()
{
int nCount = 0;
string error;
bool bRes = DoSomething(out error, ref nCount);
if (bRes)
{
Console.WriteLine("Successful");
}
else
{
Console.WriteLine($"Failed.Reason is {error}.");
}
}
private static bool DoSomething(out string error, ref int nCount)
{
nCount++;//模拟操作
bool bResult = false;//模拟操作结果
if (bResult)
{
error = "";//在函数返回之前给out类型参数变量赋值
}
else
{
error = "Error in ...";//在函数返回之前给out类型参数变量赋值
}
return bResult;
}
}
}
输出结果:
Failed.Reason is Error in …
1.4语法
C#中的语法和C++基本相同,包括运算符、条件语句、循环语句等。
有所区别的是:
1.C#创建引用类型是需要使用new关键字。
2.C#循环语句可以使用foreach结构:foreach(type item in arrays){} 。
- 其内部使用迭代器进行操作,因此只有实现了IEnumerable和IEnumerator两个接口的类型才能使用这种方式进行遍历。
- 使用foreach结构进行遍历操作是只读行为,不能更改元素内容。
3.C#其他常见运算符/关键字/函数:
- typeof():返回对象类型(非实例)的Type。使用:Type type = typeof(int) 。
- GetType():返回对象实例的Type。使用:Type type = intValue.GetType() 。
- is:判断对象是否为某一类型,含继承关系。使用:bool bRes = intValue is int 。
namespace CSharpLearn
{
class Person
{
public int m_age;
}
class Student : Person
{
public string m_school;
}
class BasicContent
{
public static void SyntaxDemo()
{
int total = 0;
int[] intArray = { 1, 2, 3 };
foreach (int intValue in intArray)
{
//intValue *= 2;//编译报错,foreach只能读取元素,无法修改元素
total += intValue;//
}
Console.WriteLine(total);
Person person = new Student();
Console.WriteLine(person.GetType() == typeof(Person));//false
Console.WriteLine(person.GetType() == typeof(Student));//true
Console.WriteLine(person is Person);//true
Console.WriteLine(person is Student);//true
}
}
}
输出结果
6
false
true
true
true
2.类
2.1封装
1.类的成员
(1)C#中类的成员类型包括字段、方法、属性。
- 字段:当于C++类中的数据成员。
- 方法:等同于C++类中的成员方法。
- 属性:相当于C++中封装了(读)(写)接口的数据成员,是为了增加数据成员的操作行为而设计的(比如将读写权限分开、取值与赋值时进行运算等)。
(2)类的成员都是使用“.”运算符访问的。实际上,C++中使用“::”符号的地方在C#中都是用“.”,比如类的静态成员、命名空间中的对象、枚举项。
- 普通成员:通过 Instance.member 方式访问。
- 静态成员:通过 Class.member 方式访问。
(3)静态成员字段。对比C++,C#中类的静态成员字段不需要额外的初始化操作。
- 如果需要简单赋值,可以在定义时进行初始化。(普通字段也可以在定义时进行初始化)
- 如果需要使用代码段赋值,则需要使用静态构造函数。
namespace CSharpLearn
{
class ClassSection
{
class Person
{
public string name = "张三";//字段,可以在定义处初始化
private int age;//字段
public int Age//属性
{
get { return age; }//此处也可以进行其他操作
set
{
//在set中限制非法值
if (value >= 0 && value <= 200)
{
age = value;
}
else
{
throw new Exception("值的范围不合法");
}
}
}
public double height { get; }//只读的属性(不需要其它处理操作时,可以省略字段定义)
public static string yell = "Love And Peace";//静态成员变量,可以在定义的时候赋值
public void OutputInfo()
{
Console.WriteLine($"name:{name},age:{age},height:{height},yell:{yell}");
}
public Person(string name, int age, double height)
{
this.name = name;
this.age = age;
this.height = height;
}
static Person()
{
yell = "Peace And Love";//在静态构造函数中给静态成员变量赋值
}
}
public static void AbstractDemo()
{
Person person1 = new Person("李四", 15, 180);
person1.OutputInfo();
}
}
}
输出结果:
name:李四,age:15,height:180,yell:Peace And Love
2.访问权限
(1)五个访问权限
修饰符 | 级别 | 适用对象 | 解释 |
---|---|---|---|
public | 公开 | 类型和类型成员 | 对访问成员没有级别限制 |
private | 私有 | 类型成员 | 只能在类的内部访问 |
protected | 受保护的 | 类型成员 | 在类的内部或者派生类中访问,不管该类和派生类是不是在同一程序集中 |
internal | 内部的 | 类型和类型成员 | 只能在同一程序集中访问 |
protected internal | 受保护的内部 | 类型成员 | 如果是继承关系,不管是不是在同一程序集中都可以访问;如果不是继承关系,则只能在同一程序集中访问 |
(2)类型自身访问权限
类型访问修饰符只有两种:public、internal。如果类型定义前面没有加任何修饰符的话,默认是internal。
(3)类class
类中的成员修饰符可以为:public、private、protected、internal、protected internal。
如果类成员(字段和方法)前面没有加修饰符的话,默认访问权限是private。
(4)抽象类abstact class
抽象类中至少要有一个抽象方法,访问类型可以定义为:public、protected、internal,不能是private。
(5)接口interface
接口中的方法默认访问权限为public,并且不能定义为其他访问权限。
(6)枚举enum
枚举中的成员默认为public访问修饰符,并且不能定义为其他访问权限。
(7)结构struct
结构中的成员默认为private访问修饰符,结构成员无法声明为protected权限,因为结构不支持继承。
2.2继承
1.基本用法:使用“:”运算符实现继承,效果等同于C++中的public继承(C#中没有其他继承方式)。如 class A:B 表示A派生继承于B。
2.使用基类:在派生类的成员初始化列表中使用 base(Arg…) 进行父类的初始化。在派生类其他位置使用 base 获得基类对象。
3.单一继承:C#中类只允许继承单个基类,可以使用接口实现多重继承。
4.覆盖/重写:与C++不同的是,函数特殊修饰符都是放在权限修饰符之后,而不是放在函数定义最后面。
- new:new 修饰符用于隐藏基类的方法。
- override:override 修饰符用于重写基类的方法。
namespace CSharpLearn
{
class ClassSection
{
class Animal
{
public int age;
public Animal(int age)
{
this.age = age;
}
public virtual void Shout()
{
Console.WriteLine("*****");
}
public void SayHello()
{
Console.WriteLine("Hello.From an animal!");
}
}
class Dog : Animal
{
public Dog(int age) : base(age)
{
}
public override void Shout()//重写
{
Console.WriteLine("WangWang");
}
public new void SayHello()//覆盖
{
base.SayHello();
Console.WriteLine($"Hello.From a dog of {age} years old!");
}
}
public static void InheritDemo()
{
Animal dog = new Dog(5);
dog.Shout();
dog.SayHello();
}
}
}
输出结果:
WangWang
Hello.From an animal!
Hello.From a dog of 5 years old!
2.3多态
C#中的多态与C++相同,也分为静态多态和动态多态。
1.静态多态性体现在编译时,通过函数重载和运算符重载实现。
2.动态多态性体现在运行时,通过虚函数重写实现。
3.结构体
1.C#中类和结构体的区别
- 类是引用类型,结构是值类型。因此结构可以不使用New操作符即可被实例化。
- 结构不支持继承,因此结构成员修饰符不能指定为 abstract、virtual 或 protected。
- 结构不能定义无参构造函数,无参构造函数(默认)是自动定义的,且不能被改变。
- 结构中声明的字段无法赋予初值。
- 结构的构造函数中,必须为结构体所有字段赋值。
2.C#中类和结构体的选择依据
类的对象是存储在堆空间中,结构存储在栈中。堆空间大,但访问速度较慢;栈空间小,访问速度相对更快。因此,当我们描述一个轻量级对象的时候,使用结构可提高效率,成本更低。当然,这也得从需求出发,假如我们在传值的时候希望传递的是对象的引用地址而不是对象的拷贝,就应该使用类了。
4.接口
接口声明了属性、方法和事件,这些都可以作为接口的成员。接口只包含成员的声明,成员的定义是派生类的责任,派生类必须实现接口声明的成员。即接口提供了派生类应遵循的标准结构。
interface IMyInterface
{
void MethodToImplement();
}
class InterfaceImplementer : IMyInterface
{
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
}
public static void InterfaceDemo()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
}
5.常用类型
5.1字符串
1.string类型本身含有各种属性以及操作方法,包括查找、替换、拆分、拼接、格式化等。
2.string类型的操作方法不会修改到本身,而是以返回值的方式提供修改后的值。
public static void StringDemo()
{
string str1 = "apple";
string str2 = "APPLE";
//用string的Compare静态函数比较两个字符串,最后一个参数控制是否忽略大小写
if (string.Compare(str1, str2, true) == 0)
{
Console.WriteLine("Equal");
}
//用string的Contains函数判断是否包含某些字符/字符串
if (str1.Contains("ap"))
{
Console.WriteLine("Contain ap");
}
//用string的IndexOf函数查找某些字符/字符串在原来字符串中的位置,找不到返回-1
int index = str1.IndexOf("pp");
if (index != -1)
{
//用string的Substring函数截断字符串,不会修改原来字符串
Console.WriteLine("Contain pp");
string str3 = str1.Substring(index, 2);
Console.WriteLine(str1);
Console.WriteLine(str3);
//用string的Replace函数替换字符串,不会修改原来字符串
string str4 = str1.Replace("pple", "ge");
Console.WriteLine(str1);
Console.WriteLine(str4);
}
//用string.Join静态函数以及Split函数进行字符串的拼接与分割
string[] strArray1 = { "apple", "banana", "orange" };
string str5 = string.Join(",", strArray1);
string[] strArray2 = str5.Split(',');
Console.WriteLine(str5);
foreach (string str in strArray2)
{
Console.WriteLine(str);
}
//用string.Format静态函数格式化字符串
int years = 10;
string str6 = years > 1 ? "years" : "year";
string str7 = string.Format("I'm {0} {1} old.", years, str6);
Console.WriteLine(str7);//I'm 10 years old.
}
5.2数组
1.C#中的数组与C++中的数组相同,但在声明方式上有差异:
- C#声明数组时需要把中括号放在数据类型后面,如 int[] intArray = new int[10];
- C++声明数组时需要把中括号放在标识符后面,如 int intArray[10];
2.C#中数组的基类是 Array,该类提供了各种用于数组的属性和方法,比如:Length、IndexOf、Sort等。
3.将数组作为函数参数时,实际传递的是指向数组的指针。
4.使用 params 关键字的参数数组用于传递未知数量的参数给函数,此时即可以传递数组实参,也可以传递一组数组元素。
static double GetAverage(int[] arr)
{
int sum = 0;
for (int i = 0; i < arr.Length; ++i)
{
sum += arr[i];
}
double avg = (double)sum / arr.Length;
return avg;
}
static int AddElements(params int[] arr)
{
int sum = 0;
foreach (int i in arr)
{
sum += i;
}
return sum;
}
public static void ArrayDemo()
{
//声明创建数组
int[] datas1 = new int[10];
int[] datas2 = { 1, 2, 3 };
int[] datas3 = new int[] { 1, 2, 3, 4, 5 };
//数组作为函数参数
double avg = GetAverage(datas3);
//avg = GetAverage(1, 2, 3);//无法使用参数数组
//函数参数数组
int sum1 = AddElements(1, 2, 3, 4, 5);//传递一组数组元素
int sum2 = AddElements(datas3);//传递数组实参
Console.WriteLine($"{avg},{sum1},{sum2}");
}
5.3集合
1.List:相当于C++中的vector。List list = new List() 。
2.Dictonary:相当于C++中的map。Dictionary<string,string> map = new Dictionary<string,string>() 。
3.HashSet:相当于C++中的set。HashSet set = new HashSet() 。
6.委托与事件
6.1委托
1.C#中的委托类似于C/C++中的函数指针,委托是存有对某个方法的引用的一种引用类型变量,委托可在运行时被改变。
2.C#中使用 delegate 关键字定义委托类型。
delegate void NumberChanger(int);
NumberChanger obj = fun;
3.委托对象可以使用“+/-”运算符进行相同类型委托的合并或移除,一个合并委托会调用其所有组件委托。
//obj为委托实例,而fun1和fun2为具有和该委托类型相同函数签名的函数
obj+=fun1;
obj+=fun2;
obj(5); //按照顺序执行fun1(5)、fun2(5)
4.在实际项目中,可以直接使用C#封装好的 Action 和 Func 两个泛型类型。
(1) Action 用于定义没有返回值的委托类型。
static void fun1();
static void fun2(int arg1,string arg2);
Action obj1 = new Action(fun1);
Action<int,string> obj2 = new Action<int,string>(fun2);
(2) Func 用于定义有返回值的委托类型。
static bool fun1();
static bool fun2(int arg1,string arg2);
Func<bool> obj1 = new Func<bool>(fun1);
Func<int,string,bool> obj2 = new Func<int,string,bool>(fun2);
5.实际例子
namespace CSharpLearn
{
delegate void NumberChanger(int n);//声明委托
class DelegateEvent
{
static int num = 10;
public static void AddNum(int p)
{
num += p;
}
public static void MultNum(int q)
{
num *= q;
}
public static void DelegateDemo()
{
//实例化委托
NumberChanger nc;
NumberChanger nc1 = new NumberChanger(AddNum);
NumberChanger nc2 = new NumberChanger(MultNum);
//上述用Action方式写
//Action<int> nc;
//Action<int> nc1 = new Action<int>(AddNum);
//Action<int> nc2 = new Action<int>(MultNum);
//组播委托,函数将按顺序调用
nc = nc1;
nc += nc2;
//调用方法
nc(5);
Console.WriteLine("Value of num is {0}", num);
}
}
}
输出结果:
75
6.2事件
1.定义:事件封装了委托类型变量,使得即使将该变量声明为public访问权限,也无法在类的外部直接调用访问,只能使用该委托变量的注册(“+=”运算符) 和注销(“-=”运算符)操作,限制了对象能力,更符合具体的事件使用场景。
2.使用:
- 在委托变量前面加上 event 关键字来定义事件变量。
- 使用事件变量的“+=”操作符来订阅事件。
- 使用事件变量的“-=”操作符来取消订阅事件。
3.实际例子
namespace CSharpLearn
{
class DelegateEvent
{
public class EventTrigger
{
private int value;
public delegate void NumManipulationHandler();//声明委托
public event NumManipulationHandler ChangeNum;//声明事件
protected void OnNumChanged()
{
if (ChangeNum != null)
{
ChangeNum();//触发事件
}
else
{
Console.WriteLine("event not fire");
}
}
public void SetValue(int n)
{
if (value != n)
{
value = n;
OnNumChanged();
}
}
public EventTrigger()
{
int n = 5;
SetValue(n);
}
}
public class EventSubscriber
{
public void Printf()
{
Console.WriteLine("event fire");
}
}
public static void EventDemo()
{
EventTrigger e = new EventTrigger();//实例化发布器,第一次没有触发事件
EventSubscriber v = new EventSubscriber();//实例化订阅器
e.ChangeNum += new EventTrigger.NumManipulationHandler(v.Printf);//注册
e.SetValue(7);
e.SetValue(9);
}
}
}
输出:
event not fire
event fire
event fire
7.泛型
C#中的泛型相当于C++中的模板,泛型允许程序人员延迟编写类或方法中的编程元素的数据类型的规范,直到在程序中实际使用它的时候。在C#中,可以使用泛型接口、泛型类、泛型方法、泛型事件和泛型委托。C#的泛型定义是在类型名称后面加上 。
namespace CSharpLearn
{
public class MyGenericArray<T>
{
private T[] array;
public MyGenericArray(int size)
{
array = new T[size];
}
public T GetItem(int index)
{
return array[index];
}
public void SetItem(int index, T value)
{
array[index] = value;
}
}
class Generic
{
public static void GenericDemo()
{
MyGenericArray<int> intArray = new MyGenericArray<int>(5);
//设置值
for (int i = 0; i < 5; ++i)
{
intArray.SetItem(i, i * 5);
}
//获取值
for (int i = 0; i < 5; ++i)
{
Console.WriteLine(intArray.GetItem(i));
}
//创建字符数组
MyGenericArray<char> charArray = new MyGenericArray<char>(5);
//...
}
}
}
8.特性
特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。
1.可以通过使用特性向程序添加声明性信息。
2.一个声明性标签是通过放置在它所应用的元素前面的方括号([ ])来描述的。
3.C#有三种预定义特性:AttributeUsage、Conditional、Obsolete。
- AttributeUsage:描述了如何使用一个自定义特性类。它规定了特性可应用到的项目的类型。
- Conditional:标记了一个条件方法,其执行依赖于指定的预处理标识符,触发方法的条件编译。
- Obsolete:标记了不应被使用的程序实体。
namespace CSharpLearn
{
class Speciality
{
public class MyClass
{
[Conditional("DEBUG")]
public static void Message(string msg)
{
Console.WriteLine(msg);
}
[Obsolete("Don't use MessageOld, use Message instead", true)]
public static void MessageOld(string msg)
{
Console.WriteLine("It is the old method");
}
}
public static void SpecialityDemo()
{
MyClass.MessageOld("Hello World");
MyClass.Message("Hello World");
Console.ReadKey();
}
}
}
9.其他
1.var关键字
定义:从 C# 3.0 开始,在方法范围内声明的变量可以具有隐式“类型”var,作用相当于C++中的auto。
使用:var list = new List(){1,3,5,7,9};
2.@符号
定义:在字符串的前面加@符号会将转义字符当做普通字符对待,作用相当于C++中的R字符。
使用:string str1 = @”C:\Windows”;
3.对象引用
C#中项目通过命名空间共享所有代码对象,不用像C++那样引入头文件。
4.前置声明
与C++不同,C#不需要做前置声明,例如可以在一个类中使用在该类后面才定义的类。文章来源:https://www.toymoban.com/news/detail-428486.html
5.资源释放文章来源地址https://www.toymoban.com/news/detail-428486.html
- C#中的资源包括托管资源和非托管资源,非托管资源需要用户手动释放,属于非托管资源的有:文件流、数据库的连接、系统的窗口句柄、打印机资源等等。
- C#中的析构函数的调用时机和GC(垃圾回收机制)一样是完全不可预测的,也不应当依赖于它被调用,因此C#提供了IDisposable接口用于设计显式释放资源的功能。
- 非托管资源类型需要继承IDisposable接口,实现
Dispose函数,在该函数中完成资源的释放,同时在析构函数中调用该函数,确保资源总能被释放。 - C#中的析构函数是确保已分配的非托管资源总能被释放的一个补救措施,即是最后的保障。
- C#提供using语句,在代码范围结束处自动调用对象的Dispose()函数释放资源。如 using(var reader = new StringReader()){}。
到了这里,关于C#基础知识的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!