Skip to content

方法

方法的基本结构

一个基本的 C# 方法定义如下:

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

其中包括:

  1. 可选的访问级别,如 public 或 private。 默认值为 private。
  2. 可选的修饰符,如 abstract 或 sealed。
  3. 返回值,或 void(如果该方法不具有)。
  4. 方法名称。
  5. 方法参数。 方法参数在括号内,并且用逗号分隔。 空括号指示方法不需要任何参数。
  6. 方法体:{/* Method statements here */ }是方法执行的逻辑代码块。return 关键字用于返回结果。

定义方法后,可以通过类实例调用它:

C#
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;
    }
}

访问修饰符

参考访问修饰符

返回类型

void

如果返回类型为 void,没有值的 return 语句仍可用于停止执行该方法。

没有return关键字,当方法到达代码块结尾时,会停止执行。

单个值(值类型,引用类型)

如果列在方法名之前的返回类型不是 void,则该方法可通过使用 return 关键字返回值。

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

多个值(元祖)

C#
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}");

参数

按值传递参数

按值传递是 C# 中的默认参数传递方式。在按值传递中,方法接收到的是参数值的副本,对参数的任何修改不会影响调用方法时传入的原始数据。

值类型

参数为值类型(内置值类型、struct、枚举、元祖),方法中参数为副本,因此在方法中对参数修改并不会影响原始值

C#
public void ChangeValue(int number)
{
    number = 10;  // 修改的是 number 的副本
}

int original = 5;
ChangeValue(original);
Console.WriteLine(original);  // 输出 5,原始值没有改变

引用类型

参数为引用类型时(class、数组等),默认情况下引用类型也是按值传递的,但它传递的是对象的引用副本。

因此,修改对象的内容会影响原始对象,但重新赋值对象不会影响原始对象。(所以string类型重新赋值不会影响原始数据)

C#
public void ModifyArray(int[] numbers)
{
    numbers[0] = 99;  // 修改数组内容
}

int[] myArray = { 1, 2, 3 };
ModifyArray(myArray);
Console.WriteLine(myArray[0]);  // 输出 99,数组内容被修改

按引用传递参数

在按引用传递中,方法接收到的是参数的引用,即传递的是参数的地址。对参数的修改会直接影响到原始数据。

按引用传递需要使用 ref、ref readonly、in 或 out 。

注意

类的成员不能具有仅在 ref、ref readonly、in 或 out 方面不同的签名。 如果类型的两个成员之间的唯一区别在于其中一个具有 ref 参数,而另一个具有 out、ref readonly 或 in 参数,则会发生编译器错误。

当某个参数具有上述修饰符之一时,相应的自变量可以具有兼容的修饰符:

  1. 参数 ref 的自变量必须包含 ref 修饰符。
  2. 参数 out 的自变量必须包含 out 修饰符。
  3. 参数 in 的自变量可以选择性包含 in 修饰符。 如果 ref 修饰符用于自变量,编译器会发出警告。
  4. ref readonly 参数的自变量应包含 in 或 ref 修饰符,但不能包含两者。 如果两个修饰符均未包含,编译器会发出警告。

ref 参数修饰符

若要使用 ref 参数,方法定义和调用方法均必须显式使用 ref 关键字,

C#
public void ModifyValue(ref int number)
{
    number += 10;  // 修改原始变量的值
}

int myNumber = 5;
ModifyValue(ref myNumber);  // 传递已初始化的变量
Console.WriteLine(myNumber);  // 输出 15,变量已被修改

注意

相对于out, ref在将参数传递给方法之前,参数必须已经初始化。这意味着调用方法时,传入的变量已经有一个有效的值。

out 参数修饰符

out 关键字也可以用于按引用传递,但与 ref 不同的是,传入方法的结构体无需初始化,且方法内必须对结构体进行赋值。

C#
public void InitializePoint(out Point p)
{
    p = new Point(10, 10);  // 必须为 out 参数赋值
}

Point myPoint;
InitializePoint(out myPoint);
Console.WriteLine(myPoint.X);  // 输出 10,结构体被初始化

ref readonly 参数修饰符

ref readonly 用于按引用传递参数,但不允许在方法内修改参数的值。它确保参数按引用传递,避免复制大型数据结构,同时保证方法内的代码不能修改它。

C#
public struct Point
{
    public int X;
    public int Y;

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

public int GetXValue(ref readonly Point p)
{
    // p.X 是只读的,不能修改 p
    return p.X;
}

Point myPoint = new Point(5, 10);
int xValue = GetXValue(ref myPoint);
Console.WriteLine(xValue);  // 输出 5

in 参数修饰符

in 和 ref readonly 类似,也是用于按引用传递参数并且不允许修改参数的值。但是 in 是 C# 7.2 引入的简化语法,主要用于告诉编译器,方法不会修改传递的参数。它比 ref readonly 更简洁且易于使用。

C#
public int GetYValue(in Point p)
{
    // p.Y 是只读的,不能修改 p
    return p.Y;
}

Point myPoint = new Point(5, 10);
int yValue = GetYValue(in myPoint);
Console.WriteLine(yValue);  // 输出 10

按引用传递引用类型

按引用传递引用类型,不仅内容可以修改,引用本身也可以被改变。

C#
public void ChangeArray(ref int[] numbers)
{
    numbers = new int[] { 10, 20, 30 };  // 修改数组的引用
}

int[] myArray = { 1, 2, 3 };
ChangeArray(ref myArray);
Console.WriteLine(myArray[0]);  // 输出 10,数组引用被修改

可选参数和默认值

可选参数允许你为方法的参数提供默认值,从而在调用方法时可以省略这些参数。默认值的作用是在调用方法时,如果没有为可选参数传递实参,编译器会使用默认值

C#
public void MethodName(int requiredParam, string optionalParam = "default value", int anotherOptionalParam = 10)
{
    // 方法体
}

//调用
// 传递所有参数
MethodName(5, "custom value", 20);

// 省略可选参数,使用默认值
MethodName(5);

// 只省略第二个可选参数,提供第一个可选参数的值
MethodName(5, "another value");

注意

  1. 必需参数必须在可选参数之前
  2. 调用时可选参数必须按顺序省略

命名参数

命名参数(Named Parameters)允许你在调用方法时显式指定参数的名称,而不仅仅是按位置传递参数。这使得代码更加清晰,尤其是当方法具有多个参数时,或是有多个可选参数时。

C#
public static void PrintPersonInfo(string name, int age, string country)
{
    Console.WriteLine($"Name: {name}, Age: {age}, Country: {country}");
}

//调用
// 按位置传递参数(传统方式)
PrintPersonInfo("Alice", 30, "USA");

// 使用命名参数,且顺序可以不一致
PrintPersonInfo(age: 30, name: "Alice", country: "USA");

// 使用命名参数时,可以根据需要只指定某些参数
PrintPersonInfo(name: "Alice", country: "USA", age: 30);

与可选参数结合使用:

命名参数和可选参数经常一起使用。当某些参数是可选的时,命名参数可以使调用方法更简洁。

C#
public static void PrintPersonInfo(string name, int age = 0, string country = "Unknown")
{
    Console.WriteLine($"Name: {name}, Age: {age}, Country: {country}");
}

// 只传递 name,age 和 country 使用默认值
PrintPersonInfo(name: "Alice");

// 只传递 name 和 country,age 使用默认值
PrintPersonInfo(name: "Alice", country: "Canada");

注意

命名参数必须跟随位置参数:如果你使用了位置参数和命名参数,命名参数必须出现在位置参数之后。

参数集合

params 关键字(可变参数)

params 关键字允许你传递可变数量的参数给方法,这些参数将被视为一个数组。

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

C#
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: ''

Tuple(元组)

Tuple 是一种可以包含多个不同类型的元素的集合,可以用于方法返回多个值或接收多种类型的参数

C#
public void PrintPersonInfo(Tuple<string, int, string> personInfo)
{
    Console.WriteLine($"Name: {personInfo.Item1}, Age: {personInfo.Item2}, Country: {personInfo.Item3}");
}

//调用
var personInfo = Tuple.Create("Alice", 30, "USA");
PrintPersonInfo(personInfo);

ValueTuple(值元组)

在 C# 7.0 之后,引入了 ValueTuple,提供了更清晰的命名方式。ValueTuple 类似于 Tuple,但允许为元组元素指定更直观的名称。它可以用于返回多个值或传递一组相关参数。

C#
public void PrintPersonInfo((string name, int age, string country) personInfo)
{
    Console.WriteLine($"Name: {personInfo.name}, Age: {personInfo.age}, Country: {personInfo.country}");
}

PrintPersonInfo(("Alice", 30, "USA"));

PrintPersonInfo((name: "Bob", age: 25, country: "UK"));

重载和重写

方法重载(Overloading)

方法重载是指在同一个类中定义多个同名的方法,但它们的参数列表(参数的个数、类型或顺序)不同。编译器会根据传递的参数来决定调用哪一个重载方法。

C#
public class Calculator
{
    // 重载方法:加法操作
    public int Add(int a, int b)
    {
        return a + b;
    }

    // 重载方法:接受三个参数的加法操作
    public int Add(int a, int b, int c)
    {
        return a + b + c;
    }

    // 重载方法:处理小数类型的加法操作
    public double Add(double a, double b)
    {
        return a + b;
    }
}

Calculator calc = new Calculator();
Console.WriteLine(calc.Add(2, 3));       // 输出 5
Console.WriteLine(calc.Add(2, 3, 4));    // 输出 9
Console.WriteLine(calc.Add(2.5, 3.5));   // 输出 6.0

注意

方法重载与返回类型无关。仅通过改变返回类型不能实现方法重载。

方法重写(Overriding)

方法重写是指在子类中重新定义从父类继承而来的虚方法或抽象方法。通过重写,子类可以为继承自父类的方法提供不同的实现。

要求:

  1. 父类方法必须使用 virtual 或 abstract 关键字修饰。
  2. 子类中的重写方法必须使用 override 关键字。
  3. 子类方法的签名(包括返回类型、方法名、参数)必须与父类被重写的方法完全一致。
C#
public class Animal
{
    // 虚方法,允许子类重写
    public virtual void Speak()
    {
        Console.WriteLine("The animal makes a sound.");
    }
}

public class Dog : Animal
{
    // 重写父类的 Speak 方法
    public override void Speak()
    {
        Console.WriteLine("The dog barks.");
    }
}

public class Cat : Animal
{
    // 重写父类的 Speak 方法
    public override void Speak()
    {
        Console.WriteLine("The cat meows.");
    }
}

Animal myDog = new Dog();
Animal myCat = new Cat();
myDog.Speak();  // 输出 "The dog barks."
myCat.Speak();  // 输出 "The cat meows."

常见的重写

Object 类

Object 类是 C# 中所有类的基类,其中有一些方法常常会被重写,以提供更适合具体类的行为:

  1. ToString():提供对象的字符串表示。
  2. Equals(object obj):判断两个对象是否相等。
  3. GetHashCode():提供对象的哈希值,通常和 Equals() 方法一起重写。

常在集合中元素判断重复时使用

参考比较器接口

Stream

Stream 是一个抽象类,表示字节序列的读取和写入操作。以下方法通常会被重写:

  1. Read(byte[] buffer, int offset, int count):从流中读取字节。
  2. Write(byte[] buffer, int offset, int count):将字节写入流。
  3. Seek(long offset, SeekOrigin origin):设置流的当前位置。
  4. Flush():清除缓冲区,将所有缓冲的输出写入底层设备。

Task 类

Task 类表示异步操作。可以继承 Task 类并重写 RunSynchronously()、Start() 和 Wait() 等方法以实现自定义的任务行为。

静态方法和实例方法

在 C# 中,方法分为两种主要类型:静态方法和实例方法。它们的主要区别在于如何调用它们、它们访问的数据范围,以及它们与类实例的关系。

静态方法

静态方法是属于类本身的方法,而不是某个类的具体实例。你不需要创建类的对象就可以调用静态方法。静态方法只能访问静态成员(静态字段、静态属性和其他静态方法)。

特点:

  1. 使用 static 关键字定义。
  2. 不需要创建类的实例就可以调用静态方法。
  3. 静态方法只能访问静态字段或调用其他静态方法,不能直接访问类的实例成员。
  4. 通常用于与类本身相关的行为,而不是与某个具体对象的状态相关的行为。
C#
public class MathUtilities
{
    // 静态方法
    public static int Add(int a, int b)
    {
        return a + b;
    }
}

// 调用静态方法
int result = MathUtilities.Add(5, 10);  // 输出: 15

静态方法的常见应用:

  1. 工具类(Utility Class)中的方法,例如数学计算、字符串操作等。
  2. 工厂方法(Factory Method)创建对象。
  3. 用于管理类级别的数据,例如单例模式中的静态方法。

注意

在多线程同时访问一个没有线程同步的静态方法时,访问到同一个线程不安全资源(按引用传递的参数,类的字段或静态变量等)可能会造成数据的混乱。

如果参数是按值传递的,每个线程都有自己的副本,线程间不会相互影响。

实例方法

实例方法是属于类的实例的方法。要调用实例方法,必须首先创建类的对象(实例)。实例方法可以访问该类的实例成员(如字段、属性)以及静态成员。

特点:

  1. 不使用 static 关键字定义。
  2. 必须通过类的实例来调用实例方法。
  3. 实例方法可以访问类的实例成员(如字段、属性)和静态成员。
  4. 通常用于操作与类的具体对象相关的数据。
C#
public class Person
{
    private string name;

    // 实例方法
    public void SetName(string newName)
    {
        name = newName;
    }

    public string GetName()
    {
        return name;
    }
}

// 创建对象,调用实例方法
Person person = new Person();
person.SetName("Alice");
Console.WriteLine(person.GetName());  // 输出: Alice

实例方法的常见应用:

  1. 操作对象的实例数据。
  2. 实现对象行为的逻辑,例如获取或修改对象状态。
  3. 用于定义对象与其他对象或系统的交互。

静态方法与实例方法的区别

静态方法实例方法
使用static关键字定义不使用 static 关键字定义
直接通过类名调用必须通过类的实例调用
只能访问类的静态成员可以访问类的静态和实例成员
无法访问this关键字可以使用 this 关键字访问当前实例
适合与类本身相关的行为和数据适合与对象实例相关的行为和数据

扩展方法

扩展方法(Extension Method)是 C# 中的一种特殊的静态方法,它允许开发者在不修改现有类或结构体的前提下,向其添加新功能。扩展方法提供了一种灵活的方式来扩展类型的功能,尤其适用于那些你无法直接修改其源代码的类型,例如框架类或第三方库的类。

C#
public static class StringExtensions
{
    // 定义扩展方法,向 string 类型添加功能
    public static int WordCount(this string str)
    {
        return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
    }
}

// 调用
string sentence = "Hello, how are you?";
int count = sentence.WordCount();  // 调用扩展方法
Console.WriteLine($"Word Count: {count}");  // 输出: Word Count: 4

工作原理与规则

扩展方法的本质是一个静态方法,编译器会将扩展方法调用转换为对静态方法的调用。因此,扩展方法只是语法糖,它并不会真正修改类的结构。

当你在代码中使用扩展方法时,编译器会根据方法签名查找静态类中是否存在匹配的扩展方法,并将该方法应用于目标类型。

规则:

  1. 必须在静态类中定义:扩展方法必须放在静态类中。
  2. 第一个参数前必须有 this:第一个参数指定要扩展的类型,并且必须以 this 关键字标记。
  3. 不能重写现有方法:扩展方法不能重写类中的现有方法。如果类型中已经有一个同名同参数的方法,扩展方法将不会被调用,优先调用原有方法。
  4. 只能扩展公开成员:扩展方法只能访问被扩展类型的公开成员,不能访问私有或受保护的成员。
  5. 扩展接口:扩展方法不仅可以用于类,还可以用于接口。

注意

当类本身已经定义了一个与扩展方法签名相同的方法时,类中的方法会优先调用,而不是扩展方法。

使用场景

  1. 向现有类添加新功能

扩展方法最常见的用途是为已经存在的类或结构体添加新功能,尤其是系统库中的类。例如,LINQ 中的大多数方法(如 Select、Where 等)都是通过扩展方法实现的。

C#
public static class IntExtensions
{
    // 为 int 类型添加扩展方法
    public static bool IsEven(this int number)
    {
        return number % 2 == 0;
    }
}

// 调用
int number = 4;
bool isEven = number.IsEven();  // 调用扩展方法
Console.WriteLine(isEven);  // 输出: True
  1. 扩展接口

可以为接口定义扩展方法,这样实现接口的所有类型都可以使用这个扩展方法

C#
public interface IAnimal
{
    void Speak();
}

public static class AnimalExtensions
{
    public static void Move(this IAnimal animal)
    {
        Console.WriteLine("The animal is moving.");
    }
}

//使用
class Dog : IAnimal
{
    public void Speak() => Console.WriteLine("Woof!");
}

IAnimal dog = new Dog();
dog.Speak();  // 输出: Woof!
dog.Move();   // 调用扩展方法,输出: The animal is moving.
  1. 提供更流畅的 API 设计

扩展方法可以帮助实现更流畅的 API 设计,尤其是在链式调用时。例如,LINQ 的扩展方法允许你通过链式调用实现对集合的复杂查询。

C#
var result = numbers.Where(n => n > 2).Select(n => n * 2).ToList();
  1. 对无法修改的类型进行扩展 有时你无法修改某个类型的代码(例如,系统类型或第三方库类型),但你仍然希望为其添加功能。这时扩展方法就是一个合适的工具。

异步方法

如果用 async 修饰符标记方法,则可以在该方法中使用 await 运算符。

当控件到达异步方法中的 await 表达式时,如果等待的任务未完成,控件将返回到调用方,并在等待任务完成前,包含 await 关键字的方法中的进度将一直处于挂起状态。

任务完成后,可以在方法中恢复执行。

C#
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

注意

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

详情参考异步编程与多线程

匿名方法

匿名方法(Anonymous Methods)是一种简洁的方式,用来定义没有名称的方法。匿名方法可以在声明和使用的同时进行定义,特别适合用于委托(delegate)或者事件处理。

基本语法:

C#
delegate(参数列表)
{
    // 方法体
}

例子:

C#
Func<int, int> square = delegate(int x)
{
    return x * x;
};

int result = square(5);  // 输出: 25

特点:

  1. 没有名称:匿名方法没有名字,直接使用 delegate 关键字定义。
  2. 局部变量访问:匿名方法可以访问其所在作用域中的局部变量(闭包特性)。
  3. 可以返回值:与普通方法一样,匿名方法可以返回值,也可以不返回值,取决于委托的签名。

在 C# 3.0 之后,Lambda 表达式的引入提供了一种更简洁的方式来编写匿名方法。Lambda 表达式实际上是匿名方法的一种语法糖,因此在大多数情况下,Lambda 表达式已经替代了匿名方法的使用。

参考Lambda 表达式

Document Base on VitePress.