ZetCode

C# 方法

最后修改于 2023 年 7 月 5 日

本文介绍 C# 方法。

在面向对象编程中,我们使用对象。对象是程序的基本构建块。对象由数据和方法组成。方法更改已创建对象的状态。它们是对象的动态部分;数据是静态部分。

C# 方法定义

方法是包含一系列语句的代码块。方法必须在类、结构或接口中声明。良好的编程实践是方法只执行一个特定任务。方法为程序带来模块化。正确使用方法可以带来以下优势:

C# 方法特性

方法的基本特性包括:

方法的访问级别由访问修饰符控制。它们设置方法的可见性。它们确定谁可以调用该方法。方法可能会向调用者返回值。如果我们的方法返回值,我们提供其数据类型。如果没有,我们使用 void 关键字来指示我们的方法不返回值。

方法参数包含在括号中,并用逗号分隔。空括号表示该方法不需要任何参数。方法块包含在 { } 字符中。该块包含一个或多个语句,这些语句在方法被调用时执行。拥有一个空方法块是合法的。

C# 方法签名

方法签名是 C# 编译器用于唯一标识方法的方式。签名由方法名称和每个形式参数的类型和种类(值、引用或输出)组成。方法签名不包括返回类型。

任何合法字符都可以用作方法名称。按照惯例,方法名称以大写字母开头。方法名称是动词或动词后跟形容词或名词。每个后续单词都以大写字母开头。以下是 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()
{
   ...
}

这是 Main 方法。它是每个控制台或 GUI 应用程序的入口点。它必须声明为 static。稍后我们将看到原因。Main 方法的返回类型可以是 voidintMain 方法的访问说明符被省略。在这种情况下,使用默认值,即 private

不建议对 Main 方法使用 public 访问说明符。它不应该被程序集中的任何其他方法调用。只有 CLR 应该能够在应用程序启动时调用它。

var bs = new Base();
bs.ShowInfo();

我们创建 Base 类的实例。我们调用对象的 ShowInfo 方法。我们说该方法是一个实例方法,因为它需要一个实例才能被调用。通过指定对象实例,后跟成员访问运算符(点),然后是方法名称来调用该方法。

C# 方法参数

参数是传递给方法的值。方法可以接受一个或多个参数。如果方法处理数据,我们必须将数据传递给方法。我们通过在括号内指定它们来实现。在方法定义中,我们必须为每个参数提供名称和类型。

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);

我们调用 addition 对象的 AddTwoValues 方法。它接受两个值。这些值被传递给该方法。该方法返回一个值,该值被赋值给 x 变量。

C# 可变数量的参数

一个方法可以接受可变数量的参数。为此,我们使用 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 方法可以接受可变数量的整数值。所有值都添加到 list 数组中。

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# 方法可以使用元组返回多个值。

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

C# 匿名方法

匿名方法是不具有名称的内联方法。匿名方法通过消除创建单独方法的需求来减少编码开销。如果没有匿名方法,开发人员通常必须创建一个类才能仅调用一个方法。

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# 按值传递参数,按引用传递参数

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 方法在 ab 变量之间交换数字。原始变量不受影响。

int a = 4;
int b = 7;

一开始,这两个变量被初始化。

Swap(a, b);

我们调用 Swap 方法。该方法接受 ab 变量作为参数。

int temp = a;
a = b;
b = temp;

Swap 方法内部,我们更改这些值。请注意,ab 变量是在本地定义的。它们仅在 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.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 关键字,它可能未初始化。方法定义和方法调用都必须使用 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 变量已声明,但未初始化。我们将该变量传递给 SetValue 方法。

void SetValue(out int i)
{
    i = 12;
}

SetValue 方法内部,它被分配一个值,该值稍后被打印到控制台。

C# 方法重载

方法重载允许创建多个具有相同名称的方法,但这些方法在输入类型上彼此不同。

方法重载有什么好处? Qt5 库提供了一个很好的用法示例。 QPainter 类有三个绘制矩形的方法。 它们的名称是 drawRect,它们的参数不同。 一个接受对浮点矩形对象的引用,另一个接受对整数矩形对象的引用,最后一个接受四个参数:x,y,宽度,高度。 如果 Qt 的开发语言 C++ 没有方法重载,则库的创建者将不得不像 drawRectRectFdrawRectRectdrawRectXYWH 这样命名方法。 使用方法重载的解决方案更加优雅。

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));

我们调用所有三个方法。

$ dotnet run
0
20
50

C# 递归

在数学和计算机科学中,递归是一种定义方法的方式,其中在方法本身的定义中应用被定义的方法。 换句话说,递归方法调用自身来完成其工作。 递归是一种广泛使用的方法,可以解决许多编程任务。

一个典型的例子是计算阶乘。

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
3628800

C# 方法作用域

在方法内部声明的变量具有方法作用域。 名称的作用域是程序文本的区域,在该区域中,可以在没有名称限定的情况下引用该名称声明的实体。 在方法内部声明的变量具有方法作用域。 也称为局部作用域。 该变量仅在此特定方法中有效。

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);
    }
}

在前面的示例中,我们定义了 x 变量,它位于 exec1exec2 方法的外部。 该变量具有类作用域。 它在 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.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 变量(也称为局部变量)可以直接通过其名称访问。 可以使用 this 关键字引用实例变量。

$ dotnet run
1
3

C# 静态方法

静态方法在没有对象实例的情况下被调用。 要调用静态方法,我们使用类的名称和点运算符。 静态方法只能使用静态成员变量。 静态方法通常用于表示不会响应对象状态而更改的数据或计算。 一个例子是数学库,它包含用于各种计算的静态方法。 我们使用 static 关键字来声明静态方法。 如果不存在静态修饰符,则该方法被称为实例方法。 我们不能在静态方法中使用 this 关键字。 它只能在实例方法中使用。

Main 方法是 C# 控制台和 GUI 应用程序的入口点。 在 C# 中,Main 方法必须是静态的。 在应用程序启动之前,尚未创建任何对象。 要调用非静态方法,我们需要一个对象实例。 静态方法在类实例化之前就存在,因此静态修饰符应用于主入口点。

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 方法。 它使用一个静态 Id 成员。

Basic.ShowInfo();

要调用静态方法,我们不需要对象实例。 我们使用类名和点运算符来调用该方法。

$ dotnet run
This is Basic class
The Id is: 2321

C# 方法隐藏

当派生类从基类继承时,它可以定义基类中已经存在的方法。 我们说我们隐藏了我们从中派生的类的方法。 为了显式地告知编译器我们隐藏方法的意图,我们使用 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");
    }
}

我们有两个类:DerivedBase 类。 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.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");
    }
}

我们创建了一个 BaseDerived 对象的数组。 我们遍历数组并在它们所有对象上调用 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() };

这里我们创建了一个 BaseDerived 对象的数组。 请注意,我们在数组声明中使用了 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 引入了局部函数。 这些是在其他方法内部定义的函数。

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 方法内部定义和调用的。

C# 密封方法

密封方法使用相同的签名重写继承的虚方法。 密封方法也应该用 override 修饰符标记。 使用 sealed 修饰符可以防止派生类进一步重写该方法。 *进一步* 这个词很重要。 首先,一个方法必须是虚方法。 它稍后必须被重写。 在这一点上,它可以被密封。

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();
    }
}

在前面的示例中,我们在类 B 中密封了方法 G

public sealed override void G()
{
    Console.WriteLine("B.G");
}

方法 G 重写了 B 类的祖先中具有相同名称的方法。 它也被密封以防止进一步重写该方法。

/*public override void G()
{
    Console.WriteLine("C.G");
}*/

这些行被注释掉,否则代码示例将无法编译。 编译器会给出以下错误:Program.cs(38,30): error CS0239: 'C.G()': cannot override inherited member 'B.G()' because it is sealed

c.G();

此行将“B.G”打印到控制台。

$ dotnet run
B.F
B.G
C.F
B.G

C# 方法的表达式主体定义

方法的表达式主体定义允许我们以非常简洁、可读的形式定义方法实现。

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 方法的主体。

public override string ToString() => $"{Name} is a {Occupation}";

表达式主体定义简化了语法。

在本文中,我们介绍了 C# 方法。

来源

方法 - 编程指南

作者

我叫 Jan Bodnar,是一位热情的程序员,拥有丰富的编程经验。 自 2007 年以来,我一直在撰写编程文章。 迄今为止,我撰写了超过 1,400 篇文章和 8 本电子书。 我拥有超过十年的编程教学经验。

列出所有 C# 教程