C# 方法定义
方法是包含一系列语句的代码块。 方法必须在类或结构中声明。 好的编程习惯是方法仅执行一项特定任务。 方法为程序带来了模块化。 正确使用方法具有以下优点:
- 减少代码重复
- 将复杂的问题分解成更简单的部分
- 提高代码的清晰度
- 重用代码
- 信息隐藏
C# 方法特征
方法的基本特征是:
- 访问权限
- 返回值类型
- 方法名称
- 方法参数
- 括号
- 语句块
方法的访问级别由访问修饰符控制。 他们设置方法的可见性。 他们确定谁可以调用该方法。 方法可以将值返回给调用方。 如果我们的方法返回一个值,我们将提供其数据类型。 如果不是,则使用void
关键字指示我们的方法不返回值。 方法参数用括号括起来,并用逗号分隔。 空括号表示该方法不需要任何参数。 方法块周围包含{}个字符。 当方法被调用时,该块包含一个或多个执行的语句。 拥有一个空的方法块是合法的。
C# 方法签名
方法签名是 C# 编译器方法的唯一标识。 签名由方法名称以及其每个形式参数的类型和种类(值,引用或输出)组成。 方法签名不包括返回类型。
可以在方法名称中使用任何合法字符。 按照约定,方法名称以大写字母开头。 方法名称是动词或动词,后跟形容词或名词。 随后的每个单词都以大写字母开头。 以下是 C# 中方法的典型名称:
- 执行
- FindId
- 集名
- 取名
- CheckIfValid
- 测试有效性
C# 简单示例
我们从一个简单的例子开始。
Program.
using System;
namespace SimpleMethod
{
class Base
{
public void ShowInfo()
{
Console.WriteLine("This is Base class");
}
}
class Program
{
static void Main(string[] args)
{
Base bs = new Base();
bs.ShowInfo();
}
}
}
我们有一个ShowInfo()
方法,它打印其类的名称。
class Base
{
public void ShowInfo()
{
Console.WriteLine("This is Base class");
}
}
每个方法都必须在类或结构内定义。 它必须有一个名字。 在我们的情况下,名称为ShowInfo()
。 方法名称之前的关键字是访问说明符和返回类型。 括号跟随方法的名称。 它们可能包含方法的参数。 我们的方法没有任何参数。
static void Main()
{
...
}
这是Main()
方法。 它是每个控制台或 GUI 应用的入口点。 必须声明为static
。 我们将在后面看到原因。 Main()
方法的返回类型可以是void
或int
。 省略了Main()
方法的访问说明符。 在这种情况下,将使用默认值private
。 不建议对Main()
方法使用public
访问说明符。 程序集中的任何其他方法都不应调用它。 当应用启动时,只有 CLR 才能调用它。
Base bs = new Base();
bs.ShowInfo();
我们创建Base
类的实例。 我们在对象上调用ShowInfo()
方法。 我们说该方法是一个实例方法,因为它需要一个实例来调用。 通过指定对象实例,成员访问运算符(点),方法名称,来调用该方法。
C# 方法参数
参数是传递给方法的值。 方法可以采用一个或多个参数。 如果方法使用数据,则必须将数据传递给方法。 我们通过在括号内指定它们来实现。 在方法定义中,我们必须为每个参数提供名称和类型。
Program.
using System;
namespace MethodParameters
{
class Addition
{
public int AddTwoValues(int x, int y)
{
return x + y;
}
public int AddThreeValues(int x, int y, int z)
{
return x + y + z;
}
}
class Program
{
static void Main(string[] args)
{
Addition a = new Addition();
int x = a.AddTwoValues(12, 13);
int y = a.AddThreeValues(12, 13, 14);
Console.WriteLine(x);
Console.WriteLine(y);
}
}
}
在上面的示例中,我们有两种方法。 其中一个带有两个参数,另一个带有三个参数。
public int AddTwoValues(int x, int y)
{
return x + y;
}
AddTwoValues()
方法采用两个参数。 这些参数具有int
类型。 该方法还向调用者返回一个整数。 我们使用return
关键字从方法中返回一个值。
public int AddThreeValues(int x, int y, int z)
{
return x + y + z;
}
AddThreeValues()
与先前的方法相似。 它带有三个参数。
int x = a.AddTwoValues(12, 13);
我们称为加法对象的AddTwoValues()
方法。 它有两个值。 这些值将传递给方法。 该方法返回一个分配给x
变量的值。
C# 变量数量可变
方法可以采用可变数量的参数。 为此,我们使用params
关键字。 params
关键字之后不允许有其他参数。 方法声明中仅允许使用一个params
关键字。
Program.
using System;
namespace SumOfValues
{
class Program
{
static void Main(string[] args)
{
Sum(1, 2, 3);
Sum(1, 2, 3, 4, 5);
}
static void Sum(params int[] list)
{
Console.WriteLine("There are {0} items", list.Length);
int sum = 0;
foreach (int i in list)
{
sum = sum + i;
}
Console.WriteLine("Their sum is {0}", sum);
}
}
}
我们创建一个Sum()
方法,该方法可以使用可变数量的参数。 该方法将计算传递给该方法的值的总和。
Sum(1, 2, 3);
Sum(1, 2, 3, 4, 5);
我们两次调用Sum()
方法。 在一种情况下,它需要 3 个参数,在第二种情况下,它需要 5 个参数。我们调用相同的方法。
static void Sum(params int[] list)
{
...
}
Sum()
方法可以采用可变数量的整数值。 所有值都添加到列表数组中。
Console.WriteLine("There are {0} items", list.Length);
我们打印列表数组的长度。
int sum = 0;
foreach (int i in list)
{
sum = sum + i;
}
我们计算列表中值的总和。
$ dotnet run
There are 3 items
Their sum is 6
There are 5 items
Their sum is 15
这是示例的输出。
C# 返回元组
C# 方法可以使用元组返回多个值。
Program.
using System;
using System.Collections.Generic;
using System.Linq;
namespace ReturingTuples
{
class Program
{
static void Main(string[] args)
{
var vals = new List<int> { 11, 21, 3, -4, -15, 16, 5 };
(int min, int max, int sum) = Basitats(vals);
Console.WriteLine($"Minimum: {min}, Maximum: {max}, Sum: {sum}");
}
static (int, int, int) Basitats(List<int> vals)
{
int sum = vals.Sum();
int min = vals.Min();
int max = vals.Max();
return (min, max, sum);
}
}
}
我们有Basitats()
方法,该方法返回整数列表的基本统计信息。
using System.Linq;
我们需要为Sum()
,Min()
和Max()
扩展方法导入System.Linq
。
var vals = new List<int> { 11, 21, 3, -4, -15, 16, 5 };
我们有一个整数值列表。 我们要根据这些值计算一些基本统计数据。
(int min, int max, int sum) = Basitats(vals);
我们使用解构操作将元组元素分配给三个变量。
static (int, int, int) Basitats(List<int> vals)
{
}
方法声明指定我们返回一个元组。
return (min, max, sum);
我们返回三个元素的元组。
$ dotnet run
Minimum: -15, Maximum: 21, Sum: 37
这是输出。
C# 匿名方法
匿名方法是没有名称的内联方法。 匿名方法消除了创建单独方法的需要,从而减少了编码开销。 如果没有匿名方法,开发人员通常不得不创建一个类来仅调用一个方法。
Program.
using System;
using System.Timers;
namespace AnonymousMethod
{
class Program
{
static void Main(string[] args)
{
var timer = new Timer();
timer.Elapsed += new ElapsedEventHandler(
delegate(object source, ElapsedEventArgs e)
{
Console.WriteLine("Event triggered at {0}", e.SignalTime);
}
);
timer.Interval = 2000;
timer.Enabled = true;
Console.ReadLine();
}
}
}
我们创建一个计时器对象,然后每 2 秒调用一个匿名方法。
var timer = new Timer();
Timer
类在应用中生成重复事件。
timer.Elapsed += new ElapsedEventHandler(
delegate(object source, ElapsedEventArgs e)
{
Console.WriteLine("Event triggered at {0}", e.SignalTime);
}
);
在这里,我们将匿名方法插入Elapsed
事件。 delegate
关键字用于表示匿名方法。
Console.ReadLine();
此时,程序等待来自用户的输入。 当我们按下 Return 键时,程序结束。 否则,程序将在事件生成之前立即完成。
C# 通过值,通过引用传递参数
C# 支持两种将参数传递给方法的方式:按值和按引用。 参数的默认传递是按值传递。 当我们按值传递参数时,该方法仅适用于值的副本。 当我们处理大量数据时,这可能会导致性能开销。
我们使用ref
关键字通过引用传递值。 当我们通过引用传递值时,该方法会收到对实际值的引用。 修改后,原始值会受到影响。 这种传递值的方式更加节省时间和空间。 另一方面,它更容易出错。
我们应该使用哪种方式传递参数? 这取决于实际情况。 假设我们有一组数据,例如员工工资。 如果我们要计算数据的某些统计信息,则无需修改它们。 我们可以传递价值观。 如果我们处理大量数据,并且计算速度至关重要,则可以参考。 如果我们要修改数据,例如 进行一些减薪或加薪,我们可以参考一下。
以下示例显示了如何通过值传递参数。
Program.
using System;
namespace PassingByValues
{
class Program
{
static int a = 4;
static int b = 7;
static void Main(string[] args)
{
Console.WriteLine("Outside Swap method");
Console.WriteLine("a is {0}", a);
Console.WriteLine("b is {0}", b);
Swap(a, b);
Console.WriteLine("Outside Swap method");
Console.WriteLine("a is {0}", a);
Console.WriteLine("b is {0}", b);
}
static void Swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
Console.WriteLine("Inside Swap method");
Console.WriteLine("a is {0}", a);
Console.WriteLine("b is {0}", b);
}
}
}
Swap()
方法在a
和b
变量之间交换数字。 原始变量不受影响。
static int a = 4;
static int b = 7;
最初,这两个变量被启动。 变量必须声明为static
,因为它们是从静态方法中使用的。
Swap(a, b);
我们称为Swap()
方法。 该方法将a
和b
变量作为参数。
int temp = a;
a = b;
b = temp;
在Swap()
方法内部,我们更改了值。 请注意,a
和b
变量是在本地定义的。 它们仅在Swap()
方法内部有效。
$ dotnet run
Outside Swap method
a is 4
b is 7
Inside Swap method
a is 7
b is 4
Outside Swap method
a is 4
b is 7
输出显示原始变量不受影响。
下一个代码示例通过引用将值传递给方法。 原始变量在Swap()
方法内更改。 方法定义和方法调用都必须使用ref
关键字。
Program.
using System;
namespace PassingByReference
{
class Program
{
static int a = 4;
static int b = 7;
static void Main(string[] args)
{
Console.WriteLine("Outside Swap method");
Console.WriteLine("a is {0}", a);
Console.WriteLine("b is {0}", b);
Swap(ref a, ref b);
Console.WriteLine("Outside Swap method");
Console.WriteLine("a is {0}", a);
Console.WriteLine("b is {0}", b);
}
static void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
Console.WriteLine("Inside Swap method");
Console.WriteLine("a is {0}", a);
Console.WriteLine("b is {0}", b);
}
}
}
在此示例中,调用 Swap()方法将更改原始值。
Swap(ref a, ref b);
我们用两个参数调用该方法。 它们前面带有ref
关键字,指示我们正在通过引用传递参数。
static void Swap(ref int a, ref int b)
{
...
}
同样在方法声明中,我们使用ref
关键字来通知编译器我们接受对参数而不是值的引用。
$ dotnet run
Outside Swap method
a is 4
b is 7
Inside Swap method
a is 7
b is 4
Outside Swap method
a is 7
b is 4
在这里,我们看到Swap()
方法确实改变了变量的值。
out
关键字类似于ref
关键字。 不同之处在于,使用ref
关键字时,必须在传递变量之前对其进行初始化。 使用out
关键字,可能无法初始化。 方法定义和方法调用都必须使用out
关键字。
Program.
using System;
namespace OutKeyword
{
class Program
{
static void Main(string[] args)
{
int val;
SetValue(out val);
Console.WriteLine(val);
}
static void SetValue(out int i)
{
i = 12;
}
}
}
一个示例显示out
关键字的用法。
int val;
SetValue(out val);
声明了 val 变量,但未初始化。 我们将变量传递给SetValue()
方法。
static void SetValue(out int i)
{
i = 12;
}
在SetValue()
方法内部,分配了一个值,该值随后会打印到控制台。
C# 方法重载
方法重载允许创建多个具有相同名称的方法,它们的输入类型彼此不同。
方法重载有什么好处? Qt5 库提供了一个很好的用法示例。 QPainter
类具有三种绘制矩形的方法。 它们的名称为drawRect()
,其参数不同。 一个引用一个浮点矩形对象,另一个引用一个整数矩形对象,最后一个引用四个参数:x,y,width,height。 如果开发 Qt 的 C++ 语言没有方法重载,则库的创建者必须将其命名为drawRectRectF()
,drawRectRect()
和drawRectXYWH()
之类的方法。 方法重载的解决方案更为优雅。
Program.
using System;
namespace Overloading
{
class Sum
{
public int GetSum()
{
return 0;
}
public int GetSum(int x)
{
return x;
}
public int GetSum(int x, int y)
{
return x + y;
}
}
class Program
{
static void Main()
{
var s = new Sum();
Console.WriteLine(s.GetSum());
Console.WriteLine(s.GetSum(20));
Console.WriteLine(s.GetSum(20, 30));
}
}
}
我们有三种方法GetSum()
。 它们的输入参数不同。
public int GetSum(int x)
{
return x;
}
这一个参数。
Console.WriteLine(s.GetSum());
Console.WriteLine(s.GetSum(20));
Console.WriteLine(s.GetSum(20, 30));
我们调用这三种方法。
$ dotnet run
0
20
50
这就是我们运行示例时得到的。
C# 递归
在数学和计算机科学中,递归是一种定义方法的方法,其中所定义的方法在其自己的定义内应用。 换句话说,递归方法会调用自身来完成其工作。 递归是解决许多编程任务的一种广泛使用的方法。
一个典型的例子是阶乘的计算。
Program.
using System;
namespace Recursion
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine(Factorial(6));
Console.WriteLine(Factorial(10));
}
static int Factorial(int n)
{
if (n == 0)
{
return 1;
} else
{
return n * Factorial(n-1);
}
}
}
}
在此代码示例中,我们计算两个数字的阶乘。
return n * Factorial(n-1);
在阶乘方法的主体内部,我们将阶乘方法称为经过修改的参数。 该函数调用自身。
$ dotnet run
720
3628800
这些就是结果。
C# 方法作用域
在方法内部声明的变量具有方法作用域。 名称的作用域是程序文本的区域,在该区域内,可以引用由名称声明的实体而无需使用名称限定。 在方法内部声明的变量具有方法作用域。 它也称为本地作用域。 该变量仅在此特定方法中有效。
Program.
using System;
namespace MethodScope
{
class Test
{
int x = 1;
public void exec1()
{
Console.WriteLine(this.x);
Console.WriteLine(x);
}
public void exec2()
{
int z = 5;
Console.WriteLine(x);
Console.WriteLine(z);
}
}
class Program
{
static void Main(string[] args)
{
var ts = new Test();
ts.exec1();
ts.exec2();
}
}
}
在前面的示例中,我们在exec1()
和exec2()
方法之外定义了x
变量。 该变量具有类作用域。 它在Test
类的定义内的任何地方都有效,例如 大括号之间。
public void exec1()
{
Console.WriteLine(this.x);
Console.WriteLine(x);
}
x 变量(也称为 x 字段)是一个实例变量。 因此,可以通过this
关键字进行访问。 它在exec1()
方法中也有效,并且可以用其裸名引用。 这两个语句都引用相同的变量。
public void exec2()
{
int z = 5;
Console.WriteLine(x);
Console.WriteLine(z);
}
也可以在exec2()
方法中访问 x 变量。 z
变量在exec2()
方法中定义。 它具有方法作用域。 仅在此方法中有效。
$ dotnet run
1
1
1
5
这是程序的输出。
在方法内部定义的变量具有本地/方法作用域。 如果局部变量与实例变量具有相同的名称,则它会遮盖实例变量。 通过使用this
关键字,类变量仍可在方法内部访问。
Program.
using System;
namespace Shadowing
{
class Test
{
int x = 1;
public void exec()
{
int x = 3;
Console.WriteLine(this.x);
Console.WriteLine(x);
}
}
class Program
{
static void Main(string[] args)
{
var ts = new Test();
ts.exec();
}
}
}
在前面的示例中,我们在exec()
方法外部和exec()
方法内部声明x
变量。 这两个变量具有相同的名称,但是它们没有冲突,因为它们存在于不同的作用域内。
Console.WriteLine(this.x);
Console.WriteLine(x);
变量的访问方式不同。 方法内定义的x
变量(也称为局部变量)仅通过其名称即可访问。 可以使用this
关键字引用实例变量。
$ dotnet run
1
3
C# 静态方法
在没有对象实例的情况下调用静态方法。 要调用静态方法,我们使用类的名称和点运算符。 静态方法只能与静态成员变量一起使用。 静态方法通常用于表示不会随对象状态变化的数据或计算。 数学库是一个示例,其中包含用于各种计算的静态方法。 我们使用static
关键字声明一个静态方法。 如果不存在静态修饰符,则该方法称为实例方法。 我们不能在静态方法中使用this
关键字。 它只能在实例方法中使用。
Main()
方法是 C# 控制台和 GUI 应用的入口点。 在 C# 中,要求Main()
方法是静态的。 在应用启动之前,尚未创建任何对象。 要调用非静态方法,我们需要有一个对象实例。 静态方法在实例化类之前就已存在,因此将静态方法应用于主入口点。
Program.
using System;
namespace StaticMethod
{
class Basic
{
static int Id = 2321;
public static void ShowInfo()
{
Console.WriteLine("This is Basic class");
Console.WriteLine("The Id is: {0}", Id);
}
}
class Program
{
static void Main(string[] args)
{
Basic.ShowInfo();
}
}
}
在我们的代码示例中,我们定义了静态ShowInfo()
方法。
static int Id = 2321;
静态方法只能使用静态变量。
public static void ShowInfo()
{
Console.WriteLine("This is Basic class");
Console.WriteLine("The Id is: {0}", Id);
}
这是我们的静态ShowInfo()
方法。 它与静态 ID 成员一起使用。
Basic.ShowInfo();
要调用静态方法,我们不需要对象实例。 我们通过使用类的名称和点运算符来调用该方法。
$ dotnet run
This is Basic class
The Id is: 2321
C# 隐藏方法
当派生类从基类继承时,它可以定义基类中已经存在的方法。 我们说隐藏了我们从中派生的类的方法。 为了明确告知编译器我们打算隐藏方法的意图,我们使用new
关键字。 没有此关键字,编译器将发出警告。
Program.
using System;
namespace HidingMethods
{
class Base
{
public void Info()
{
Console.WriteLine("This is Base class");
}
}
class Derived : Base
{
public new void Info()
{
base.Info();
Console.WriteLine("This is Derived class");
}
}
class Program
{
static void Main(string[] args)
{
var d = new Derived();
d.Info();
}
}
}
我们有两个类:Derived
和Base
类。 Derived
类继承自Base
类。 两者都有一种称为Info()
的方法。
class Derived : Base
{
...
}
(:)字符用于从类继承。
public new void Info()
{
base.Info();
Console.WriteLine("This is Derived class");
}
这是Derived
类中Info()
方法的实现。 我们使用new
关键字通知编译器我们正在从基类中隐藏方法。 请注意,我们仍然可以达到原始的Info()
方法。 借助base
关键字,我们也调用了Base
类的Info()
方法。
$ dotnet run
This is Base class
This is Derived class
我们已经调用了这两种方法。
C# 覆盖方法
现在,我们将引入两个新的关键字:virtual
关键字和override
关键字。 它们都是方法修饰符。 它们用于实现对象的多态行为。 virtual
关键字创建一个虚拟方法。 可以在派生类中重新定义虚拟方法。 稍后在派生类中,我们使用override
关键字重新定义相关方法。 如果派生类中的方法前面带有override
关键字,则派生类的对象将调用该方法,而不是基类方法。
Program.
using System;
namespace Overriding
{
class Base
{
public virtual void Info()
{
Console.WriteLine("This is Base class");
}
}
class Derived : Base
{
public override void Info()
{
Console.WriteLine("This is Derived class");
}
}
class Program
{
static void Main(string[] args)
{
Base[] objs = { new Base(), new Derived(), new Base(),
new Base(), new Base(), new Derived() };
foreach (Base obj in objs)
{
obj.Info();
}
}
}
}
我们创建Base
和Derived
对象的数组。 我们遍历数组并在所有数组上调用Info()
方法。
public virtual void Info()
{
Console.WriteLine("This is Base class");
}
这是Base
类的虚拟方法。 期望在派生类中重写它。
public override void Info()
{
Console.WriteLine("This is Derived class");
}
我们将覆盖Derived
类中的基本Info()
方法。 我们使用override
关键字。
Base[] objs = { new Base(), new Derived(), new Base(),
new Base(), new Base(), new Derived() };
在这里,我们创建Base
和Derived
对象的数组。 请注意,我们在数组声明中使用了Base
类型。 这是因为Derived
类可以继承,因此可以转换为Base
类。 相反的说法是不正确的。 将两个对象放在一个数组中的唯一方法是对所有可能的对象使用在继承层次结构中最顶层的类型。
foreach (Base obj in objs)
{
obj.Info();
}
我们遍历数组,并在数组中的所有对象上调用Info()
。
$ dotnet run
This is Base class
This is Derived class
This is Base class
This is Base class
This is Base class
This is Derived class
这是输出。
现在,将new
关键字更改为override
关键字。 再次编译该示例并运行它。
$ dotnet run
This is Base class
This is Base class
This is Base class
This is Base class
This is Base class
This is Base class
这次我们有不同的输出。
C# 本地函数
C# 7.0 引入了本地功能。 这些是在其他方法中定义的函数。
Program.
using System;
namespace LocalFunction
{
class Program
{
static void Main(string[] args)
{
Console.Write("Enter your name: ");
string name = Console.ReadLine();
string message = BuildMessage(name);
Console.WriteLine(message);
string BuildMessage(string value)
{
string msg = String.Format("Hello {0}!", value);
return msg;
}
}
}
}
在示例中,我们有一个局部函数BuildMessage()
,它在Main()
方法内部定义和调用。
C# 密封方法
密封方法将覆盖具有相同签名的继承虚拟方法。 密封方法也应标有倍率修饰符。 使用sealed
修饰符可防止派生类进一步覆盖该方法。 和这两个字很重要。 首先,方法必须是虚拟的。 必须稍后将其覆盖。 至此,可以将其密封。
Program.
using System;
namespace SealedMethods
{
class A
{
public virtual void F()
{
Console.WriteLine("A.F");
}
public virtual void G()
{
Console.WriteLine("A.G");
}
}
class B : A
{
public override void F()
{
Console.WriteLine("B.F");
}
public sealed override void G()
{
Console.WriteLine("B.G");
}
}
class C : B
{
public override void F()
{
Console.WriteLine("C.F");
}
/*public override void G()
{
Console.WriteLine("C.G");
}*/
}
class SealedMethods
{
static void Main(string[] args)
{
B b = new B();
b.F();
b.G();
C c = new C();
c.F();
c.G();
}
}
}
在前面的示例中,我们密封了类 B 中的方法G()
。
public sealed override void G()
{
Console.WriteLine("B.G");
}
方法G()
会覆盖B
类的祖先中具有相同名称的方法。 它也被密封以防止进一步取代该方法。
/*public override void G()
{
Console.WriteLine("C.G");
}*/
这些行被注释,因为否则代码示例将无法编译。 编译器将给出以下错误:Program.(38,30):错误 0239:’C.G()’:无法覆盖继承的成员’B.G()’,因为它是密封的
c.G();
此行将“ B.G()”打印到控制台。
$ dotnet run
B.F
B.G
C.F
B.G
C# 方法的表达式主体定义
方法的表达主体定义使我们能够以非常简洁,易读的形式定义方法实现。
method declaration => expression
这是一般语法。
Program.
using System;
namespace ExpBodyDef
{
class User
{
public string Name { get; set; }
public string Occupation { get; set; }
public override string ToString() => $"{Name} is a {Occupation}";
}
class Program
{
static void Main (string[] args)
{
var user = new User();
user.Name = "John Doe";
user.Occupation = "gardener";
Console.WriteLine(user);
}
}
}
在示例中,我们为ToString()
方法的主体提供了一个表达式主体定义。
public override string ToString() => $"{Name} is a {Occupation}";
表达式主体定义简化了语法。
标签:Console,C#,void,int,WriteLine,方法,class From: https://blog.csdn.net/hccee/article/details/143831904