方法是包含一系列语句的代码块。方法必须在类或结构中声明。方法只执行一项特定任务是一种很好的编程习惯。方法为程序带来模块化。正确使用方法会带来以下好处:
方法的基本特点是:
方法的访问级别由访问修饰符控制。他们设置方法的可见性。他们确定谁可以调用该方法。方法可能会向调用者返回一个值。如果我们的方法返回一个值,我们提供它的数据类型。如果不是,我们使用 void 关键字来指示我们的方法不返回值。方法参数用括号括起来并用逗号分隔。空括号表示该方法不需要参数。方法块被 { } 字符包围。该块包含一个或多个在调用该方法时执行的语句。有一个空的方法块是合法的。
方法签名是 C# 编译器方法的唯一标识。签名由方法名称及其每个形式参数的类型和种类(值、引用或输出)组成。方法签名不包括返回类型。
任何合法字符都可以用在方法的名称中。按照惯例,方法名称以大写字母开头。方法名称是动词或动词后跟形容词或名词。随后的每个单词都以大写字符开头。以下是 C# 中方法的典型名称:
我们从一个简单的例子开始
Program.cs
- 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() { ... }
这是主要方法。它是每个控制台或 GUI 应用程序的入口点。它必须声明为静态的。我们稍后会看到为什么。 Main 方法的返回类型可以是 void 或 int。 Main 方法的访问说明符被省略。在这种情况下,会使用默认的,即私有的。不建议对 Main 方法使用公共访问说明符。它不应该被程序集中的任何其他方法调用。只有 CLR 应该能够在应用程序启动时调用它。
var bs = new Base(); bs.ShowInfo();
我们创建一个基类的实例。我们在对象上调用 ShowInfo 方法。我们说该方法是一个实例方法,因为它需要一个实例才能被调用。通过指定对象实例,后跟成员访问运算符(点,然后是方法名称)来调用该方法。
参数是传递给方法的值。方法可以采用一个或多个参数。如果方法处理数据,我们必须将数据传递给方法。我们通过在括号内指定它们来做到这一点。在方法定义中,我们必须为每个参数提供名称和类型。
Program.cs
- 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 变量的值。
一个方法可以接受可变数量的参数。为此,我们使用 params 关键字。 params 关键字后不允许有其他参数。方法声明中只允许使用一个 params 关键字。
Program.cs
- 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# 方法可以使用元组返回多个值。
Program.cs
- 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
匿名方法是没有名称的内联方法。 匿名方法通过消除创建单独方法的需要来减少编码开销。 如果没有匿名方法,开发人员通常不得不创建一个类来调用一个方法。
Program.cs
- 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# 支持将参数传递给方法的两种方式:按值和按引用。参数的默认传递是按值传递的。当我们按值传递参数时,该方法仅适用于值的副本。当我们处理大量数据时,这可能会导致性能开销。
我们使用 ref 关键字通过引用传递值。当我们通过引用传递值时,该方法接收到对实际值的引用。修改时会影响原始值。这种传递值的方式更节省时间和空间。另一方面,它更容易出错。
我们应该使用哪种传递参数的方式?这取决于实际情况。假设我们有一组数据,例如员工的工资。如果我们想计算数据的一些统计量,我们不需要修改它们。我们可以通过值传递。如果我们处理大量数据并且计算速度很关键,我们通过引用传递。如果我们想修改数据,例如做一些减薪或加薪的事,我们不妨参考一下。
下面的例子展示了我们如何通过值传递参数。
Program.cs
- 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);
我们称之为交换方法。 该方法将 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
Program.cs
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 关键字。
Program.cs
int val; SetValue(out val); Console.WriteLine(val); void SetValue(out int i) { i = 12; }
一个示例显示了 out 关键字的用法。
int val; SetValue(out val);
val 变量已声明,但未初始化。
void SetValue(out int i) { i = 12; }
在 SetValue 方法内部,它被分配了一个值,该值稍后会打印到控制台。
方法重载允许创建多个具有相同名称但输入类型彼此不同的方法。
方法重载有什么好处?Qt5库给出了一个很好的例子。QPainter类有三个方法来绘制一个矩形。它们的名字是drawRect,它们的参数不同。引用一个整数矩形对象,最后一个一个有四个参数:x、y、width、height。库必须将方法命名为drawRectRectF、drawRectRect、drawRectXYWH。方法重载的解决方案更优雅。
Program.cs
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));
We call all three methods.
$ dotnet run
0
20
50s
递归,在数学和计算机科学中,是一种定义方法的方法,其中被定义的方法在其自己的定义中应用。换句话说,递归方法调用自己来完成它的工作。递归是一种广泛使用的方法来解决许多问题编程任务。
一个典型的例子是阶乘的计算。
Program.cs
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
3628800s
一个方法内部声明的变量有一个方法范围,一个方法内部声明的变量有一个方法范围。
Program.cs
- 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 关键字在方法内部访问。
Program.cs
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 变量,也称为局部变量,只是通过其名称访问。
$ dotnet run
1
3s
静态方法是在没有对象实例的情况下调用的。要调用静态方法,我们使用类的名称和点运算符。静态方法只能与静态成员变量一起使用。静态方法通常用于表示数据或计算不响应对象状态而改变。一个例子是一个数学库,它包含用于各种计算的静态方法。我们使用 static 关键字来声明一个静态方法。当不存在静态修饰符时,该方法被称为实例方法我们不能在静态方法中使用 this 关键字。
Main 方法是 C# 控制台和 GUI 应用程序的入口点。对象实例。静态方法在类实例化之前存在,因此静态方法应用于主入口点。
Program.cs
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 方法。
Basic.ShowInfo();
要调用静态方法,我们不需要对象实例。
$ dotnet run This is Basic class The Id is: 2321
当派生类从基类继承时,它可以定义已经存在于基类中的方法。我们说我们隐藏了我们派生的类的方法。显式通知编译器我们打算隐藏一个方法,我们使用 new 关键字。
Program.cs
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 方法。关键字,我们调用 Info Base 类的方法也是如此。
$ dotnet run This is Base class This is Derived class
We have invoked both methods.
现在我们引入两个新的关键字:virtual 关键字和 override 关键字。它们都是方法修饰符。它们用于实现对象的多态行为。
virtual关键字创建虚拟方法。虚拟方法可以在派生类中重新定义。稍后在派生类中,我们使用override关键字重新定义所讨论的方法。如果派生类中的方法前面有override关键字,则派生类的对象将调用该方法,而不是基类方法。
Program.cs
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"); } }
我们创建一个基对象和派生对象的数组。我们遍历数组并对所有数组调用Info方法
public virtual void Info() { Console.WriteLine("This is Base class"); }
这是基类的虚拟方法。它应该在派生类中重写。
public override void Info() { Console.WriteLine("This is Derived class"); }
我们重写派生类中的基信息方法。我们使用override关键字。
Base[] objs = { new Base(), new Derived(), new Base(), new Base(), new Base(), new Derived() };
在这里,我们创建一个基本对象和派生对象的数组。注意,我们在数组声明中使用了基类型。这是因为派生类可以转换为基类,因为它继承自基类。相反的情况并非如此。将两个对象放在一个数组中的唯一方法是对所有可能的对象使用继承层次结构中最顶层的类型。
foreach (Base obj in objs) { obj.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
现在更改新关键字的覆盖关键字。再次编译示例并运行它。
$ 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#7.0引入了局部函数。这些是在其他方法中定义的函数。
Program.cs
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方法中定义和调用的。
密封方法重写具有相同签名的继承虚拟方法。密封方法也应标记超控修改器。使用密封修饰符可防止派生类进一步重写该方法。“进一步”这个词很重要。首先,方法必须是虚拟的。它必须稍后被覆盖。在这一点上,它可以被密封。
Program.cs
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"); }*/
这些行被注释,因为否则代码示例将无法编译。编译器将给出以下错误:程序。cs(38,30):错误CS0239:'C。G()':无法覆盖继承的成员“B”。G()'因为它是密封的c、 G();
This line prints "B.G" to the console.
$ dotnet run B.F B.G C.F B.G
方法的表达式体定义允许我们以非常简洁、可读的形式定义方法实现。
method declaration => expression
Program.cs
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方法的主体提供了表达式主体定义。公共重写字符串ToString()=>$“{Name}是{职业}”;表达式体定义简化了语法。