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