>> >> >> Reference << << << <<<<<<Ref>>>>>>
Function
Modified: 2025-06-01 | Author:ljf12825

在C#中,函数通常被称为方法(Method)\

// 基本结构
[修饰符] [返回类型] 方法名([参数列表])
{
    // 方法体
}

参数传递

  1. 值传递(默认) 传入的是参数的副本,方法内修改不影响外部
void Change(int x) { x = 10; }
int a = 5;
Change(a); // a 还是 5
  1. 引用传递ref 传入的是变量的引用,方法内修改会影响外部,传入时必须已经赋值
void Change(ref int x) { x = 10; }
int a = 5;
Change(ref a); // a 变成 10
  1. 输出参数out 必须在方法内赋值,用来返回多个结果,传入时可以不赋值
void Divide(int a, int b, out int result, out int remainder)
{
    result = a / b;
    remainder = a % b;
}

int res, rem;
Devide(10, 3, out res, out rem); // res = 3, rem = 1
  1. 只读引用in(C# 7.2+) 方法内只能读取,不可更改(只读保证),传入时必须已经赋值
    避免大对象复制,提升性能,同时保证只读安全
void Change(in int x) 
{
    Console.WriteLine(x);
    // x = 8; // error
}
  1. 可选参数(Optional Parameters) 在定义方法时,给参数一个默认值,调用方法时,如果不传这个参数,就使用默认值
    默认值必须是编译时常量或者default(T)
void Greet(string name = "Guest", int times = 1) => for (int i = 0; i < times; ++i) Console.WriteLine($"Hello, {name}!");
Greet(); // Hello, Guest!(使用默认)
Greet("Alice"); // Hello, Alice!
Greet("Bob", 3); // Hello, Bob(输出三次)

注意:

void Print(int x = 10, string y = "hello", object obj = null, int z = default(T)) {}
  1. 命名参数(Named Parameters) 调用方法时,可以显式写出参数名,而不是按顺序传参,可以提高可读性,尤其是参数很多的时候,可以和可选参数结合使用
void CreateUser(string username, int age, bool isAdmin = false) => Console.WriteLine($"{username}, {age}, Admin={isAdmin}");

// 常规调用
CreateUser("Tom", 20, true);

// 命名参数调用(顺序可变)
CreateUser(age: 25, username: "Alice", isAdmin: true);

// 结合可选参数
CreateUser(username: "Bob", age: 30); // isAdmin使用默认值

可选参数和命名参数结合的优势
减少方法重载
使用重载支持多种调用

void Log(string message) { ... }
void Log(string message, int level) { ... }

可以直接写

void Log(string message, int level = 1) { ... }

提升可读性

// 不易读
SendEmail("test@example.com", "hi", true, false, true);

// 易读
SendEmail(to: "test@example.com", subject: "hi", cc: true, bcc: false, isImportant: true);

注意事项\

Greet("Alice", times: 3); // 正确
Greet(name: "Alice", 3); // 错误
  1. params参数 params修饰符允许方法接收数量不定的参数
    调用方法时可以传入:0个参数,多个参数,或者一个数组
    编译器会自动把多个参数打包成一个数组传给方法
void PrintNumbers(params int[] numbers) => foreach (int n in numbers) Console.WriteLine(n);

// 调用方式
PrintNumbers(); // 什么都不打印
PrintNumbers(1, 2, 3, 4); // 打印 1 2 3 4
PrintNumbers(new int[] {5, 6, 7}); // 打印 5 6 7

等价于写了一个接收int[]的函数,但调用时更灵活

限制条件
只能有一个params参数

void Foo(params int[] a, params string[] b); // 错误

必须是方法参数列表中的最后一个

void Foo(int x , params int[] numbers); // 正确
void Bar(params int[] numbers, int x); // 错误

params可以是任意类型的数组(int[]string[]object[]等)
示例:使用pramas + object 传入不同类型参数

void PrintAll(params object[] items) => foreach (var item in items) Console.WriteLine(item);

PrintAll(1, "hello", 3.14, true);
// 输出:1 hello 3.14 Ture

这种方式常见于日志系统,类似Console.WriteLine的实现

示例:和其他参数混用

void Log(string tag, params string[] messages) => Console.WriteLine($"[{tag}]" + string.Join(", ", message));

Log("INFO", "Game Start", "Player Joined");
Log("ERROR"); // 没有消息也行

注意事项
每次调用带params的方法,都会分配一个数组(哪怕只传1个参数)
如果方法在性能敏感的地方(比如游戏循环内)频繁调用,可能会导致GC压力
优化方案:提供带数组的重载,避免数组分配

Main Function

在C#程序中,必须有一个Main方法作为入口。它通常写在一个类或结构体内

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("Hello World");
    }
}

Main函数的几种有效签名

static void Main();                // 无返回值,无参数
static void Main(string[] args);   // 无返回值,有参数
static int Main();                 // 有返回值,无参数
static int Main(string[] args);    // 有返回值,有参数

命令行参数

假设编译成MyApp.exe,然后运行

MyApp.exe hello world 123

在代码里

static void Main(string[] args)
{
    foreach (var arg in args) Console.WriteLine(arg);
}

命令行参数的作用时允许用户在启动应用程序时,从外部向程序传递配置信息或数据,从而让程序的行为可以根据输入动态改变,而无需修改代码本身。这极大地提高了程序的灵活性和可重用性
常见场景:

  1. 配置程序运行模式 允许用户指定程序以不同的模式运行,例如开启调试模式、详细输出模式或指定使用哪种算法
  1. 传递输入/输出文件路径 这是最常见的用途之一。程序本身不硬编码文件路径,而是由用户通过参数指定要处理的文件以及结果输出的位置
  1. 设置程序选项和标志 用于开启或关闭特定功能,或者设置一些简单的值
  1. 自动化脚本和任务 在批处理文件(.bat)、PowerShell脚本或CI/CD流程中,经常通过命令行参数来调用和控制应用程序,实现自动化

  2. 开发与调试 开发者可以在IDE中预设命令行参数,方便在调试时测试程序对不同参数的处理逻辑

在程序中获取命令行参数

  1. 使用Main方法的参数(最常用) Main方法是C#应用程序的入口点,它可以被定义为一个接受字符串数组参数的方法,这个数组就是命令行参数
using System;

namespace CommandLineArgsDemo
{
    class Program
    {
        // Main 方法的参数 string[] args 就是命令行参数数组
        static void Main(string[] args)
        {
            Console.WriteLine("Number of command line arguments: " + args.Length);

            for (int i = 0; i < args.Length; i++)
            {
                Console.WriteLine($"Arg[{i}] = {args[i]}");
            }

            // 简单的参数处理示例
            if (args.Length > 0 && args[0] == "--help")
            {
                Console.WriteLine("This is the help message.");
            }
        }
    }
}
dotnet run -- arg1 arg2 "third argument" --help
- 注意:在`dotnet run`命令中,`--`之后的参数才会传递给程序。`args`数组将包含:`["arg1", "arg2", "third argument", "--help"]`
.\CommandLineArgsDemo.exe arg1 arg2 "third argument" --help
  1. 使用Environment.GetCommandLineArgs 这个方法返回一个字符串数组,其中也包含了命令行参数。与Main方法的参数不同的是
using System;

namespace CommandLineArgsDemo
{
    class Program
    {
        static void Main() // Main 方法可以不接受参数
        {
            // 使用 Environment.GetCommandLineArgs
            string[] allArgs = Environment.GetCommandLineArgs();

            Console.WriteLine("Executable path: " + allArgs[0]);
            Console.WriteLine("Arguments:");

            for (int i = 1; i < allArgs.Length; i++) // 从1开始,跳过exe路径
            {
                Console.WriteLine($"  [{i}] {allArgs[i]}");
            }
        }
    }
}

复杂命令行参数

对于简单的-flag value格式,自己写循环和逻辑判断就足够了。但如果参数非常复杂,例如支持--long-option-s(短选项)、可选参数等,手动解析就非常麻烦
可以使用专门的命令行参数解析库,它们可以自动处理各种复杂的场景,并提供--help帮助文档生成等功能 流行的NuGet包有

  1. System.CommandLine(.NET推荐)
  1. CommandLineParser

Lambda

Lambda表达式本质上是匿名函数(没有名字的函数),可以用来简化委托和表达式树的写法。它的形式是

(参数列表) => 表达式或语句块

=>读作goes to 左边是输入参数,右边是返回结果或逻辑

x => x * x

这是一个接收一个参数x并返回x * x的函数

基础用法

  1. 单参数表达式
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 25

这里Func<int, int>代表输入int,返回int

  1. 多参数表达式
Func<int, int, int> add = (a, b) => a + bl
Console.WriteLine(add(3, 4)); // 7
  1. 无参数
Func<int> getRandom = () => new Random().Next(1, 10);
Console.WriteLine(getRandom());
  1. Lambda的语句块写法 如果逻辑复杂,可以用花括号写多条语句
Func<int, int, int> multiplyAndAdd = (a, b) =>
{
  int product = a * b;
  return product + 10;
};
Console.WriteLine(multiplyAndAdd(2, 3)); // 16
  1. Lambda与委托 传统写法(委托 + 匿名方法)
Func<int, int> square = delegate (int x) { return x * x; };

Lambda简化后

Func<int, int> square = x => x * x;
  1. LINQ中使用
int[] nums = { 1, 2, 3, 4, 5 };;
var evenNums = nums.Where(n => n % 2 == 0);

foreach (var n in evenNums) Console.WriteLine(n); // 2, 4

排序

List<string> names = new() { "Tom", "Jerry", "Alice" };
names.Sort((a, b) => a.Length.CompareTo(b.Length));
  1. Lambda的类型推断 C#会根据上下文推断参数和返回类型,不用显式写类型
var list = new List<int> {1, 2, 3};
list.ForEach(n => Console.WriteLine(n));

这里n自动推断为int

  1. Lambda捕获外部变量(闭包) Lambda可以“记住”它定义时的上下文变量
int factor = 10;
Func<int, int> multiplier = x => x * factor;

Console.WriteLine(multiplier(5)); // 50

注意:factor是捕获变量,如果后面改factor,Lambda内的值也会改变

  1. Action与Func
Action<string> greet = name => Console.WriteLine($"Hello {name}");
greet("World");
  1. Lambda与事件 Lambda可以简化事件订阅
button.Click += (sender, e) => MessageBox.Show("Button clicked!");
// 传统方法需要定义一个单独的方法
  1. 表达式树(Expression Trees) 高级用法:Lambda不仅可以编译成委托,还可以编译成数据结构(表达式树),允许在运行时分析、转换或翻译代码
    例如LINQ to SQL将C#代码翻译成SQL语句
// 这是一个表达式Lambda,它被编译器识别为表达式树
System.Linq.Expressions.Expression<Func<int, bool>> isEvenExpression = n => n % 2 == 0;

// 这只是一个普通的委托
Func<int, bool> isEvenDelegate = n => n % 2 == 0;

isEvenExpression不是一个可执行的方法,而是一个描述n => n % 2 == 0这个逻辑的树形数据结构,可以被其他组件(如ORM框架)解析

局部函数(C# 7.0+)

局部函数就是定义在方法(或属性、构造函数等)内部的函数
它只在当前方法作用域内可见,外部不能之际调用

class Calculator
{
  public int SumToN(int n)
  {
    int Add(int x, int y) => x + y; // 局部函数
    int total = 0;
    for (int i = 1; i <= n; ++i)
      total = Add(total, i);
    return total;
  }
}

这里的Add是个局部函数,只能在SumToN内使用

局部函数的优势

  1. 封装性更强 避免只把方法内部用的小逻辑暴露成类的公有/私有方法

  2. 可读性更好 把复杂逻辑拆成小块,但又不会污染类的命名空间

  3. 性能比匿名函数更优

局部函数的特性

  1. 可访问外部变量
void PrintSquares(int n)
{
  int counter = 0;

  void PrintOne(int x) // 局部函数
  {
    counter++;
    Console.WriteLine(x * x);
  }

  for (int i = 1; i <= n; ++i) PrintOne(i);

  Console.WriteLine($"调用了{counter}次")
}
  1. 支持递归
int Factorial(int n)
{
  int Inner(int x)
  {
    if (x <= 1) return 1;
    return x * Inner(x - 1);
  }
  return Inner(n);
}
  1. 可以是异步函数
async Task<int> GetDataAsync()
{
  async Task<int> Fetch() // 局部异步函数
  {
    await Task.Delay(500);
    return 42;
  }
  return await Fetch();
}
  1. 可以用static修饰(C# 8+) 表示不捕获外部变量,避免闭包
int AddNumbers(int a, int b)
{
  static int Add(int x, int y) => x + y;
  return Add(a, b);
}

使用场景

异步

顶级语句(Top-level statements)(C# 9.0+)

在C# 9.0之后,微软引入了“顶级语句”的概念,它的作用是:让简单程序(特别是控制台应用)写起来更简洁,不必再写class Programstatic void Main
比如以前要写

using System;

class Program
{
  static void Main(string[] args)
  {
    Console.WriteLine("Hello, World!");
  }
}

有了顶级语句之后,只需要

using System;

Console.WriteLine("Hello, World!");

这里没有类,也没有Main,但编译器会自动生成一个Program类和Main方法,将顶级语句放入其中
所以顶级语句只是语法糖,不影响本质

限制与规则

  1. 只能有一个文件使用顶级语句
  1. 命名空间、类、方法必须卸载顶级语句之后
using System;

Console.WriteLine("Hello");

class MyClass { }
  1. 顶级语句不能出现在方法或类内部
  2. 适合小型程序、示例代码、脚本化开发