🤖 作者:包瑞清(richie bao): lastmod: 2024-11-08T16:11:12+08:00

6.1 基本语法规则

6.1.1 函数/方法的定义与调用

函数(方法)是用于执行特定任务或计算的代码块,其基本组成包括标识函数的函数名;传入数据用于执行任务的参数列表;执行任务的函数体;和函数执行完后返回的结果,即返回值(如果有)。通过函数(方法)定义,使代码模块化,即将代码分解成小块,每块处理一个独立的任务,便于理解和维护;通过调用已定义的函数避免重复编写相同的代码,实现代码重用;也可以通过函数隐藏复杂的实现,用户只需要关注函数的接口,而不必关心函数(方法)的内部实现。表为 Python 和 C 系列语言函数定义的基本语法。

Py C/C++,C#

Python 函数定义使用def关键字,后跟函数名、参数列表和冒号。调用函数时,使用函数名并传入参数;如果无传入参数,则为空。

在 C/C++ 和 C# 中的函数定义整体结构相似,通过指定返回值类型、函数名和参数列表定义函数。但需要注意,C# 中的函数为定义在类内部的方法,因此通常在 C# 中将函数称为方法,通过实例化对象来调用实例方法,或直接调用静态方法。调用函数(方法)时,使用函数(方法)名并传入参数;如果无传入参数,则为空。

  def function_name(parameters):
    # 函数体
    return value  # 可选
  
  return_type function_name(parameter1_type parameter1, parameter2_type parameter2, ...) {
    // 函数(方法)体
    return value;  // 可选,取决于返回类型
}
  

6.1.2 函数/方法的用法

用法 Py C C++ C#

无参数传入 函数定义与调用

定义的函数如果无参数传入,则圆括号内的参数列表可以为空。对于 C/C++ 也可以传入void参数,明确表示函数没有参数,即意味着该函数不接收任何参数。

  def greet():
    print("Hello, Python!")

greet()
  
  🡮
Hello, Python!
  
  #include <stdio.h>

void greet() {
	printf("Hello, C!\n");
}

int main() {
	greet();

	return 0;
}
  
  🡮
Hello, C!
  
  #include <iostream>

void greet() {
	std::cout << "Hello, C++!" << std::endl;
}

int main() {
	greet();

	return 0;
}
  
  🡮
Hello, C++!
  
  using System;

class Program
{
    static void greet()
    {
        Console.WriteLine("Hello, C#!");
    }
    static void Main()
    {
        greet();
    }
}
  
  🡮
Hello, C#!
  

有参数传入 函数定义与调用

Python 为动态类型语言,与定义变量时一样,不需要指定函数传入参数的数据类型和函数返回值的类型。而 C/C++ 和 C# 为静态类型语言,要在函数声明和定义中明确传入参数的类型和函数(方法)返回值的类型。

Python 引入了类型提示(Type Hinting)附1,允许在函数定义时为参数和返回值添加类型注释,以提高代码的可读性和维护性。类型提示为在变量后使用冒号:,并指定数据类型来表示;函数的返回值则使用->符号。Python 的类型注释仅作为注释参考,不会强制约束类型。

  def greet(name):
    print(f"Hello, {name}!")

greet("Python")
  
  🡮
Hello, Python!
  
  • 类型提示
  def add(a: int, b: int) -> int:
    return a + b

a: int = 5
b: int = 3
result = add(a, b)
print(result)
  
  🡮
8
  
  #include <stdio.h>

void greet(char name[]) {
	printf("Hello, %s!\n",name);
}

int main() {
	char name[] = "C";
	greet(name);

	return 0;
}
  
  🡮
Hello, C!
  
  #include <iostream>
#include <string>
#include <format>

void greet(std::string name) {
	std::cout << std::format("Hello, {}!",name) << std::endl;
}

int main() {
	std::string name = "C++";
	greet(name);

	return 0;
}
  
  🡮
Hello, C++!
  
  using System;

class Program
{
    static void greet(string name)
    {
        Console.WriteLine($"Hello, {name}!");
    }
    static void Main()
    {
        string name = "C#";
        greet(name);
    }
}
  
  🡮
Hello, C#!
  

参数类型和传递方式

Python、C、C++ 和 C# 中,函数(方法)的参数传递方式有所不同。其参数传递方式的对比如表。

语言 按值传递(Pass by Value) 按引用传递(Pass by Reference) 按指针传递(Pass by Pointer) 输出参数 默认参数 收集(可变)参数 命名参数
Py ✅ 默认方式(对应不可变类型) ❌(不支持),但相当于可变类型作为参数传入 ❌(不支持) ❌(不支持) ✅使用默认值 *args/**kwargs ✅ 支持关键字参数
C ✅ 默认方式 ❌(不支持) ✅使用指针 ❌(不支持) ❌(不支持) ❌(不支持) ❌(不支持)
C++ ✅ 默认方式 ✅ 使用&符号 ✅使用指针 ❌(不支持) ✅支持默认值 ❌(不支持) ❌(不支持)
C# ✅ 默认方式 ✅ 使用ref关键字 ❌(不支持) ✅使用out关键字 ✅支持默认值 ❌(不支持) ✅支持命名参数

说明:

  • 按值传递:传递的是参数的副本,函数内部的修改不会影响外部变量。
  • 按引用传递:通过引用(如C++中的&,C#中的ref)传递参数,允许函数内直接修改外部变量。
  • 按指针传递:传递变量的地址,函数内通过指针可以修改外部变量(在C/C++中使用)。
  • 输出参数:用来在函数内返回多个值,例如C#中的out关键字。
  • 默认参数:允许为函数参数提供默认值,如果调用时未传入参数则使用默认值(Python、C++、C#支持)。
  • 收集(可变)参数:允许传递任意数量的参数,如 Python 的*args**kwargs
  • 命名参数:允许按名字传递参数,例如Python的关键字参数和C#的命名参数。
  • 参数匹配语法(Argument Matching Syntax)

函数的参数匹配包括两个位置,一是定义函数时传入的参数;二是调用函数时传入的参数。常规模式为位置参数,按照顺序从左到右对应参数;调用时可以给定关键字参数,不受位置参数顺序的影响,但是需要将关键字参数放置于位置参数之后。如果是在定义函数时,给定关键字参数,则为该参数指定默认值,即默认参数。当调用时,不传递该参数值,则以提供的默认值替代;收集参数(Varargs collecting)包括只有一个星号*的元组形式收集模式,和包括有两个星号**的字典形式收集模式。

不同的匹配语法可以根据需要自由组合。但排序通常为,一般模式在前,再跟元组收集,再跟字典收集。如果位置不对,会引发异常提示,可以根据提示修改位置,直至满足要求。

语法(Syntax) 位置(Location) 解释(Interpretation)
def func(name) 函数(Function) 常规参数(位置参数):按位置或名称匹配任何传递值
def func(name=value) 函数(Function) 配置函数默认参数值,如果没有在调用中传递值(配置参数默认值 default value)
def func(*name) 函数(Function) 以元组形式匹配并收集剩余的位置参数:收集参数(Varargs collecting)-位置参数
def func(**name) 函数(Function) 以字典的形式匹配并收集剩余的关键字参数:收集参数(Varargs collecting)-关键字参数
def func(*other, name) 函数(Function) 只能在调用中通过关键字传递的参数
def func(*, name=value) 函数(Function) 只能在调用中通过关键字传递的参数
func(value) 调用(Caller) 常规参数(位置参数):按位顺序匹配(从左至右)
func(name=value) 调用(Caller) 关键字参数:按名称匹配
func(*iterable) 调用(Caller) 将可迭代对象作为单独的位置参数传入:按位顺序匹配
func(**dict) 调用(Caller) 将字典键值对作为关键字参数传入:按键名匹配
  x = 2
y = 3
z = 5
xyz_lst = [2, 3, 5]
xyz_dict = {"X": 2, "Y": 3, "Z": 5}


# 常规参数
def xyz_normal(X, Y, Z):
    print(X, Y, Z)


xyz_normal(x, y, z)
xyz_normal(X=x, Y=y, Z=z)
xyz_normal(x, y, Z=z)
xyz_normal(*xyz_lst)
xyz_normal(**xyz_dict)

print("--" * 20)


# 配置参数默认值
def xyz_default(X, Y=7, Z=9):
    print(X, Y, Z)


xyz_default(x, y, z)
xyz_default(x, y)
xyz_default(x)

print("--" * 20)


# 收集参数-位置参数
def xyz_collect_positional(*args):
    print(args)


xyz_collect_positional(x, y, z)
xyz_collect_positional(xyz_lst)

print("--" * 20)


# 收集参数-位置参数-变化组合
def xyz_collect_positional_alter(X, *args, Z):
    a, b, c = args
    print(X, args, Z)
    print(a, b, c)


xyz_collect_positional_alter(x, 12, 13, 15, Z=z)
xyz_collect_positional_alter(11, 12, 13, 15, Z=z)

print("--" * 20)


# 收集参数-关键字参数
def xyz_collect_keyword(**args):
    print(args)


xyz_collect_keyword(x=2, y=3, z=5)
xyz_collect_keyword(**xyz_dict)

print("--" * 20)


# 只能由关键字传递参数
def xyz_keyword(X, *, Y, Z):
    print(X, Y, Z)


xyz_keyword(x, Y=3, Z=5)

print("--" * 20)


# 组合匹配
def xyz_normal_collect(X_n, Y_d=97, *pargs, **kargs):
    print(X_n, Y_d, pargs, kargs)


xyz_normal_collect(x, y, *xyz_lst, **xyz_dict)
  
  🡮
2 3 5
2 3 5
2 3 5
2 3 5
2 3 5
----------------------------------------
2 3 5
2 3 9
2 7 9
----------------------------------------
(2, 3, 5)
([2, 3, 5],)
----------------------------------------
2 (12, 13, 15) 5
12 13 15
11 (12, 13, 15) 5
12 13 15
----------------------------------------
{'x': 2, 'y': 3, 'z': 5}
{'X': 2, 'Y': 3, 'Z': 5}
----------------------------------------
2 3 5
----------------------------------------
2 3 (2, 3, 5) {'X': 2, 'Y': 3, 'Z': 5}
  
  • 函数参数的传递方式

在 Python 中,函数的参数传递分为可变(mutable)和不可变(immutable)数据类型两种。不可变类型包括:intfloatstrtuple等。当不可变类型作为参数传入函数时,是按值传递。当在函数内部对参数重新赋值,只会创建一个新对象,原始对象保持不变。而可变类型包括:listdictsetbytearray等。当可变类型作为参数传入函数时,是按引用传递(本质上是地址的引用)。在函数内部修改可变对象,会直接影响到原始对象。为了避免意外修改可变对象,可以创建副本规避意外发生。

  # 传入可变(list, dict)和不可变(int)数据类型
def mutable_func(collection_lst, collection_dict, val):
    collection_lst[-1] = 99
    collection_dict["C"] = 77
    val += 11


val = 55
lst = [1, 2, 3, 4, 5]
dictionary = {"A": 1, "B": 2, "C": 3}
mutable_func(lst, dictionary, val)
print(lst, "\n", dictionary, "\n", val)

# 创建副本规避无意修改可变数据变量
def safe_modify(lst):
    lst = lst.copy() # 创建副本
    lst.append(33)
    print("Inside function:", lst)


lst = [1, 2, 3, 4, 5]
safe_modify(lst)
print("Outside function:", lst)

# 默认参数是可变类型时,会引发潜在问题
def add_item(item, my_list=[]):
    my_list.append(item)
    return my_list


print(add_item(10)) # 输出: [10]
print(add_item(20)) # 输出: [10, 20] 共享了同一个对象!

# 解决动态默认参数的陷阱,可以将默认参数修改为不可变类型的默认值
def add_item_adj(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list


print(add_item_adj(10))
print(add_item_adj(20))
  
  🡮
[1, 2, 3, 4, 99]
 {'A': 1, 'B': 2, 'C': 77}
 55
Inside function: [1, 2, 3, 4, 5, 33]
Outside function: [1, 2, 3, 4, 5]
[10]
[10, 20]
[10]
[20]
  
  • 函数参数的类型

C 语言函数参数处理需要显示管理内存和类型,通过指针和const修饰符可以灵活、安全地处理多种参数传递场景。

参数类型 描述 示例
基本类型参数 直接传递基本数据类型 void func(int a)
数组参数 传递数组指针 void func(int arr[],int size)
指针参数 传递地址以实现引用传递 void func(int *p)
结构体参数 按值或引用传递结构体 void func(struct Point p)
函数指针参数 动态传递函数实现灵活回调 void func(int (*op)(int,int))
  #include <stdio.h>

// 基本类型参数。直接传递基本数据类型(如 int, float, char 等)
void print_sum(int a, int b) {
	printf("Sum: %d\n", a + b);
}

// 数组作为参数。数组作为函数参数时,实际上是传递了数组的指针(指针类型为 int*)
void print_array(int arr[], int size) {
	for (int i = 0; i < size; i++) {
		printf("%d, ", arr[i]);
	}
	printf("\n");
}

// 指针参数。指针可用于传递复杂类型的数据或实现按引用传递
void update_value(int* p) {
	*p = 99;
}

struct Point {
	int x, y;
};

// 结构体作为参数——按值传递。复制整个结构体,开销较大
void print_point(struct Point p) {
	printf("Point: (%d,%d)\n", p.x, p.y);
}

// 结构体作为参数——按引用传递。传递指向结构体的指针,效率较高。
void updated_point(struct Point* p) {
	p->x = 10;
	p->y = 20;
}

// 函数指针作为参数。可以将函数的地址作为参数传递,用于回调函数或动态行为
void execute(int a, int b, int(*operation)(int, int)) {
	printf("Result: %d\n", operation(a, b));
}

int add(int x, int y) { return x + y; }
int multiply(int x, int y) { return x * y; }


int main() {
	print_sum(5, 3);

	int arr[] = { 1,2,3,4,5 };
	int arr_size = sizeof(arr) / sizeof(arr[0]);
	print_array(arr, arr_size);

	int p = 42;
	update_value(&p);
	printf("Updated p: %d\n", p);

	struct Point pt = { 5,9 };
	print_point(pt);

	updated_point(&pt);
	printf("Point: (%d,%d)\n", pt.x, pt.y);

	execute(5, 3, &add);
	execute(5, 3, &multiply);

	return 0;
}
  
  🡮
Sum: 8
1, 2, 3, 4, 5,
Updated p: 99
Point: (5,9)
Point: (10,20)
Result: 8
Result: 15
  
  • 函数参数的传递方式

默认情况下,C 语言的函数参数是按值传递。函数接收的是实参的副本,而非原始变量本身。但可以通过指针传递参数,实现间接修改变量值。而对于需要传递但不修改的数组或指针,可以使用const避免修改参数变量,以确保安全。

  #include <stdio.h>

void printArray(int* arr, int size) {
	for (int i = 0; i < size; i++) {
		printf("%d ", *(arr + i)); // 访问索引 i 处的元素
	}
	printf("\n");
}

void argsCallByValue(int x) {
	x = 10; // 不影响原始变量
}

void argsCallByReference(int* x) {
	*x = 10; // 会修改原始变量
}

void arrayPointer(int arr[]) {
	arr[0] = 99;
}

// 使用 const 修饰符保护参数
void arrayPointerConst(const int arr[]) {
	//arr[0] = 77; // 执行该代码行将会提示错误
	printf("Do not modify array");
}

int main() {
	int a = 5;
	argsCallByValue(a);
	printf("%d\n", a);

	argsCallByReference(&a);
	printf("%d\n", a);

	int arr[] = { 1,2,3,4,5 };
	arrayPointer(arr);

	int size = sizeof(arr) / sizeof(arr[0]);
	printArray(arr, size);

	arrayPointerConst(arr);

	return 0;
}
  
  🡮
5
10
99 2 3 4 5
Do not modify array
  
  • 函数参数的类型

C++ 在 C 语言的基础上支持现代编程的多种需求,更加灵活和丰富,是的函数的参数使用方式更强大,并保持了对性能的高要求。

参数类型 描述 示例
基本类型参数 传递基本数据类型的副本 void func(int a)
引用参数 传递引用,可直接修改实参 void func(int& a)
指针参数 传递指针,通过解引用附2访问或修改 void func(int* a)
常量引用参数 只读引用,保护参数不被修改 void func(const T& a)
默认参数 参数有默认值,可省略对应参数使用默认值 void func(int a=10)
模板参数 使用模板支持任意类型 template <typename T> T func(T a)
函数对象参数 使用函数指针或 Lambda 表达式 void func(auto operation)
  #include <iostream>
#include <format>

// 基本类型参数。传递基本数据类型,如 int, float, char 等
void printSum(int a, int b) {
	std::cout << std::format("Sum: {}\n", a + b);
}

// 引用参数。使用 & 传递参数,可以在函数中直接修改实参
void increment(int& x) {
	x++;
}

// 指针参数。通过指针传递参数,用于动态内存管理或修改实参
void updateValue(int* p) {
	if (p) {
		*p = 66;
	}
}

// 常量引用参数。使用 const 修饰引用参数,避免复制大对象的开销,同时保护数据不被修改
void printMessage(const std::string& message) {
	std::cout << message << std::endl;
}

// 数组参数。传递数组时,实际上传递的是数组的指针
void printArray(int arr[], int size) {
	for (int i = 0; i < size; i++) {
		std::cout << arr[i] << ",";
	}
	std::cout << std::endl;
}

struct Point {
	int x, y;
};

// 结构体和类对象参数。结构体和类可以按值传递,也可以按引用或指针传递
void setPoint(Point& p) {
	p.x = 5;
	p.y = 9;
}

// 函数指针和函数对象作为参数。函数指针或函数对象(如 Lambda 或 std::function)可以作为参数,用于回调或动态行为
void execute(int a, int b, int (*operation)(int, int)) {
	std::cout << "Result: " << operation(a, b) << std::endl;
}

int add(int x, int y) { return x + y; }
int multiply(int x, int y) { return x * y; }


int main() {
	printSum(5, 3);

	int a = 5;
	increment(a);
	std::cout << std::format("Updated a: {}\n", a);

	int b = 7;
	updateValue(&b);
	std::cout << std::format("Updated b: {}\n", b);

	std::string message = "Using const modifiers reference arguments.";
	printMessage(message);

	int arr[] = { 1,2,3,4,5 };
	int arrSize = sizeof(arr) / sizeof(arr[0]);
	printArray(arr, arrSize);

	Point pt;
	setPoint(pt);
	std::cout<<std::format("Point: {},{}\n", pt.x, pt.y);

	int c = 2, d = 5;
	execute(c, d, &add);
	execute(c, d, &multiply);

	return 0;
}
  
  🡮
Sum: 8
Updated a: 6
Updated b: 66
Using const modifiers reference arguments.
1,2,3,4,5,
Point: 5,9
Result: 7
Result: 10
  
  • C++ 特有的参数特性
  #include <iostream>
#include <format>
#include <functional>

// 默认参数。C++ 支持为函数参数设置默认值。调用时如果未传入该参数,则使用提供的默认值
void greet(const std::string& name = "Guest") {
	std::cout << std::format("Hello, {}!\n", name);
}

// 函数重载。C++ 支持同名函数的多种定义,只要保证参数的数量或类型不同
void print(int x) {
	std::cout << std::format("Integer: {}\n", x);
}

void print(const std::string& s) {
	std::cout << std::format("String: {}\n", s);
}

// 模板函数。使用模板实现通用函数,支持任意类型的参数
template<typename T>
T add(T x, T y) {
	return x + y;
}

// 可变参数模板。支持接收任意数量的参数
template<typename... Args>
void printAll(Args... args) {
	((std::cout << args << ","), ...) << std::endl;
}

// Lambda 表达式作为参数。允许使用匿名函数直接作为参数
void execute(int a, int b, const std::function<int(int, int)>& operation) {
	std::cout << "Result: " << operation(a, b) << std::endl;
}

int main() {
	greet();
	greet("Henry");

	print(57);
	std::string s = "Function overloading.";
	print(s);

	int intAdd = add(5, 3);
	std::cout << std::format("Integer: {}\n", intAdd);
	float floatAdd = add(5.2, 3.8);
	std::cout << std::format("Float: {}\n", floatAdd);

	printAll(7, 8, 3.14, "string");

	execute(10, 20, [](int x, int y) {return x + y; });

	return 0;
}
  
  🡮
Hello, Guest!
Hello, Henry!
Integer: 57
String: Function overloading.
Integer: 8
Float: 9
7,8,3.14,string,
Result: 30
  
  • 函数参数的传递方式

在 C++ 中,函数参数传递的方法主要包括按值传递、按引用传递、按指针传递和常量引用传递。当不需要修改参数,并其数据量较小时,通常用按值传递;如需修改原始值或避免复制大型数据对象时,考虑使用按引用传递;需要动态分配内存或处理数组时,一般用按指针传递;当只访问大型数据对象时,可以用按常量引用传递。

  #include <iostream>
#include <format>

// 按值传递(Pass by Value)。传递参数的副本,函数内部的修改不会影响实参
void argsCallByValue(int x) {
	x = 10;
}

// 按引用传递(Pass by Reference)。使用 & 符号,传递参数的引用,函数内部对参数的修改会直接作用于实参
void argsCallByReference(int& x) {
	x = 10;
}

// 按指针传递(Pass by Pointer)。传递参数的地址,函数可以通过解引用修改实参或访问动态分配的数据
void argsCallByPointer(int* x) {
	*x = 50;
}

// 常量引用传递(Pass by Const Reference)。用于只读参数,避免值复制,并防止函数修改参数变量数据
void display(const std::string& s) {
	std::cout << s << std::endl;
}


int main() {
	int a = 5;
	argsCallByValue(a);
	std::cout << std::format("{}\n", a);

	argsCallByReference(a);
	std::cout << std::format("{}\n", a);

	argsCallByPointer(&a);
	std::cout << std::format("{}\n", a);

	display("Do not modify args.");

	return 0;
}
  
  🡮
5
10
50
Do not modify args.
  
  • 函数参数的类型和传递方式

C# 中的函数参数支持多种传递方式和特性。默认情况下,参数按值传递,传递的是参数的副本;通过refout关键字可以按引用传递,允许函数修改参数值,其中ref参数需在调用前初始化,而out参数由函数负责初始化。C# 还支持默认参数(default)和具名参数(named),允许在函数调用时省略或重新指定某些参数的值。此外,使用params关键字可以传递可变数量的参数,in关键字提供只读引用传递。C# 还允许传递委托或 Lambda表达式作为参数,实现动态回调和功能扩展。通过这些特性,C# 的函数参数传递机制灵活多样,满足各种编程需求。

参数类型 描述 示例
按值传递 默认方式,传递参数副本,无法修改实参 void func(int x)
按引用传递 通过引用传递,修改会影响原始变量 void func(ref int x)
输出参数 通过引用传递,调用前无需初始化 void func(out int x)
只读引用参数 参数只读引用,提高性能,不允许修改 void func(in int x)
参数数组 传递任意数量参数,支持0个或多个 void func(params int[] nums)
默认参数 为参数指定默认值,可以省略部分参数调用 void func(int x = 10)
具名参数 按参数名指定值,不限制参数顺序 func(name: "ALice")
委托参数 传递函数指针或 Lambda 表达式 void func(Action action)
泛型参数 函数支持多种类型,提高复用性 T func<T>(T x, T y)
  using System;
using static System.Formats.Asn1.AsnWriter;
using System.Xml.Linq;
using System.ComponentModel.DataAnnotations;

class Program
{
    // 按值传递(Pass by Value)。默认情况下,C# 中的函数参数按值传递,函数接收参数的副本,无法修改原始值
    static void argsCallByValue(int x)
    {
        x = 10;
    }

    // 按引用传递(Pass by Reference)。使用 ref 或 out 关键字传递参数,使函数能够修改原始变量的值。ref 参数:需要在调用函数前初始化参数
    static void argsCallByReference(ref int x)
    {
        x = 10;
    }

    // out 参数:调用前不需要初始化参数,但函数必须在内部赋值
    static void argsCallByOut(out int x, out int y)
    {
        x = 30;
        y = x;
    }

    // 默认参数(Default parameters)。C# 支持为函数参数提供默认值,调用时如果省略了这些参数则使用提供的默认值
    static void greet(string name = "Guest")
    {
        Console.WriteLine($"Hello, {name}!");
    }

    // 具名参数(Named Parameters)。调用函数时可以按参数名显示传递参数值,增强代码可读性,且允许参数顺序变化
    static void display(string firstName, string lastName)
    {
        Console.WriteLine($"Name: {firstName} {lastName}");
    }

    // 可选参数(Optional Parameters)。参数声明时使用 params 关键字,可以传递任意数量的参数,或者不传递参数
    static void printNumbers(params int[] numbers)
    {
        foreach (var num in numbers)
        {
            Console.Write($"{num}, ");
        }
    }

    //  数组参数。C# 默认是通过引用传递数组,即修改数组元素会改变原数组
    static void modifyArray(int[] arr)
    {
        arr[0] = 99;
    }

    static void Main()
    {
        int a = 5;
        argsCallByValue(a);
        Console.WriteLine(a);

        argsCallByReference(ref a);
        Console.WriteLine(a);

        int b;
        argsCallByOut(out a, out b);
        Console.WriteLine($"a = {a};b = {b}");

        greet();
        greet("Henry");

        display(lastName: "Smith", firstName: "John");

        printNumbers(1, 2, 3);
        printNumbers();

        int[] numbers = { 1, 2, 3 };
        modifyArray(numbers);
        Console.WriteLine($"\n{numbers[0]}");
    }
}
  
  🡮
5
10
a = 30;b = 30
Hello, Guest!
Hello, Henry!
Name: John Smith
1, 2, 3,
99
  
  • C# 函数参数的特性
  using System;
using static System.Formats.Asn1.AsnWriter;
using System.Xml.Linq;
using System.ComponentModel.DataAnnotations;

class Program
{
    // in 参数。传递的是只读引用,保证函数无法修改该参数
    static void display(in int x)
    {
        Console.WriteLine(x);
    }

    // 泛型函数的参数。使用泛型函数支持任意类型的参数
    static T add<T>(T x, T y) where T : struct
    {
        return (dynamic)x + (dynamic)y;
    }

    // 具名和默认参数结合。调用时只需指定必要的参数
    static void configure(int width = 1920, int height = 1080, bool fullscreen = true)
    {
        Console.WriteLine($"Width: {width}, Height: {height}, Fullscreen: {fullscreen}");
    }

    // 委托和 Lambda 表达式作为参数。用于回调和动态行为
    static void executeAction(Action action)
    {
        action();
    }

    static void Main()
    {
        int value = 5;
        display(value);

        int sumInt = add(5, 3);
        Console.WriteLine(sumInt.ToString());
        double sumDouble = add(1.5, 2.5);
        Console.WriteLine(sumDouble.ToString());
        double sumDoubleInt = add(3, 2.5);
        Console.WriteLine(sumDoubleInt.ToString());

        configure(height: 720);

        executeAction(() => Console.WriteLine("Hello from lambda!"));
    }
}
  
  🡮
5
8
4
5.5
Width: 1920, Height: 720, Fullscreen: True
Hello from lambda!
  

🤖 泛型函数add()代码行解释:

  static T add<T>(T x, T y) where T : struct
{
    return (dynamic)x + (dynamic)y;
}
  

由 C# 语言定义了一个泛型方法add(),用于对两个相同类型的参数进行加法操作。

1. 方法声明

  static T add<T>(T x, T y) where T : struct
  
  • static:该方法是静态方法。因此不需要实例化类即可调用。
  • T:为泛型参数,表示方法可以接受任意类型的数据,但该类型在方法执行时通过类型推断来确定。
  • T x, T y:方法的两个参数xy,其类型都为T,为相同类型。
  • where T:struct:为泛型约束,要求T类型必须是值类型(如intfloatdouble等),而不能是引用类型(如、接口等)。

2. 方法体

  return (dynamic)x + (dynamic)y;
  
  • dynamic:此关键字会在运行时进行类型解析,而不是在编译时进行类型检查。通过将xy转换为dynamic类型,C# 编译器允许执行在运行时解析的操作(如加法),意味着xy的具体类型将会在执行时被确定。
  • (dynamic)x + (dynamic)y:将xy转换为dynamic类型后,其加法操作将在运行时执行,而不依赖于编译时的类型检查。因此可以对任意类型的数值进行加法操作(为值类型并符合struct的约束)。

返回值

Python 是动态类型语言,函数的返回值类型在定义函数时无需明确指定;C/C++ 和 C# 是静态类型语言,函数的返回值类型在定义函数时需要明确指定。表为几种语言的返回值类型支持和返回多个值时的方式。

语言 返回值类型支持 多返回值方式
Python 动态返回类型;可以用类型提示(非强制) 通过元组返回多个值
C 静态返回类型;返回单个值;通过指针或结构体struct返回多个值 指针,结构体struct
C++ 静态返回类型;支持返回引用、std::pairstd::tuple std::pairstd::tuple、类/结构体
C# 静态返回类型;支持泛型和out参数 TupleValueTupleout参数
  a, b = 5, 3

# 返回单个值
def add(a, b):
    return a + b


result_add = add(a, b)
print(result_add)

# 返回多个值
def add_subtraction(a, b):
    sumR = a + b
    subR = a - b
    return sumR, subR


result_addSub = add_subtraction(a, b)
print(result_addSub)

# 没有返回值
def add_void(a, b):
    print("Nothing to return!")
    return


result_addVoid = add_void(a, b)
print(result_addVoid)
  
  🡮
8
(8, 2)
Nothing to return!
None
  
  #include <stdio.h>

void printArray(int* arr, int size) {
	for (int i = 0; i < size; i++) {
		printf("%d ", *(arr + i)); // 访问索引 i 处的元素
	}
	printf("\n");
}


// 返回单个值
int add(int a, int b) {
	return a + b;
}

// 返回指针
int* addSubtraction(int a, int b) {
	int sumR = a + b;
	int subR = a - b;

	int sumSub[2];
	sumSub[0] = sumR;
	sumSub[1] = subR;
	return sumSub;
}

// 没有返回值
void addVoid(int a, int b) {
	printf("Nothing to return!");
}

int main() {
	int a = 5, b = 3;
	int resultAdd = add(a, b);
	printf("%d\n", resultAdd);

	int* resultAddSub = addSubtraction(a, b);
	int size = sizeof(resultAddSub) / sizeof(result_addSub[0]);
	printArray(resultAddSub, size);

	addVoid(a, b);

	return 0;
}
  
  🡮
8
8 2
Nothing to return!
  
  #include <iostream>
#include <format>

// 返回单个值
int add(int a, int b) {
	return a + b;
}
// std::pair 形式返回多个值
std::pair<int, int> addSubtraction(int a, int b) {
	return { a + b,a - b };
}

// 没有返回值。使用指针,在函数中修改这些变量的值
void addSubtraction(int a, int b, int* sum, int* difference) {
	*sum = a + b;
	*difference = a - b;
}

// 没有返回值
void addVoid(int a, int b) {
	std::cout << "Nothing to return!" << std::endl;
}

// 返回引用
int& get_reference(int& a) {
	return a;
}


int main() {
	int a = 5, b = 3;
	int result_add = add(a, b);
	std::cout << result_add << std::endl;

	std::pair<int, int> result_addSub = addSubtraction(a, b);
	std::cout << std::format("{},{}\n", result_addSub.first, result_addSub.second);

	int sum, difference;
	addSubtraction(a, b, &sum, &difference);
	std::cout << std::format("{},{}\n", sum, difference);

	addVoid(a, b);

	int a_ref = get_reference(a);
	std::cout << std::format("{}", a_ref);

	return 0;
}
  
  🡮
8
8,2
8,2
Nothing to return!
5
  
  using System;

class Program
{
    // 返回单个值
    static int add(int a, int b)
    {
        return a + b;
    }

    // 返回多个值(使用 Tuple)
    static Tuple<int, int> addSubtraction(int a, int b)
    {
        return Tuple.Create(a + b, a - b);
    }

    // 返回多个值(使用 ValueTuple)
    static (string name, int id, double score) getStudentInfoValueTuple(string name, int id, double score)
    {
        return (name, id, score);
    }      

    // 返回多个值(使用 out 参数)
    static void addSubtractionOut(int a, int b, out int sum, out int difference)
    {
        sum = a + b;
        difference = a - b;
    }

    // 没有返回值
    static void addVoid(int a, int b)
    {
        Console.WriteLine("Nothing to return!");
    }

    // 泛型返回
    static T display<T>(T value)
    {
        Console.WriteLine("Value: " + value);
        Console.WriteLine("Type: " + typeof(T).Name);

        return value;
    }

    static void Main()
    {
        int a = 5, b = 3;
        int resultAdd = add(a, b);
        Console.WriteLine(resultAdd);

        Tuple<int, int> result_addSub = addSubtraction(a, b);
        Console.WriteLine($"{result_addSub.Item1},{result_addSub.Item2}");

        (string name, int id, double score) student = getStudentInfoValueTuple("Alice", 30, 95.5);
        Console.WriteLine($"Name: {student.id}, Id: {student.name}, Score: {student.score}"); 

        int sum, difference;
        addSubtractionOut(a, b, out sum, out difference);
        Console.WriteLine($"{sum},{difference}");

        addVoid(a, b);

        var value1 = display("Hello.");
        Console.WriteLine("Returned value: " + value1);
        var value2 = display(70);
        Console.WriteLine("Returned value: " + value2);
        var value3 = display(3.1415926);
        Console.WriteLine("Returned value: " + value3);       
    }
}
  
  🡮
8
8,2
Name: 30, Id: Alice, Score: 95.5
8,2
Nothing to return!
Value: Alice.
Type: String
Returned value: Alice.
Value: 30
Type: Int32
Returned value: 30
Value: 95.5
Type: Double
Returned value: 95.5
  

函数声明

Python 中不需要提前声明函数,其函数在代码中首次定义时自动声明。解释器在运行时按顺序执行代码,即代码的执行顺序就是定义和调用的顺序,因此只要在调用前定义了该函数,Python 解释器就可以识别并调用该函数。

C/C++ 语言中,函数声明(function declaration,function prototype)用于告诉编译器函数的名称,返回类型和参数类型(函数的签名),以便在代码中调用函数时,编译器可以识别并验证函数的正确性。函数声明通常位于文件的顶部、头文件中,或者在调用函数之前,其语法格式为return_type function_name(parameter_type1 parameter_name1, parameter_type2 parameter_name2, ...);。当函数的定义出现在调用之后或者在不同的文件中时,函数声明是必要的,以帮助编译器在编译阶段确认函数的存在及其参数类型,避免找不到函数定义的编译错误。在大型项目中,通常将函数声明放在头文件中,可以在多个源文件中使用相同的函数定义。

C# 编译器会自动识别代码中的方法和成员,只要方法存在于同一个类或命名空间中,编译器便可识别,不用在调用之前先进行声明。

Python 不用函数声明。

下述示例调换了函数greet()main()函数的位置,如果不在文件开始声明函数,而直接在main()函数中调用greet(),则会提示'greet' undefined;等错误信息。

  #include <stdio.h>

void greet(char name[]); // 函数声明

int main() {
	char name[] = "C";
	greet(name);

	return 0;
}

void greet(char name[]) {
	printf("Hello, %s!\n", name);
}
  
  🡮
Hello, C!
  

下述示例调换了函数greet()main()函数的位置,如果不在文件开始声明函数,而直接在main()函数中调用greet(),则会提示'greet': identifier not found等错误信息。

  #include <iostream>
#include <string>
#include <format>

void greet(std::string name); // 函数声明

int main() {
	std::string name = "C++";
	greet(name);

	return 0;
}

void greet(std::string name) {
	std::cout << std::format("Hello, {}!", name) << std::endl;
}
  
  🡮
Hello, C++!
  

C# 不用函数声明。

6.2 特殊的函数/方法

用法 Py C C++ C#

匿名函数(Lambda)

Lambda 表达式(函数/方法)是表示匿名函数(不需要命名的函数)的一种简洁方式。不同编程语言中的 lambda 语法和功能有所不同。Python 的 lambda 表达式最简洁、灵活,适合快速定义函数; C 语言本身没有原生的 lambda 表达式,只能通过函数指针或 struct来模拟;C++ 提供了 lambda 表达式功能,支持捕获变量、返回类型推导等高级特性;C# 与 C++ 类似,提供了简洁的 lambda 语法,当更注重与委托系统和语言集成查询(Language Integrated Query,LINQ)的结合。对比如表。

特性/语言 Py C C++ C#
Lambda 语法 lambda parameters: expression 不原生支持 [capture](parameter_list) -> return_type {function_body} parameters => expression
函数类型 匿名函数 函数指针 匿名函数对象 匿名方法(委托)
捕获外部变量 不支持 不支持 支持(按引用或按值) 支持(闭包捕获)
与 STL / LINQ等函数方法配合使用 支持(例如 mapfilter等) 不适用 支持(STL 算法) 支持(LINQ查询、事件等)
返回类型 自动推导 固定,取决于函数 可以显示指定或自动推导 可以显示指定或自动推导

Python 的匿名函数由lambda关键字定义,语法示例为lambda x,y:x+y,其中:之前为传入的参数;之后为表达式。匿名函数没有函数名(但可以赋予变量),通常用于在需要函数对象的地方快速定义小的、一次性使用的函数。

  from functools import reduce

# 基本的匿名函数
add = lambda x, y: x + y
print(add(5, 3))

# 用于排序。给定 sort() 方法的键函数(即参数 key),根据函数返回值对列表排序
people = [("Alice", 30), ("Bob", 25), ("Charlie", 35)]
people.sort(key=lambda person: person[1])
print(people)

# 配合 filter()。用于过滤序列,保留符合条件的元素
numbers = [1, 2, 3, 4, 5]
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)

# 配合 map()。将函数作用于序列的每个元素
squared = list(map(lambda x: x**2, numbers))
print(squared)

# 配合 reduce()。对序列中的元素根据函数进行累计计算。需要导入 functools 模块
product = reduce(lambda x, y: x * y, numbers)
print(product)
  
  🡮
8
[('Bob', 25), ('Alice', 30), ('Charlie', 35)]
[2, 4]
[1, 4, 9, 16, 25]
120
  

C 语言不支持匿名函数,但可以借助函数指针来模拟匿名函数行为。

  #include <stdio.h>
#include <stdlib.h>

void printArrayInt(int arr[], int size) {
	printf("{");
	for (int i = 0; i < size; i++) {
		printf("%d", arr[i]);
		if (i < size - 1) printf(", ");
	}
	printf("}\n");
}


// 定义一个函数,用函数指针调用该函数
int add(int x, int y) {
	return x + y;
}

// 用于接受函数指针作为参数的 qsort 等函数,模拟匿名函数的行为
int compare(const void* a, const void* b) {
	return (*(int*)a - *(int*)b);
}

int main() {
	int (*func_ptr)(int, int) = add; // 用函数指针调用 add 函数
	printf("Result: %d\n", func_ptr(5, 3));

	int arr[] = { 5,3,8,1,4 };
	size_t arr_size = sizeof(arr) / sizeof(arr[0]);

	qsort(arr, arr_size, sizeof(int), compare);
	printArrayInt(arr, arr_size);

	return 0;
}
  
  🡮
Result: 8
{1, 3, 4, 5, 8}
  

🤖 compare函数代码行解释

  int compare(const void* a, const void* b) {
	return (*(int*)a - *(int*)b);
}
  

1. 函数声明

  int compare(const void* a, const void* b) 
  
  • int:函数的返回类型为int,表示返回一个整数值。
  • compare:函数名称,定义比较两个元素的函数。
  • const void* aconst void* b:这两个参数是指向常量void类型的指针。void指针是一种通用指针类型,可以指向任何数据类型;const是为了确保函数不会修改指向的数据;因为该函数用于qsort排序函数,其接受void*类型指针,所以ab必须是void*类型指针,支持对任何类型的数据进行排序。

2. 函数体

  return (*(int*)a - *(int*)b);
  
  • *(int*)aaconst void*类型指针。通过(int *)aa强制转换为指向int类型的指针;然后通过*运算符解引用指针,获取a所指向的值,并将其视为一个int类型的值。*(int*)b解释同*(int*)a
  • *(int*)a - *(int*)b:该表达式用于计算ab所指向整数值之间的差,返回结果为负数、正数或0。

3. 返回值

返回值用于qsort函数来决定如何排序数组中的元素。如果为负数,a应排在b前面;如果为正数,a应该排在b后面;如果为0, 表示ab相等,其顺序保持不变。

C++11 引入了 lambda 表达式(匿名函数),其基本语法为[capture](parameter_list) -> return_type {function_body},其中[]为捕获列表,用来指定从外部作用域捕获那些变量。可以捕获外部变量的值或引用;()为参数列表,和普通函数一样,指定函数的参数;->返回类型,可选,指定函数返回的类型。如果返回类型可以从表达式推导出来,则可以省略;{}为函数体,定义函数的实际操作。

  #include <iostream>
#include <format>
#include <vector>
#include <algorithm>

int main() {

    // 一个简单的 lambda 函数。[]为空,不捕获任何外部变量。()为空,内部没有参数
	auto printX = []() {
		int x = 10;
		std::cout << std::format("x = {}\n", x);
		};

	printX();

    // 带参数的 lambda 函数。[]为空,不捕获任何外部变量。(int a, int b) 为接受两个整型参数。-> int 指定返回类型为整型
	auto add = [](int a, int b)->int {
		return a + b;
		};

	std::cout << std::format("Sum: {}\n", add(5, 3));

    // 捕获外部变量——按值捕获。示例中 [x,y] 可以捕获外部变量的副本
	int x = 50, y = 30;    
	auto printSumByVal = [x, y]() {
		std::cout << std::format("Sum-by value: {}\n", x + y);
		};
	printSumByVal();

    // [] 为捕获外部变量的副本,x 和 y 为按值捕获,即在 lambda 函数创建时复制一份 x 和 y 的值。当外部变量在 lambda 函数执行后才修改,lambda 函数依然使用创建时的原始值,而不会用修改后的值
	x = 60;
	y = 40;
	printSumByVal();
	std::cout << x << ";" << y << std::endl;

    // 捕获外部变量——按引用捕获。[&]为按引用捕获。示例中 [&x, &y] 捕获 x 和 y 的引用,lambda 中对 x 和 y 的修改会影响外部的 x 和 y 的值
	auto printSumByRef = [&x, &y]() {
		std::cout << std::format("Sum-by reference: {}\n", x + y);
		x += 5;
		y += 5;
		};
	printSumByRef();
	std::cout << x << ";" << y << std::endl;

    // 捕获特定变量。可以混合使用按值捕获和按引用捕获
	auto printSumByRefVal = [x, &y]() {
		std::cout << std::format("Sum-by reference and val: {}\n", x + y);
		};
	printSumByRefVal();
	x += 5;
	y += 5;
	printSumByRefVal();    

    // 捕获所有外部变量——按值捕获。[=]捕获所有外部变量的副本
	int z = 100;
	auto printSumAllByVal = [=]() {
		std::cout << std::format("Sum-by value: {}\n", x + y + z);
		};
	printSumAllByVal();

    // 捕获所有外部变量——按引用捕获。[&]捕获所有外部变量的引用
	z = 200;
	auto printSumAllByRef = [&]() {
		std::cout << std::format("Sum-by reference: {}\n", x + y + z);
		};
	printSumAllByRef();

    // 如果 lambda 函数的返回类型无法自动推导,则需明确指定返回类型
	auto multiply = [](double a, double b)->double {
		return a * b;
		};
	std::cout << std::format("{}\n", multiply(3.5, 2.5));

    // lambda 函数经常与 std::for_earch,std::sort 等 STL 算法配合使用
	std::vector<int> vec = { 3,4,5,6,7 };
	std::for_each(vec.begin(), vec.end(), [](int n) {
		std::cout << n << " ";
		});

    // lambda 不能修改按值捕获的变量,但是如果要修改,可以使用 mutable 关键字
	auto counter = [count = 0]() mutable {
		return ++count;
		};

	std::cout<<std::endl;
	std::cout << counter() << std::endl;
	std::cout << counter() << std::endl;
	std::cout << counter() << std::endl;

	return 0;
}
  
  🡮
x = 10
Sum: 8
Sum-by value: 80
Sum-by value: 80
60;40
Sum-by reference: 100
65;45
Sum-by reference and val: 110
Sum-by reference and val: 115
Sum-by value: 210
Sum-by reference: 310
8.75
3 4 5 6 7
1
2
3
  

🤖 解释 std::for_each和 lambda 函数配合使用的方法

  std::vector<int> vec = { 3,4,5,6,7 };
std::for_each(vec.begin(), vec.end(), [](int n) {
    std::cout << n << " ";
    });
  

1. 声明并初始化std::vector<int>

  std::vector<int> vec = { 3,4,5,6,7 };
  
  • std::vector<int>:声明一个std::vector类型的容器(向量),元素类型为intstd::vector是 C++ 标准库提供的动态数组类,可以存储任意数量的元素,可参考数据结构部分的详细解释。
  • { 3,4,5,6,7 }:通过初始化列表对vec初始化,向向量中添加了5个整数元素:3,4,5,6,7。

2. 调用std::for_each遍历向量

  std::for_each(vec.begin(), vec.end(), [](int n) {
    std::cout << n << " ";
    });
  
  • std::for_each(vec.begin(), vec.end(), ...):是 C++ 标准库的一个算法,用于遍历容器中的每个元素,并对每个元素执行给定的操作。
  • vec.begin():返回指向vec容器首元素的迭代器,即指向第一个元素3
  • vec.end():返回指向vec容器尾后元素的迭代器,即指向7之后的位置。
  • std::for_each会对vec.begin()vec.end()之间的每个元素执行第3个参数中的操作。

3. lambda 函数

  [](int n) {
    std::cout << n << " ";
    }
  
  • []为空,lambda 表达式不会捕获任何外部变量。
  • (int n):lambda 的参数列表,表示 lambda 接收一个 int 类型的参数 n。每次调用时,n将被vec中的当前元素替代。
  • std::cout << n << " ";:为 lambda 函数体。在函数体内,调用std::cout输出当前元素n和一个空格" "。目的是输出每个元素,并以空格分隔。

🤖 解释counter lambda 函数

  auto counter = [count = 0]() mutable {
    return ++count;
    };
  
  • auto counterauto关键字用于自动推导变量counter的类型。由于counter被初始化为一个 lambda 表达式,因此编译器会自动推导其类型为std::function或某种函数类型。counter是一个可调用的函数对象,即定义的 lambda 函数。
  • [count = 0 ]:为 lambda 的捕获列表,表示从外部作用域捕获变量。count = 0是捕获变量count的方式,通过值捕获将count初始化为 0。lambda 捕获的是count的副本,而不是引用外部作用域的count。按值捕获使得count是 lambda 内部的一个局部变量,并初始值为0。
  • () mutable()是 lambda 的参数列表,表示这个 lambda 不接收任何参数。mutable关键字允许 lambda 在函数体内修改捕获的变量。默认情况下,lambda 捕获的变量是只读的,但使用mutable后,可以在 lambda 内部修改这些捕获的变量。即使捕获的是按值传递的变量,mutable也会使这些变量可以在 lambda 执行时被修改。
  • { return ++count; }:为 lambda 的函数体,即 lambda 执行时的具体操作。++count对捕获的count变量执行自增操作,并返回自增后的结果。由于count是在 lambda 内部作为局部变量捕获的副本,因此每次调用counter时,count都会自增并保持其最新值。

C# 中的匿名函数可以通过委托delegate和 lambda 表达式实现,通常用于创建没有显示名称的函数,用于临时回调或者作为参数传递给方法。lambda 表达式的基本语法为parameters => expression,包括参数列表,定义传递给函数的参数;=>为 lambda 操作符,用于将参数列表和函数体分开;及函数体,可以是一个表达式或一个包含多条语句的代码块。

  using System;

class Program
{
    static void printArrayAny(Array arr)
    {
        Console.Write("{");
        for (int i = 0; i < arr.Length; i++)
        {
            if (i == arr.Length - 1)
            {
                Console.Write($"{arr.GetValue(i)}}}\n");
            }
            else
            {
                Console.Write($"{arr.GetValue(i)},");
            }

        }
    }

    public event Action<string> OnMessageReceived;

    static void Main()
    {
        // 通过 delegate 关键字定义匿名函数。
        Func<int, int, int> addDelegate = delegate (int x, int y)
        {
            return x + y;
        };

        Console.WriteLine(addDelegate(5, 3));

        // 一个简单的 lambda 表达式示例
        Func<int, int, int> addLambda = (x, y) => x + y;
        Console.WriteLine(addLambda(5, 3));

        // 带有语句块的 lambda 表达式
        Func<int, int, int> subtract = (x, y) =>
        {
            int result = x - y;
            return result;
        };
        Console.WriteLine(subtract(5, 3));

        // 捕获外部变量。C# 的 lambda 表达式可以捕获外部作用域的变量,意味着可以在 lambda 表达式内访问和修改外部变量,例如 factor 变量
        int factor = 2;
        Func<int, int> multiply = x => x * factor;
        Console.WriteLine(multiply(5));

        factor = 3;
        Console.WriteLine(multiply(5));

        // 在参数列表中指定参数类型
        Func<int, int, int> multiplyInt = (int x, int y) => x * y;
        Console.WriteLine(multiplyInt(7, 8));

        // lambda 表达式常与语言集成查询(Language Integrated Query,LINQ)配合使用,来实现查询、过滤和排序等操作
        var numbers = new[] { 1, 2, 3, 4, 5, 6 };
        var evenNumbers = numbers.Where(x => x % 2 == 0).ToArray();
        printArrayAny(evenNumbers);

        // lambda 也用于事件处理,尤其简化委托的创建
        Program program = new Program();
        program.OnMessageReceived += message => Console.WriteLine("Received: " + message);
        program.OnMessageReceived?.Invoke("Hello, Lambda!");
        program.OnMessageReceived += messageExtra => Console.WriteLine("Received Extra: " + messageExtra);
        program.OnMessageReceived?.Invoke("Hi, C#!");
    }
}
  
  🡮
8
8
2
10
15
56
{2,4,6}
Received: Hello, Lambda!
Received: Hi, C#!
Received Extra: Hi, C#!
  

🤖 解释addDelegate匿名方法定义

  Func<int, int, int> addDelegate = delegate (int x, int y)
{
    return x + y;
};
  

1. Func<int,int,int>

  • Func是 C# 中的一种委托类型,表示一个接受参数并返回值的函数。Func<int,int,int>表示一个接受两个int类型参数并返回一个int类型结果的委托类型,代表一个方法(函数)。

2. = delegate(int x,int y)

  • delegate是 C# 中定义匿名方法(或无名方法)的关键字。delegate(int x,int y)定义了一个匿名方法,该方法接收两个整数参数xy,并且没有名称。(int x,int y)为匿名方法的参数列表

3. 方法体: {return x + y}

  • 执行方法体内语句块。

🤖 解释 lambda 表达式处理事件的订阅和触发

  Program program = new Program();
program.OnMessageReceived += message => Console.WriteLine("Received: " + message);
program.OnMessageReceived?.Invoke("Hello, Lambda!");
  

1. 创建Program类的实例

  Program program = new Program();
  
  • 创建了Program类的一个实例programProgram类中定义有一个名为OnMessageReceived的事件,后续代码会通过这个事件来处理消息。

2. 订阅事件并使用 lambda 表达式

  program.OnMessageReceived += message => Console.WriteLine("Received: " + message);
  
  • program.OnMessageReceivedOnMessageReceivedProgram类中的一个事件。事件通常用于允许其他类或对象响应类中的某些操作。OnMessageReceivedEventHandler<string>类型的事件,即传递一个字符串类型的消息。
  • +=:这个操作符用于将一个处理方法或 lambda 表达式添加到事件的订阅列表中。当事件触发时,所有订阅了这个事件的方法都会别调用。
  • message => Console.WriteLine("Received: " + message);:为 lambda 表达式,用于定义事件的处理方法。当OnMessageReceived事件被触发时,会调用这个方法。
  • message是事件处理方法的参数,代表接收到的消息。
  • Console.WriteLine("Received: " + message);:当事件触发时,消息会被输出到控制台,并在消息前添加字符串"Received: "

3. 触发事件

  program.OnMessageReceived?.Invoke("Hello, Lambda!");
  
  • ?.操作符是一个空(值)条件操作符,以确保在触发事件之前,事件OnMessageReceived不是null。如果事件没有任何订阅者,Invoke就不会被调用,从而避免空引用异常。
  • Invoke("Hello, Lambda!"):调用OnMessageReceived事件并传递一个字符串"Hello, Lambda!"。当事件被触发时,所有订阅了此事件的处理程序(示例为 lambda 表达式)将被执行,且传入的参数("Hello, Lambda!")会被传递给这些处理程序。

委托(Delegate)和回调函数

委托(Delegate)是编程语言中的一种类型,允许将方法或函数作为参数传递给其他函数,或者使得函数可以在运行时被动态调用。不同语言对委托的实现有不同的概念和实现方式,Python 使用函数对象、回调函数或闭包来模拟委托; C 使用函数指针来模拟委托;C++ 可以使用函数指针、std::function或 lambda 表达式来实现委托;C# 内建委托类型,通过delegate关键字来定义,并且支持事件、回调等功能。不同语言的实现方式各有特点,但本质上都是通过将方法或函数作为参数传递,从而实现动态调用的机制。

在 Python 中,没有明确的“委托”类型,但可以使用函数对象、回调函数或闭包模拟委托行为。

  • 使用回调函数

在 Python 中,函数本身是对象,可以直接传递给其他函数,类似于委托。

  def greeting(name):
    return f"Hello, {name}!"


def print_message(func, name):
    print(func(name)) # 调用委托函数

# 委托行为
print_message(greeting, "Alice")
  
  🡮
Hello, Alice!
  
  • 使用闭包
  def delegate():
    def greet(name):
        return f"Hello, {name}!"
    return greet


greeting_func = delegate() # 获取闭包函数
print(greeting_func("Bob"))
  
  🡮
Hello, Bob!
  

在 Python 中,闭包(Closure)是指一个函数对象(通常是嵌套函数)能够“记住”并访问其外部函数的局部变量,即使外部函数已经返回。闭包的本质是函数和它的外部作用域(nonlocal 或 global)变量的绑定关系。闭包包括外部函数(封装一个局部变量);内部函数(引用了外部函数的局部变量);外部函数返回内部函数:这使得内部函数在外部函数执行完毕后依然访问外部函数的局部变量。

  def outer_function(outer_variable):
    # 外部函数的局部变量
    def inner_function(inner_variable):
        # 内部函数引用外部函数的变量
        return outer_variable + inner_variable

    # 返回内部函数
    return inner_function

# 创建一个闭包
closure = outer_function(10)
# 调用闭包函数
print(closure(5))
  
  🡮
15
  

闭包能够创建带有状态的函数,可以实现数据隐藏和封装;维护一个函数的内部状态,而不暴露该状态。例如用闭包实现一个计时器的程序。

  def make_counter():
    count = 0

    def counter():
        nonlocal count
        count += 1
        return count

    return counter

counter1 = make_counter()
counter2 = make_counter()

print(counter1())
print(counter1())
print(counter2())
print(counter2())
  
  🡮
1
2
1
2
  

make_counter是一个外部函数,定义了一个局部变量countcounter是内部函数,其每次被调用时会增加count的值,并返回该值。通过nonlocal关键字,counter能够修改外部函数make_countercount变量。counter1counter2分别是两个独立的计数器,拥有各自的count状态,不会互相干扰。

C 中没有内置的委托类型,但可以通过函数指针模拟委托。函数指针允许将函数传递给其他函数,或者动态地调用不同地函数。下列中将函数地地址(函数指针)传递给printMessage,实现类似委托地效果。

  #include <stdio.h>

void greeting(const char* name) {
	printf("Hello, %s!\n", name);
}

void printMessage(void (*func)(const char*), const char* name) {
	func(name);
}

int main() {
	printMessage(greeting, "Charlie");

	return 0;
}
  
  🡮
Hello, Charlie!
  

C++ 中,委托通常通过函数指针、Lambda表达式或std::function实现。

  • 使用函数指针
  #include <iostream>
#include <format>

void greeting(const std::string& name) {
	std::cout << std::format("Hello, {}!", name);
}

void printMessage(void (*func)(const std::string&), const std::string& name) {
	func(name); // 调用委托函数
}

int main() {
	printMessage(greeting, "David");

	return 0;
}
  
  🡮
Hello, David!
  
  • std::function

std::function定义在<functional>头文件中,是一个通用的函数包装器,语法为std::function<R(Args...)>R为函数返回类型;Args...为函数参数类型),可以将任何符合特定签名的可调用对象(如普通函数、函数指针、Lambda 表达式、成员函数指针、函数对象等)存储和传递,可用于回调函数的实现,并允许在不需要显示指定回调类型的情况下注册多个回调函数。

  #include <iostream>
#include <format>
#include <functional>

void exampleFunction(int x) {
	std::cout << std::format("Function called with value: {}\n", x);
}

int main() {
    // 创建一个 std::function 对象,接受一个 int 参数并返回 void
	std::function<void(int)>func;

    // 将普通函数绑定到 std::function
	func = exampleFunction;

    // 调用通过 std::function 绑定的函数
	func(42);

	return 0;
}
  
  🡮
Function called with value: 42
  

使用 Lambda 表达式

std::function可以存储 Lambda 表达式。

  #include <iostream>
#include <format>
#include <functional>

int main() {
    // 使用 lambda 表达式创建 std::function
	std::function<void(int)>func = [](int x) {
		std::cout << std::format("Lambda called with value: {}\n", x);
		};
	func(10);

	return 0;
}
  
  🡮
Lambda called with value: 10
  

使用成员函数指针

std::function可以存储成员函数指针,并通过对象实例调用。

  #include <iostream>
#include <format>
#include <functional>

class MyClass {
public:
	void memberFunction(int x) {
		std::cout << std::format("Member function called with value: {}\n", x);
	}
};

int main() {
	MyClass obj;

    // 创建 std::function 对象,并绑定成员函数
	std::function<void(MyClass&, int)>func = &MyClass::memberFunction;

    // 使用对象实例调用成员函数
	func(obj, 100);

	return 0;
}
  
  🡮
Member function called with value: 100
  

存储和调用回调函数

  #include <iostream>
#include <format>
#include <functional>

void callback(int value) {
	std::cout << std::format("Callback called with value: {}\n", value);
}

void executeCallback(std::function<void(int)>callback, int value) {
	callback(value); // 调用回调函数
}

int main() {
	executeCallback(callback, 5);

	return 0;
}
  
  🡮
Callback called with value: 5
  

存储多个回调函数

  #include <iostream>
#include <format>
#include <functional>
#include <vector>

void callback1(int value) {
	std::cout << std::format("Callback-1 called with value: {}\n", value);
}

void callback2(int value) {
	std::cout << std::format("Callback-2 called with value: {}\n", value);
}


int main() {
	std::vector<std::function<void(int)>>callbacks;
	callbacks.push_back(callback1);
	callbacks.push_back(callback2);
	
    // 调用所有回调函数
	for (auto& cb : callbacks) {
		cb(7);
	}

	return 0;
}
  
  🡮
Callback-1 called with value: 7
Callback-2 called with value: 7
  
  • 委托的定义

C# 的委托(Delegate)是一个类型安全的函数指针,允许方法作为参数传递,或者允许在运行时动态调用方法。委托可以用来实现事件处理、回调、及灵活的代码设计。委托是一个类型,定义了方法签名(即方法的返回类型和参数类型),例如delegate int AddDelegate(int a, int b),定义了一个AddDelegate的委托类型,接受两个int类型的参数并返回一个int。委托可以指向任何符合签名的方法。

  • 创建和使用委托
  using System;

class Program
{
    // 定义一个委托类型
    public delegate int AddDelegate(int a, int b); 

    // 定义一个方法
    public static int Add(int a, int b)
    {
        return a + b;
    }

    static void Main()
    {
        // 创建一个委托实例,并指向 Add 方法 
        AddDelegate addDelegate = new AddDelegate(Add);

        // 通过委托调用方法 
        int result = addDelegate(5, 3);
        Console.WriteLine(result);

    }
}
  
  🡮
8
  
  • 多播委托(Multicast Delegate)

多播委托可以指向多个方法。多个方法会按顺序执行,并且返回值会被忽略,除非最后一个方法返回一个值。

  using System;

public delegate void MethodsDelegate(string message);

class Program
{
    static void Method1(string message)
    {
        Console.WriteLine($"Method-1: {message}");
    }

    static void Method2(string message)
    {
        Console.WriteLine($"Method-2: {message}");
    }


    static void Main()
    {
        MethodsDelegate deleg = Method1;
        deleg += Method2; // 添加一个方法到委托链
        deleg("Hello, Delegates!"); // 会依次调用 Method1 和 Method2
    }
}
  
  🡮
Method-1: Hello, Delegates!
Method-2: Hello, Delegates!
  
  • 配合使用匿名方法和 Lambda 表达式
  using System;

public delegate int AddDelegate(int a, int b);

class Program
{

    static void Main()
    {
        // 配合使用匿名方法
        AddDelegate addDelegateAnonymous = delegate (int a, int b)
        {
            return a + b;
        };
        Console.WriteLine(addDelegateAnonymous(5, 3));

        // 配合使用 Lambda 表达式
        AddDelegate addDelegateLambda = (a, b) => a + b;
        Console.WriteLine(addDelegateLambda(5, 3));
    }
}
  
  🡮
8
8
  
  • 委托作为事件处理器

委托常用于事件处理,指定事件处理方法的签名。

  using System;

public delegate void EventHandler(string message);

class Publisher
{
    public event EventHandler Notify;

    public void TriggerEvent(string message)
    {
        Notify?.Invoke(message); // 触发事件
    }
}

class Subscriber
{
    public void OnNotify(string message)
    {
        Console.WriteLine("Event received: " + message);
    }
}

class Program
{

    static void Main()
    {
        Publisher publisher = new Publisher();
        Subscriber subscriber = new Subscriber();

        publisher.Notify += subscriber.OnNotify; // 订阅事件
        publisher.TriggerEvent("Hello Event!");
    }
}
  
  🡮
Event received: Hello Event!
  
  1. public delegate void EventHandler(string message);:定义了一个委托类型EventHandler,表示一个接受string类型参数且没有返回值的方法签名。这个委托签名会被用来定义事件处理程序(事件订阅方法)的签名。
  2. class Publisher:定义一个名为Publisher的类,代表事件的发布者,负责触发事件。
  3. public event EventHandler Notify:在Publisher中定义一个名为Notify的事件,其中event关键字表示这是一个事件,而EventHandler是事件的委托类型。即,Notify事件可以将一个或多个符合EventHandler委托签名的方法关联起来(即订阅者方法)。
  4. public void TriggerEvent(string message):定义一个名为TriggerEvent的方法,接受一个string类型的参数message,用于触发事件。
  5. Notify?.Invoke(message);Notify是一个委托类型的事件,Invoke是委托的调用方法,会触发所有订阅这个事件的方法。?.为空(值)条件操作符,如果Notify事件有订阅者(即不为null),就调用Invoke方法触发事件;如果没有订阅者(即Notifynull),则不执行任何动作,避免引发空引用异常。
  6. class Subscriber:定义一个名为Subscriber的类,代表事件的订阅者。订阅者会定义响应事件的方法。
  7. public void OnNotify(string message):订阅者类中的方法OnNotify用于响应事件。这个方法的签名符合EventHandler委托的签名(接受一个string参数并返回void)。当事件触发时,OnNotify会被调用,并输出message的内容。
  8. Publisher publisher = new Publisher();:创建一个Publisher类的实例,代表事件的发布者。
  9. Subscriber subscriber = new Subscriber();:创建一个Subscriber类的实例,代表事件的订阅者。
  10. publisher.Notify += subscriber.OnNotify; :该行代码实现事件订阅,+=将订阅的方法(Notify)添加到事件的委托调用列表中,表示当Notify事件触发时,subscriber.OnNotify会被调用,即为Notify事件的一个处理方法。
  11. publisher.TriggerEvent("Hello Event!");:触发publisher实例的Notify事件。调用TriggerEvent方法时,事件被触发,Notify委托会调用所有已经订阅的处理方法(如subscriber.OnNotify)。
  • 委托的类型
  1. Func<T, TResult>:代表返回类型为TResult,并且接受类型为T的方法的委托。
  2. Action<T>:代表无返回值且接受类型为T的方法的委托。
  3. Predicate<T>:代表接受类型为T的参数并返回bool的方法的委托。
  using System;

class Program
{

    static void Main()
    {
        Func<int, int, int> addFunc = (a, b) => a + b;
        Console.WriteLine(addFunc(5, 3));

        Action<string> printAction=message=>Console.WriteLine(message);
        printAction("Hello, Action!");

        Predicate<int> isPositive = number => number > 0;
        Console.WriteLine(isPositive(3));

    }
}
  
  🡮
8
Hello, Action!
True
  

内联函数

内联函数(inline function)是一种优化机制。Python 通过解释执行不进行内联;C/C++ 提供了inline关键字,允许编译器决定是否进行内联,从而提高小函数的性能;C# 使用MethodImplOptions.AggressiveInlining提供类似的功能,但内联是否发生依赖于即时编译器(Just-In-Time,JIT) 编译器的决定。内联函数通常用于小函数计算的开销,而对于大型函数或复杂计算,内联函数可能会导致代码膨胀,生成大量重复的代码,增加编译时间和程序体积。

不支持。

  #include <stdio.h>

inline int square(int x) {
	return x * x;
}

int main() {
	int result = square(5);
	printf("Square: %d\n", result);

	return 0;
}
  
  🡮
Square: 25
  
  #include <iostream>
#include <format>

inline int square(int x) {
	return x * x;
}

int main() {
	std::cout << std::format("Square: {}\n", square(5));

	return 0;
}
  
  🡮
Square: 25
  
  using System;
using System.Runtime.CompilerServices;

class Program
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    static int Add(int a,int b)
    {
        return a + b;
    }
    static void Main()
    {
        int c = Add(5, 3);
        Console.WriteLine(c);
    }
}
  
  🡮
8
  

函数/方法重载

函数/方法重载(Function Overloading)是指在同一作用域内,可以定义多个同名但参数列表不同的函数,编译器会根据函数调用时传递的参数类型和数量来决定调用哪个版本的函数。Python 和 C 不支持函数重载。C++ 和 C# 支持函数/方法重载,会根据参数的类型、数量和顺序来选择正确的函数。

不支持。

不支持。

  #include <iostream>

int add(int a, int b) {
	return a + b;
}

double add(double a, double b) {
	return a + b;
}

int main() {
	std::cout << add(5, 3) << std::endl;
	std::cout << add(5.3, 3.3) << std::endl;

	return 0;
}
  
  🡮
8
8.6
  
  using System;

class Program
{
    static int Add(int a, int b)
    {
        return a + b;   
    }

    static double Add(double a, double b)
    {
        return a + b;
    }

    static void Main()
    {
        Console.WriteLine(Add(5, 3));
        Console.WriteLine(Add(5.3, 3.3));
    }
}
  
  🡮
8
8.6
  

递归函数

递归是指函数/方法调用自身的一种编程技术。在递归中,问题被分解成更小的相同问题,直到遇到一个基准条件(终止条件),终止递归。因此递归函数通常包含两部分,一是基准条件,即避免无限递归的条件;二是递归调用,将问题简化成更小的子问题。阶乘(Factorial)即为一个经典的递归问题,定义为n! = n * (n - 1) * (n - 2) * ... * 1,特别地,0!=1。用递归的方式来计算阶乘,其过程为,

  factorial(5) = 5 * factorial(4)
factorial(4) = 4 * factorial(3)
factorial(3) = 3 * factorial(2)
factorial(2) = 2 * factorial(1)
factorial(1) = 1 * factorial(0)
factorial(0) = 1  // 这是基准条件
  

在这个过程中,factorial(0)是基准条件,即当n等于0时,递归停止,返回1。然后递归逐层返回计算结果,最终factorial(5)会返回120。

  def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n - 1)


print(factorial(5))
  
  🡮
120
  
  #include <stdio.h>

int factorial(int n) {
	if (n == 0)
		return 1;
	else
		return n * factorial(n - 1);
}

int main() {
	printf("%d\n", factorial(5));
	return 0;
}
  
  🡮
120
  
  #include <iostream>

int factorial(int n) {
	if (n == 0)
		return 1;
	else
		return n * factorial(n - 1);
}

int main() {
	std::cout << factorial(5) << std::endl;

	return 0;
}
  
  🡮
120
  
  using System;

class Program
{
    static int Factorial(int n)
    {
        if (n == 0)
            return 1;
        else
            return n*Factorial(n-1);
    }

    static void Main()
    {
        Console.WriteLine(Factorial(5));
    }
}
  
  🡮
120
  

6.3 Python 的函数装饰器

Python 的函数装饰器是一种函数,用于修改或扩展其他函数的行为,其本质是接受一个函数作为参数并返回一个新函数的函数。这种机制可以在不改变原始函数代码的情况下,动态的为函数添加功能,例如添加日志、权限校验、性能计时等,并可实现更复杂的功能扩展。

解释过程 代码示例

1. 函数调用另一个函数(函数作为参数)

定义3个函数,其中say_hello(name)be_awesome(name)传入的为常规参数(不是以函数作为参数),并进行了不同方式字符串格式化;而greet_bob(greeter_func),从 greeter_func("Bob")语句可以判断出函数参数greeter_func为一个函数,并将say_hello(name)be_awesome(name)函数作为参数传入到函数greet_bob(greeter_func)

  def say_hello(name):
    return f"Hello {name}" 


def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"


def greet_bob(greeter_func):
    return greeter_func("Bob")


print(greet_bob(say_hello))
print(greet_bob(be_awesome))
  
  🡮
Hello Bob
Yo Bob, together we are the awesomest!
  

2.内置/嵌套函数(inner functions)

如果函数内部存在多个内置/嵌套函数,函数定义的前后位置并不重要,主要由执行语句的顺序确定。同时,内部/嵌套函数属于父函数parent()的局部作用域,仅在parent()内部作为局部变量存在。

  def parent():
    print("Printing from the parent() function")

    def first_child():
        print("Printing from the first_child() function")

    def second_child():
        print("Printing from the second_child() function")

    second_child()
    first_child()


parent()
  
  🡮
Printing from the parent() function
Printing from the second_child() function
Printing from the first_child() function
  

3.函数返回值为一个函数

Python 允许使用函数作为返回值,示例parent()函数返回了一个内置函数。返回函数时,为return first_child,是一个没有给()的函数名,意味返回first_child函数的引用。如果给了(),则是返回first_child函数的一个结果。当函数返回值为函数,则返回值(通常赋予于新的变量名)可以像普通函数一样调用。

  def parent(num):
    def first_child():
        return "Hi, I am Emma"

    def second_child():
        return "Call me Liam"

    if num == 1:
        return first_child
    else:
        return second_child


first = parent(1)
second = parent(2)

print(first)
print(first())
print(second())
  
  🡮
<function parent.<locals>.first_child at 0x000001CEF59763E0>
Hi, I am Emma
Call me Liam
  

4.简单的装饰器

say_whee = my_decorator(say_whee)返回my_decorator(func)父函数内置/嵌套函数wrapper()的引用,为return wrapper。该内置函数包含父类传入的一个函数参数func,并执行func()。执行say_whee()即执行父类my_decorator(func)内的wrapper()内置函数,此时该函数已经独立于父函数my_decorator(func),并包含有执行wrapper()函数所需的所有参数,这里为参数函数func。因此,say_whee = my_decorator(say_whee)中的say_whee为一个闭包(Closure,或Lexical Closure),存储了一个函数和与其关联的环境参数。

内置函数wrapper()实际上对传入的参数函数say_whee()的功能进行了增加,即“装饰”,所以可以说装饰器的作用是对一个函数进行包装,修改已有的功能等。

  def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")

    return wrapper


def say_whee():
    print("Whee!")


say_whee = my_decorator(say_whee)
say_whee()
  
  🡮
Something is happening before the function is called.
Whee!
Something is happening after the function is called.
  

say_whee = not_during_the_night(say_whee)装饰,其根据条件判断执行不同的操作,如果满足7 <= datetime.now().hour < 22,则执行外部函数say_whee();否则,什么都不发生。

  from datetime import datetime


def not_during_the_night(func):
    def wrapper():
        if 7 <= datetime.now().hour < 22:
            func()
        else:
            pass  # Hush, the neighbors are asleep

    return wrapper


def say_whee():
    print("Whee!")


say_whee = not_during_the_night(say_whee)
say_whee()
  
  🡮
Whee!
  

5.语法糖(Syntactic Sugar)

上面的装饰器方法笨拙。为了简化代码过程,Python 允许用@symbol方式使用装饰器,有时称为pie语法。示例与上述结果一致,但是通过@my_decoratorpie方法,替代了say_whee = not_during_the_night(say_whee)代码,简化操作。

  def my_decorator(func):
    def wrapper():
        print("Something is happening before the function is called.")
        func()
        print("Something is happening after the function is called.")

    return wrapper


@my_decorator
def say_whee():
    print("Whee!")


say_whee()
  
  🡮
Something is happening before the function is called.
Whee!
Something is happening after the function is called.
  

6.带参数的装饰器

wrapper_do_twice(*args, **kwargs)内置函数传入参数为*args**kwargs, 接受任意数量的位置参数和关键字参数,并将其传入参数函数。

  def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        func(*args, **kwargs)

    return wrapper_do_twice


@do_twice
def greet(name):
    print(f"Hello {name}")


greet("World")
  
  🡮
Hello World
Hello World
  

7.装饰器的返回值

如果装饰器要返回值,do_twice(func)内置函数 wrapper_do_twice(*args, **kwargs)在调用参数函数func时,需要执行return func(*args, **kwargs), 返回参数函数的返回值。

  def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func(*args, **kwargs)
        return func(*args, **kwargs)

    return wrapper_do_twice


@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"


hi_adam = return_greeting("Adam")
print("-" * 50)
print(hi_adam)

print("-" * 50)
print(return_greeting)
print(return_greeting.__name__)
print("-" * 50)
print(help(return_greeting))
  
  🡮
Creating greeting
Creating greeting
--------------------------------------------------
Hi Adam
--------------------------------------------------
<function do_twice.<locals>.wrapper_do_twice at 0x000002A8EE2E18A0>
wrapper_do_twice
--------------------------------------------------
Help on function wrapper_do_twice in module __main__:

wrapper_do_twice(*args, **kwargs)

None
  

可以接收装饰器内置/嵌套函数的返回值,赋予变量func_returned,并返回该结果。该结果最终将由hi_adam变量接收。

  def do_twice(func):
    def wrapper_do_twice(*args, **kwargs):
        func_returned=func(*args, **kwargs)
        return func_returned

    return wrapper_do_twice


@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"


hi_adam = return_greeting("Adam")
print(hi_adam)
  
  🡮
Creating greeting
Hi Adam
  

8.保留原始函数的信息-自省(introspection)调整

自省是指一个对象在运行时了解自己属性的能力。例如,一个函数知道它自己的名字和文档。在上述示例中,通过return_greeting.__name__help(return_greeting)等方式可以查看函数对象相关属性,但是,发现给出的是wrapper_do_twice的内置函数,而不是return_greeting函数,因此可以通过functools @functools.wraps(func)方法解决这个问题,保留原始函数的信息。

  import functools


def do_twice(func):
    @functools.wraps(func)
    def wrapper_do_twice(*args, **kwargs):
        func_returned = func(*args, **kwargs)
        return func_returned

    return wrapper_do_twice


@do_twice
def return_greeting(name):
    print("Creating greeting")
    return f"Hi {name}"


hi_adam = return_greeting("Adam")
print("-" * 50)
print(hi_adam)

print("-" * 50)
print(return_greeting)
print(return_greeting.__name__)
print("-" * 50)
print(help(return_greeting))
  
  🡮
Creating greeting
--------------------------------------------------
Hi Adam
--------------------------------------------------
<function return_greeting at 0x0000016C56CD1940>
return_greeting
--------------------------------------------------
Help on function return_greeting in module __main__:

return_greeting(name)

None
  

9. 带参数的装饰器

装饰器中可以带参数,例如@repeat(num_times=3)num_times=3。此时对装饰器函数做了调整,增加了一层嵌套内置函数,传递装饰器参数。

  import functools


def repeat(num_times):
    def decorator_repeat(func):
        @functools.wraps(func)
        def wrapper_repeat(*args, **kwargs):
            for _ in range(num_times):
                value = func(*args, **kwargs)
            return value

        return wrapper_repeat

    return decorator_repeat


@repeat(num_times=3)
def greet(name):
    print(f"Hello {name}")


greet("Galaxy")
  
  🡮
Hello Galaxy
Hello Galaxy
Hello Galaxy
  

10.多个装饰器装饰一个函数

可以将多个装饰器堆叠在一起,应用在一个函数上。此时装饰器执行的顺序是从内到外,例如示例中先执行@decor,返回值为20;而后再执行@decor1,返回值为400。

  def decor1(func):
    def inner():
        x = func()
        return x * x

    return inner


def decor(func):
    def inner():
        x = func()
        return 2 * x

    return inner


@decor1
@decor
def num():
    return 10


print(num())
  
  🡮
400
  

11.decorator模块简化装饰器

使用decorator模块库的@decorator装饰器装饰“装饰函数”,可以简化装饰器定义。例如示例代码取消了内置函数,将原始函数和输入参数都在do_print(func,*args, **kwargs)装饰函数中一起输入。

  from decorator import decorator


@decorator
def do_print(func, *args, **kwargs):
    print("Hi {}!".format(*args, **kwargs))
    return func(*args, **kwargs)


@do_print
def greet(name):
    print(f"Hello {name}!")


greet("World")
  
  🡮
Hi World!
Hello World!
  

6.4 作用域和命名空间

6.4.1 作用域

在编程中,作用域是指变量、函数或对象在代码块中的可访问范围。

作用域 Py C C++ C#

作用域类型

  • 局部作用域(Local Scope)

为函数或方法内部定义的变量,其作用范围仅限于该函数或方法内。局部变量的生命周期从函数调用开始,到函数执行结束。

  def foo():
    x = 10 # 局部变量
    print(x)


foo()
# print(x) # 如果运行,将提示错误信息: NameError: name 'x' is not defined
  
  🡮
10
  
  • 嵌套作用域(Enclosing Scope)

嵌套函数(函数内部定义函数)中,外部函数的局部作用域。内部函数可以访问外部函数的变量,但不能直接修改,除非使用nonlocal

访问外部函数变量 修改外部函数变量
  def outer():
    x = 10 # 外部函数变量

    def inner():
        print(x) # 内部函数访问外部函数变量

    inner()


outer()
  
  🡮
10
  
  def outer():
    x = 10 # 外部函数变量

    def inner():
        nonlocal x # 声明内层函数内可以修改外部作用域变量
        x = 20

    inner()
    print(x)


outer()
  
  🡮
20
  
  • 全局作用域(Global Scope)

模块级别定义的变量,作用范围为整个模块。全局变量可以在模块内的任何位置被访问,但在函数内修改时需用global声明。

访问全局变量 修改全局变量
  x = 10 # 全局变量

def foo():
    print(x) # 访问全局变量


foo()
  
  🡮
10
  
  x = 10 # 全局变量


def foo():
    global x # 声明局部作用域可以修改全局变量
    x = 20 # 修改全局变量


foo()
print(x)
  
  🡮
20
  
  • 内置作用域(Built-in Scope)

由 Python 解释器提供的内置作用域,包括标准函数(如lenprint)等。其优先级最低,只在局部、嵌套和全局作用域中找不时才会访问内置作用域。

  def foo():
    print(len([1, 2, 3])) # len() 为内置函数

foo()
  
  🡮
3
  

Python 按照 LEGB 顺序查找变量,依次为 局部(Local) -> 嵌套(Enclosing) -> 全局(Global)-> 内置(Built-in)。

  1. Local(局部作用域):当前函数或代码块内部的变量。
  2. Enclosing(嵌套作用域):嵌套函数的外部函数变量。
  3. Global(全局作用域):模块级别的变量;
  4. Built-in(内置作用域):Python 的内置函数或变量。
  x = "global"

def outer():
    x = "enclosing"

    def inner():
        x = "local"
        print(x) # 按 LEGB 顺序查找变量

    inner()


outer()
  
  🡮
local
  
  • 块作用域(Block Scope)

由大括号{}定义的代码块内的变量具有块作用域。块作用域变量只能在其定义的块内可见,块外无法访问。代码块结束后,块作用域变量被销毁。

  #include <stdio.h>

int main() {
	{
		int x = 10; // x 的作用域仅限于这个块
		printf("x = %d\n", x);
	}
	// printf("x = %d\n", x); // 将会提示错误信息:identifier "x" is undefined

	return 0;
}
  
  🡮
x = 10
  
  • 文件作用域(File Scope)

在所有函数之外定义的变量或函数具有文件作用域,其在文件中从定义的位置开始,到文件末尾都可见。全局变量具有文件作用域,可以在整个文件中访问。通过static关键字可以限制文件作用域的变量或函数只能在当前文件中使用。

  #include <stdio.h>

int globalVar = 100;

static int staticVar = 50;

int main() {
	printf("globalVar = %d\n", globalVar);
	printf("staticVar = %d\n", staticVar);
	return 0;
}
  
  🡮
globalVar = 100
staticVar = 50
  
  • 函数作用域(Function Scope)

goto语句的标签具有函数作用域。标签的作用范围仅限于定义它们的函数内部,不能被其他函数访问。

  #include <stdio.h>

void test() {
	goto myLabel; // 跳转到 myLabel 标签
	printf("This will be skipped.\n");

myLabel:
	printf("Jumped to label!\n");
}

int main() {
	test();

	return 0;
}
  
  🡮
Jumped to label!
  
  • 函数原型作用域(Function Prototype Scope)

函数参数在函数原型中有作用域。参数的作用域仅限于函数原型内部,函数定义中可以使用不同的名字。

  #include <stdio.h>

// 函数原型中的参数 a 作用域仅限于此处
void func(int a);

int main() {
	func(9);

	return 0;
}

// 函数定义中可以使用不同的参数名
void func(int b) { 
	printf("b = %d\n", b);
}
  
  🡮
b = 9
  
  • 代码块嵌套与屏蔽规则

当嵌套的代码块中定义了与外部代码块同名的变量时,内部变量会屏蔽外部变量。

  #include <stdio.h>

int main() {
	int x = 5; // 外部变量 x
	{
		int x = 10; // 屏蔽外部变量
		printf("Inner x = %d\n", x);
	}
	printf("Outer x = %d\n", x);

	return 0;
}
  
  🡮
Inner x = 10
Outer x = 5
  
  • 静态变量的作用域

使用static声明的变量具有静态存储持续性,但遵循其定义位置决定的作用域规则。函数内部的静态变量具有块作用域,但其值在程序运行期间保持不变。文件作用域的静态变量仅对当前文件可见。

  #include <stdio.h>

void counter() {
	static int count = 0; // 静态变量,块作用域
	count++;
	printf("Count = %d\n", count);
}

int main() {
	counter();
	counter();

	return 0;
}
  
  🡮
Count = 1
Count = 2
  
  • 全局作用域(Global Scope)

在所有函数或类之外定义的标识符,具有全局作用域,其在整个文件中都可以访问,除非被局部变量隐藏,并在程序开始时分配内存,程序结束时销毁。

  #include <iostream>
#include <format>

int globalVar = 100; // 全局变量

void printGlobal() {
	std::cout << std::format("Global variable: {}\n", globalVar);
}

int main() {
	printGlobal();

	return 0;
}
  
  🡮
Global variable: 100
  
  • 局部作用域(Local Scope)

在函数、语句块或循环内部定义的标识符,仅在所在的函数或语句块内有效,超出范围则不可访问。当函数被调用时分配内存,函数返回时销毁。

  #include <iostream>
#include <format>

void localScopeExample() {
	int localVar = 200; // 局部变量
	std::cout << std::format("Local variable: {}\n", localVar);
}

int main() {
	localScopeExample();
	// std::cout << localVar; // 提示错误信息,identifier "localVar" is undefined

	return 0;
}
  
  🡮
Local variable: 200
  
  • 类作用域(Class Scope)

在类内定义的成员(变量、函数、类型等)具有类作用域。类的成员可以通过类的对象访问,但如果成员是private,则只有类的成员函数能够访问,并随着对象的创建和销毁而变化。示例中publicVar是类MyClass的成员,其在类的作用域内可用。

  #include <iostream>
#include <format>

class MyClass {
public:
	int publicVar; // 公有成员变量

	MyClass(int val) {
		publicVar = val;
	}

	void printVar() {
		std::cout << std::format("Public variable: {}\n", publicVar);
	}
};

int main() {
	MyClass obj(300);
	obj.printVar(); // 访问类成员

	return 0;
}
  
  🡮
Public variable: 300
  
  • 块作用域(BLock Scope)

在一对{}之间定义的标识符,仅在该代码块内有效,退出代码块后则不可见,其在代码块开始时分配内存,退出时销毁。

  #include <iostream>
#include <format>

int main() {
	{
		int blockVar = 400; // 块作用域变量
		std::cout << std::format("Block variable: {}\n", blockVar);
	}
	// std::cout << blockVar; // 返回错误提示信息,identifier "blockVar" is undefined

	return 0;
}
  
  🡮
Block variable: 400
  
  • 函数作用域(Function Scope)

为在函数内部定义的标识符,通常指的是形参和局部变量,仅在函数体内有效。函数被调用时,内存被分配;返回时内存被销毁。

  #include <iostream>
#include <format>

void exampleFunction(int param) {
	int localVar = 500; // 局部变量
	std::cout << std::format("Parameter: {}\n", param);
	std::cout << std::format("Local variable: {}\n", localVar);
}

int main() {
	exampleFunction(700); // 调用函数

	return 0;
}
  
  🡮
Parameter: 700
Local variable: 500
  

C++ 的作用域机制通过限制标识符的有效范围来避免命名冲突,并帮助管理变量的生命周期。通过合理使用作用域,可以使代码更易于维护和理解。

C# 中的作用域是指程序中定义的变量、方法或其他成员可以被访问的范围,其影响着代码的可读性、维护性以及变量的生命周期。

  • 局部作用域(Local Scope)

局部变量是在方法、构造函数或代码块({})中声明的变量,其只能在定义它们的代码块中访问,在代码块结束时销毁,并只能在声明的位置之后访问,也不允许声明同名变量。

  using System;

class Program
{
    static void Example()
    {
        int localVar = 10; // 局部变量
        Console.WriteLine(localVar); // 有效
    }

    static void Main()
    {
        Example();
        // Console.WriteLine(localVar); // 无效,超出作用域,返回错误提示,The name 'localVar' does not exist in the current context
    }
}
  
  🡮
10
  
  • 类作用域(Class Scope)

类作用域适用于类的字段、方法和属性,其可以在类的范围内访问。使用访问修饰符(privatepublicprotectedinternal)控制可见性。在实例方法中,可以通过this访问非静态成员。静态成员在整个类范围内共享。

  using System;

class MyClass
{
    private int field = 57; // 类作用域,私有字段
    public void ShowField()
    {
        Console.WriteLine(field); // 访问类字段
    }
}

class Program
{
    static void Main()
    {
        MyClass obj= new MyClass();
        obj.ShowField();
    }
}
  
  🡮
57
  
  • 静态作用域(Static Scope)

静态成员属于类而不是特定实例,作用域为整个程序,其不需要实例化类即可访问,并在程序运行期间唯一存在,不会重复创建。

  using System;

class MyClass
{
    public static int StaticValue = 100; // 静态作用域
}

class Program
{
    static void Main()
    {
        Console.WriteLine(MyClass.StaticValue); // 静态成员访问
    }
}
  
  🡮
100
  
  • 块作用域(Block Scope)

块作用域指变量仅在特定代码块({})内可见和可用,可用于循环、条件语句等。

  using System;


class Program
{
    static void Example()
    {
        if (true)
        {
            int blockVar = 30; // 块作用域
            Console.WriteLine(blockVar);
        }
        // Console.WriteLine(blockVar); // 错误,超出作用域,提示信息为,The name 'blockVar' does not exist in the current context
    }

    static void Main()
    {
        Example();
    }
}
  
  🡮
30
  
  • 参数作用域(Parameter Scope)

方法的参数在方法的内部具有作用域,其在方法外不可访问,并参数变量覆盖同名的外部变量。

  using System;


class Program
{
    static void ShowMessage(string message)
    {
        Console.WriteLine(message);
    }
    static void Main()
    {
        ShowMessage("Hello, C#!");
    }
}
  
  🡮
Hello, C#!
  
  • 命名空间作用域(Namespace Scope)

命名空间作用域包含在命名空间中定义的所有类型和成员。如果导入一个命名空间,则该命名空间内的所有内容对导入对象都可用。应防止名称冲突。

  using System;

namespace MyNamespace
{
    class MyClass
    {
        public void MyMethod()
        {
            Console.WriteLine("Namespace scope!");
        }
    }
}

class Program
{
    static void Main()
    {
        MyNamespace.MyClass obj = new MyNamespace.MyClass();
        obj.MyMethod();
    }
}
  
  🡮
Namespace scope!
  
  • 全局作用域(Global Scope)

C# 没有传统意义上的全局变量,但可以使用静态类或文件范围的功能实现类似效果,其静态成员对整个程序可见。但不能直接在顶层定义变量。

  using System;

static class Global
{
    public static int GlobalValue = 70; // 全局范围
}

class Program
{
    static void Main()
    {
       Console.WriteLine(Global.GlobalValue); // 全局访问
    }
}
  
  🡮
70
  

常见问题和注意事项

  • 局部变量与全局变量同名

函数内的局部变量会覆盖同名的全局变量。

  x = 10

def foo():
    x = 20 # 局部变量
    print(x)


foo()
print(x)
  
  🡮
20
10
  
  • 未定义的局部变量

函数内若引用未定义的变量会报错。

  def foo():
    print(x) # 会提示错误信息:NameError: name 'x' is not defined


foo()
  
  • 动态命名空间

使用global()locals()查看变量的命名空间。

  x = 10

def foo():
    y = 20
    print("Global: ", globals())
    print("Local: ", locals())


foo()
  

static关键字

static关键字有两种主要用途:静态存储期和限制链接性。具体用途取决于static在不同上下文中的位置。

A. 静态存储期

当在变量声明前使用static时,则改变了该变量的存储期,使其从自动变量变为静态变量。静态变量在程序的整个执行期间都存在,而不是像普通局部变量那样在函数调用结束时被销毁。

  • 静态局部变量

如果static用于局部变量,则会使该变量在函数调用之间保持其值,即使退出函数,变量的值仍然保留。以静态变量的作用域给出的代码为例,static int count = 0;定义了count为一个静态局部变量,不会在每次调用counter()函数时重新初始化,而是保持其上一次调用的值。每次调用counter()函数时,count的值都会累加,而不是每次从0开始。

  • 静态全局变量

static用于全局变量时,则该变量只在定义它的文件中可见,而不可以在其他文件中访问,从而限制了变量的作用域,只在当前文件中有效。

  // file1.c
static int globalVar = 10;  // 静态全局变量

void printVar() {
	printf("Global Var: %d\n", globalVar);
}


// file2.c
extern void printVar(); // 尝试访问 file1.c 中的函数

int main() {
	printVar(); // 只能调用函数,无法直接访问 globalVar 静态全局变量
	
	return 0;
}
  

B. 限制链接性

static也可以用于函数的定义,使得该函数仅在当前文件中可见,防止在其他文件中被调用。这有利于模块化编程,避免函数名冲突。

  // file1.c
static void myFunction() {
	printf("This function is private to file1.c\n");
}

// file2.c
extern void myFunction(); // 提示错误,myFunction 在 file2.c 中不可见


int main() {
	myFunction(); // 错误,myFunction 不能在 file2.c 中调用

	return 0;
}
  
  • 作用域内不允许声明同名变量
  using System;

class Program
{
    static void Example()
    {
        int a = 10;
        int a = 20; // 提示错误信息,The variable 'a' is assigned but its value is never used
    }
    static void Main()
    {
       Example();
    }
}
  
  • 内层作用域的变量会隐藏外层作用域的变量
  using System;

class Program
{
    static void Example()
    {
        int x = 10;
        {
            int x = 20; // 提示错误信息,A local or parameter named 'x' cannot be declared in this scope because that name is used in an enclosing local scope to define a local or parameter
            Console.WriteLine(x);
        }
    }
    static void Main()
    {
       Example();
    }
}
  

6.4.2 命名空间

命名空间(Namespace)是编程语言中的一种机制,用于组织代码并防止命名冲突,通过创建一个逻辑容器、将标识符(如变量名、函数名、类名等)隔离在不同的空间中,从而避免命名冲突。命名空间的主要作用有,

1. 避免命名冲突: 当项目规模变大时,不同部分可能使用相同的名称。命名空间可以确保这些名称在逻辑上是隔离的。

2. 组织代码: 命名空间将相关的功能分组,使代码结构清晰易读。

3. 访问控制: 通过命名空间限制或明确标识某些符号的作用域,增强代码的安全性和可维护性。

命名空间中的标识符互不影响(隔离性);且很多语言支持嵌套命名空间,形成层级结构;并允许使用完整路径访问标识符,也可以通过别名或导入简化访问(灵活引用);一些语言也支持运行时加载命名空间(如 Python 的动态导入等)。如果将命名空间类比作文件夹结构,全局命名空间则为根目录;子命名空间为文件夹;不同文件夹可以包含相同的文件名(标识符),但访问时需要指定路径(命名空间)。

命名空间是一种代码组织和隔离机制,以避免命名冲突,提升代码可读性和可维护性。不同编程语言对命名空间的支持和实现方法虽有所不同,但核心目标一致,即为开发者提供清晰、安全的代码管理工具。命名空间在大型项目开发中可以通过分模块的命名空间组织代码,便于团队协作;集成第三方库时,避免引入库后产生命名冲突;而同一项目中的不同功能模块可以使用相同的标识符,因为命名空间隔离而互不干扰。

Py C C++ C#

在 Python 中,命名空间是一个字典(内部实现),用于映射变量名到对象。每个作用域都有一个相关的命名空间。但 Python 没有显示的 namespace关键字,而是通过作用域规则(LEGB)管理命名空间的查找顺序。可以使用globals()locals()访问命名空间。

  # 定义全局命名空间
x = "global"

def foo():
    # 局部命名空间
    y = "local"

    print(x) # 查找全局命名空间
    print(y) # 查找局部命名空间


foo()
  
  🡮
global
local
  

C 语言中没有像 C++ 和 C# 那样明确的命名空间概念。C 使用文件级作用域(文件内的标识符)和全局作用域(程序中的所有函数和全局变量)的组合来管理命名冲突,控制变量的生命周期和可访问性。

  #include <stdio.h>

char x[] = "global"; // 全局变量

void foo() {
	char y[] = "local"; // 局部变量
	printf("%s\n", x); // 访问全局变量
	printf("%s\n", y); // 访问局部变量
}

int main() {
	foo();

	return 0;
}
  
  🡮
global
local
  

C++ 引入了显示的namespace关键字来定义命名空间,用于组织和避免标识符冲突。命名空间可以嵌套,也可以进行别名声明,通过::运算符访问命名空间中的元素。

  #include <iostream>
#include <format>

namespace MyNamespace {
	int x = 10;
	void print() {
		std::cout << "Inside MyNamespace" << std::endl;
	}
}

int main() {
	std::cout << std::format("{}\n", MyNamespace::x); // 访问命名空间内的变量
	MyNamespace::print(); // 访问命名空间内的函数

	return 0;
}
  
  🡮
10
Inside MyNamespace
  

C# 使用namespace关键字来定义命名空间,允许组织类、结构、接口等成员。命名空间可以嵌套,并避免命名冲突,用using关键字来简化命名空间的访问。

  using System;

namespace MyNamespace
{
    class MyClass
    {
        public int x = 20;
        public void Print()
        {
            Console.WriteLine("Inside MyNamespace");
        }
    }
}

class Program
{    
    static void Main()
    {
        MyNamespace.MyClass obj = new MyNamespace.MyClass(); // 实例化命名空间中的类
        Console.WriteLine(obj.x); // 访问类中的字段
        obj.Print(); // 访问类中的方法
    }
}
  
  🡮
20
Inside MyNamespace
  

Python、C、C++ 和 C# 命名空间的对比。Python 中通过作用域来管理命名空间,不需要显示的命名空间定义;C 语言使用简单的作用域规则管理全局和局部变量,但没有namespapce关键字;C++ 和 C# 提供了强大的命名空间功能,允许显示地用namespace定义和管理命名空间,特别适用于大型项目,以避免命名冲突。

特性 Python C C++ C#
显示命名空间 namespace namespace
命名空间定义 使用作用域规则(LEGB)管理 通过全局变量和局部变量管理 使用namespace关键字 使用namespace关键字
嵌套支持 支持嵌套命名空间 支持嵌套命名空间
命名空间访问 通过作用域规则, globals()locals() 通过作用域规则 使用::运算符访问命名空间成员 使用.using访问命名空间成员

注释(Notes):

① functools,为Python 模块,用于高阶函数:作用于或返回其他函数的函数。一般来说,任何可调用对象都可以被视为该模块的函数。(https://docs.python.org/3/library/functools.html)。

② decorator 模块,是一个 Python 装饰器模块,目标是使定义保持签名的函数装饰器和装饰器工厂变得容易。(https://pypi.org/project/decorator/)。

③ mypy,是 Python 的静态类型检查器(https://mypy.readthedocs.io/en/stable/index.html)。

④ PEP 484,Python 的 Type Hints 文档(https://peps.python.org/pep-0484/)。