C#基础之方法

文章目录

  • [1 方法](#1 方法)
    • [1.1 定义方法](#1.1 定义方法)
    • [1.2 参数传递](#1.2 参数传递)
      • [1.2.1 按值传递参数](#1.2.1 按值传递参数)
      • [1.2.2 按引用传递参数](#1.2.2 按引用传递参数)
      • [1.2.3 按输出传递参数](#1.2.3 按输出传递参数)
      • [1.2.4 可变参数 params](#1.2.4 可变参数 params)
      • [1.2.5 具名参数](#1.2.5 具名参数)
      • [1.2.6 可选参数](#1.2.6 可选参数)
    • [1.3 匿名方法](#1.3 匿名方法)
      • [1.3.1 Lambda 表达式](#1.3.1 Lambda 表达式)
        • [1.3.1.1 定义](#1.3.1.1 定义)
        • [1.3.1.2 常用类型](#1.3.1.2 常用类型)
        • [1.3.1.3 Lambda 表达式与 LINQ](#1.3.1.3 Lambda 表达式与 LINQ)
        • [1.3.1.4 Lambda 表达式的捕获变量](#1.3.1.4 Lambda 表达式的捕获变量)
    • [1.4 外部方法](#1.4 外部方法)
      • [1.4.1 定义](#1.4.1 定义)
      • [1.4.2 使用](#1.4.2 使用)
    • [1.5 扩展方法](#1.5 扩展方法)
      • [1.5.1 定义](#1.5.1 定义)
      • [1.5.2 操作示例](#1.5.2 操作示例)

1 方法

一个方法是把一些相关的语句组织在一起,用来执行一个任务的语句块。每一个 C# 程序至少有一个带有 Main 方法的类。

1.1 定义方法

当定义一个方法时,从根本上说是在声明它的结构的元素。在 C# 中,定义方法的语法如下:

cs 复制代码
<Access Specifier> <Return Type> <Method Name>(Parameter List)
{
   Method Body
}

下面是方法的各个元素:

  • Access Specifier:访问修饰符,这个决定了变量或方法对于另一个类的可见性。
  • Return type:返回类型,一个方法可以返回一个值。返回类型是方法返回的值的数据类型。如果方法不返回任何值,则返回类型为 void。
  • Method name:方法名称,是一个唯一的标识符,且是大小写敏感的。它不能与类中声明的其他标识符相同。
  • Parameter list:参数列表,使用圆括号括起来,该参数是用来传递和接收方法的数据。参数列表是指方法的参数类型、顺序和数量。参数是可选的,也就是说,一个方法可能不包含参数。
  • Method body:方法主体,包含了完成任务所需的指令集。

下面的代码片段显示一个函数 FindMax,它接受两个整数值,并返回两个中的较大值。它有 public 访问修饰符,所以它可以使用类的实例从类的外部进行访问。

cs 复制代码
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 按值传递参数

这是参数传递的默认方式。在这种方式下,当调用一个方法时,会为每个值参数创建一个新的存储位置。

实际参数的值会复制给形参,实参和形参使用的是两个不同内存中的值。所以,当形参的值发生改变时,不会影响实参的值,从而保证了实参数据的安全。下面的实例演示了这个概念:

cs 复制代码
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 传递的是对堆上对象的引用。

下面的实例演示了这点:

cs 复制代码
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 关键字来从函数中返回两个值。输出参数会把方法输出的数据赋给自己,其他方面与引用参数相似。

下面的实例演示了这点:

cs 复制代码
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 的使用格式为:

cs 复制代码
public 返回类型 方法名称( params 类型名称[] 数组名称 )

实例

cs 复制代码
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 具名参数

具名参数允许调用方法时显式指定参数名称,而不是按位置传递参数。这对有多个参数的方法特别有用,因为可以选择性地设置某些参数,而忽略其他参数。

优点:

  • 提高代码可读性。
  • 避免因参数顺序出错而导致的问题
cs 复制代码
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 可选参数

可选参数允许为方法的某些参数指定默认值,这样调用方法时可以省略这些参数

cs 复制代码
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 查询和委托。

cs 复制代码
(parameters) => expression
// 或
(parameters) => { statement; }

其中:

  • parameters:传递给 Lambda 表达式的参数,可以省略括号 ()(当只有一个参数时)。
  • =>:称为goes to操作符,分隔参数和方法体。
  • expression 或 statements:要执行的代码,简单情况可以用一个表达式,复杂逻辑可以使用代码块 {}

注意:

  • 作用范围:Lambda 表达式可以捕获外部变量,但要小心,这可能导致意外的闭包效果。
  • 类型推断:C# 支持类型推断,Lambda 表达式的参数类型在多数情况下可以自动推断,无需显式声明类型。
  • 简洁性:对于简单逻辑,可以直接写成 Lambda 表达式;对于复杂逻辑,最好用命名方法,避免代码难以阅读。

实例

cs 复制代码
// 示例:使用 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 表达式

cs 复制代码
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> 进行筛选和排序:

cs 复制代码
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 表达式一致。

cs 复制代码
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文件的名称和入口点(即方法的名称)。以下是一个简单的例子:

cs 复制代码
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.AutoCharSet.AnsiCharSet.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,用于将字符串的首字母大写:

cs 复制代码
// 定义一个静态类来容纳扩展方法
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 类型添加一个扩展方法,计算该整数的平方:

cs 复制代码
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
    }
}
相关推荐
慧都小妮子24 分钟前
Spire.PDF for .NET【页面设置】演示:旋放大 PDF 边距而不改变页面大小
pdf·c#·.net·spire.pdf·报表控件
中东大鹅43 分钟前
【JavaScript】下拉框的实现
前端·javascript·css·html
Domain-zhuo44 分钟前
什么是前端构建工具?比如(Vue2的webpack,Vue3的Vite)
前端·javascript·vue.js·webpack·node.js·vue·es6
yanmengying1 小时前
VUE脚手架练习
前端·javascript·vue.js
APItesterCris1 小时前
对于大规模的淘宝API接口数据,有什么高效的处理方法?
linux·服务器·前端·数据库·windows
突然暴富的我1 小时前
html button 按钮单选且 高亮
前端·javascript·html
用户49430538293802 小时前
一种简单粗暴的大屏自适应方案,原理及案例
前端
fury_1232 小时前
怎么获取键值对的键的数值?
java·前端·数据库
午后书香2 小时前
看两道关于异步的字节面试题...
前端·javascript·面试
用户2404817096212 小时前
我来助你:Coze帮你零代码生成智能体
前端·人工智能·coze