Menu

C# 方法:参数、返回值、params、重载与 Main

如何在 C# 中声明和调用方法:返回类型与 void、参数、表达式主体方法、用元组返回多个值、重载、params 数组、静态方法与实例方法、Main 方法、递归和局部函数。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

方法是一段有名字的代码,它接收输入(参数),完成工作,并且可以交回一个结果(返回值)。在 C# 中,每个方法都位于某个类型内部;C# 没有独立存在的函数。

输出:

Headphones   48.00
Cable        11.40

方法的组成

public static decimal PriceWithTax(decimal net, decimal taxRate)
{
    return net * (1 + taxRate);
}
  • 访问修饰符(public、private 等):谁可以调用它。省略时,方法对其所在类型是 private 的。
  • static:方法属于类型本身,而不属于某个对象。下文会详细介绍。
  • 返回类型:它返回的值的类型,不返回值时写 void。
  • 名称:按惯例使用 PascalCase,通常是动词(Calculate、SendEmail、TryParse)。
  • 参数列表:零个或多个 type name 对。每个参数都是一个局部变量,用调用方传入的实参初始化。
  • 方法体:花括号之间的语句。

名称加上参数类型(以及 ref、out 或 in 修饰符)构成了方法的签名。返回类型不属于签名,这一点对重载很重要。

返回值与 void

return 会立即结束方法并交回一个值。有返回类型的方法必须在每一条执行路径上都返回该类型的值:

static string Grade(int score)
{
    if (score >= 50)
        return "pass";
}   // error CS0161: 'Program.Grade(int)': not all code paths return a value

编译器会检查每个分支。补上缺失的 return "fail";,或者如果走到那里本身就是 bug,就写一个 throw。

void 方法不返回任何值。它仍然可以用单独的 return; 提前退出:

输出:

ana@mail.com: newsletter sent
ben@mail.com: not subscribed, skipped
False
True

表达式主体方法

当方法体只有一个表达式时,可以用 => 代替花括号和 return:

static decimal Discount(decimal price) => price * 0.1m;
static bool IsAdult(int age) => age >= 18;
static void Log(string message) => Console.WriteLine($"[log] {message}");

这还是同一个方法,只是写得更短。它也适用于 void 方法,只要方法体是一个表达式(一次调用、一次赋值)。

参数是副本

参数默认按值传递:形参接收的是实参的一个副本。在方法内部给参数赋值,不会影响调用方的变量:

输出:

inside: 42
after:  21
scores: 70, 85, 100

List 被改变了,因为对于引用类型,被复制的值是引用:方法和调用方指向同一个列表对象。在方法内部重新赋值 scores = new List<int>() 不会影响调用方。要让方法能给调用方的变量赋值,使用 ref 或 out;要给参数设置默认值并按名称传参,使用可选参数和命名参数。

用元组返回多个值

从 C# 7.0 起,方法可以返回一个带命名元素的元组,调用方可以把它解构成几个独立的变量:

输出:

min 15, max 27, avg 21.0
range 12

_ 会丢弃你不需要的元素。如果结果在很多地方使用,一个带命名属性的小类或记录类型比元组更能说明数据的结构。

重载

多个方法可以同名,只要它们的参数列表在数量、类型或类型顺序上有所不同。编译器会为每次调用选出最匹配的那个:

输出:

12.57
13.50
25

Area(5) 选择了 int 版本,因为它完全匹配;Area(2.0) 选择了 double 版本。方法不能只在返回类型上不同:两个分别返回 int 和 double 的 Area(int) 方法会产生错误 CS0111,Type 'Program' already defines a member called 'Area' with the same parameter types,因为像 Area(5) 这样的调用无法区分它们。

没有任何重载能接受的调用同样会在编译时失败:Area(1, 2, 3) 是 CS1501,No overload for method 'Area' takes 3 arguments,Area("big") 是 CS1503,Argument 1: cannot convert from 'string' to 'double'。

params:数量不定的参数

params 数组参数可以接收任意数量的参数,包括零个。编译器会替你构建数组:

输出:

empty cart: 0 item(s), 0
one item: 1 item(s), 9.99
three items: 3 item(s), 19.75
from array: 2 item(s), 50

规则:params 必须是最后一个参数(否则报 CS0231),一个方法最多只能有一个,传入一个现成的数组时会把这个数组原样交过去。Console.WriteLine("{0} and {1}", a, b) 和 string.Format 都是 params object[] 方法。C# 13 把 params 扩展到了 List<T>、ReadOnlySpan<T> 等其他集合类型;在此之前只允许数组。

静态方法与实例方法

static 方法属于类型本身,通过类型名调用:Math.Max(3, 7)、Program.Total(...)。实例方法属于某个对象,可以使用该对象的字段:

输出:

12

在静态方法中不通过对象直接调用实例方法,是 C# 初学者最常犯的错误:

class Program
{
    void Greet() => Console.WriteLine("hi");

    static void Main()
    {
        Greet();
        // error CS0120: An object reference is required for the non-static field, method, or property 'Program.Greet()'
    }
}

把 Greet 标记为 static,或者创建一个对象再用它调用:new Program().Greet();。static 页面会更深入地介绍静态成员和静态类。

Main 方法

Main 是程序开始执行的地方。它必须是 static,下面这些签名都是合法的:

static void Main() { }
static void Main(string[] args) { }            // command-line arguments
static int Main(string[] args) { return 0; }   // exit code for the shell
static async Task Main() { }                    // C# 7.1 and later

async Task Main 形式让入口点可以直接使用 await(参见 async 和 await)。从 C# 9 起,文件也可以改为包含顶级语句:完全没有类或 Main 的代码,就像新建的 dotnet new console 项目那样。编译器会在外面生成类和 Main 方法,所以它们是同一回事,只是写得更短;args 依然可用。

递归

方法可以调用自己。每次调用都有自己的参数和局部变量,并且必须有一个基线条件来终止这条调用链:

输出:

120
2432902008176640000
35

如果没有基线条件,或者输入的层级太深,每次调用都会增加一个栈帧,直到程序因 StackOverflowException 而崩溃。这个异常无法被捕获,所以对于深度未知的输入,应该把递归改写成循环。

局部函数

C# 7.0 引入了局部函数:在另一个方法内部声明、只在那里可见的方法。它们可以使用外层方法的变量:

// C# 7.0 and later
static int CountValid(string[] codes)
{
    int count = 0;
    foreach (string code in codes)
    {
        if (IsValid(code)) count++;
    }
    return count;

    bool IsValid(string c) => c.Length == 6 && char.IsLetter(c[0]);
}

它们让辅助函数紧挨着唯一的调用方,而不必暴露给类的其他部分。从 C# 8 起,可以把它们标记为 static,禁止捕获外部变量。在无法使用局部函数的地方,同一个类中的私有静态方法也能完成同样的工作,就像本页其他示例那样。

常见问题

C# 中方法和函数有什么区别?

在 C# 中,每个函数都声明在类、结构体或记录类型里面,称为方法。C# 没有独立存在的函数。即使是顶级语句(C# 9)和局部函数(C# 7),也会被编译成某个生成的类型或外层类型的方法。“函数”只是一种非正式的叫法,也用来指 lambda 和委托。

C# 中 params 的作用是什么?

params 让一个方法接收数量不定的同类型参数:static int Sum(params int[] numbers) 可以这样调用:Sum()、Sum(4)、Sum(4, 5, 6) 或 Sum(existingArray)。编译器会把这些参数打包成一个数组。params 参数必须是最后一个参数,而且一个方法只能有一个。

C# 中的 Main 方法是什么?

Main 是程序的入口点:运行时最先调用的方法。它必须是 static,可以返回 void 或 int(退出码),还可以接收 string[] args 作为命令行参数。从 C# 7.1 起它也可以是 async Task Main,从 C# 9 起顶级语句可以让编译器替你生成它。

如何从 C# 方法返回多个值?

返回一个元组:static (int Min, int Max) Range(int[] values) 并写 return (min, max);。调用方可以读取 result.Min,或者用 var (lo, hi) = Range(values); 解构。如果这些值在代码里总是一起出现,用一个小的类或记录类型会更清晰;out 参数是更老的替代方案。

为什么会出现“An object reference is required for the non-static field, method, or property”错误?

一个 static 方法(比如 Main)直接调用了实例方法。静态方法属于类型本身,没有可以用来调用实例方法的对象。要么把被调用的方法标记为 static,要么先创建一个对象:var app = new Program(); app.Greet();。

“not all code paths return a value”是什么意思?

错误 CS0161:一个有返回类型的方法中,至少有一条路径在没有 return 的情况下走到了右花括号。通常是 if 返回了一个值,但后面没有 else,也没有最终的 return。在方法末尾加一个 return(或 throw)即可。

Coddy 编程语言插图

和 Coddy 一起学编程

开始