C# 中的方法

方法是包含一系列语句的代码块。 程序通过调用该方法并指定任何所需的方法参数使语句得以执行。 在 C# 中,每个执行的指令均在方法的上下文中执行。 Main 方法是每个 C# 应用程序的入口点,并在启动程序时由公共语言运行时 (CLR) 调用。

注意

本主题讨论命名的方法。 有关匿名函数的信息,请参阅 Lambda 表达式

方法签名

通过指定以下内容在 classrecordstruct 中声明方法:

  • 可选的访问级别,如 publicprivate。 默认值为 private
  • 可选的修饰符,如 abstractsealed
  • 返回值,或 void(如果该方法不具有)。
  • 方法名称。
  • 任何方法参数。 方法参数在括号内,并且用逗号分隔。 空括号指示方法不需要任何参数。

这些部分一同构成方法签名。

重要

出于方法重载的目的,方法的返回类型不是方法签名的一部分。 但是在确定委托和它所指向的方法之间的兼容性时,它是方法签名的一部分。

以下实例定义了一个包含五种方法的名为 Motorcycle 的类:

using System;

abstract class Motorcycle
{
    // Anyone can call this.
    public void StartEngine() {/* Method statements here */ }

    // Only derived classes can call this.
    protected void AddGas(int gallons) { /* Method statements here */ }

    // Derived classes can override the base class implementation.
    public virtual int Drive(int miles, int speed) { /* Method statements here */ return 1; }

    // Derived classes can override the base class implementation.
    public virtual int Drive(TimeSpan time, int speed) { /* Method statements here */ return 0; }

    // Derived classes must implement this.
    public abstract double GetTopSpeed();
}

Motorcycle 类包括一个重载的方法 Drive。 两个方法具有相同的名称,但必须根据其参数类型来区分。

方法调用

方法可以是实例的或静态的。 调用实例方法需要将对象实例化,并对该对象调用方法;实例方法可对该实例及其数据进行操作。 通过引用该方法所属类型的名称来调用静态方法;静态方法不对实例数据进行操作。 尝试通过对象实例调用静态方法会引发编译器错误。

调用方法就像访问字段。 在对象名称(如果调用实例方法)或类型名称(如果调用 static 方法)后添加一个句点、方法名称和括号。 自变量列在括号里,并且用逗号分隔。

该方法定义指定任何所需参数的名称和类型。 调用方调用该方法时,它为每个参数提供了称为自变量的具体值。 实参必须与形参类型兼容,但调用代码中使用的实参名(如果有)不需要与方法中定义的形参名相同。 在下面示例中,Square 方法包含名为 i 的类型为 int 的单个参数。 第一种方法调用将向 Square 方法传递名为 numint 类型的变量;第二个方法调用将传递数值常量;第三个方法调用将传递表达式。

public class SquareExample
{
   public static void Main()
   {
      // Call with an int variable.
      int num = 4;
      int productA = Square(num);

      // Call with an integer literal.
      int productB = Square(12);

      // Call with an expression that evaluates to int.
      int productC = Square(productA * 3);
   }

   static int Square(int i)
   {
      // Store input argument in a local variable.
      int input = i;
      return input * input;
   }
}

方法调用最常见的形式是使用位置自变量;它会以与方法参数相同的顺序提供自变量。 因此,可在以下示例中调用 Motorcycle 类的方法。 例如,Drive 方法的调用包含两个与方法语法中的两个参数对应的自变量。 第一个成为 miles 参数的值,第二个成为 speed 参数的值。

class TestMotorcycle : Motorcycle
{
    public override double GetTopSpeed()
    {
        return 108.4;
    }

    static void Main()
    {

        TestMotorcycle moto = new TestMotorcycle();

        moto.StartEngine();
        moto.AddGas(15);
        moto.Drive(5, 20);
        double speed = moto.GetTopSpeed();
        Console.WriteLine("My top speed is {0}", speed);
    }
}

调用方法时,也可以使用命名的自变量,而不是位置自变量。 使用命名的自变量时,指定参数名,然后后跟冒号(":")和自变量。 只要包含了所有必需的自变量,方法的自变量可以任何顺序出现。 下面的示例使用命名的自变量来调用 TestMotorcycle.Drive 方法。 在此示例中,命名的自变量以相反于方法参数列表中的顺序进行传递。

using System;

class TestMotorcycle : Motorcycle
{
    public override int Drive(int miles, int speed)
    {
        return (int)Math.Round(((double)miles) / speed, 0);
    }

    public override double GetTopSpeed()
    {
        return 108.4;
    }

    static void Main()
    {

        TestMotorcycle moto = new TestMotorcycle();
        moto.StartEngine();
        moto.AddGas(15);
        var travelTime = moto.Drive(speed: 60, miles: 170);
        Console.WriteLine("Travel time: approx. {0} hours", travelTime);
    }
}
// The example displays the following output:
//      Travel time: approx. 3 hours

可以同时使用位置自变量和命名的自变量调用方法。 但是,只有当命名参数位于正确位置时,才能在命名自变量后面放置位置参数。 下面的示例使用一个位置自变量和一个命名的自变量从上一个示例中调用 TestMotorcycle.Drive 方法。

var travelTime = moto.Drive(170, speed: 55);

继承和重写方法

除了类型中显式定义的成员,类型还继承在其基类中定义的成员。 由于托管类型系统中的所有类型都直接或间接继承自 Object 类,因此所有类型都继承其成员,如 Equals(Object)GetType()ToString()。 下面的示例定义 Person 类,实例化两个 Person 对象,并调用 Person.Equals 方法来确定两个对象是否相等。 但是,Equals 方法不是在 Person 类中定义;而是继承自 Object

using System;

public class Person
{
   public String FirstName;
}

public class ClassTypeExample
{
   public static void Main()
   {
      var p1 = new Person();
      p1.FirstName = "John";
      var p2 = new Person();
      p2.FirstName = "John";
      Console.WriteLine("p1 = p2: {0}", p1.Equals(p2));
   }
}
// The example displays the following output:
//      p1 = p2: False

类型可以使用 override 关键字并提供重写方法的实现来重写继承的成员。 方法签名必须与重写的方法的签名一样。 下面的示例类似于上一个示例,只不过它重写 Equals(Object) 方法。 (它还重写 GetHashCode() 方法,因为这两种方法用于提供一致的结果。)

using System;

public class Person
{
    public String FirstName;

    public override bool Equals(object obj)
    {
        var p2 = obj as Person;
        if (p2 == null)
            return false;
        else
            return FirstName.Equals(p2.FirstName);
    }

    public override int GetHashCode()
    {
        return FirstName.GetHashCode();
    }
}

public class Example
{
    public static void Main()
    {
        var p1 = new Person();
        p1.FirstName = "John";
        var p2 = new Person();
        p2.FirstName = "John";
        Console.WriteLine("p1 = p2: {0}", p1.Equals(p2));
    }
}
// The example displays the following output:
//      p1 = p2: True

快速参考

C# 中的所有类型不是值类型就是引用类型。 有关内置值类型的列表,请参阅类型。 默认情况下,值类型和引用类型均按值传递给方法。

按值传递参数

值类型按值传递给方法时,传递的是对象的副本而不是对象本身。 因此,当控件返回调用方时,对已调用方法中的对象的更改对原始对象无影响。

下面的示例按值向方法传递值类型,且调用的方法尝试更改值类型的值。 它定义属于值类型的 int 类型的变量,将其值初始化为 20,并将该类型传递给将变量值改为 30 的名为 ModifyValue 的方法。 但是,返回方法时,变量的值保持不变。

using System;

public class ByValueExample
{
   public static void Main()
   {
      int value = 20;
      Console.WriteLine("In Main, value = {0}", value);
      ModifyValue(value);
      Console.WriteLine("Back in Main, value = {0}", value);
   }

   static void ModifyValue(int i)
   {
      i = 30;
      Console.WriteLine("In ModifyValue, parameter value = {0}", i);
      return;
   }
}
// The example displays the following output:
//      In Main, value = 20
//      In ModifyValue, parameter value = 30
//      Back in Main, value = 20

引用类型的对象按值传递到方法中时,将按值传递对对象的引用。 也就是说,该方法接收的不是对象本身,而是指示该对象位置的自变量。 控件返回到调用方法时,如果通过使用此引用更改对象的成员,此更改将反映在对象中。 但是,当控件返回到调用方时,替换传递到方法的对象对原始对象无影响。

下面的示例定义名为 SampleRefType 的类(属于引用类型)。 它实例化 SampleRefType 对象,将 44 赋予其 value 字段,并将该对象传递给 ModifyObject 方法。 该示例执行的内容实质上与先前示例相同,即均按值将自变量传递到方法。 但因为使用了引用类型,结果会有所不同。 ModifyObject 中所做的对 obj.value 字段的修改,也会将 Main 方法中的自变量 rtvalue 字段更改为 33,如示例中的输出值所示。

using System;

public class SampleRefType
{
    public int value;
}

public class ByRefTypeExample
{
    public static void Main()
    {
        var rt = new SampleRefType();
        rt.value = 44;
        ModifyObject(rt);
        Console.WriteLine(rt.value);
    }

    static void ModifyObject(SampleRefType obj)
    {
        obj.value = 33;
    }
}

按引用传递参数

如果想要更改方法中的自变量值并想要在控件返回到调用方法时反映出这一更改,请按引用传递参数。 要按引用传递参数,请使用 refout 关键字。 还可以使用 in 关键字,按引用传递值以避免复制,但仍防止修改。

下面的示例与上一个示例完全一样,只是换成按引用将值传递给 ModifyValue 方法。 参数值在 ModifyValue 方法中修改时,值中的更改将在控件返回调用方时反映出来。

using System;

public class ByRefExample
{
   public static void Main()
   {
      int value = 20;
      Console.WriteLine("In Main, value = {0}", value);
      ModifyValue(ref value);
      Console.WriteLine("Back in Main, value = {0}", value);
   }

   static void ModifyValue(ref int i)
   {
      i = 30;
      Console.WriteLine("In ModifyValue, parameter value = {0}", i);
      return;
   }
}
// The example displays the following output:
//      In Main, value = 20
//      In ModifyValue, parameter value = 30
//      Back in Main, value = 30

引用参数所使用的常见模式涉及交换变量值。 将两个变量按引用传递给一个方法,然后该方法将二者内容进行交换。 下面的示例交换整数值。

using System;

public class RefSwapExample
{
   static void Main()
   {
      int i = 2, j = 3;
      System.Console.WriteLine("i = {0}  j = {1}" , i, j);

      Swap(ref i, ref j);

      System.Console.WriteLine("i = {0}  j = {1}" , i, j);
   }

   static void Swap(ref int x, ref int y)
   {
      int temp = x;
      x = y;
      y = temp;
   }
}
// The example displays the following output:
//      i = 2  j = 3
//      i = 3  j = 2

通过传递引用类型的参数,可以更改引用本身的值,而不是其单个元素或字段的值。

参数数组

有时,向方法指定精确数量的自变量这一要求是受限的。 通过使用 params 关键字来指示一个参数是一个参数数组,可通过可变数量的自变量来调用方法。 使用 params 关键字标记的参数必须为数组类型,并且必须是该方法的参数列表中的最后一个参数。

然后,调用方可通过以下四种方式中的任一种来调用方法:

  • 传递相应类型的数组,该类型包含所需数量的元素。
  • 向该方法传递相应类型的单独自变量的逗号分隔列表。
  • 传递 null
  • 不向参数数组提供参数。

以下示例定义了一个名为 GetVowels 的方法,该方法返回参数数组中的所有元音。 Main 方法演示了调用方法的全部四种方式。 调用方不需要为包含 params 修饰符的形参提供任何实参。 在这种情况下,参数是一个空数组。

using System;
using System.Linq;

class ParamsExample
{
    static void Main()
    {
        string fromArray = GetVowels(new[] { "apple", "banana", "pear" });
        Console.WriteLine($"Vowels from array: '{fromArray}'");

        string fromMultipleArguments = GetVowels("apple", "banana", "pear");
        Console.WriteLine($"Vowels from multiple arguments: '{fromMultipleArguments}'");
        
        string fromNull = GetVowels(null);
        Console.WriteLine($"Vowels from null: '{fromNull}'");

        string fromNoValue = GetVowels();
        Console.WriteLine($"Vowels from no value: '{fromNoValue}'");
    }

    static string GetVowels(params string[] input)
    {
        if (input == null || input.Length == 0)
        {
            return string.Empty;
        }

        var vowels = new char[] { 'A', 'E', 'I', 'O', 'U' };
        return string.Concat(
            input.SelectMany(
                word => word.Where(letter => vowels.Contains(char.ToUpper(letter)))));
    }
}

// The example displays the following output:
//     Vowels from array: 'aeaaaea'
//     Vowels from multiple arguments: 'aeaaaea'
//     Vowels from null: ''
//     Vowels from no value: ''

可选参数和自变量

方法定义可指定其参数是必需的还是可选的。 默认情况下,参数是必需的。 通过在方法定义中包含参数的默认值来指定可选参数。 调用该方法时,如果未向可选参数提供自变量,则改为使用默认值。

参数的默认值必须由以下几种表达式中的一种来赋予:

  • 常量,例如文本字符串或数字。

  • default(SomeType) 形式的表达式,其中 SomeType 可以是值类型或引用类型。 如果是引用类型,那么它实际上与指定 null 相同。 可以使用 default 字面量,因为编译器可以从参数的声明中推断出类型。

  • new ValType() 形式的表达式,其中 ValType 是值类型。 这会调用该值类型的隐式无参数构造函数,该函数不是类型的实际成员。

    注意

    在 C# 10 及更高版本中,当 new ValType() 形式的表达式调用某一值类型的显式定义的无参数构造函数时,编译器便会生成错误,因为默认参数值必须是编译时常数。 使用 default(ValType) 表达式或 default 字面量提供默认参数值。 有关无参数构造函数的详细信息,请参阅结构类型一文的结构初始化和默认值部分。

如果某个方法同时包含必需的和可选的参数,则在参数列表末尾定义可选参数,即在定义完所有必需参数之后定义。

下面的示例定义方法 ExampleMethod,它具有一个必需参数和两个可选参数。

using System;

public class Options
{
    public void ExampleMethod(int required, int optionalInt = default,
                              string? description = default)
   {
        var msg = $"{description ?? "N/A"}: {required} + {optionalInt} = {required + optionalInt}";
        Console.WriteLine(msg);
   }
}

如果使用位置自变量调用包含多个可选自变量的方法,调用方必须逐一向所有需要自变量的可选参数提供自变量。 例如,在使用 ExampleMethod 方法的情况下,如果调用方向 description 形参提供实参,还必须向 optionalInt 形参提供一个实参。 opt.ExampleMethod(2, 2, "Addition of 2 and 2"); 是一个有效的方法调用;opt.ExampleMethod(2, , "Addition of 2 and 0"); 生成编译器错误“缺少自变量”。

如果使用命名的自变量或位置自变量和命名的自变量的组合来调用某个方法,调用方可以省略方法调用中的最后一个位置自变量后的任何自变量。

下面的示例三次调用了 ExampleMethod 方法。 前两个方法调用使用位置自变量。 第一个方法同时省略了两个可选自变量,而第二个省略了最后一个自变量。 第三个方法调用向必需的参数提供位置自变量,但使用命名的自变量向 description 参数提供值,同时省略 optionalInt 自变量。

public class OptionsExample
{
   public static void Main()
   {
      var opt = new Options();
      opt.ExampleMethod(10);
      opt.ExampleMethod(10, 2);
      opt.ExampleMethod(12, description: "Addition with zero:");
   }
}
// The example displays the following output:
//      N/A: 10 + 0 = 10
//      N/A: 10 + 2 = 12
//      Addition with zero:: 12 + 0 = 12

使用可选参数会影响重载决策,或影响 C# 编译器决定方法应调用哪个特定重载时所使用的方式,如下所示:

  • 如果方法、索引器或构造函数的每个参数是可选的,或按名称或位置对应于调用语句中的单个自变量,且该自变量可转换为参数的类型,则方法、索引器或构造函数为执行的候选项。
  • 如果找到多个候选项,则会将用于首选转换的重载决策规则应用于显式指定的自变量。 将忽略可选形参已省略的实参。
  • 如果两个候选项不相上下,则会将没有可选形参的候选项作为首选项,对于这些可选形参,已在调用中为其省略了实参。 这是重载决策中的常规引用的结果,该引用用于参数较少的候选项。

返回值

方法可以将值返回到调用方。 如果列在方法名之前的返回类型不是 void,则该方法可通过使用 return 关键字返回值。 带 return 关键字且后跟与返回类型匹配的变量、常数或表达式的语句将向方法调用方返回该值。 具有非空的返回类型的方法都需要使用 return 关键字来返回值。 return 关键字还会停止执行该方法。

如果返回类型为 void,没有值的 return 语句仍可用于停止执行该方法。 没有 return 关键字,当方法到达代码块结尾时,将停止执行。

例如,这两种方法都使用 return 关键字来返回整数:

class SimpleMath
{
    public int AddTwoNumbers(int number1, int number2)
    {
        return number1 + number2;
    }

    public int SquareANumber(int number)
    {
        return number * number;
    }
}

若要使用从方法返回的值,调用方法可以在相同类型的值足够的地方使用该方法调用本身。 也可以将返回值分配给变量。 例如,以下两个代码示例实现了相同的目标:

int result = obj.AddTwoNumbers(1, 2);
result = obj.SquareANumber(result);
// The result is 9.
Console.WriteLine(result);
result = obj.SquareANumber(obj.AddTwoNumbers(1, 2));
// The result is 9.
Console.WriteLine(result);

在这种情况下,使用本地变量 result存储值是可选的。 此步骤可以帮助提高代码的可读性,或者如果需要存储该方法整个范围内自变量的原始值,则此步骤可能很有必要。

有时,需要方法返回多个值。 可以使用“元组类型”和“元组文本”轻松执行此操作。 元组类型定义元组元素的数据类型。 元组文本提供返回的元组的实际值。 在下面的示例中,(string, string, string, int) 定义 GetPersonalInfo 方法返回的元组类型。 表达式 (per.FirstName, per.MiddleName, per.LastName, per.Age) 是元组文本;方法返回 PersonInfo 对象的第一个、中间和最后一个名称及其使用期限。

public (string, string, string, int) GetPersonalInfo(string id)
{
    PersonInfo per = PersonInfo.RetrieveInfoById(id);
    return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}

然后调用方可通过类似以下的代码使用返回的元组:

var person = GetPersonalInfo("111111111");
Console.WriteLine($"{person.Item1} {person.Item3}: age = {person.Item4}");

还可向元组类型定义中的元组元素分配名称。 下面的示例展示 GetPersonalInfo 方法的替代版本,该方法使用命名的元素:

public (string FName, string MName, string LName, int Age) GetPersonalInfo(string id)
{
    PersonInfo per = PersonInfo.RetrieveInfoById(id);
    return (per.FirstName, per.MiddleName, per.LastName, per.Age);
}

然后可修改上一次对 GetPersonalInfo 方法的调用,如下所示:

var person = GetPersonalInfo("111111111");
Console.WriteLine($"{person.FName} {person.LName}: age = {person.Age}");

如果将数组作为自变量传递给一个方法,并修改各个元素的值,则该方法不一定会返回该数组,尽管选择这么操作的原因是为了实现更好的样式或功能性的值流。 这是因为 C# 会按值传递所有引用类型,而数组引用的值是指向该数组的指针。 在下面的示例中,引用该数组的任何代码都能观察到在 DoubleValues 方法中对 values 数组内容的更改。


using System;

public class ArrayValueExample
{
   static void Main(string[] args)
   {
      int[] values = { 2, 4, 6, 8 };
      DoubleValues(values);
      foreach (var value in values)
         Console.Write("{0}  ", value);
   }

   public static void DoubleValues(int[] arr)
   {
      for (int ctr = 0; ctr <= arr.GetUpperBound(0); ctr++)
         arr[ctr] = arr[ctr] * 2;
   }
}
// The example displays the following output:
//       4  8  12  16

扩展方法

通常,可以通过两种方式向现有类型添加方法:

  • 修改该类型的源代码。 当然,如果并不拥有该类型的源代码,则无法执行该操作。 并且,如果还添加任何专用数据字段来支持该方法,这会成为一项重大更改。
  • 在派生类中定义新方法。 无法使用其他类型(如结构和枚举)的继承来通过此方式添加方法。 也不能使用此方式向封闭类“添加”方法。

使用扩展方法,可向现有类型“添加”方法,而无需修改类型本身或在继承的类型中实现新方法。 扩展方法也无需驻留在与其扩展的类型相同的程序集中。 要把扩展方法当作是定义的类型成员一样调用。

有关详细信息,请参阅扩展方法

异步方法

通过使用异步功能,你可以调用异步方法而无需使用显式回调,也不需要跨多个方法或 lambda 表达式来手动拆分代码。

如果用 async 修饰符标记方法,则可以在该方法中使用 await 运算符。 当控件到达异步方法中的 await 表达式时,如果等待的任务未完成,控件将返回到调用方,并在等待任务完成前,包含 await 关键字的方法中的进度将一直处于挂起状态。 任务完成后,可以在方法中恢复执行。

注意

异步方法在遇到第一个尚未完成的 awaited 对象或到达异步方法的末尾时(以先发生者为准),将返回到调用方。

异步方法通常具有 Task<TResult>TaskIAsyncEnumerable<T>void 返回类型。 void 返回类型主要用于定义需要 void 返回类型的事件处理程序。 无法等待返回 void 的异步方法,并且返回 void 方法的调用方无法捕获该方法引发的异常。 异步方法可以具有任何类似任务的返回类型

在下面的示例中,DelayAsync 是一个异步方法,包含返回整数的 return 语句。 由于它是异步方法,其方法声明必须具有返回类型 Task<int>。 因为返回类型是 Task<int>DoSomethingAsyncawait 表达式的计算将如以下 int result = await delayTask 语句所示得出整数。

class Program
{
    static Task Main() => DoSomethingAsync();

    static async Task DoSomethingAsync()
    {
        Task<int> delayTask = DelayAsync();
        int result = await delayTask;

        // The previous two statements may be combined into
        // the following statement.
        //int result = await DelayAsync();

        Console.WriteLine($"Result: {result}");
    }

    static async Task<int> DelayAsync()
    {
        await Task.Delay(100);
        return 5;
    }
}
// Example output:
//   Result: 5

异步方法不能声明任何 inrefout 参数,但是可以调用具有这类参数的方法。

有关异步方法的详细信息,请参阅使用 Async 和 Await 的异步编程异步返回类型

Expression-Bodied 成员

具有立即仅返回表达式结果,或单个语句作为方法主题的方法定义很常见。 以下是使用 => 定义此类方法的语法快捷方式:

public Point Move(int dx, int dy) => new Point(x + dx, y + dy);
public void Print() => Console.WriteLine(First + " " + Last);
// Works with operators, properties, and indexers too.
public static Complex operator +(Complex a, Complex b) => a.Add(b);
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);

如果该方法返回 void 或是异步方法,则该方法的主体必须是语句表达式(与 lambda 相同)。 对于属性和索引器,两者必须是只读的,并且不使用 get 访问器关键字。

迭代器

迭代器对集合执行自定义迭代,如列表或数组。 迭代器使用 yield return 语句返回元素,每次返回一个。 到达 yield return 语句后,会记住当前位置,以便调用方可以请求序列中的下一个元素。

迭代器的返回类型可以是 IEnumerableIEnumerable<T>IAsyncEnumerable<T>IEnumeratorIEnumerator<T>

有关更多信息,请参见 迭代器

另请参阅