实现类方法和参数

已完成

方法是包含一系列语句的代码块。 程序通过调用该方法并指定任何必需的方法参数来执行语句。 在 C# 中,每个执行的指令都在方法的上下文中执行。

方法签名

方法通过指定在类、记录或结构中声明:

  • 可选访问级别,例如 public 或 private。 默认值为 private。
  • 可选修饰符,如 abstract 或 sealed。
  • 如果方法没有,则返回值或 void。
  • 方法名称。
  • 任何方法参数。 方法参数括在括号中,用逗号分隔。 空括号指示该方法不需要任何参数。

这些部分协同工作以形成方法签名。

重要

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

以下示例定义一个名为 Motorcycle 的类,该类包含五种方法:


namespace MotorCycleExample
{
    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。 这两个 Drive 方法的名称相同,但参数类型不同。

方法调用

方法可以是 实例,也可以 静态。 必须实例化对象以在该实例上调用实例方法;实例方法对该实例及其数据进行作。 通过引用方法所属的类型的名称来调用静态方法;静态方法不对实例数据进行作。 尝试通过对象实例调用静态方法将生成编译器错误。

调用方法类似于访问字段。 在对象名称(如果要调用实例方法)或类型名称(如果调用静态方法),添加句点、方法的名称和括号。 参数列在括号内,用逗号分隔。

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


public static 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;
    }
}

值和引用参数

C# 中的类型是值类型或引用类型。 默认情况下,值类型和引用类型都按值传递给方法。

值类型参数

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

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


public static class ByValueExample
{
    public static void Main()
    {
        var 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 对象,为其 value 字段分配 44 个,并将对象传递给 ModifyObject 方法。 此示例实质上与上一个示例相同(它按值将参数传递给方法)。 但是,结果不同,因为使用引用类型而不是值类型。 对 ModifyObject 字段 obj.value 所做的修改也会更改参数 value 字段,rt。 当 Main 方法显示 rt 的值时,我们看到它已更新为 33,如示例所示。


public class SampleRefType
{
    public int value;
}

public static class ByRefTypeExample
{
    public static void Main()
    {
        var rt = new SampleRefType { value = 44 };
        Console.WriteLine("In Main, rt.value = {0}", rt.value);
        ModifyObject(rt);
        Console.WriteLine("Back in Main, rt.value = {0}", rt.value);
    }

    static void ModifyObject(SampleRefType obj)
    {
        obj.value = 33;
        Console.WriteLine("In ModifyObject, obj.value = {0}", obj.value);
    }
}
// The example displays the following output:
//      In Main, rt.value = 44
//      In ModifyObject, obj.value = 33
//      Back in Main, rt.value = 33

引用类型参数

如果要更改方法中的参数值,并且希望在控件返回到调用方法时反映该更改,则通过引用传递参数。 若要按引用传递参数,请使用 ref 或 out 关键字。 还可以通过引用传递值以避免复制,但仍会阻止使用 in 关键字进行修改。

以下示例与上一个示例相同,但值通过对 ModifyValue 方法的引用传递。 当参数的值在 ModifyValue 方法中修改时,当控件返回到调用方时,将反映值的变化。


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

    private 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

ref 参数使用的常见模式涉及交换变量的值。 通过引用将两个变量传递给方法,该方法交换其内容。 以下示例交换整数值。


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

        Swap(ref i, ref j);

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

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

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

参数集合

有时,指定方法的确切参数数的要求是限制性的。 通过使用 params 关键字来指示参数是参数集合,允许使用可变数量的参数调用方法。 使用 params 关键字标记的参数必须是集合类型,并且它必须是方法的参数列表中的最后一个参数。

然后,调用方可以通过参数参数的四种方式之一调用该方法:

  • 通过传递包含所需元素数的相应类型的集合。 该示例使用集合表达式,以便编译器创建适当的集合类型。
  • 通过将相应类型的各个参数的逗号分隔列表传递给方法。 编译器创建适当的集合类型。
  • 通过 null。
  • 不向参数集合提供参数。

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


static class ParamsExample
{
    static void Main()
    {
        string fromArray = GetVowels(["apple", "banana", "pear"]);
        Console.WriteLine($"Vowels from collection expression: '{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 IEnumerable<string>? input)
    {
        if (input == null || !input.Any())
        {
            return string.Empty;
        }

        char[] vowels = ['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: ''

注意

在 C# 13 之前,params 修饰符只能与单维数组一起使用。

返回方法的值

方法可以将值返回到调用方。 如果返回类型(方法名称之前列出的类型)未 void,则该方法可以使用 return 关键字返回值。 带有 return 关键字的语句,后跟与返回类型匹配的变量、常量或表达式将该值返回给方法调用方。 使用非 void 返回类型的方法需要使用 return 关键字返回值。 return 关键字也会停止执行该方法。

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

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


class SimpleMath
{
    public int AddTwoNumbers(int number1, int number2) =>
        number1 + number2;

    public int SquareANumber(int number) =>
        number * number;
}

此示例使用表达式体形成员来分配方法的返回值。 此语法是使用单个语句(表达式)计算返回值的方法的简写。

还可以选择使用语句正文和 return 语句定义方法:


class SimpleMathExtnsion
{
    public int DivideTwoNumbers(int number1, int number2)
    {
        return number1 / number2;
    }
}

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


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 = obj2.DivideTwoNumbers(6,2);
// The result is 3.
Console.WriteLine(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# 按值传递所有引用类型,数组引用的值是指向数组的指针。

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 访问器关键字。