C# 中的关键字解析

前言

最近在一个初创公司里做全栈开发。一开始以为去做ASP.NET (后端),结果微信小程序开发缺人 (汗)。反正闲着也是闲着,趁这个机会学习点C#中的基础知识。


1. abstract 关键字

C#中,关键字abstract用于声明抽象类或类中的抽象成员

抽象类特点:

  1. 抽象类不能实例化 (不能使用new关键字)。
  2. 抽象类可能包含抽象成员(方法、属性)和非抽象成员的实现。
  3. 只有抽象类中才允许抽象方法声明。
  4. 抽象类通常作为基类被子类继承,子类必须使用override实现抽象类中声明的抽象成员。

抽象方法特点:

  1. 凡是包含抽象方法的类都是抽象类。
  2. 抽象方法声明不提供实际的实现,因此没有方法主体。(抽象属性同理)
  3. 在抽象方法声明中不能使用staticvirtual关键字。
    • 抽象方法没有具体实现,所以不能使用static关键字来声明为静态方法。
    • 抽象方法是隐式的虚拟方法,所以不需要virtual关键字。

举个例子:

  • 枪(抽象基类)是可以 ShootReload(抽象方法)。
  • 比如AK47M4 (非抽象类子类),它们拥有 ShootReload 的功能。
  • 但具体是怎么 ShootReloadAK47M4是不一样的(具体实现)。
// 抽象类:Firearms
public abstract class Firearms
{
protected abstract void Shoot(); // 抽象方法
protected abstract void Reload();

protected void Init() {...} // 非抽象方法

protected abstract string FireRate { get; set; } // 抽象属性
}

// 子类:AK47
public class AK47 : Firearms
{
private string fireRate;

protected override void Shoot() {...} // 具体实现

protected override void Reload() {...}

protected override string FireRate
{
get { return fireRate; }
set { fireRate = value; }
}
}

为什么要用抽象类,而不是使用一般类class或者接口interface

  1. 共享代码:抽象类可以定义一组相关类的通用行为和属性,并提供默认的实现。通过继承抽象类,子类可以继承和重用抽象类中的代码,减少代码的重复编写。这样可以提高代码的可维护性和扩展性。(这里指的是在 abstract class 里声明的非抽象方法)
  2. 代码扩展性:抽象类可以包含抽象方法,这些方法在抽象类中没有具体的实现。通过继承抽象类并实现这些抽象方法,子类可以在具体的业务逻辑中实现自己的代码。这种方式使得代码具有扩展性,可以适应未来需求的变化。
  3. 部分实现:抽象类既可以包含抽象方法,也可以包含具体的实现。这使得抽象类可以提供一些默认的行为,同时也给子类提供了一定的灵活性。子类可以选择性地覆盖抽象类中的方法,或者直接使用抽象类中的实现。
  4. (待补充)

注意, 从 C# 8.0 开始 interface 就可以在接口里提供默认的实现


2. as 关键字

C#中,关键字as用于类型转换。

  • 在实际应用中,通常会与null检查一起使用as关键字。这是因为,如果转换不成功,as会返回null,而不是抛出异常。这使得as关键字在面对类型转换失败时的行为更加温和。
public class Animal { ... }
public class Dog : Animal { ... }

Animal animal = new Dog();
Dog dog = animal as Dog;

if (dog != null)
{
Console.WriteLine("Successful");
}
else
{
Console.WriteLine("Failed");
}

上面代码中,as关键字尝试将animal转换为Dog类型。由于animal对象实际上是一个Dog类型的对象,所以转换成功。如果animal对象是Animal类型的对象,则上面代码会转换失败。


3. base 关键字

C#中,base关键字用于在子类中引用基类的成员或调用基类的构造函数

  • base关键字只能用于子类,而不是基类中。
  • 如果基类成员是私有的,即使使用base关键字也无法访问。
  • 在静态方法中使用 base 关键字将产生错误。
  • 如果基类中有多个构造函数重载,子类在使用base关键字调用父类构造函数时,需要选择要调用的具体构造函数,并且提供与所选基类构造函数匹配的参数列表。

下面是base关键字的几种常见用法

  • 调用基类的构造函数

在派生类的构造函数中,可以使用base关键字调用基类的构造函数。

public class BaseClass
{
// 基类的构造函数 1
public BaseClass(int x) { ... }

// 基类的构造函数 2
public BaseClass(int x, int y) { ... }
}

public class DerivedClass : BaseClass
{
// 派生类的构造函数
// 执行其他派生类的构造逻辑
public DerivedClass(int x) : base(x) { ... }

public DerivedClass(int x, int y) : base(x, y) { ... }
}

var x = new DerivedClass(1);
var y = new DerivedClass(1, 2);

上面代码中,DerivedClass继承自BaseClass。在DerivedClass的构造函数中,使用base(x)调用了基类BaseClass的构造函数,并传递参数x

  • 引用基类的成员

派生类中定义了一个与基类相同名称的成员变量或方法时,可以使用base关键字来访问它。

public class BaseClass
{
// 基类的方法
public void SomeMethod() { ... }
}

public class DerivedClass : BaseClass
{
// 调用基类的方法
public void SomeMethod()
{
base.SomeMethod();
// ... 其他逻辑
}
}

上面代码中,DerivedClass继承自BaseClass。在DerivedClass中的SomeMethod方法中,使用base.SomeMethod()调用了基类BaseClass的方法。

为什么要使用base去调用基类的构造函数?

  1. 继承基类的行为和状态: 通过调用基类的构造函数,子类可以继承基类的行为和状态。基类可能包含一些重要的初始化逻辑,以确保它的成员和属性处于正确的状态。
  2. 提供基类所需的初始化参数: 如果基类的构造函数需要接收参数来进行初始化,派生类可以通过调用基类构造函数并传递适当的参数,来提供必要的信息。
  3. 避免冗余代码: 如果基类的构造函数已经包含了一些通用的、所有派生类都需要的初始化代码,那么在派生类中通过使用base关键字调用基类构造函数,可以避免重复编写这些初始化代码。
  4. (待补充)

4. byte 关键字

C#中,byte关键字用于声明一个8位无符号(unsigned)整数类型的变量。

byte关键字在C#中具有多种用途和应用场景

  • 存储和处理二进制数据: byte类型是一个8位无符号整数,范围从0255。因此,它非常适合用于存储和处理二进制数据,如图像、音频、视频、文件等。
  • 网络编程: 在网络编程中,常常需要使用byte类型来读取和写入数据流、处理网络字节序等。
  • 数据序列化和反序列化: 序列化是将对象转换为字节序列的过程,而反序列化则是将字节序列转换回对象。在数据序列化和反序列化过程中,byte类型通常用于表示和操作字节数据。

举个例子:

// 读取图像文件的字节数据
byte[] imageData = File.ReadAllBytes("image.jpg");
// 对数据进行压缩,返回压缩后的字节数据
byte[] compressedData = CompressData(originalData);

5. 异常处理关键字

C#中,try关键字用于定义一个try块,表示其中可能会出现异常。

  • try块后面通常跟随一个或多个catch块,用于处理可能抛出的特定异常。最后还可以选择性地包含一个finally块,它包含的代码无论是否抛出异常都会被执行。
using System;
using System.IO;

StreamReader reader = null;

try
{
reader = new StreamReader("nonexistent_file.txt");
Console.WriteLine(reader.ReadToEnd());
}
catch (FileNotFoundException ex)
{
// 处理文件找不到的异常
Console.WriteLine("File not found: " + ex.FileName);
}
catch (Exception ex)
{
// 处理其他类型的异常
Console.WriteLine("An error occurred: " + ex.Message);
}
finally
{
// 确保关闭StreamReader对象,释放系统资源
if (reader != null)
{
reader.Close();
}
}

上面代码中,如果try块中的代码引发了异常,程序会立即跳转到catch块,寻找与异常类型匹配的catch块,并执行相应的代码块。每个catch块可以捕获并处理特定类型的异常。

  • 处理异步函数的异常
    异步函数通常返回一个TaskTask<T>对象。可以在try块中await这个任务,并在catch块中处理可能的异常。
// 有可能抛出异常的异步操作
public async Task SomeAsyncOperation() { ... }

public async Task HandleAsyncOperation()
{
try
{
await SomeAsyncOperation();
}
catch (Exception ex)
{
Console.WriteLine($"An exception occurred: {ex.Message}");
}
}

另外,不是所有的异常都应该被捕获。在大多数情况下,只应该捕获知道如何处理的异常。对于不知道如何处理的异常,最好让它们传播出去,这样调用者或者全局异常处理器可以捕获并处理它们。

(这里暂时省略处理迭代器方法中的异常,待补充)


6. class 关键字

C#中,class关键字用于定义一个类。

  • 类是一种引用类型,它定义了一组属性(字段、常量和事件)、方法和索引器的组合。类可以直接实例化,也可以作为基类继承。
class MyClass
{
// 类的成员变量
private int myInt;

// 类的构造函数
public MyClass(int value)
{
myInt = value;
}

// 类的成员方法
public void PrintValue()
{
Console.WriteLine("MyInt = " + myInt);
}
}

C# 中仅允许单一继承,即一个类仅能从一个基类继承实现。但是,一个类可拥有多个接口 (重要)。

继承示例
class ClassA { }
单一class DerivedClass : BaseClass { }
无,实现两个接口class ImplClass : IFace1, IFace2 { }
单一,实现一个接口class ImplDerivedClass : BaseClass, IFace1 { }
  • 下面是一些类的使用方法

  • (1) 访问修饰符: 类的访问修饰符决定了它在代码中的可见性。默认情况下,类是internal的,意味着它只在同一程序集中可见。类的成员默认是private的,只能在类的内部访问。

  • (2) 静态类: 使用static关键字可以定义静态类和静态成员。静态类不能实例化,不能被继承,且只能包含静态成员。静态成员只有一份,属于类本身,而不属于类的任何实例。

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

public static double SquareRoot(double value)
{
return Math.Sqrt(value);
}
}

MathUtils.Add(1, 2)
MathUtils.SquareRoot(4.0)
  • (3) 构造函数: 类可以有一个或多个构造函数,用于创建类的实例。构造函数的名称必须与类名相同,且不返回任何值。

  • (4) 静态构造函数: 用于初始化静态成员或执行静态初始化代码。它没有参数列表,也不能直接调用。静态构造函数在类的第一个实例或静态成员被访问之前自动调用,并且只执行一次

访问静态成员时调用

public class MyClass
{
static int myStaticField;
static readonly int myStaticReadOnlyField;

static MyClass()
{
// 静态构造函数用于初始化静态成员
myStaticField = 10;
myStaticReadOnlyField = 20;
}

public static void MyMethod()
{
Console.WriteLine("MyStaticField: " + myStaticField);
Console.WriteLine("MyStaticReadOnlyField: " + myStaticReadOnlyField);
}
}

// 调用静态方法,静态构造函数会在此时自动执行
MyClass.MyMethod();

// MyStaticField: 10
// MyStaticReadOnlyField: 20

创建类的实例时调用

public class MyClass
{
static int myStaticField;
static readonly int myStaticReadOnlyField;

static MyClass()
{
myStaticField = 10;
myStaticReadOnlyField = 20;
}

public MyClass()
{
Console.WriteLine("MyStaticField: " + myStaticField);
Console.WriteLine("MyStaticReadOnlyField: " + myStaticReadOnlyField);
}
}

// 创建类的第一个实例,静态构造函数会在此时自动调用
MyClass obj = new MyClass();

// MyStaticField: 10
// MyStaticReadOnlyField: 20
  • (5) 内部类: 内部类是定义在另一个类内部的类。内部类拥有访问外部类成员的特权,可以用于实现更复杂的逻辑和封装。(注意,虽然内部类可以访问其外部类的所有成员,但反过来不成立。)
class OuterClass
{
private int outerField = 10;

public class InnerClass
{
OuterClass outer;

// 构造函数,通过参数将外部类实例传入
public InnerClass(OuterClass outer)
{
this.outer = outer;
}

public void InnerMethod()
{
Console.WriteLine(outer.outerField);
}
}

public InnerClass CreateInnerClass()
{
return new InnerClass(this);
}
}

OuterClass outer = new OuterClass();
OuterClass.InnerClass inner = outer.CreateInnerClass();
inner.InnerMethod(); // 10
  • (6) 泛型类: 泛型类具有在实例化时可以指定参数类型的特性,从而提供了更灵活和类型安全的代码重用方式。
public class MyGenericClass<T>
{
private T myField;

public MyGenericClass(T value)
{
myField = value;
}

public T MyMethod()
{
return myField;
}
}

MyGenericClass<int> intObj = new MyGenericClass<int>(10);
int value = intObj.MyMethod(); // 返回值为 10

MyGenericClass<string> stringObj = new MyGenericClass<string>("Hello");
string text = stringObj.MyMethod(); // 返回值为 "Hello"

C#还有许多其他不同的类,比如之前介绍的抽象类,还有接口,密封类,枚举类,部分类,特性类,委托类等等。(待补充)


7. const 关键字

C#中,const关键字来声明某个常量字段或局部变量。常量字段和常量局部变量不是变量并且不能修改。

const int X = 0;
public const double GravitationalConstant = 6.673e-11;
private const string ProductName = "Visual C#";

8. decimal 关键字

C#中,当所需的精度由小数点右侧的位数决定时,decimal关键字是合适的。

  • 它是一种数据类型,用于存储精确的十进制数值(支持小数点后28的精确度),适用于需要高精度计算的场景,例如财务和货币计算。
decimal myDecimal = 3.14m; // 使用后缀 "m" 表示 decimal 类型
float myFloat = 3.14f; // 使用后缀 "f" 表示 float 类型
double myDouble = 3.14; // 默认为 double 类型

注意,decimal类型虽然具有较高的精度和准确性,它的计算速度通常比其他浮点类型,比如 floatdouble,慢。


9. delegate 关键字

C#中,delegate关键字用于声明和使用委托。

它类似于一个装着方法的容器,可以将方法(实例方法,静态方法)作为对象进行传递,但前提是委托和对应传递方法的签名得是相同的,签名指的是他们的参数类型和返回值类型。(类似回调函数或者Thunk函数)

// 委托的声明,指定了返回值类型为 int,接收两个 int 类型的参数。
public delegate int MyDelegate(int num1, int num2);

public class Calculator
{
// 使用委托的一个方法,这里的 method 参数是一个 Operation 类型的委托
public int Calculate(int num1, int num2, MyDelegate callback)
{
// 在Calculate方法中调用委托
return callback(num1, num2);
}
}

// 两个符合 Operation 委托签名的方法
public static int Add(int num1, int num2)
{
return num1 + num2;
}

public static int Subtract(int num1, int num2)
{
return num1 - num2;
}

Calculator calculator = new Calculator();

// 使用委托将 Add 方法作为参数传入
int result = calculator.Calculate(10, 5, Add);
Console.WriteLine(result); // 输出:15

// 使用委托将 Subtract 方法作为参数传入
result = calculator.Calculate(10, 5, Subtract);
Console.WriteLine(result); // 输出:5

注意,在C#中创建委托对象时,并不一定需要使用new关键字来实例化委托。

  • 使用new关键字和委托类型的构造函数创建委托对象
MyDelegate myDelegate = new MyDelegate(ShowMessage);

在这种情况下,我们使用new MyDelegate语法创建了一个委托对象myDelegate,并将ShowMessage方法绑定到委托上。

  • 使用隐式方法组转换来创建委托对象
MyDelegate myDelegate = ShowMessage;

在这种情况下,我们直接将方法名ShowMessage赋值给委托对象myDelegate,编译器会自动进行隐式方法组转换,将方法转换为委托对象。

  • 使用匿名方法创建委托对象
MyDelegate myDelegate = delegate(string message)
{
Console.WriteLine(message);
};

匿名方法的语法是使用关键字delegate后跟一个参数列表和方法体。

  • 使用Lambda表达式创建委托对象
// Lambda表达式的参数部分只使用了参数名"message",而没有指定参数类型。
// 编译器会根据委托类型推断出参数类型。
MyDelegate myDelegate = (message) => Console.WriteLine(message);

在这种情况下,我们使用Lambda表达式定义了一个匿名方法,将其赋值给委托对象myDelegate

  • 那什么情况下应该使用委托delegate

  • (1) 事件处理: 委托广泛用于事件处理模型中。通过定义委托类型和事件,可以将事件与特定的处理方法关联起来。当事件发生时,委托会调用绑定的方法,从而实现事件的处理和响应。

// 声明一个委托
public delegate void EventHandler(string message);

public class EventPublisher
{
// 声明一个事件,使用之前定义的委托
public event EventHandler Event;

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

public class EventSubscriber
{
public void Subscribe(EventPublisher publisher)
{
// 订阅事件
publisher.Event += HandleEvent;
}

private void HandleEvent(string message)
{
// 处理事件
Console.WriteLine("Event received: " + message);
}
}


// 创建事件发布者和订阅者
EventPublisher publisher = new EventPublisher();
EventSubscriber subscriber = new EventSubscriber();

// 订阅事件
subscriber.Subscribe(publisher);

// 触发事件
publisher.TriggerEvent("Hello, World!");
  • (2) 回调函数: 委托可用作回调函数的一种方式。当一个方法需要在完成后通知另一个方法时,可以将委托作为参数传递给该方法,并在适当的时候调用委托以执行回调操作。
public delegate void MyCallbackDelegate(string message);

public class Processor
{
private MyCallbackDelegate callback;

public Processor(MyCallbackDelegate callback)
{
this.callback = callback;
}

public void Process()
{
// Processing...
// Once processing is done, callback is called:
callback("Processing completed!");
}
}

// Create a new processor, passing it a callback function:
Processor processor = new Processor(MyCallback);

// Start the processing:
processor.Process();

static void MyCallback(string message)
{
Console.WriteLine(message);
}
  • (3) 多播委托 (委托链): 委托还支持多播(multicast)的功能,即一个委托可以绑定多个方法。这使得可以将多个方法作为委托的调用列表,并按顺序依次调用它们。
delegate void MyDelegate();

MyDelegate myDelegate = Method1;
myDelegate += Method2;
myDelegate += Method3;
myDelegate -= Method4; // 删除调用列表中的方法
myDelegate(); // 调用委托,依次执行绑定的方法
  • (4) 泛型委托: C#中的委托支持泛型,可以更好地支持类型安全性和代码重用。
delegate T MyGenericDelegate<T>(T value);

static int Square(int x)
{
return x * x;
}

MyGenericDelegate<int> squareDelegate = Square;
int result = squareDelegate(5);
  • (5) LINQLambda 表达式: 在LINQ中,委托用于定义查询操作。而Lambda表达式实质上是一种特殊的委托,以一种更简洁和强大的方式定义匿名函数。
using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
public static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

// 定义一个委托,检查一个数是否为偶数
Func<int, bool> isEven = x => x % 2 == 0;

// 使用LINQ和委托找出所有偶数
IEnumerable<int> evens = numbers.Where(isEven);

// 打印所有的偶数
foreach (int number in evens)
{
Console.WriteLine(number);
}
}
}

注意,在C#中,不一定非要使用delegate关键字来定义委托。其实,Func<T, TResult>Action<T>等都是内置的委托类型,你可以直接使用它们,而无需使用delegate关键字。(C#中内置的委托类型)

  • (6) 定制或扩展方法: 在C#中,可以利用委托去定制或扩展方法,改变其行为或增加新的功能。例如,可以使用委托来定制比较方法,从而对集合进行自定义排序。
public delegate int Comparison<in T>(T x, T y);

List<int> numbers = new List<int> { 5, 10, 8, 3, 6 };

// 创建一个委托实例,传入一个比较方法
Comparison<int> comparison = (x, y) => y.CompareTo(x);

// 使用 List<T> 类的 Sort 方法,将委托作为参数传入
numbers.Sort(comparison);

// numbers: { 10, 8, 6, 5, 3 }
  • (7)(待补充)

10. enum 关键字

C# 中,enum 关键字用于声明枚举类型。

  • 枚举是一种值类型,它表示一个固定的值集合。这些值被称为枚举成员,并且每一个都有一个关联的常量值。默认情况下,第一个枚举成员的值是0,后续的成员值依次递增1,但是可以显式地改变这个顺序。

  • 假设有一个枚举类型定义了一周的天数

public enum DayOfWeek
{
Sunday = 0,
Monday = 1,
Tuesday = 2,
Wednesday = 3,
Thursday = 4,
Friday = 5,
Saturday = 6
}
  • 可以创建一个变量来存储当前的星期几
DayOfWeek today = DayOfWeek.Monday;
  • 可以将枚举值转换为其他类型,例如整数或字符串
int dayNumber = (int)DayOfWeek.Friday;  // 5
string dayName = DayOfWeek.Friday.ToString(); // "Friday"
  • 反过来,也可以将其他类型转换为枚举值
DayOfWeek day = (DayOfWeek)5;  // DayOfWeek.Friday
DayOfWeek day = Enum.Parse<DayOfWeek>("Friday"); // DayOfWeek.Friday

DayOfWeek day;
bool success = Enum.TryParse<DayOfWeek>("Sunday", out day); // true

如果枚举表示的是一组位标志,那么应该使用[Flags]属性,并为枚举成员指定2的幂次方值。这样就可以使用位运算符来组合、添加或移除枚举值。

[Flags]
public enum AccessRights
{
None = 0, // 0000
Read = 1, // 0001
Write = 2, // 0010
Execute = 4, // 0100
FullControl = Read | Write | Execute // 0111
}

在这个例子中,AccessRights枚举定义了一组访问权限,每个权限是二进制位的一个标志。这允许我们将不同的权限组合到一起,然后通过位运算检查特定的权限是否存在。

// 设置读和写权限
AccessRights rights = AccessRights.Read | AccessRights.Write;

// 检查是否有写权限
if ((rights & AccessRights.Write) == AccessRights.Write)
{
Console.WriteLine("有写权限");
}
else
{
Console.WriteLine("无写权限");
}

除了位运算符的使用,也可以直接使用HasFlag方法来检查特定的权限是否存在。

AccessRights rights = AccessRights.Read | AccessRights.Write;

rights.HasFlag(AccessRights.Write) // true

11. event 关键字

C#中,event关键字用于声明一个事件。(如何发布符合 .NET 准则的事件

  • 事件是由对象在特殊情况下触发的一种机制,例如用户点击了一个按钮,或者某个操作已经完成。对象可以订阅这个事件,这样当事件发生时,就会调用订阅者定义的事件处理程序。

在事件的定义中,通常会有两部分组成。一部分是声明事件的委托(delegate),另一部分是事件本身。下面是一个简单的event使用例子

// 定义一个委托
public delegate void MyEventHandler(string message);

public class Publisher
{
// 声明一个事件
public event MyEventHandler MyEvent;

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

public class Subscriber
{
public void Subscribe(Publisher publisher)
{
// 订阅事件
publisher.MyEvent += HandleEvent;
}

private void HandleEvent(string message)
{
Console.WriteLine($"收到了来自Publisher的消息: {message}");
}
}

public class Program
{
public static void Main(string[] args)
{
var publisher = new Publisher();
var subscriber = new Subscriber();

// 订阅事件
subscriber.Subscribe(publisher);

// 发布事件
publisher.RaiseEvent("Hello, World!");
}
}

上面例子可以看出,事件帮助了事件发布者和订阅者之间的解耦。发布者并不需要知道谁订阅了它的事件,也不需要知道事件将如何被处理。这使得我们可以独立地修改发布者和订阅者的代码,而不需要担心它们之间的依赖关系。

  • event关键字使用注意点

  • (1) 避免内存泄漏

    • 如果一个对象订阅了另一个对象的事件,那么在不需要订阅事件时应当及时取消订阅,以避免发生内存泄漏。(只要事件发布者还在内存中,订阅者就不会被垃圾回收,即使订阅者已经不再使用)
public class Subscriber
{
public void Subscribe(Publisher publisher)
{
publisher.MyEvent += HandleEvent;
}

public void Unsubscribe(Publisher publisher)
{
publisher.MyEvent -= HandleEvent;
}

private void HandleEvent(object sender, MyEventArgs e)
{
Console.WriteLine($"收到了来自Publisher的消息: {e.Message}");
}
}
  • (2) 委托类型
    • System.EventHandlerSystem.EventHandler<T> 是标准的事件委托类型,推荐在事件定义中使用这些类型以提高代码的可读性和一致性。
public class Publisher
{
// 使用标准的EventHandler
public event EventHandler MyEvent;

// 使用泛型的EventHandler
public event EventHandler<MyEventArgs> MyEventWithArgs;

protected virtual void OnMyEvent()
{
MyEvent?.Invoke(this, EventArgs.Empty);
}

protected virtual void OnMyEventWithArgs(MyEventArgs e)
{
MyEventWithArgs?.Invoke(this, e);
}
}

public class MyEventArgs : EventArgs
{
public string Message { get; set; }
}
  • (3) 事件访问器
    • 事件访问器允许在事件被添加或移除时执行自定义的逻辑。这对于调试或管理订阅者列表非常有用。
using System;

public class MyEventArgs : EventArgs
{
public string Message { get; set; }
}

public class Publisher
{
private EventHandler<MyEventArgs> myEvent;

public event EventHandler<MyEventArgs> MyEvent
{
add
{
Console.WriteLine("添加事件处理程序");
myEvent += value;
}
remove
{
Console.WriteLine("移除事件处理程序");
myEvent -= value;
}
}

protected virtual void OnMyEvent(MyEventArgs e)
{
myEvent?.Invoke(this, e);
}

public void RaiseEvent(string message)
{
OnMyEvent(new MyEventArgs { Message = message });
}
}

public class Subscriber
{
public void Subscribe(Publisher publisher)
{
publisher.MyEvent += HandleEvent;
}

public void Unsubscribe(Publisher publisher)
{
publisher.MyEvent -= HandleEvent;
}

private void HandleEvent(object sender, MyEventArgs e)
{
Console.WriteLine($"收到了来自Publisher的消息: {e.Message}");
}
}

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

// 订阅事件
subscriber.Subscribe(publisher);

// 触发事件
publisher.RaiseEvent("Hello World!");

// 取消订阅事件
subscriber.Unsubscribe(publisher);
}
}

12. explicit 关键字

C#中,explicit关键字用于类型转换。

  • explicit关键字通常用在用户定义的类型转换操作中,允许从一种数据类型转换到另一种数据类型,例如从类到接口,或者从父类到子类等等。

  • 在某些情况下,转换操作可能导致数据丢失或抛出异常。使用explicit关键字可以强制开发者明确地处理转换,从而减少在转换过程中出现错误的可能性。

下面是一个explicit关键字的例子

public class Fahrenheit
{
public float Temperature { get; set; }

// 推荐用 lambda 表达式写
public static explicit operator Celsius(Fahrenheit f)
{
return ConvertToFahrenheit(f);
}

private static Celsius ConvertToFahrenheit(Fahrenheit f)
{
float celsiusTemperature = (5.0f / 9.0f) * (f.Temperature - 32);
return new Celsius(celsiusTemperature);
}
}

public class Celsius
{
public float Temperature { get; set; }

public Celsius(float temp)
{
Temperature = temp;
}
}

Fahrenheit f = new Fahrenheit { Temperature = 100 };
Celsius c = (Celsius)f; // 显式转换

上面例子中,不能直接将Fahrenheit对象转换为Celsius,必须明确地进行转换。


13. foreach 关键字

C#中,foreach关键字用于遍历集合(如数组,列表等)中的元素。

  • 它的工作原理是通过调用集合对象的GetEnumerator方法来获取IEnumeratorIEnumerator<T>对象,然后在每次迭代中调用MoveNext方法和Current属性。

注意,后面IEnumeratorIEnumerator<T>统称为枚举器

var fibNumbers = new List<int> { 0, 1, 1, 2, 3, 5, 8, 13 };
foreach (int element in fibNumbers)
{
Console.Write($"{element} ");
}
// Output:
// 0 1 1 2 3 5 8 13

下面是一些foreach关键字的使用方法

  • (1) 实现枚举器接口的集合
    C#中,许多内置集合类型都实现了枚举器接口,这使得它们可以使用foreach循环进行迭代。
Dictionary<string, int> dictionary = new Dictionary<string, int>
{
{ "One", 1 },
{ "Two", 2 },
{ "Three", 3 }
};

foreach (KeyValuePair<string, int> item in dictionary)
{
Console.WriteLine($"Key: {item.Key}, Value: {item.Value}");
}
  • (2) 实现枚举器接口的自定义类
    如果想要自定义类能够被foreach循环遍历,需要实现枚举器接口。这个接口定义了一个方法,GetEnumerator,返回一个实现了枚举器接口的对象。
public class MyCollection : IEnumerable<int>
{
private int[] data = { 1, 2, 3, 4, 5 };

public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i < data.Length; i++)
{
yield return data[i];
}
}

IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

// 使用自定义集合
MyCollection collection = new MyCollection();

foreach (int number in collection)
{
Console.WriteLine(number);
}
  • (3) 异步流的迭代
    异步流的迭代是C# 8.0新增的特性,这使得我们能够在处理异步操作时,写出更高效和更易于理解的代码。异步流的迭代通过await foreach关键字实现。它能够异步地迭代这样的序列,而不会阻塞主线程。
public async IAsyncEnumerable<int> GenerateSequence()
{
for (int i = 0; i < 20; i++)
{
await Task.Delay(100); // 模拟异步操作
yield return i;
}
}

public async Task TestAsyncStream()
{
await foreach (var number in GenerateSequence())
{
Console.WriteLine(number);
}
}

在上述示例中,GenerateSequence方法生成了一个异步序列。每个元素都是在等待一段时间后生成的,以模拟异步操作。TestAsyncStream方法使用await foreach语句来异步地迭代这个序列。每次迭代都会异步地等待下一个元素。


14. interface 关键字

C# 中,接口interface是一种定义行为契约的类型。接口声明一组方法、属性、事件或索引器,但不提供其实现。类class或结构体struct可以实现一个或多个接口,并提供接口中定义的成员的具体实现。

public interface IAnimal
{
void Speak();
void Move();
}
  • 类或结构体实现接口时,必须提供接口中定义的所有成员的具体实现。
interface IPoint
{
// Property signatures:
int X { get; set; }

int Y { get; set; }

double Distance { get; }
}

class Point : IPoint
{
// Constructor:
public Point(int x, int y)
{
X = x;
Y = y;
}

// Property implementation:
public int X { get; set; }

public int Y { get; set; }

// Property implementation
public double Distance =>
Math.Sqrt(X * X + Y * Y);
}

class MainClass
{
static void PrintPoint(IPoint p)
{
Console.WriteLine("x={0}, y={1}", p.X, p.Y);
}

static void Main()
{
IPoint p = new Point(2, 3);
Console.Write("My Point: ");
PrintPoint(p);
}
}
// Output: My Point: x=2, y=3
  • 接口可以从一个或多个其他接口继承。实现该接口的类必须实现所有继承的接口成员。
public interface IDrawable
{
void Draw();
}

public interface IResizable
{
void Resize(double factor);
}

public class Shape : IDrawable, IResizable
{
public double Width { get; private set; }
public double Height { get; private set; }

public Shape(double width, double height)
{
Width = width;
Height = height;
}

// 实现 IDrawable 接口的方法
public void Draw()
{
Console.WriteLine($"Drawing a shape with width {Width} and height {Height}.");
}

// 实现 IResizable 接口的方法
public void Resize(double factor)
{
Width *= factor;
Height *= factor;
Console.WriteLine($"Resized shape to width {Width} and height {Height}.");
}
}
  • 接口与抽象类的区别
    • 实现: 接口中不能包含任何实现(C# 8.0 的默认接口方法除外),而抽象类可以包含实现。
    • 多重继承: 一个类可以实现多个接口,但只能继承一个抽象类。
    • 构造函数: 接口不能有构造函数,而抽象类可以。
    • 成员类型: 接口中不能包含字段(成员变量),而抽象类可以。
public interface IShape
{
// 接口不能有构造函数
}

public abstract class Shape
{
public Shape()
{
// 构造函数逻辑
}
}
public interface IShape
{
// 接口中不能包含字段
double CalculateArea(double size);
}

public abstract class Shape
{
protected double area;

public abstract double CalculateArea(double size);

public double GetArea()
{
return area;
}
}

15. internal 关键字

C#中,internal关键字用于指定成员的可访问性。internal修饰符使成员在同一程序集assembly内可访问,但在程序集外不可访问。(这里的程序集不是指同一个文件, 也不是指namespace)

  • 程序集Assembly: 程序集是.NET中的基本部署单元,可以是一个DLLEXE文件。一个程序集包含一个或多个命名空间和类型,包含了元数据和代码。程序集是.NET中的物理边界。

  • 类的internal可访问性

    • 在以下示例中,MyClass类被声明为internal,因此它只能在同一程序集内访问。
// 文件1:MyClass.cs
internal class MyClass
{
public void Display()
{
Console.WriteLine("Hello from MyClass!");
}
}

// 文件2:Program.cs
class Program
{
static void Main()
{
MyClass myClass = new MyClass();
myClass.Display();
}
}

在上面的示例中,MyClass类和它的Display方法都可以在Program类中访问,因为它们在同一个程序集内。

  • 方法的internal可访问性
    • 在以下示例中,MyClass类是public的,但其中的InternalMethod方法被声明为internal,因此它只能在同一程序集内访问。
// 文件1:MyClass.cs
public class MyClass
{
internal void InternalMethod()
{
Console.WriteLine("Hello from InternalMethod!");
}
}

// 文件2:Program.cs
class Program
{
static void Main()
{
MyClass myClass = new MyClass();
myClass.InternalMethod();
}
}

在上面的示例中,MyClassInternalMethod方法可以在Program类中访问,因为它们在同一个程序集内。

  • 跨程序集访问
    • 如果尝试从另一个程序集访问internal成员,会导致编译错误。例如,假设有两个项目: ProjectAProjectBProjectA定义了一个internal类,而ProjectB试图访问它。
// ProjectA - 文件1:MyClass.cs
internal class MyClass
{
public void Display()
{
Console.WriteLine("Hello from MyClass!");
}
}

// ProjectB - 文件1:Program.cs
class Program
{
static void Main()
{
MyClass myClass = new MyClass(); // 编译错误:'MyClass' is inaccessible due to its protection level
myClass.Display();
}
}

在这个示例中,由于MyClass被声明为internal,所以它不能在ProjectB中访问,会导致编译错误。

  • 选择internal而不选择public的原因
    • 封装实现细节: 当你希望封装类、方法或其他成员,只让它们在同一个程序集内使用,而不暴露给外部程序集时,可以使用internal修饰符。
    • 控制API公开性: 在设计库或框架时,internal修饰符可以帮助控制哪些类型和成员应暴露给库的使用者,哪些应保持内部使用。

16. is 关键字

is关键字在C#中用于检查对象是否是特定类型的实例。

  • 类型检查
    • 使用is可以检查对象是否是特定类型的实例。如果是,则返回true,否则返回false
object obj = "Hello, World!";
if (obj is string)
{
Console.WriteLine("obj is a string.");
}
else
{
Console.WriteLine("obj is not a string.");
}
  • 模式匹配
    • C# 7.0开始,is关键字还支持模式匹配。这使得检查类型和转换类型可以在一个操作中完成。
object obj = "Hello, World!";
if (obj is string s)
{
Console.WriteLine($"obj is a string with value: {s}");
}
else
{
Console.WriteLine("obj is not a string.");
}

在这个示例中,如果objstring类型,is关键字不仅检查类型,还将obj转换为string并赋值给变量s。然后可以直接使用s

  • 使用is进行null检查
    • is关键字还可以用来检查对象是否为null
object obj = null;
if (obj is null)
{
Console.WriteLine("obj is null.");
}
else
{
Console.WriteLine("obj is not null."); // 相当于 is not
}
  • 检查接口实现
    • is关键字也可以用于检查对象是否实现了某个接口。
public interface IAnimal
{
void Speak();
}

public class Dog : IAnimal
{
public void Speak()
{
Console.WriteLine("Woof!");
}
}

object obj = new Dog();
if (obj is IAnimal animal)
{
animal.Speak(); // Outputs: Woof!
}
else
{
Console.WriteLine("obj does not implement IAnimal.");
}

17. namespace 关键字

C#中,namespace(命名空间) 是用于组织代码和防止命名冲突的一种机制。命名空间提供了一种逻辑上的分组方式,使得开发者可以更容易地管理和维护代码。

  • 定义命名空间
namespace MyApplication
{
public class MyClass
{
public void MyMethod()
{
Console.WriteLine("Hello from MyMethod!");
}
}
}

在这个示例中,MyClass类被定义在MyApplication命名空间中。

  • 使用命名空间
    • 要使用某个命名空间中的类型,可以通过using关键字导入命名空间。
using MyApplication;

class Program
{
static void Main()
{
MyClass myClass = new MyClass();
myClass.MyMethod();
}
}
  • 命名空间的好处
    • 组织代码: 通过使用命名空间,可以将相关的类、接口、枚举等进行分组,增加代码的可读性和维护性。
    • 避免命名冲突: 在大型项目中,不同模块或库中可能会有同名的类型,通过使用命名空间,可以避免这些冲突。

18. null 关键字

null关键字在C#中表示对象引用不指向任何实例。它是引用类型的默认值,意味着变量不引用任何对象。

  • 常见用途 (省略部分常见情况)
  • (1) 空合并运算符
    • 空合并运算符(??)提供了简洁的语法来处理null值。如果左操作数为null,则返回右操作数。
string str = null;
string result = str ?? "default value";
Console.WriteLine(result); // 输出 "default value"
  • (2) 空条件运算符
    • 空条件运算符(?.)在访问成员或调用方法时检查null。如果对象为null,表达式返回null而不引发异常。
MyClass obj = null;
obj?.DoSomething(); // 不会引发 NullReferenceException

int[] arr = null;
int? value = arr?[0]; // value 为 null
  • (3) 处理值类型的null
    • 对于值类型,不能直接赋值null。但是,可以使用可空类型(Nullable<T>T?)来表示可以为null的值类型。
int? nullableInt = null;
if (nullableInt.HasValue)
{
Console.WriteLine(nullableInt.Value);
}
else
{
Console.WriteLine("nullableInt is null");
}

19. out 关键字

C#中,out关键字用于方法参数,表示该参数将由方法初始化并返回给调用方。out参数在方法调用时不需要被初始化,但在方法返回之前必须被赋值。

  • 定义和使用out参数
    • 在方法声明中使用out关键字,并在方法调用时也使用out关键字。
public class Program
{
public static void Main()
{
int result;
bool success = TryParse("123", out result);
if (success)
{
Console.WriteLine($"Parsed number: {result}");
}
else
{
Console.WriteLine("Failed to parse number.");
}
}

public static bool TryParse(string input, out int number)
{
return int.TryParse(input, out number);
}
}
  • 使用out返回多个值
    • 通过out参数,可以让方法返回多个值。
public class Program
{
public static void Main()
{
double area, perimeter;
CalculateCircle(5, out area, out perimeter);
Console.WriteLine($"Area: {area}, Perimeter: {perimeter}");
}

public static void CalculateCircle(double radius, out double area, out double perimeter)
{
area = Math.PI * radius * radius;
perimeter = 2 * Math.PI * radius;
}
}
  • outref的区别

    • 初始化要求: 使用out参数的方法不要求在调用前初始化该参数,而ref参数必须在调用前初始化。
    • 赋值要求: 使用out参数的方法必须在方法返回之前为该参数赋值,而ref参数不强制要求在方法内赋值。
  • 以下限制适用于使用out关键字

    • 异步方法中不允许使用out参数。
    • 迭代器方法中不允许使用out参数。
    • 属性不能作为out参数传递。

20. override 关键字

override关键字在C#中用于方法、属性、索引器或事件,表示派生类中重写基类的虚方法、虚属性、虚索引器或虚事件。通过使用override,派生类可以提供基类成员的新实现。

  • 定义基类和派生类
    • 在基类中,使用virtual关键字定义可以被重写的成员。在派生类中,使用override关键字重写这些成员。
public class BaseClass
{
public virtual void Display()
{
Console.WriteLine("BaseClass Display method");
}
}

public class DerivedClass : BaseClass
{
public override void Display()
{
Console.WriteLine("DerivedClass Display method");
}
}
  • 使用override重写属性和方法
    • 可以使用override关键字重写基类中的虚属性。(重写方法如上)
public class BaseClass
{
public virtual int Number { get; set; }

public virtual void ShowNumber()
{
Console.WriteLine("Number: " + Number);
}
}

public class DerivedClass : BaseClass
{
private int _number;

public override int Number
{
get { return _number; }
set { _number = value; }
}

public override void ShowNumber()
{
Console.WriteLine("Derived Number: " + Number);
}
}
  • 注意事项
    • 虚成员: 只有基类中标记为virtualabstract的成员才能被重写。
    • 访问修饰符: 重写的成员的访问级别必须与基类中的成员一致,或者更严格。例如,如果基类中的虚方法是public,则派生类中的重写方法也必须是public,不能是protectedprivate

21. params 关键字

params关键字在C#中用于指定一个方法参数数组,使得方法可以接受可变数量的参数。

  • 使用params关键字的参数必须是方法的最后一个参数,并且它可以接受任意数量的参数,包括零个参数。
public void PrintNumbers(params int[] numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}

PrintNumbers(1, 2, 3, 4); // 输出: 1 2 3 4
PrintNumbers(5, 6); // 输出: 5 6
PrintNumbers(); // 输出: (空)
  • 使用params的理由
  • (1) 灵活性和简洁性
    • params关键字提供了在方法调用时的灵活性和简洁性。它允许调用者传递任意数量的参数,而不需要显式地创建一个数组。这使得方法调用更简洁、更直观。
public void PrintNumbers(int[] numbers)
{
foreach (int number in numbers)
{
Console.WriteLine(number);
}
}

// 调用方法时需要创建数组
PrintNumbers(new int[] { 1, 2, 3, 4, 5 });
PrintNumbers(new int[] { 10, 20 });
PrintNumbers(new int[] { }); // 传递一个空数组

如果不使用params关键字,则需要显式地创建数组

  • (2) 支持多参数重载
    • params关键字允许方法支持多种参数重载,这在需要处理多个不同参数数量的情况下非常有用。它避免了为每种可能的参数数量定义多个重载方法。
public void DisplayItems(params string[] items)
{
foreach (var item in items)
{
Console.WriteLine(item);
}
}

// 调用方法时
DisplayItems("Apple", "Banana", "Cherry");
DisplayItems("Dog", "Cat");
DisplayItems(); // 传递零个参数

22. private 关键字

private关键字是C#中的访问修饰符,用于控制类或结构体成员的可访问性。标记为private的成员只能在定义它们的类或结构体内部访问。

  • 使用private关键字可以隐藏类的实现细节,限制对类内部数据的直接访问,从而提高代码的封装性和安全性。
class Employee2
{
private readonly string _name = "FirstName, LastName";
private readonly double _salary = 100.0;

public string GetName()
{
return _name;
}

public double Salary
{
get { return _salary; }
}
}

class PrivateTest
{
static void Main()
{
var e = new Employee2();

// The data members are inaccessible (private), so
// they can't be accessed like this:
// string n = e._name;
// double s = e._salary;

// '_name' is indirectly accessed via method:
string n = e.GetName();

// '_salary' is indirectly accessed via property
double s = e.Salary;
}
}
  • 注意事项
    • privateC#中最严格的访问修饰符,仅限于在类或结构体内部访问。
    • private成员不能在派生类中访问,即使是通过继承。
    • 在结构体中,所有成员默认为private,而在类中,成员默认为private

23. protected 关键字

protected关键字是C#中的访问修饰符,用于控制类或结构体成员的可访问性。标记为protected的成员只能在以下两种情况下访问:

  • 在定义该成员的类内部访问
  • 在派生类中访问

这种访问修饰符允许你在保持数据封装的同时,为派生类提供访问基类成员的能力。

public class Animal
{
// 受保护的字段
protected string name;

// 公共方法
public void SetName(string name)
{
this.name = name;
}

// 受保护的方法
protected void DisplayInfo()
{
Console.WriteLine($"Animal Name: {name}");
}
}

public class Dog : Animal
{
// 公共方法
public void ShowDetails()
{
// 访问基类的受保护成员
DisplayInfo();
}
}

public class Program
{
public static void Main()
{
Dog dog = new Dog();
dog.SetName("Buddy");

// 访问派生类的公共方法
dog.ShowDetails();

// 直接访问基类的受保护成员会导致编译错误
// dog.name = "Buddy"; // 错误
// dog.DisplayInfo(); // 错误
}
}
  • 使用protected的原因
    • 继承与扩展: protected关键字允许派生类访问基类的成员,从而支持类的继承和扩展。
    • 数据封装: 通过限制成员的访问权限,protected关键字有助于保护类的内部状态,同时允许派生类进行必要的访问。

以下是对protected internalprivate protected访问修饰符的详细解释

  • protected internal关键字结合了protectedinternal的特性,成员可以在以下两种情况下访问
    • 同一程序集: internal允许同一程序集中的任何代码访问。
    • 派生类: protected允许派生类访问,即使它们在不同的程序集中。

因此,protected internal的成员可以被同一程序集中的所有代码访问,也可以被其他程序集中派生类访问。下面是一个例子

  • 图形库 Assembly: GraphicsLibrary.dll: Shape.cs
namespace GraphicsLibrary
{
public class Shape
{
// 受保护的内部成员
protected internal double Area { get; set; }

protected internal void CalculateArea()
{
// 默认的计算逻辑,可能被派生类覆盖
Area = 0;
}
}
}
  • 图形库 Assembly: GraphicsLibrary.dll: Circle.cs
namespace GraphicsLibrary
{
public class Circle : Shape
{
public double Radius { get; set; }

public Circle(double radius)
{
Radius = radius;
}

public override void CalculateArea()
{
// 覆盖基类的计算逻辑
Area = Math.PI * Radius * Radius;
}

public void DisplayArea()
{
Console.WriteLine($"The area of the circle is: {Area}");
}
}
}
  • 使用图形库的应用程序 Assembly: GraphicsApp.exe: Program.cs
using GraphicsLibrary;

namespace GraphicsApp
{
class Program
{
static void Main(string[] args)
{
Circle circle = new Circle(5);
circle.CalculateArea();
circle.DisplayArea();

// 在同一程序集中可以访问 protected internal 成员
double area = circle.Area;
Console.WriteLine($"Accessing area directly: {area}");
}
}
}

总而言之,protected internal允许灵活地在同一程序集内访问共享的实现细节,同时保留跨程序集继承的能力。

  • private protected关键字结合了privateprotected的特性,成员只能在以下两种情况下访问
    • 同一类内部: private允许在同一类内部访问。
    • 同一程序集中派生类: protected允许在派生类中访问,但前提是这些派生类在同一程序集中。

因此,private protected的成员只能被定义它们的类及其同一程序集中的派生类访问,不能被其他程序集的派生类访问。下面是一个例子

  • 订单系统 Assembly: OrderSystem.dll: Order.cs
namespace OrderSystem
{
public class Order
{
// 受保护的私有成员
private protected string OrderId { get; set; }
private protected decimal Amount { get; set; }

public Order(string orderId, decimal amount)
{
OrderId = orderId;
Amount = amount;
}

private protected void DisplayOrderDetails()
{
Console.WriteLine($"Order ID: {OrderId}, Amount: {Amount}");
}
}
}
  • 订单系统 Assembly: OrderSystem.dll: SpecialOrder.cs
namespace OrderSystem
{
public class SpecialOrder : Order
{
public SpecialOrder(string orderId, decimal amount) : base(orderId, amount)
{
}

public void ShowSpecialOrderDetails()
{
// 可以访问基类的 private protected 成员
DisplayOrderDetails();
}
}
}
  • 外部应用程序 Assembly: ExternalApp.dll: Program.cs
using OrderSystem;

namespace ExternalApp
{
class Program
{
static void Main(string[] args)
{
SpecialOrder specialOrder = new SpecialOrder("SO123", 1000.00m);
specialOrder.ShowSpecialOrderDetails();

// 不能在其他程序集访问 private protected 成员
// specialOrder.OrderId = "SO456"; // 编译错误
// specialOrder.DisplayOrderDetails(); // 编译错误
}
}
}

总而言之,private protected提供了一种更严格的访问控制,防止不同程序集中的类访问某些内部细节,这在保护敏感数据或内部实现细节时特别有用。


24. readonly 关键字

readonly关键字在C#中用于修饰字段,表示该字段在初始化后不能被更改。readonly字段只能在以下情况下赋值

  • 字段声明时: 可以在声明字段时直接赋值。
  • 构造函数中: 可以在类的构造函数中赋值,包括实例构造函数和静态构造函数。

readonly字段与const常量的不同之处在于,const是编译时常量,必须在声明时赋值,并且其值在编译时就确定,而readonly字段可以在运行时被赋值,并且可以根据构造函数的逻辑赋予不同的值。

public class Person
{
public readonly string Name;
public readonly int Age;

// 字段声明时赋值
public readonly string Country = "USA";

// 构造函数中赋值
public Person(string name, int age)
{
Name = name;
Age = age;
}

public void Display()
{
Console.WriteLine($"Name: {Name}, Age: {Age}, Country: {Country}");
}
}

public class Program
{
public static void Main()
{
Person person = new Person("Alice", 30);
person.Display();

// 试图修改 readonly 字段会导致编译错误
// person.Name = "Bob"; // 编译错误
// person.Age = 35; // 编译错误
// person.Country = "Canada"; // 编译错误
}
}
  • constreadonly的区别
    • const适用于值在编译时就确定不变的情况,如数学常量pi
    • readonly适用于值在运行时确定且一旦初始化后不再改变的情况,如配置文件中的某些设置。
public class Constants
{
public const double Pi = 3.14159; // 编译时常量
public readonly DateTime CreationTime; // 运行时常量

public Constants()
{
CreationTime = DateTime.Now; // 可以在构造函数中赋值
}
}

25. ref 关键字

ref关键字在C#中用于方法参数,表示参数以引用方式传递。这意味着在方法内对参数的任何修改都会影响到调用方传递的实际变量。使用ref关键字的要求

  • 在方法声明中使用: 必须在方法的参数列表中使用ref关键字。
  • 在方法调用时使用: 调用方法时也必须在实际参数前加上ref关键字。
  • 初始化要求: 传递给ref参数的变量必须在调用方法之前初始化。
public class Program
{
public static void Increment(ref int number)
{
number++;
}

public static void Main()
{
int value = 5;
Console.WriteLine($"Before: {value}"); // 输出: Before: 5
Increment(ref value);
Console.WriteLine($"After: {value}"); // 输出: After: 6
}
}

如果上面的示例中没有使用ref,那么第二个Console的输出就还会是5。原因是方法接收参数的副本,方法内对参数的修改不会影响到外部的实际变量。

  • 使用ref的场景
    • 需要修改调用方变量的场景: 当你希望在方法内修改调用方传递的变量时,可以使用ref关键字。例如,交换两个变量的值。
    • 需要返回多个值的场景: 虽然方法本身只能返回一个值,但可以通过refout参数返回多个值。
public class Program
{
public static void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}

public static void Main()
{
int x = 10;
int y = 20;
Console.WriteLine($"Before Swap: x = {x}, y = {y}"); // 输出: Before Swap: x = 10, y = 20
Swap(ref x, ref y);
Console.WriteLine($"After Swap: x = {x}, y = {y}"); // 输出: After Swap: x = 20, y = 10
}
}
  • refout的区别
    • 初始化要求: 传递给ref参数的变量必须在调用方法之前初始化。传递给out参数的变量可以在方法调用之前未初始化,但必须在方法内部赋值。
    • 主要用途: ref用于传递数据给方法,并且希望方法内部修改该数据。out用于方法返回多个值。

26. sealed 关键字

sealed关键字在C#中用于类和方法,提供了不同的功能

  • 用于类: 防止其他类从它派生。也就是说,sealed类不能作为基类,无法被继承。
  • 用于方法: 当用在基类中的虚方法上时,防止派生类进一步重写该方法。

当一个类被标记为sealed时,它不能被继承。这通常用于防止不希望的继承,或者当类的设计不需要被扩展时。使用sealed类可以提高性能,因为它的成员在编译时可以进行更多的优化。

public sealed class FinalClass
{
public void Display()
{
Console.WriteLine("This is a sealed class.");
}
}

// 以下代码将会导致编译错误,因为不能继承一个 sealed 类
// public class DerivedClass : FinalClass
// {
// }

当一个类继承自基类,并且重写了基类中的虚方法时,可以使用sealed关键字阻止进一步的重写。这种情况下,sealed关键字必须与override一起使用。

public class BaseClass
{
public virtual void Display()
{
Console.WriteLine("BaseClass Display method.");
}
}

public class DerivedClass : BaseClass
{
public sealed override void Display()
{
Console.WriteLine("DerivedClass Display method.");
}
}

// 以下代码将会导致编译错误,因为 Display 方法被 sealed
// public class FurtherDerivedClass : DerivedClass
// {
// public override void Display()
// {
// Console.WriteLine("FurtherDerivedClass Display method.");
// }
// }
  • 使用sealed的场景
    • 提高安全性: 通过防止类的继承,可以保护类的实现细节,防止子类意外地改变类的行为。
    • 性能优化: 标记为sealed的类和方法可以进行更多的编译时优化,因为编译器知道这些类和方法不会被重写。
    • 限制扩展性: 当一个类的设计不适合扩展时,可以使用sealed防止进一步的继承。

27. static 关键字

static关键字在C#中用于声明类成员和类本身。它表示这些成员或类与具体实例无关,而是属于类型本身。

  • 静态类 static class
    • 定义: 静态类是不能被实例化的类,所有成员都必须是静态的。
    • 特性: 静态类通常用于组织一组相关的静态方法和常量,不允许创建静态类的实例。
    • 用途: 常用于实用类Utility class或库,提供静态方法进行通用操作。
public static class MathUtilities
{
public static int Add(int a, int b)
{
return a + b;
}

public static int Multiply(int a, int b)
{
return a * b;
}
}
  • 静态类 static 成员
    • 静态成员包括静态字段、静态属性、静态方法、静态事件等。它们属于类本身,而不是类的具体实例。
    • 静态字段: 所有实例共享的字段,可以用于存储全局状态或常量。
    • 静态属性: 提供对静态字段的访问,通常用于封装。
    • 静态方法: 可以在不创建类实例的情况下调用,用于执行与类实例无关的操作。
    • 静态构造函数: 用于初始化静态字段或执行其他静态初始化,只在类首次被访问时调用一次。
public class Counter
{
public static int Count { get; private set; }

public static void Increment()
{
Count++;
}
}

// 使用静态成员
Counter.Increment();
Console.WriteLine(Counter.Count); // 输出: 1
  • 静态构造函数 static constructor
    • 静态构造函数用于初始化静态字段或执行类级别的初始化。它在类的第一次使用时被调用,且只调用一次。
    • 特性: 没有访问修饰符,因为它不能由外部代码调用;不接受参数,因为它是自动调用的。
    • 用途: 通常用于初始化静态数据。
public class Example
{
public static int Value;

static Example()
{
Value = 42; // 静态初始化
}
}

// 使用静态成员
Console.WriteLine(Example.Value); // 输出: 42

static关键字在C#中非常重要,用于定义不依赖于具体对象实例的类和成员。它提供了一个全局的、共享的访问点,适用于需要统一管理状态或提供通用操作的场景。


28. struct 关键字

struct关键字在C#中用于定义值类型。值类型与引用类型(如类)不同,值类型在内存中直接存储其数据,而不是存储对数据的引用。结构体是一种轻量级的数据结构,通常用于表示小型数据对象。

  • 内存分配: 值类型的数据通常分配在栈上,而不是堆上。
  • 数据存储: 值类型直接包含其数据,而引用类型包含对数据的引用。
  • 无继承: 结构体不能继承自其他结构体或类,也不能被继承。但它们可以实现接口。
  • 无参数的构造函数: 不能定义无参数的构造函数,因为默认的无参数构造函数始终存在,用于初始化字段为其默认值。
public struct Point
{
public int X { get; set; }
public int Y { get; set; }

public Point(int x, int y)
{
X = x;
Y = y;
}

public void Display()
{
Console.WriteLine($"Point at ({X}, {Y})");
}
}

public class Program
{
public static void Main()
{
Point p1 = new Point(10, 20);
p1.Display(); // 输出: Point at (10, 20)

Point p2 = p1; // 结构体的值复制
p2.X = 30;
p2.Display(); // 输出: Point at (30, 20)
p1.Display(); // 输出: Point at (10, 20) - p1 未受影响
}
}
  • struct 的适用场景
    • 轻量级数据结构: 结构体适合用于表示轻量级的数据对象,如二维坐标、颜色、时间等。
    • 不可变对象: 因为结构体是值类型,通常可以设计成不可变对象,提供更高的安全性和一致性。
    • 性能考虑: 由于结构体在栈上分配,访问速度较快,适合于频繁分配和释放的小型对象。

29. switch 关键字

这里主要介绍switch表达式

switch表达式是C# 8.0引入的一种简洁且功能强大的分支选择机制。与传统的switch语句不同,switch表达式不仅能根据输入值进行分支,而且更灵活地结合了模式匹配的特性。

var result = expression switch
{
pattern1 => result1,
pattern2 => result2,
pattern3 => result3,
_ => defaultResult
};

switch表达式提供了一种简洁且灵活的方式来进行条件分支选择,与传统switch语句相比,它不仅减少了代码量,还提高了代码的可读性和维护性。


30. this 关键字

this关键字在C#中用于引用当前实例的成员,包括字段、方法、属性、事件和索引器。它在类的非静态成员中非常有用。此外,this关键字还有其他特定用途,如调用构造函数和扩展方法。

  • 引用当前实例的成员
    • 在类的实例方法或属性中,可以使用this关键字引用当前实例的成员,尤其是在成员名称与方法参数名称相同时。
public class Person
{
private string name;

public Person(string name)
{
// 使用 this 引用当前实例的 name 字段
this.name = name;
}

public void Display()
{
Console.WriteLine($"Name: {this.name}");
}
}

public class Program
{
public static void Main()
{
Person person = new Person("Alice");
person.Display(); // 输出: Name: Alice
}
}
  • 调用其他构造函数
    • this关键字可以在构造函数中调用同一个类的其他构造函数,称为构造函数重载。
public class Rectangle
{
public int Width { get; set; }
public int Height { get; set; }

public Rectangle() : this(0, 0) // 调用具有两个参数的构造函数
{
}

public Rectangle(int width, int height)
{
this.Width = width;
this.Height = height;
}

public void Display()
{
Console.WriteLine($"Width: {Width}, Height: {Height}");
}
}

public class Program
{
public static void Main()
{
Rectangle rect1 = new Rectangle();
rect1.Display(); // 输出: Width: 0, Height: 0

Rectangle rect2 = new Rectangle(10, 20);
rect2.Display(); // 输出: Width: 10, Height: 20
}
}
  • 扩展方法的调用
    • this关键字在扩展方法的定义中用于指示被扩展的类型。
public static class StringExtensions
{
public static bool IsNullOrEmpty(this string value)
{
return string.IsNullOrEmpty(value);
}
}

public class Program
{
public static void Main()
{
string str = null;
bool result = str.IsNullOrEmpty(); // 使用扩展方法
Console.WriteLine(result); // 输出: True
}
}

在这个示例中,IsNullOrEmpty方法是一个扩展方法,它使用this关键字将string类型扩展为包含该方法的类型。this关键字指示value参数为被扩展的类型的实例。


31. throw 关键字

throw关键字在C#中用于引发异常。异常是一种在程序执行过程中出现的错误或意外情况,使用throw可以中断程序的正常执行流程,并转到相关的异常处理代码。

  • 引发异常
    • 使用throw关键字可以显式引发一个异常,通常在检测到程序状态不合法或出现错误时使用。
public class Program
{
public static void Main()
{
int divisor = 0;
try
{
int result = Divide(10, divisor);
Console.WriteLine(result);
}
catch (DivideByZeroException ex)
{
Console.WriteLine($"Caught an exception: {ex.Message}");
}
}

public static int Divide(int dividend, int divisor)
{
if (divisor == 0)
{
throw new DivideByZeroException("Divisor cannot be zero.");
}
return dividend / divisor;
}
}
  • 重新引发异常
    • 在捕获异常后,可能希望重新引发异常以允许外部代码进一步处理。在catch块中使用throw可以重新引发当前捕获的异常。
using System;
using System.IO;

public class FileProcessor
{
public void ProcessFile(string filePath)
{
try
{
string content = ReadFile(filePath);
Console.WriteLine("File content:");
Console.WriteLine(content);
}
catch (IOException ex)
{
Console.WriteLine("An I/O error occurred while processing the file.");
throw; // 重新引发异常
}
catch (Exception ex)
{
Console.WriteLine("An unexpected error occurred.");
throw; // 重新引发异常
}
}

private string ReadFile(string filePath)
{
if (string.IsNullOrWhiteSpace(filePath))
{
throw new ArgumentNullException(nameof(filePath), "File path cannot be null or empty.");
}

if (!File.Exists(filePath))
{
throw new FileNotFoundException("The specified file does not exist.", filePath);
}

return File.ReadAllText(filePath);
}
}

public class Program
{
public static void Main()
{
FileProcessor processor = new FileProcessor();
try
{
processor.ProcessFile("nonexistentfile.txt");
}
catch (Exception ex)
{
// 在这里可以进一步处理异常,例如记录日志或显示用户友好的错误消息
Console.WriteLine($"Exception caught in Main: {ex.Message}");
}
}
}
  • 注意事项
    • 使用异常类型: throw关键字后面必须跟一个异常对象。异常对象通常是从System.Exception类派生的类实例。
    • 重新引发的正确方式: 在catch块中重新引发异常时,使用throw; 而不是throw ex;。前者保留了原始异常的堆栈跟踪信息,而后者会重置堆栈跟踪。
catch (Exception ex)
{
// 不推荐,因为会丢失原始异常的堆栈跟踪
throw ex;
}

catch (Exception ex)
{
// 推荐的方式,保留原始异常的堆栈跟踪
throw;
}

32. using 关键字

using关键字在C#中有两种主要的用法

  • 资源管理 (using语句): 用于定义一个范围,在该范围结束时会自动释放特定的资源。通常用于处理非托管资源,如文件、数据库连接等,需要显式释放的资源。
  • 命名空间导入 (using指令): 用于导入命名空间,以便在代码中可以简化对命名空间内类型的引用。

资源管理 (using语句)

  • using语句用于确保实现了IDisposable接口的对象在使用完后被正确地释放。IDisposable接口包含一个Dispose方法,用于释放资源。
using System;
using System.IO;

public class Program
{
public static void Main()
{
using (StreamReader reader = new StreamReader("example.txt"))
{
string content = reader.ReadToEnd();
Console.WriteLine(content);
} // reader 对象在此处被释放
}
}

命名空间导入 (using指令)

  • using指令用于在文件的顶部导入命名空间,以便在代码中简化对该命名空间内类型的引用。这可以减少代码中的冗长类型名称,并提高代码的可读性。
using System;
using System.Collections.Generic;

public class Program
{
public static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3 };
Console.WriteLine(string.Join(", ", numbers));
}
}

别名: 可以使用using指令为类型或命名空间创建别名。

using Project = MyCompany.ProjectNamespace;

public class Program
{
public static void Main()
{
Project.MyClass obj = new Project.MyClass();
obj.MyMethod();
}
}

33. virtual 关键字

virtual关键字在C#中用于修饰方法、属性、事件或索引器,表示这些成员可以在派生类中被重写。

  • 使用virtual关键字定义的成员提供了一个默认的实现,但允许派生类通过override关键字提供新的实现,从而改变基类的行为。

  • 虚方法 Virtual Methods

    • 虚方法是指在基类中定义,并允许派生类重写的方法。虚方法在基类中提供了默认实现,但派生类可以通过重写它来提供新的行为。
public class BaseClass
{
public virtual void Display()
{
Console.WriteLine("BaseClass Display");
}
}

public class DerivedClass : BaseClass
{
public override void Display()
{
Console.WriteLine("DerivedClass Display");
}
}
  • 虚属性 Virtual Properties
    • 虚属性类似于虚方法,它们允许在派生类中重写基类中的属性。
public class BaseClass
{
private string _name;

public virtual string Name
{
get { return _name; }
set { _name = value; }
}
}

public class DerivedClass : BaseClass
{
private string _name;

public override string Name
{
get { return _name; }
set { _name = value.ToUpper(); } // 强制将名字转换为大写
}
}
  • 虚事件 Virtual Events
    • 虚事件与虚方法类似,允许派生类重写事件的订阅add和取消订阅remove行为。
using System;

public class BaseClass
{
public virtual event EventHandler MyEvent;

protected virtual void OnMyEvent(EventArgs e)
{
// 检查是否有订阅者,然后引发事件
MyEvent?.Invoke(this, e);
}

public void TriggerEvent()
{
// 调用 OnMyEvent 方法来引发事件
OnMyEvent(EventArgs.Empty);
}
}

public class DerivedClass : BaseClass
{
private EventHandler _eventHandlers;

public override event EventHandler MyEvent
{
add
{
Console.WriteLine("Adding event handler");
_eventHandlers += value;
}
remove
{
Console.WriteLine("Removing event handler");
_eventHandlers -= value;
}
}

protected override void OnMyEvent(EventArgs e)
{
Console.WriteLine("DerivedClass specific event handling");
_eventHandlers?.Invoke(this, e);
}
}

public class Program
{
public static void Main()
{
DerivedClass obj = new DerivedClass();

// 添加事件处理程序
obj.MyEvent += (sender, e) => Console.WriteLine("Event Handler 1");
obj.MyEvent += (sender, e) => Console.WriteLine("Event Handler 2");

// 触发事件
obj.TriggerEvent();

// 移除事件处理程序
obj.MyEvent -= (sender, e) => Console.WriteLine("Event Handler 2");
obj.TriggerEvent();
}
}
  • virtual的使用场景
    • 多态性: 虚方法和属性允许派生类重写基类的实现,从而实现多态性。基类的方法可以调用派生类的实现,而无需了解派生类的具体类型。
    • 默认行为: 虚方法和属性可以提供基类的默认行为,派生类可以选择接受默认行为或提供自己的实现。

为什么选择virtual而不是abstract

  • (1) virtual关键字

    • 定义: virtual关键字用于定义可以在派生类中重写的成员。虚成员在基类中有一个默认的实现,派生类可以选择重写该实现。
    • 用途: 当基类提供一个默认的实现,但派生类可以根据需要覆盖该实现时,使用virtual。它允许提供一个基础行为,并在派生类中进行扩展或修改。
  • (2) abstract关键字

    • 定义: abstract关键字用于定义一个没有实现的成员,派生类必须提供该成员的具体实现。抽象成员没有方法体,类本身也必须是抽象的abstract class
    • 用途: 当基类强制派生类实现某些方法或属性时,使用abstract。它用于定义一个抽象的接口或行为规范,所有的派生类都必须实现这些行为。
public abstract class Animal
{
public abstract void Speak();
}

public class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("Bark");
}
}

public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("Meow");
}
}

34. get 和 set 关键字

getset关键字在C#中用于定义属性的访问器。

  • get访问器用于返回属性的值,而set访问器用于为属性分配新值。
public class Person
{
private string _name;

public string Name
{
get
{
return _name; // 返回字段的值
}
set
{
_name = value; // 设置字段的值
}
}
}

public class Program
{
public static void Main()
{
Person person = new Person();
person.Name = "Alice"; // 调用 set 访问器
Console.WriteLine(person.Name); // 调用 get 访问器
}
}
  • 自动属性
    • 自动属性是一种简化语法,它让编译器自动生成私有的后备字段。使用自动属性时,不需要显式定义 getset访问器的实现。
public class Person
{
public string Name { get; set; } // 自动属性
}

public class Program
{
public static void Main()
{
Person person = new Person();
person.Name = "Alice"; // 自动属性的 set 访问器
Console.WriteLine(person.Name); // 自动属性的 get 访问器
}
}
  • 只读和写入属性
    • 只读属性: 只定义get访问器的属性。
    • 写入属性: 只定义set访问器的属性。
// 只读属性
public class Person
{
private string _name;

public string Name
{
get { return _name; }
}

public Person(string name)
{
_name = name;
}
}

// 写入属性
public class Person
{
private string _name;

public string Name
{
set { _name = value; }
}

public string GetName()
{
return _name;
}
}
  • 属性的访问控制
    • 可以为属性的getset访问器指定不同的访问修饰符,以控制它们的访问级别。
public class Person
{
private string _name;

public string Name
{
get { return _name; }
private set { _name = value; }
}

public Person(string name)
{
Name = name; // 可以在类内部设置值
}
}

在这个示例中,Name属性的set访问器是私有的,因此只能在Person类内部设置Name的值,而get 访问器是公有的,允许从外部读取Name的值。


35. add 和 remove 关键字

addremove关键字在C#中用于自定义事件的访问器。这两个关键字允许你定义事件处理程序的添加和移除逻辑,它们通常与event关键字一起使用。

using System;

public class MyEventPublisher
{
private EventHandler _myEvent;

public event EventHandler MyEvent
{
add => _myEvent += value;
remove => _myEvent -= value;
}

public void RaiseEvent()
{
_myEvent?.Invoke(this, EventArgs.Empty);
}
}

public class Program
{
public static void Main()
{
MyEventPublisher publisher = new MyEventPublisher();

EventHandler handler = (sender, e) => Console.WriteLine("Event triggered");
publisher.MyEvent += handler; // 触发 add 访问器
publisher.RaiseEvent(); // 输出 "Event triggered"

publisher.MyEvent -= handler; // 触发 remove 访问器
publisher.RaiseEvent(); // 没有输出
}
}

通过自定义addremove访问器,开发者可以完全控制事件的订阅和取消订阅过程,使得事件系统更加灵活和可控。


36. yield 语句

yield语句在C#中用于实现迭代器,该语句使一个方法、get访问器或operator返回一个序列的元素而不必创建临时的集合。这对于在需要延迟执行或处理大数据集合时非常有用,因为它允许按需生成序列中的元素。

  • 基本概念

    • 迭代器: 一个对象,它实现了IEnumerableIEnumerator接口,提供一种逐一访问序列中元素的方法,而无需暴露底层表示。
    • 延迟执行: yield语句使得方法的执行在每次返回一个元素后可以暂停,并在下一次需要元素时恢复。这种特性被称为延迟执行。
  • 基本用法 yield returnyield break

    • yield return: 用于返回一个序列中的下一个元素,并暂停方法的执行,直到序列中的下一个元素被请求。
    • yield break: 用于终止迭代,立即退出迭代器块。
using System;
using System.Collections.Generic;

public class FibonacciGenerator
{
public static IEnumerable<int> GenerateFibonacci(int maxTermCount)
{
int previous = 0;
int current = 1;
int count = 0;

while (count < maxTermCount)
{
yield return previous;

int next = previous + current;
previous = current;
current = next;
count++;
}
}
}

public class Program
{
public static void Main()
{
Console.WriteLine("Fibonacci Sequence (first 10 terms):");
foreach (int number in FibonacciGenerator.GenerateFibonacci(10))
{
Console.WriteLine(number);
}
}
}
  • yield的优点
    • 简化代码: 使用yield可以轻松地创建返回多个值的迭代器,而无需创建临时集合或手动实现 IEnumerableIEnumerator接口。
    • 节省内存: 由于yield语句支持延迟执行,它只在需要时生成元素,避免了一次性加载所有数据,节省了内存。
    • 提高性能: 在处理大数据集或耗时的计算时,yield可以提高性能,因为它允许在序列中逐一访问元素,而不是一次性生成所有元素。

37. async 关键字

async关键字在C#中用于标识一个方法、lambda表达式或匿名方法是异步的。异步方法允许你在不阻塞调用线程的情况下执行长时间运行的操作(如I/O操作、网络请求等)。这种机制使得应用程序可以保持响应,而不需要为每个长时间运行的任务创建新线程。

  • 基本概念
    • 异步方法: 使用async关键字修饰的方法,它可以包含一个或多个await表达式。当异步方法执行到 await表达式时,它会异步地等待操作完成,而不阻塞当前线程。
    • 返回类型: 异步方法通常返回TaskTask<T>对象,表示一个异步操作的结果。如果方法没有返回值,则返回Task;如果方法有返回值,则返回Task<T>,其中T是返回值的类型。
using System;
using System.Net.Http;
using System.Threading.Tasks;

public class Program
{
public static async Task Main(string[] args)
{
string url = "https://jsonplaceholder.typicode.com/posts";
string result = await FetchDataAsync(url);
Console.WriteLine(result);
}

public static async Task<string> FetchDataAsync(string url)
{
using (HttpClient client = new HttpClient())
{
HttpResponseMessage response = await client.GetAsync(url);
response.EnsureSuccessStatusCode();
string responseData = await response.Content.ReadAsStringAsync();
return responseData;
}
}
}
  • 重要注意事项
    • 异步所有: 异步方法内部调用的所有可能阻塞的操作也应尽可能使用异步版本。
    • 错误处理: 在异步方法中,可以像处理同步代码中的异常一样使用try-catch进行异常处理。未处理的异常会在返回的Task对象上引发。

38. lambda 表达式

Lambda表达式是C#中的一种匿名函数,它可以包含表达式或语句块,并且可以用来创建委托或表达式树类型。Lambda表达式提供了一种简洁的语法,用于定义内联的函数或将函数作为参数传递给方法。Lambda表达式通常用于LINQ查询、事件处理程序和函数式编程。

  • 使用单行表达式的Lambda表达式
Func<int, int, int> multiply = (x, y) => x * y;
Console.WriteLine(multiply(2, 3)); // 输出: 6

在这个示例中,multiply是一个Func<int, int, int>类型的委托,它接受两个int参数并返回一个 intLambda表达式(x, y) => x * y定义了这个委托的实现。

  • 使用语句块的Lambda表达式
Action<string> greet = name =>
{
string greeting = $"Hello, {name}!";
Console.WriteLine(greeting);
};
greet("World"); // 输出: Hello, World!

在这个示例中,greet是一个Action<string>类型的委托,它接受一个string参数并返回voidLambda表达式name => { ... }使用了语句块来执行多行操作。

  • 无参数的Lambda表达式
Func<int> getRandomNumber = () => new Random().Next(1, 100);
Console.WriteLine(getRandomNumber()); // 输出一个随机数

在这个示例中,getRandomNumber是一个Func<int>类型的委托,它不接受任何参数并返回一个intLambda表达式() => new Random().Next(1, 100)定义了一个没有参数的函数。

  • 单个参数的简写形式
Func<int, int> square = x => x * x;
Console.WriteLine(square(5)); // 输出: 25

在这个示例中,square是一个Func<int, int>类型的委托,它接受一个int参数并返回一个intLambda表达式x => x * x省略了参数类型和括号。

  • LINQ查询
    • Lambda表达式广泛用于LINQ查询中,作为查询表达式的条件和投影。
var numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.Where(n => n % 2 == 0);
foreach (var number in evenNumbers)
{
Console.WriteLine(number); // 输出: 2, 4
}
  • 委托和事件处理
    • Lambda表达式可以用来简洁地定义委托和事件处理程序。
Button btn = new Button();
btn.Click += (sender, e) => Console.WriteLine("Button clicked!");

39. 匿名方法

匿名方法Anonymous MethodsC#中的一种定义委托实例的方式,它允许你在代码中直接声明一个没有名称的方法。匿名方法提供了一种简洁的方式来创建内联的委托,而不必显式地定义一个单独的方法。

  • 匿名方法使用delegate关键字,并且可以包含参数和方法体。
using System;

public class Program
{
public delegate void DisplayMessage(string message);

public static void Main()
{
// 使用匿名方法定义委托实例
DisplayMessage display = delegate(string message)
{
Console.WriteLine(message);
};

// 调用匿名方法
display("Hello, World!");
}
}
  • 匿名方法和Lambda表达式比较
// 匿名方法
Func<int, int> square1 = delegate(int x) { return x * x; };

// Lambda 表达式
Func<int, int> square2 = x => x * x;
  • 何时使用匿名方法
    • 复杂逻辑: 当方法体包含复杂逻辑或多行代码时,可以选择使用匿名方法。
    • 捕获多个变量: 需要捕获多个外部变量或参数时,匿名方法可以提供更清晰的语法结构。

40. 泛型函数

泛型函数Generic Functions是指可以接受不同类型参数的函数。C#中的泛型函数通过在函数的定义中使用类型参数,使得函数可以独立于具体的数据类型来编写。在调用泛型函数时,具体的类型会被替换为实际使用的类型。这样,泛型函数可以在不重复编写代码的情况下适应多种数据类型。

  • 定义泛型函数
    • C#中,泛型函数通过在方法名称后使用尖括号<T>形式的类型参数来定义,其中T是类型参数的占位符。可以有多个类型参数,例如<T, U>等。
public class Program
{
public static void Main()
{
Print<int>(123); // 输出: 123
Print<string>("Hello"); // 输出: Hello
Print<double>(3.14); // 输出: 3.14
}

public static void Print<T>(T value)
{
Console.WriteLine(value);
}
}
  • 多个类型参数
    • 泛型函数可以有多个类型参数,使用逗号分隔。
public static void Swap<T, U>(ref T first, ref U second)
{
T temp = first;
first = (T)(object)second;
second = (U)(object)temp;
}

在这个示例中,Swap方法使用了两个类型参数TU,可以交换不同类型的变量。

  • 类型约束
    • 可以对泛型类型参数施加约束,以限制可用的类型。例如,限制类型参数必须实现某个接口或继承自某个类。
public static void Display<T>(T value) where T : IComparable<T>
{
Console.WriteLine(value);
}

在这个示例中,Display<T>方法只能接受实现了IComparable<T>接口的类型。

  • 泛型函数的优点
    • 代码重用: 泛型函数允许编写通用的代码,可以与不同的数据类型一起使用,而无需为每种类型编写重复的代码。
    • 类型安全: 使用泛型可以在编译时检查类型一致性,避免运行时错误。
    • 性能: 泛型函数不会像非泛型集合类那样引入装箱和拆箱操作(对于值类型),因此可以提高性能。

41. LINQ (语言集成查询)

LINQ(语言集成查询)是C#提供的一种功能,使得开发者可以使用类似SQL的语法来查询和操作各种数据源,如对象集合、数据库、XML等。LINQ提供了一种统一的查询语法和机制,使得数据查询和操作更加简洁、类型安全和可维护。

  • LINQ的基本概念

    • 统一的查询语法: 无论数据来源是什么,LINQ提供了统一的查询语法,简化了数据访问和操作的过程。
    • 延迟执行: LINQ查询通常是延迟执行的,即只有在查询结果被真正访问时,才会执行查询。这可以提高性能,尤其是在处理大型数据集时。
  • LINQ查询语法

    • 查询语法类似SQL,是一种声明式的语法,描述我们希望从数据源中提取什么样的数据。
int[] numbers = { 5, 10, 8, 3, 6, 12 };
var numQuery = from num in numbers
where num % 2 == 0
orderby num
select num;

foreach (int num in numQuery)
{
Console.WriteLine(num);
}
  • LINQ方法语法
    • 方法语法使用标准查询运算符方法,这些方法是静态方法,可以像链条一样连接起来。
int[] numbers = { 5, 10, 8, 3, 6, 12 };
var numQuery = numbers.Where(num => num % 2 == 0)
.OrderBy(num => num)
.Select(num => num);

foreach (int num in numQuery)
{
Console.WriteLine(num);
}
  • LINQ的核心概念之一就是它可以作用于实现了IEnumerable<T>接口的任何数据源。
using System;
using System.Collections.Generic;
using System.Linq;

public class Program
{
public static void Main()
{
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

// 使用 LINQ 查询
IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0);

// 枚举查询结果
foreach (int num in evenNumbers)
{
Console.WriteLine(num); // 输出: 2, 4
}
}
}

总而言之,LINQC#中的一个强大功能,它提供了一个统一的方式来查询和操作各种数据源。通过 LINQ,开发者可以使用简单、清晰且类型安全的代码来执行复杂的数据操作。


附录

文件还未上传 Github