C#基础之方法
1 方法
一个方法是把一些相关的语句组织在一起,用来执行一个任务的语句块。每一个 C# 程序至少有一个带有 Main
方法的类。
1.1 定义方法
当定义一个方法时,从根本上说是在声明它的结构的元素。在 C# 中,定义方法的语法如下:
<Access Specifier> <Return Type> <Method Name>(Parameter List)
{
Method Body
}
下面是方法的各个元素:
Access Specifier
:访问修饰符,这个决定了变量或方法对于另一个类的可见性。Return type
:返回类型,一个方法可以返回一个值。返回类型是方法返回的值的数据类型。如果方法不返回任何值,则返回类型为 void。Method name
:方法名称,是一个唯一的标识符,且是大小写敏感的。它不能与类中声明的其他标识符相同。Parameter list
:参数列表,使用圆括号括起来,该参数是用来传递和接收方法的数据。参数列表是指方法的参数类型、顺序和数量。参数是可选的,也就是说,一个方法可能不包含参数。Method body
:方法主体,包含了完成任务所需的指令集。
下面的代码片段显示一个函数 FindMax,它接受两个整数值,并返回两个中的较大值。它有 public 访问修饰符,所以它可以使用类的实例从类的外部进行访问。
class NumberManipulator
{
public int FindMax(int num1, int num2)
{
/* 局部变量声明 */
int result;
if (num1 > num2)
result = num1;
else
result = num2;
return result;
}
...
}
1.2 参数传递
当调用带有参数的方法时,需要向方法传递参数。在 C# 中,有三种向方法传递参数的方式:
方式 | 描述 |
---|---|
值参数 | 这种方式复制参数的实际值给函数的形式参数,实参和形参使用的是两个不同内存中的值。在这种情况下,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。 |
引用参数 | 这种方式复制参数的内存位置的引用给形式参数。这意味着,当形参的值发生改变时,同时也改变实参的值。 |
输出参数 | 这种方式可以返回多个值。 |
1.2.1 按值传递参数
这是参数传递的默认方式。在这种方式下,当调用一个方法时,会为每个值参数创建一个新的存储位置。
实际参数的值会复制给形参,实参和形参使用的是两个不同内存中的值。所以,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。下面的实例演示了这个概念:
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(int x, int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
int b = 200;
Console.WriteLine("在交换之前,a 的值: {0}", a);
Console.WriteLine("在交换之前,b 的值: {0}", b);
/* 调用函数来交换值 */
n.swap(a, b);
Console.WriteLine("在交换之后,a 的值: {0}", a);
Console.WriteLine("在交换之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}
结果:
在交换之前,a 的值:100
在交换之前,b 的值:200
在交换之后,a 的值:100
在交换之后,b 的值:200
结果表明,即使在函数内改变了值,值也没有发生任何的变化。
1.2.2 按引用传递参数
引用参数是一个对变量的内存位置的引用。当按引用传递参数时,与值参数不同的是,它不会为这些参数创建一个新的存储位置。引用参数表示与提供给方法的实际参数具有相同的内存位置。
在 C# 中,使用 ref
关键字声明引用参数。使用 ref
,可以让方法直接操作调用方变量本身,而不是它的副本。
ref
的作用:
- 普通传值(按值传递):方法接收的是变量的副本,对副本的修改不会影响原变量。
- 引用传递(ref 传递):方法接收的是变量的引用,对引用指向的内存进行修改,会影响原变量。
栈与堆的关系:
- 值类型(如 int, float)通常存储在栈上。引用传递时,传递的是栈上变量的引用。
- 引用类型(如对象、数组)存储在堆上。引用传递时,
ref
传递的是对堆上对象的引用。
下面的实例演示了这点:
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void swap(ref int x, ref int y)
{
int temp;
temp = x; /* 保存 x 的值 */
x = y; /* 把 y 赋值给 x */
y = temp; /* 把 temp 赋值给 y */
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
int b = 200;
Console.WriteLine("在交换之前,a 的值: {0}", a);
Console.WriteLine("在交换之前,b 的值: {0}", b);
/* 调用函数来交换值 */
n.swap(ref a, ref b);
Console.WriteLine("在交换之后,a 的值: {0}", a);
Console.WriteLine("在交换之后,b 的值: {0}", b);
Console.ReadLine();
}
}
}
结果:
在交换之前,a 的值:100
在交换之前,b 的值:200
在交换之后,a 的值:200
在交换之后,b 的值:100
结果表明,swap 函数内的值改变了,且这个改变可以在 Main 函数中反映出来。
1.2.3 按输出传递参数
return
语句可用于只从函数中返回一个值。但是,可以使用 输出参数 out
关键字来从函数中返回两个值。输出参数会把方法输出的数据赋给自己,其他方面与引用参数相似。
下面的实例演示了这点:
using System;
namespace CalculatorApplication
{
class NumberManipulator
{
public void getValue(out int x )
{
int temp = 5;
x = temp;
}
static void Main(string[] args)
{
NumberManipulator n = new NumberManipulator();
/* 局部变量定义 */
int a = 100;
Console.WriteLine("在方法调用之前,a 的值: {0}", a);
/* 调用函数来获取值 */
n.getValue(out a);
Console.WriteLine("在方法调用之后,a 的值: {0}", a);
Console.ReadLine();
}
}
}
结果:
在方法调用之前,a 的值: 100
在方法调用之后,a 的值: 5
1.2.4 可变参数 params
当声明一个方法时,不能确定要传递给函数作为参数的参数数目。C# 参数数组解决了这个问题,参数数组
通常用于传递未知数量的参数给函数。
在使用数组作为形参时,C# 提供了 params
关键字,使调用数组为形参的方法时,既可以传递数组实参,也可以传递一组数组元素。但是,一个方法中只能有一个可变参数 params
,且必须在形参列表最后一个位置
params
的使用格式为:
public 返回类型 方法名称( params 类型名称[] 数组名称 )
实例
using System;
namespace ArrayApplication
{
class ParamArray
{
public int AddElements(params int[] arr)
{
int sum = 0;
foreach (int i in arr)
{
sum += i;
}
return sum;
}
}
class TestClass
{
static void Main(string[] args)
{
ParamArray app = new ParamArray();
int sum = app.AddElements(512, 720, 250, 567, 889);
Console.WriteLine("总和是: {0}", sum);
Console.ReadKey();
}
}
}
结果:
总和是: 2938
1.2.5 具名参数
具名参数
允许调用方法时显式指定参数名称,而不是按位置传递参数
。这对有多个参数的方法特别有用,因为可以选择性地设置某些参数,而忽略其他参数。
优点:
- 提高代码可读性。
- 避免因参数顺序出错而导致的问题
void PrintDetails(string name, int age, string city)
{
Console.WriteLine($"Name: {name}, Age: {age}, City: {city}");
}
// 具名参数调用
PrintDetails(name: "Alice", age: 25, city: "New York");
// 顺序可以随意调整
PrintDetails(city: "Los Angeles", name: "Bob", age: 30);
1.2.6 可选参数
可选参数
允许为方法的某些参数指定默认值,这样调用方法时可以省略这些参数
void Greet(string name, string greeting = "Hello")
{
Console.WriteLine($"{greeting}, {name}!");
}
// 调用时省略可选参数
Greet("Alice"); // 输出:Hello, Alice!
// 调用时显式传递可选参数
Greet("Bob", "Hi"); // 输出:Hi, Bob!
1.3 匿名方法
在 C# 中,匿名函数
是一种没有名字的方法,可以在代码中定义和使用。
匿名方法(Anonymous methods
) 提供了一种传递代码块作为委托参数的技术。
在匿名方法中不需要指定返回类型,它是从方法主体内的 return
语句推断的。
1.3.1 Lambda 表达式
1.3.1.1 定义
Lambda
表达式是一个简洁的语法,用于创建匿名函数。它们通常用于 LINQ
查询和委托。
(parameters) => expression
// 或
(parameters) => { statement; }
其中:
parameters
:传递给Lambda
表达式的参数,可以省略括号 ()(当只有一个参数时)。=>
:称为goes to
操作符,分隔参数和方法体。expression 或 statements
:要执行的代码,简单情况可以用一个表达式,复杂逻辑可以使用代码块{}
。
注意:
- 作用范围:Lambda 表达式可以捕获外部变量,但要小心,这可能导致意外的闭包效果。
- 类型推断:C# 支持类型推断,Lambda 表达式的参数类型在多数情况下可以自动推断,无需显式声明类型。
- 简洁性:对于简单逻辑,可以直接写成 Lambda 表达式;对于复杂逻辑,最好用命名方法,避免代码难以阅读。
实例
// 示例:使用 Lambda 表达式定义一个委托
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(2, 3)); // 输出 5
// 示例:使用 Lambda 表达式过滤数组中的元素
int[] numbers = { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (var num in evenNumbers)
{
Console.WriteLine(num); // 输出 2 4
}
1.3.1.2 常用类型
Lambda
表达式通常用于定义委托或作为方法的参数,例如:
Func
:定义具有返回值的Lambda
表达式。Action
:定义不具有返回值的Lambda
表达式。Predicate
:定义返回bool
类型的Lambda
表达式,常用于条件判断。
示例:不同类型的 Lambda 表达式
Func<int, int, int> multiply = (x, y) => x * y;
Console.WriteLine(multiply(3, 4)); // 输出:12
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice"); // 输出:Hello, Alice!
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(4)); // 输出:True
1.3.1.3 Lambda 表达式与 LINQ
Lambda
表达式在 LINQ
查询中常用于指定筛选条件、排序方式等。例如,对一个 List<int>
进行筛选和排序:
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var evenNumbers = numbers.Where(n => n % 2 == 0).OrderBy(n => n);
foreach (var num in evenNumbers)
{
Console.WriteLine(num); // 输出:2, 4, 6
}
1.3.1.4 Lambda 表达式的捕获变量
Lambda
表达式可以捕获
外部作用域中的变量。捕获的变量与闭包类似,生命周期与 Lambda
表达式一致。
int factor = 3;
Func<int, int> multiplyByFactor = n => n * factor;
Console.WriteLine(multiplyByFactor(5)); // 输出:15
在这里,factor 是一个外部变量,multiplyByFactor 捕获了 factor,并在 Lambda 表达式中使用它。
1.4 外部方法
1.4.1 定义
外部方法是指那些用非托管代码
(如C或C++
)编写的函数,这些函数被编译成动态链接库(DLL
)或其他形式的可执行文件。C#
程序通过P/Invoke(Platform Invocation Services)
机制调用这些外部方法,P/Invoke
允许托管代码(如C#)调用非托管代码。
在C#中,调用外部方法(也称为外部函数
或P/Invoke方法)之前需要先声明,这是因为C#
是一种类型安全的语言,它需要在编译时知道方法的签名(即方法的名称、参数类型和返回类型)。外部方法通常指的是那些用非托管代码
(如C或C++
编写的DLL
文件)实现的方法。
为什么需要先声明:
- 类型安全:
C#
编译器需要在编译时知道方法的签名,以确保调用时传递的参数类型和数量是正确的。 - 元数据:声明外部方法时,可以提供关于方法的元数据(如
DLL
名称、入口点名称等),这些信息是运行时环境(CLR,Common Language Runtime
)用来加载和调用非托管代码所必需的。 - 编译时检查:通过声明外部方法,编译器可以在编译时检查调用的正确性,减少运行时错误。
1.4.2 使用
在C#中,可以使用DllImport
属性来声明外部方法。DllImport
属性指定了包含该方法的DLL
文件的名称和入口点(即方法的名称)。以下是一个简单的例子:
using System;
using System.Runtime.InteropServices;
class Program
{
// 声明外部方法
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);
static void Main()
{
// 调用外部方法
IntPtr handle = GetModuleHandle("kernel32.dll");
Console.WriteLine("Handle: " + handle);
}
}
在这个例子中,GetModuleHandle
是一个外部方法,它位于kernel32.dll
中。我们使用DllImport
特性来声明这个方法,并在Main方法中调用它。
注意事项:
- 字符集:使用
CharSet
属性指定字符集(如CharSet.Auto
、CharSet.Ansi
或CharSet.Unicode
),这会影响字符串参数的传递方式。 - 调用约定:默认情况下,
P/Invoke
使用stdcall
调用约定。如果外部方法使用其他调用约定(如cdecl),需要使用CallingConvention
枚举来指定。 - 错误处理:调用外部方法时,可能会遇到各种错误(如找不到DLL、方法签名不匹配等)。因此,适当的错误处理是非常重要的。
1.5 扩展方法
1.5.1 定义
在 C#
中,扩展方法(Extension Method
)是允许在不修改类代码或创建子类的情况下,为现有类型添加
新方法的一种机制。扩展方法
本质上是一个静态方法
,但它的调用方式
和实例方法
类似,使得我们可以在不修改源代码的情况下增强类的功能。
扩展方法必须满足以下条件:
- 必须定义在
静态类
中 - 方法本身必须是
静态的
- 第一个参数必须使用
this
关键字修饰,并指定参数类型
即要扩展的类型
this
关键字的作用:
- 标识扩展方法:
this
关键字告诉编译器这是一个扩展方法
,而不是普通的静态方法
。 - 指定扩展类型:
this
后面的参数类型就是要扩展的类型。 - 调用方式:可以像调用实例方法一样调用扩展方法。
扩展方法的优点:
- 无需修改原类:无需更改或继承现有类型即可添加新方法。
- 提高代码可读性:扩展方法让代码更具可读性,因为它们可以像实例方法一样调用。
- 方便维护:扩展方法通常集中在一个静态类中,有助于代码模块化和维护。
注意事项:
- 扩展方法的命名冲突:如果类型本身已经定义了一个与扩展方法同名的方法,实例方法优先。扩展方法只会在没有实例方法的情况下被调用。
this
关键字仅用于第一个参数
:扩展方法的第一个参数是要扩展的类型,必须用this
关键字修饰。扩展方法只能有一个 this 参数。- 命名空间的引用:要使用扩展方法,必须引用扩展方法所在的命名空间。
- 适用场景:扩展方法通常用于增强不可修改的类(如第三方库类)或基础类型(如 string、int 等)而不推荐滥用
1.5.2 操作示例
以下示例演示如何为 string 类型定义一个扩展方法 ToCapitalize,用于将字符串的首字母大写:
// 定义一个静态类来容纳扩展方法
public static class StringExtensions
{
// 定义一个静态扩展方法,用于将字符串首字母大写
public static string ToCapitalize(this string input)
{
if (string.IsNullOrEmpty(input))
return input;
return char.ToUpper(input[0]) + input.Substring(1).ToLower();
}
}
// 使用扩展方法
public class Program
{
public static void Main()
{
string text = "hello world";
// 调用扩展方法,就像调用实例方法一样
string capitalizedText = text.ToCapitalize();
Console.WriteLine(capitalizedText); // 输出 "Hello world"
}
}
为 int 类型添加一个扩展方法,计算该整数的平方:
public static class IntExtensions
{
public static int Square(this int number)
{
return number * number;
}
}
public class Program
{
public static void Main()
{
int number = 5;
int square = number.Square(); // 调用扩展方法
Console.WriteLine(square); // 输出 25
}
}