Visual Studio中快捷键

F12:转到定义。

Alt+F12:查看定义。

Ctrl+F:查找。

Ctrl+K和Ctrl+F:格式化选中的代码。

Ctrl+K和Ctrl+S:封装一块区域。

Ctrl+K和Ctrl+C:注释代码。

Ctrl+K和Ctrl+U:取消注释。

Shift+方向键:向各个方向选中。

双TAB:补全代码。

Ctrl+TAB:活动视图。

Visual Studio调试

  • F5:开始或继续调试(如果调试过程中已暂停,则恢复执行)。
  • Ctrl+F5:直接运行程序(不进行调试)。
  • F9:设置/删除断点。
  • F10:逐过程调试(跳过函数调用,执行当前行后跳到下一行代码)。
  • F11:逐语句调试(进入方法内部,逐行执行代码)。
  • Shift+F11:退出当前方法,返回到调用方法的位置。
  • Shift+F5:结束调试
  • Ctrl+Alt+Q:查看局部变量。
  • 可以使用鼠标拖动跳过语句

编码命名规范

1. 类和结构体的命名

类和结构体的名称应该使用 PascalCase(首字母大写,后续每个单词的首字母也大写)

/// <summary>
/// 代表一个员工的类
/// </summary>
public class Employee
{
    // 类的实现...
}

2. 方法和属性的命名

方法和属性名称也应该使用 PascalCase,通常使用动词或动词短语来命名方法,名词或名词短语来命名属性。

public class Person
{
    private string firstName;
    private string lastName;

    /// <summary>
    /// 获取全名
    /// </summary>
    public string FullName
    {
        get { return $"{firstName} {lastName}"; }
    }

    /// <summary>
    /// 打印个人信息的方法
    /// </summary>
    public void PrintFullName()
    {
        Console.WriteLine(FullName);
    }
}

3. 字段的命名

字段(尤其是私有字段)通常采用 camelCase(首字母小写),并且在字段前加上一个下划线_以区别于属性或局部变量。

public class Employee
{
    // 私有字段使用 camelCase,并加上下划线
    private string _firstName;
    private string _lastName;

    // 公开的属性使用 PascalCase
    public string FirstName
    {
        get { return _firstName; }
        set { _firstName = value; }
    }
}

4. 常量和静态字段的命名

常量(const)和静态只读字段(readonly)通常采用 全部大写字母,多个单词之间使用下划线分隔。

public class MathConstants
{
    // 常量采用全大写字母,单词之间用下划线分隔
    public const double PI = 3.14159;
    public const double E = 2.71828;
}

5. 接口的命名

接口的名称通常以字母 I 开头,并采用 PascalCase。接口名称通常是名词或名词短语,表示实现该接口的对象应该具有的功能。

/// <summary>
/// 计算税金的接口
/// </summary>
public interface ITaxCalculator
{
    /// <summary>
    /// 计算税金的方法
    /// </summary>
    /// <param name="amount">金额</param>
    /// <returns>返回计算后的税金</returns>
    double CalculateTax(double amount);
}

6. 事件的命名

事件名称通常使用 PascalCase

public class Button
{
    // 事件命名规则:使用 PascalCase,通常以 Event 结尾
    public event EventHandler ButtonClicked;

    public void Click()
    {
        // 触发事件
        ButtonClicked?.Invoke(this, EventArgs.Empty);
    }
}

7. 局部变量的命名

局部变量的命名通常使用 camelCase,与字段和属性区分开来。变量名应该简洁且有意义,通常为名词。

public class Calculator
{
    public double Add(double firstNumber, double secondNumber)
    {
        // 局部变量采用 camelCase
        double result = firstNumber + secondNumber;
        return result;
    }
}

8. 命名空间的命名

命名空间的命名通常使用 PascalCase,并以公司或组织的名称为前缀,确保全局唯一性。

namespace MyCompany.MyProject.Utilities
{
    public class Logger
    {
        // 类实现...
    }
}

9. 类型参数的命名

对于泛型类型参数,C# 推荐使用单个字母作为类型参数名,

public class Box<T>
{
    public T Content { get; set; }
}

 整理代码方式

预编译执行 Region

通过编辑器(如 Visual Studio)折叠这些代码块,从而减少不相关部分的可视化干扰。

  • 作用:代码 分成一个区域,方便折叠和展开

  • 区域首部:#region

  • 区域尾部:#endregion

public class Person
{
    #region Fields
    private string firstName;
    private string lastName;
    #endregion

    #region Constructors
    public Person(string firstName, string lastName)
    {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    #endregion

    #region Properties
    public string FullName => $"{firstName} {lastName}";
    #endregion

    #region Methods
    public void PrintFullName()
    {
        Console.WriteLine(FullName);
    }
    #endregion
}

使用Header注释,可以在UNITY中分类 类的字段

 [Header("基本参数")]
 public float normalSpeed;
 public float chasespeed;

使用 Partial 类和方法

partial 关键字可以让你将一个类的定义分散到多个文件中,方便组织和管理大型类。当类文件过大时,partial 类可以将其分割为多个部分,这样每部分专注于不同的功能或领域。

// Person.Part1.cs
public partial class Person
{
    private string firstName;
    private string lastName;

    public Person(string firstName, string lastName)
    {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

// Person.Part2.cs
public partial class Person
{
    public string FullName => $"{firstName} {lastName}";
}

枚举

  • 枚举(enum) 是一种值类型,它允许你为一组相关的常数赋予名字。使用枚举可以提高代码的可读性和维护性,尤其是在需要表示一组固定值(如星期几、状态码等)时。
enum Day
{
    Sunday,    // 默认为 0
    Monday,    // 默认为 1
    Tuesday,   // 默认为 2
    Wednesday, // 默认为 3
    Thursday,  // 默认为 4
    Friday,    // 默认为 5
    Saturday   // 默认为 6
}
  • enum 关键字用于声明枚举类型。
  • 枚举成员(例如 Value1Value2 等)自动从 0 开始编号,依次递增

枚举的常用操作

  1. 将枚举转换为整数值

Day today = Day.Monday;
int dayValue = (int)today;  // 将枚举转换为整数
Console.WriteLine(dayValue);  // 输出 1

    2.将整数值转换为枚举成员

int dayValue = 3;
Day day = (Day)dayValue;  // 将整数转换为枚举值
Console.WriteLine(day);  // 输出 Wednesday

    3.枚举的 ToString 方法

Day today = Day.Friday;
Console.WriteLine(today.ToString());  // 输出 "Friday"

   4.枚举的 GetValues 方法

foreach (Day day in Enum.GetValues(typeof(Day)))
{
    Console.WriteLine(day);  // 输出所有枚举成员:Sunday, Monday, ..., Saturday
}

//Enum.GetValues(typeof(DaysOfWeek)) 会返回一个包含 DaysOfWeek 枚举中所有值的数组,
//结果是一个 Array 类型的对象

/*foreach (DaysOfWeek day in Enum.GetValues(typeof(DaysOfWeek)))
foreach 是 C# 中用于遍历集合的语法,它将自动迭代集合中的每个元素。
DaysOfWeek day 是每次循环时使用的变量,
它表示 Enum.GetValues(typeof(DaysOfWeek)) 返回的每个元素。
在本例中,day 会依次赋值为 DaysOfWeek 枚举中的每个枚举值(如 Sunday, Monday 等)。

 typeof 是一个关键字,用于获取类型的 Type 对象。它返回一个表示指定类型的 Type 实例

typeof 只能与类型名称一起使用,不能用于对象实例。

1.获取类型的 Type 实例
你可以使用 typeof 获取类型的 Type 对象,这样可以在运行时查看类型的详细信息。

using System;

public class MyClass
{
    public void Display()
    {
        Console.WriteLine("Hello, World!");
    }
}

public class Program
{
    public static void Main()
    {
        Type type = typeof(MyClass); // 获取 MyClass 类型的 Type 实例
        Console.WriteLine(type); // 输出:MyNamespace.MyClass
    }
}

与反射结合使用
通过 Type 对象,你可以使用反射来查看类的成员、调用方法、获取属性等。

与泛型一起使用
在泛型方法中,typeof 可以用于获取泛型类型的 Type 对象。

5.枚举与 Flags 特性
[Flags] 特性通常用于枚举值需要组合的场景,比如权限管理、状态管理等。[Flags] 使得枚举的每个值都可以看作是一个独立的二进制位,通过按位运算可以组合多个值。

using System;

[Flags]
enum DAY
{
    value1 = 1,   // 0001
    value2 = 2,   // 0010
    value3 = 4,   // 0100
    value4 = 8,   // 1000
    value5 = 16   // 10000
}

class Program
{
    static void Main()
    {
        DAY day3 = DAY.value1 | DAY.value2;  // 组合 value1 和 value2
        Console.WriteLine(day3);  // 输出: value1, value2
    }
}


/*value1 的值是 1(二进制 0001)
value2 的值是 2(二进制 0010)
value3 的值是 4(二进制 0100)
value4 的值是 8(二进制 1000)
value5 的值是 16(二进制 10000)
这样,使用按位或(|)运算符组合 value1 和 value2 会得到 3(0001 | 0010 = 0011),ToString() 方法会根据 [Flags] 特性,正确显示 value1, value2。

面向对象

在 C# 中,类(Class)和对象(Object)是面向对象编程(OOP)的核心概念。类是对象的模板或蓝图,而对象则是类的实例。让我们详细了解 C# 中的类与对象。

1. 类(Class)

类是一个定义了对象属性(字段)和行为(方法)的数据结构,它描述了一组对象共有的特征和操作。类本身并不占用内存空间,只有通过创建对象(实例)时,内存才会为该对象分配空间。

类的基本语法

class ClassNamec
{
    // 字段(成员变量)
    public int field;

    // 属性(属性是字段的封装)
    public string Property { get; set; }

    // 带参数的构造函数
    public ClassName(int field)
    {
        this.field = field;
    }
    //也可以有不带参数的构造函数

    // 方法(行为)
    public void Display()
    {
        Console.WriteLine("Field value: " + field);
    }
}

2. 对象(Object)

对象是类的实例。通过类的构造函数,可以创建一个类的对象。每个对象都有自己的实例数据(字段值),并且可以调用类的方法。

ClassName objectName = new ClassName(10); // 创建对象并初始化
objectName.Display();  // 调用对象的方法

3. 类与对象的示例

using System;

class Car
{
    // 字段(成员变量)
    public string make;
    public string model;
    public int year;

    // 构造函数
    public Car(string make, string model, int year)
    {
        this.make = make;
        this.model = model;
        this.year = year;
    }

    // 方法(行为)
    public void StartEngine()
    {
        Console.WriteLine("The engine of the " + year + " " + make + " " + model + " is started.");
    }

    // 属性
    public string Description
    {
        get { return year + " " + make + " " + model; }
    }
}

class Program
{
    static void Main()
    {
        // 创建 Car 类的对象
        Car myCar = new Car("Toyota", "Corolla", 2020);

        // 调用对象的方法
        myCar.StartEngine();

        // 访问对象的属性
        Console.WriteLine("Car description: " + myCar.Description);
    }
}


//The engine of the 2020 Toyota Corolla is started.
//Car description: 2020 Toyota Corolla

4. 类的成员

类的成员包括字段属性方法构造函数事件索引器等。

字段(Field)

字段是类的数据成员,通常用于存储对象的状态。

public int age;  // 字段

构造函数(Constructor)

构造函数是创建类对象时自动调用的特殊方法。构造函数用于初始化对象的状态。

public Person(string name, int age)  // 构造函数
{
    this.name = name;
    this.age = age;
}

静态方法和静态构造函数有所不同。静态构造函数用于初始化类的静态成员,并且只在类的首次使用时调用一次。静态构造函数没有访问修饰符,且不能带参数。

public class MyClass
{
    static int staticValue;

    // 静态构造函数
    static MyClass()
    {
        staticValue = 100;
        Console.WriteLine("静态构造函数执行");
    }

    public static void ShowStaticValue()
    {
        Console.WriteLine("静态值: " + staticValue);
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 静态构造函数会在第一次使用类时自动调用
        MyClass.ShowStaticValue();
    }
}

方法(Method)

方法是定义在类中的函数,用来定义类的行为。

public void SayHello()
{
    Console.WriteLine("Hello!");
}

静态方法 
在c#中静态函数(或静态方法)是与类本身关联的,而不是与类的实例关联。静态方法不能访问类的实例成员,只能访问类的静态成员。静态方法通常用于执行不依赖于对象实例的操作,或为类提供通用的工具方法。

静态方法通过在方法声明前使用 static 关键字来定义。例如:

public class MyClass
{
    // 静态方法
    public static void MyStaticMethod()
    {
        Console.WriteLine("这是一个静态方法");
    }

    // 非静态方法
    public void MyInstanceMethod()
    {
        Console.WriteLine("这是一个实例方法");
    }
}

class Program
{
    static void Main(string[] args)
    {
        // 调用静态方法
        MyClass.MyStaticMethod();

        // 可以创建对象实例来调用非静态方法
        MyClass myObject = new MyClass();
        myObject.MyInstanceMethod();
    }
}
  1. 与实例无关:静态方法只能访问类的静态字段和静态属性,不能访问实例字段或实例属性
  2. 只能访问静态成员:静态方法内部无法直接使用非静态成员(除非通过实例化类)。
  3. 类级别的共享:静态方法属于类本身,而不是类的实例。所有类的实例共享同一个静态方法。

  

属性(Property)

属性用于封装类中的字段,允许在访问字段时执行额外的代码(如数据验证或计算)。

public string Name  // 属性
{
    get { return name; }
    set { name = value; }
}

 属性的基本概念

  • get 方法用于获取(读取)属性的值。
  • set 方法用于设置(写入)属性的值。
public string Property { get; set; }

/*public:这是访问修饰符,表示该属性是公开的,任何类外部的代码都可以访问它。
string:这是属性的数据类型,表示这个属性的值是一个字符串。
Property:这是属性的名称,你可以通过它来访问或修改该属性的值。
{ get; set; }:这是属性的 自动实现 语法。C# 会自动生成一个隐式的字段来存储这个属性的值。
get:提供获取属性值的方法。
set:提供设置属性值的方法。set 的 value 代表外部传入的新值。
3. 自动实现属性如何工作:
当你定义这个属性时,编译器会自动生成一个私有的字段(通常名字是 <Property>k__BackingField)来存储该属性的值。你不需要手动写这个字段或 get、set 的具体实现。

实际上,这段代码等价于下面的写法:

*/
private string _property;  // 私有字段

public string Property
{
    get
    {
        return _property;  // 返回字段值
    }
    set
    {
        _property = value;  // 设置字段值
    }
}

5.封装

封装(Encapsulation)指的是将对象的状态(字段)和行为(方法)封装在一起,并对外界提供访问这些字段和方法的途径。通过封装,类的内部实现细节被隐藏,外界只能通过公开的方法(即公共接口)与对象交互。这可以有效地保护数据不被直接修改,避免外部代码直接访问类的内部数据结构。

1. 访问修饰符

在C#中,访问修饰符用于控制类成员的访问权限。最常用的修饰符包括:

  • private:表示成员仅能在当前类中访问。
  • public:表示成员可以在任何地方访问。
  • protected:表示成员只能在当前类和派生类中访问。
  • internal:表示成员只能在当前程序集内访问。

2.通过属性进行封装

C# 提供了属性(Property)机制,简化了对字段的封装。属性通常结合 getset 访问器来控制对字段的访问。

public class BankAccount
{
    private decimal balance;

    // 使用属性封装余额
    public decimal Balance
    {
        get { return balance; }
        private set { balance = value; }  // 通过私有的 set 防止外部直接修改余额
    }

    // 公共方法:提供存款操作
    public void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            Balance += amount;
        }
        else
        {
            Console.WriteLine("Deposit amount must be positive.");
        }
    }

    // 公共方法:提供取款操作
    public void Withdraw(decimal amount)
    {
        if (amount > 0 && amount <= Balance)
        {
            Balance -= amount;
        }
        else
        {
            Console.WriteLine("Insufficient balance or invalid amount.");
        }
    }
}

class Program
{
    static void Main()
    {
        BankAccount account = new BankAccount();
        
        account.Deposit(1000);  // 存款 1000
        Console.WriteLine($"Current balance: {account.Balance}");  // 输出: Current balance: 1000

        account.Withdraw(500);  // 取款 500
        Console.WriteLine($"Current balance: {account.Balance}");  // 输出: Current balance: 500

        account.Withdraw(600);  // 尝试取款超过余额
        Console.WriteLine($"Current balance: {account.Balance}");  // 输出: Current balance: 500
    }
}

6.继承

继承(Inheritance),它允许一个类继承另一个类的成员(字段、属性、方法等),从而实现代码的重用和扩展。继承可以让子类获得父类的行为和属性,同时还可以覆盖(override)父类的方法,或者增加子类特有的成员。

父类(基类):被继承的类。包含被子类继承的成员。

子类(派生类):继承父类的类,可以访问父类的公有和保护成员,并可以重写父类的方法或扩展新的功能。

1. 基本继承语法

在 C# 中,继承通过 : 符号实现。子类继承父类时,可以访问父类的公共(public)和受保护(protected)成员。

// 父类(基类)
public class Animal
{
    public string Name { get; set; }

    public void Eat()
    {
        Console.WriteLine("This animal is eating.");
    }
}

// 子类(派生类)
public class Dog : Animal
{
    public void Bark()
    {
        Console.WriteLine("The dog is barking.");
    }
}

2. 继承和方法重写 

C# 支持方法的重写(Override)。在子类中,可以重写父类的方法来改变其实现。为了实现方法重写,父类的方法需要使用 virtual 修饰符,子类中的方法使用 override 关键字。

// 父类(基类)
public class Animal
{
    public string Name { get; set; }

    // 允许被重写的方法
    public virtual void Speak()
    {
        Console.WriteLine("The animal makes a sound.");
    }
}

// 子类(派生类)
public class Dog : Animal
{
    // 重写父类的 Speak 方法
    public override void Speak()
    {
        Console.WriteLine("The dog barks.");
    }
//在重写中加入 base.speak();代表添加父类功能而不是重写
}

public class Program
{
    public static void Main()
    {
        Animal animal = new Animal();
        animal.Speak();  // 输出: The animal makes a sound.

        Dog dog = new Dog();
        dog.Speak();     // 输出: The dog barks.
    }
}

 3. 构造函数与继承

子类会继承父类的字段和方法,但父类的构造函数并不会被自动继承。子类需要显式地调用父类的构造函数,可以通过 base 关键字来实现。

// 父类(基类)
public class Animal
{
    public string Name { get; set; }

    // 父类构造函数
    public Animal(string name)
    {
        Name = name;
        Console.WriteLine("Animal constructor called.");
    }
}

// 子类(派生类)
public class Dog : Animal
{
    // 子类构造函数
    public Dog(string name) : base(name)  // 显式调用父类构造函数
    {
        Console.WriteLine("Dog constructor called.");
    }
}

public class Program
{
    public static void Main()
    {
        Dog dog = new Dog("Buddy");
        // 输出:
        // Animal constructor called.
        // Dog constructor called.
    }
}

 4.隐藏成员(方法隐藏)

如果子类定义了与父类相同名字的方法(但没有使用 override 关键字),那么父类的方法将被隐藏,子类方法将覆盖父类的方法,除非显式调用父类方法。

// 父类(基类)
public class Animal
{
    public void Speak()
    {
        Console.WriteLine("The animal makes a sound.");
    }
}

// 子类(派生类)
public class Dog : Animal
{
    // 隐藏父类的 Speak 方法
    public new void Speak()
    {
        Console.WriteLine("The dog barks.");
    }
}

public class Program
{
    public static void Main()
    {
        Animal animal = new Animal();
        animal.Speak();  // 输出: The animal makes a sound.

        Dog dog = new Dog();
        dog.Speak();     // 输出: The dog barks.

        Animal animalDog = new Dog();
        animalDog.Speak();  // 输出: The animal makes a sound. (因为调用的是父类的方法)
    }
}

 父类可以储存子类,但是子类不能存储父类.

7.抽象类

抽象类abstract class)是一种不能直接实例化的类。抽象类的主要目的是作为其他类的基类,提供共同的接口或实现部分功能,同时要求派生类必须实现某些方法。

  1. 不能直接实例化:你不能创建抽象类的实例,必须通过继承该类并实现所有抽象方法的子类来创建实例。

  2. 可以包含抽象方法:抽象类可以包含抽象方法,这些方法没有实现(仅有方法签名),要求派生类必须实现这些方法。

  3. 可以包含非抽象方法:除了抽象方法,抽象类还可以包含已实现的方法,派生类可以继承并使用这些方法,也可以重写(通过 override)它们。

  4. 可以包含字段、属性和事件:抽象类可以包含字段、属性、事件和构造函数等成员。

  5. 可用于定义共通接口和实现部分功能:抽象类常用于定义多个子类共享的基础功能,同时规定子类需要实现的功能。

using System;

public abstract class Animal
{

    public Animal()
    {
        Console.WriteLine("An animal is created.");
    }
    // 抽象方法,派生类必须实现
    public abstract void MakeSound();

    // 非抽象方法,派生类可以继承
    public void Sleep()
    {
        Console.WriteLine("The animal is sleeping.");
    }
}

public class Dog : Animal
{
    // 实现抽象方法
    public override void MakeSound()
    {
        Console.WriteLine("Woof!");
    }
}

public class Cat : Animal
{
    // 实现抽象方法
    public override void MakeSound()
    {
        Console.WriteLine("Meow!");
    }
}

class Program
{
    static void Main()
    {
        // 不能直接实例化抽象类
        // Animal animal = new Animal();  // 错误:不能实例化抽象类
        
        // 必须通过派生类实例化
        Animal dog = new Dog();
        dog.MakeSound();  // 输出: Woof!
        dog.Sleep();      // 输出: The animal is sleeping.

        Animal cat = new Cat();
        cat.MakeSound();  // 输出: Meow!
        cat.Sleep();      // 输出: The animal is sleeping.
    }
}

8.多态

多态是面向对象编程的核心特性之一,它允许不同的对象以相同的方式被处理,具体的行为依赖于对象的实际类型。在 C# 中,多态主要有两种形式:

1.编译时多态(静态多态):通过方法重载和运算符重载实现。

方法重载(Method Overloading)

方法重载是指在同一个类中,允许多个方法具有相同的名称,但方法的参数列表不同(参数数量或类型不同)。

public class Calculator
{
    // 加法方法,重载
    public int Add(int a, int b)
    {
        return a + b;
    }

    // 加法方法,重载
    public double Add(double a, double b)
    {
        return a + b;
    }

    // 加法方法,重载
    public string Add(string a, string b)
    {
        return a + b;
    }
}

public class Program
{
    public static void Main()
    {
        Calculator calc = new Calculator();
        
        Console.WriteLine(calc.Add(1, 2));            // 输出: 3
        Console.WriteLine(calc.Add(1.5, 2.3));        // 输出: 3.8
        Console.WriteLine(calc.Add("Hello, ", "World!")); // 输出: Hello, World!
    }
}

运算符重载是允许自定义运算符的行为,使得运算符可以用于自定义类型。C# 中可以重载大部分运算符,但不能重载逻辑运算符(如 &&||)和一些其他特定运算符(如 =)。

public class Point
{
    public int X { get; set; }
    public int Y { get; set; }

    // 重载加法运算符
    public static Point operator +(Point p1, Point p2)
    {
        return new Point
        {
            X = p1.X + p2.X,
            Y = p1.Y + p2.Y
        };
    }

    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

public class Program
{
    public static void Main()
    {
        Point p1 = new Point { X = 1, Y = 2 };
        Point p2 = new Point { X = 3, Y = 4 };
        Point p3 = p1 + p2;  // 使用重载的 + 运算符

        Console.WriteLine(p3);  // 输出: (4, 6)
    }
}

2.运行时多态(动态多态):通过方法重写和接口实现。

类的方法重写不再赘述

接口多态实现如下接口章节介绍

9.接口

接口是一种定义类或结构体必须实现的契约。接口只定义成员的签名(如方法、属性、事件、索引器),而没有实现这些成员的具体内容。类或结构体可以实现一个或多个接口,并提供这些成员的具体实现。

接口特点

只能声明方法、属性、事件和索引器:接口中不能包含字段、构造函数或静态成员。接口中的成员通常都是抽象的,要求实现接口的类提供具体的实现。

实现多个接口:C# 支持多接口实现,一个类可以实现多个接口。

接口中的成员默认是公开的:接口中的所有成员默认是 public,并且不能包含访问修饰符。

接口不能包含实现:接口本身不能包含任何方法体。所有方法、属性、事件都只是签名,实际的实现必须在实现接口的类或结构体中提供。

接口的基本语法

using System;

// 定义接口
public interface IAnimal
{
    void Speak();
    void Eat();
}

// 实现接口的类
public class Dog : IAnimal
{
    // 提供接口成员的实现
    public void Speak()
    {
        Console.WriteLine("Woof!");
    }

    public void Eat()
    {
        Console.WriteLine("The dog is eating.");
    }
}

public class Cat : IAnimal
{
    // 提供接口成员的实现
    public void Speak()
    {
        Console.WriteLine("Meow!");
    }

    public void Eat()
    {
        Console.WriteLine("The cat is eating.");
    }
}

class Program
{
    static void Main()
    {
        // 使用接口类型引用对象
        IAnimal dog = new Dog();
        dog.Speak();  // 输出: Woof!
        dog.Eat();    // 输出: The dog is eating.

        IAnimal cat = new Cat();
        cat.Speak();  // 输出: Meow!
        cat.Eat();    // 输出: The cat is eating.
    }
}

多接口实现

一个类可以实现多个接口。如果多个接口有相同的方法签名,类需要提供单一的实现。

public interface IAnimal
{
    void Eat();
}

public interface IFlyable
{
    void Fly();
}

public class Bird : IAnimal, IFlyable
{
    public void Eat()
    {
        Console.WriteLine("Bird is eating...");
    }

    public void Fly()
    {
        Console.WriteLine("Bird is flying...");
    }
}

 接口可以实现多态,即我们可以通过接口类型来引用不同类的对象,这样在运行时就可以实现动态绑定。接口类型的变量可以指向任何实现了该接口的对象。

public interface IAnimal
{
    void Speak();
}

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

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

public class Program
{
    public static void Main(string[] args)
    {
        IAnimal animal1 = new Dog();
        IAnimal animal2 = new Cat();

        // 通过接口调用不同类的实现
        animal1.Speak();  // 输出: Woof!
        animal2.Speak();  // 输出: Meow!
    }
}

 接口继承

接口本身也可以继承其他接口,这样可以通过接口继承来扩展接口的功能。一个接口可以继承多个接口,这样实现该接口的类必须实现所有继承接口中的成员。

public interface IPlayable
{
    void Play();
}

public interface IRecordable : IPlayable
{
    void Record();
}

public class MediaDevice : IRecordable
{
    public void Play()
    {
        Console.WriteLine("Playing media...");
    }

    public void Record()
    {
        Console.WriteLine("Recording media...");
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        IRecordable device = new MediaDevice();
        device.Play();    // 输出: Playing media...
        device.Record();  // 输出: Recording media...
    }
}

接口与抽象类的比较

相同点

都不能实例化

无论是接口还是抽象类,都不能直接实例化对象(即不能使用 new 关键字创建它们的实例)。必须通过继承(抽象类)或实现(接口)来创建对象。

都可以包含抽象成员

抽象类中的方法或属性可以没有实现(即抽象的),接口中的方法默认是没有实现的,因此两者都要求子类或实现类提供具体的实现。

都支持多态

无论是接口还是抽象类,都可以用于实现多态。你可以使用父类型(接口或抽象类)来引用子类的对象,并通过方法调用实现不同的行为。

 不同点

// 使用接口
public interface IDrinkable
{
    void Drink();
}

public interface IEatable
{
    void Eat();
}

public class PersonWithInterface : IDrinkable, IEatable
{
    public void Drink()
    {
        Console.WriteLine("Drinking...");
    }

    public void Eat()
    {
        Console.WriteLine("Eating...");
    }
}

// 使用抽象类
public abstract class Animal
{
    public abstract void MakeSound();
    
    public void Sleep()
    {
        Console.WriteLine("Sleeping...");
    }
}

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

public class Program
{
    public static void Main()
    {
        // 接口实现
        var person = new PersonWithInterface();
        person.Drink(); // 输出: Drinking...
        person.Eat();   // 输出: Eating...

        // 抽象类实现
        Animal dog = new Dog();
        dog.MakeSound(); // 输出: Bark!
        dog.Sleep();     // 输出: Sleeping...
    }
}

何时使用接口,何时使用抽象类?

使用接口

当你希望定义多个类共享同样的行为,但不关心它们的内部实现时。

当你希望实现多继承时(一个类可以实现多个接口)。

当你想要提供一种松耦合的设计,使类之间的关系更灵活时。

使用抽象类

当你希望为多个类提供共享的实现代码时(例如,多个类共享一个基类的实现)。

当你需要定义一个类的基础行为,并要求子类在继承时实现特定的功能时。

当你需要控制某些成员的访问修饰符时(例如保护方法)。

 结构体

 结构体(struct 是一种值类型

结构体通常用于表示小的数据结构,通常是一些相关的数值数据,它们是值类型而不是引用类型。结构体的特点使得它在某些场景下比类更为高效,尤其是当你需要一个简单的、不需要继承和复杂行为的数据类型时。

1. 结构体的定义

结构体通过 struct 关键字来定义。例如:

public struct Point
{
    public int X;
    public int Y;

    // 构造函数
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 方法
    public void Print()
    {
        Console.WriteLine($"Point: ({X}, {Y})");
    }
}

2. 创建结构体实例

结构体是值类型,因此它的实例直接存储数据。你可以通过直接创建实例或使用构造函数来初始化结构体。

class Program
{
    static void Main()
    {
        // 直接创建结构体实例
        Point p1 = new Point(10, 20);
        p1.Print();  // 输出: Point: (10, 20)

        // 使用默认构造函数,所有字段默认为 0
        Point p2 = new Point();
        p2.Print();  // 输出: Point: (0, 0)
    }
}

3. 结构体的特点

  1. 值类型(Value Type):结构体是值类型,这意味着它们在赋值给其他变量时会被复制,而不是引用相同的内存位置。因此,当结构体实例被赋值或传递给方法时,操作的是副本,而不是原始对象。

    Point p1 = new Point(10, 20);
    Point p2 = p1;  // p2 是 p1 的副本
    p2.X = 30;
    Console.WriteLine(p1.X);  // 输出: 10, p1.X 不会受到 p2 修改的影响
    
  2. 没有继承:结构体不能继承自其他类或结构体(即它们不能是派生类型),但它们可以实现接口。

    public struct Point : IComparable
    {
        public int X;
        public int Y;
    
        public int CompareTo(object obj)
        {
            if (obj is Point)
            {
                var other = (Point)obj;
                return X.CompareTo(other.X);
            }
            return 0;
        }
    }
    
  3. 不能定义默认构造函数:结构体不能定义无参数的构造函数。每个结构体都有一个默认构造函数,它会将所有字段初始化为默认值(如 0falsenull 等)。在 C# 9.0 或更高版本下,编写以下代码并在 VS2022 中运行,不会出现编译错误:

    public struct MyStruct
    {
        // 编译错误:不能定义无参数构造函数
        // public MyStruct() { }
    }
    
  4. 性能优化:由于结构体是值类型,它们通常比类更轻量。在内存中,结构体的存储方式更高效(尤其是小结构体),因为它们直接存储数据而没有额外的堆分配和垃圾回收负担。但是,过大的结构体会增加复制的开销,因此需要根据实际情况权衡。

  5. 不可为 null:结构体是值类型,不能为 null,除非使用 可空类型(Nullable Types)。可以通过 Nullable<T>T? 来使结构体变为可空类型。

    Point? p = null;  // Point? 是可空类型,可以为 null
    
  6. 结构体的默认值:结构体的所有字段都将被默认初始化为默认值(数值为 0,布尔类型为 false,引用类型为 null 等)。

    public struct MyStruct
    {
        public int number;     // 默认为 0
        public bool flag;      // 默认为 false
    }
    
    MyStruct s = new MyStruct();  // s.number 为 0,s.flag 为 false
    

4.结构体与类的区别

Class 在创建实例(new)的时候,是在堆中分配内存,然后返回的是那片内存的地址
Struct 在创建实例 在栈中分配,引用变量 存的是地址,值变量存的是直接的数据

如果想要让Struct 也能被函数内的修改,使用ref

泛型

泛型它允许你在定义类、接口、方法和委托时使用类型参数,而不必指定具体的类型。泛型使得代码更加灵活、类型安全,并且能够避免类型转换带来的错误。

泛型类

public class MyGenericClass<T>
{
    private T _value;

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

    public T GetValue()
    {
        return _value;
    }
}

public class Program
{
    public static void Main()
    {
        MyGenericClass<int> intObject = new MyGenericClass<int>(5);
        Console.WriteLine(intObject.GetValue()); // 输出: 5

        MyGenericClass<string> stringObject = new MyGenericClass<string>("Hello");
        Console.WriteLine(stringObject.GetValue()); // 输出: Hello
    }
}

 泛型方法

public class Program
{
    public static void Print<T>(T value)
    {
        Console.WriteLine(value);
    }

    public static void Main()
    {
        Print<int>(42);    // 输出: 42
        Print<string>("Hello, Generics!");  // 输出: Hello, Generics!
    }
}

 泛型接口

public interface IComparer<T>
{
    int Compare(T x, T y);
}

public class IntComparer : IComparer<int>
{
    public int Compare(int x, int y)
    {
        return x.CompareTo(y);
    }
}

public class Program
{
    public static void Main()
    {
        IComparer<int> comparer = new IntComparer();
        Console.WriteLine(comparer.Compare(3, 4)); // 输出: -1 (3 < 4)
    }
}

泛型约束

泛型允许在类型参数上加上约束(constraint),从而限制可传入的类型范围。常见的约束包括:

where T : struct

struct 约束指定泛型类型参数必须是值类型,这意味着不能传递引用类型(如类或接口)。

public class MyClass<T> where T : struct
{
    // T 必须是值类型
}

where T : class

class 约束指定泛型类型参数必须是引用类型。这意味着不能传递值类型(如 intdouble 等)。

public class MyClass<T> where T : class
{
    // T 必须是引用类型
}

where T : new()

约束 T new() 约束要求泛型类型参数必须具有一个公共无参数构造函数。这对于创建泛型类型的实例非常有用。

public class MyClass<T> where T : new()
{
    public T CreateInstance()
    {
        return new T();  // 必须能够创建 T 类型的实例
    }
}

接口约束

 通过接口约束,泛型类型参数必须实现特定的接口。这允许我们调用接口的方法或属性。

public class MyClass<T> where T : IComparable
{
    public void Compare(T obj1, T obj2)
    {
        int result = obj1.CompareTo(obj2);  // T 必须实现 IComparable 接口
    }
}

 类继承约束

public class MyClass<T> where T : BaseClass
{
    // T 必须是 BaseClass 类型或其派生类
}

 多个约束

一个泛型类型参数可以有多个约束。多个约束时,可以通过 , 分隔。

public class MyClass<T> where T : class, IComparable, new()
{
    // T 必须是引用类型、实现 IComparable 接口并具有无参数构造函数
}

容器

1. List<T>

List<T> 是一个动态数组,能够根据需要自动扩展容量。它的特点包括:

按索引访问元素:你可以通过索引快速访问其中的元素。

动态调整大小:当添加元素时,List<T> 会自动调整其容量。

支持多种操作:包括添加、删除、查找、排序、反转等。

创建List

// 创建一个空的 List,元素类型为 int
List<int> list = new List<int>();

// 创建一个 List 并初始化
List<int> listWithValues = new List<int> { 1, 2, 3, 4 };

// 从其他集合创建 List
List<int> listFromArray = new List<int>(new int[] { 1, 2, 3, 4 });

添加元素

List<int> numbers = new List<int>();

// 添加单个元素
numbers.Add(10);
numbers.Add(20);

// 添加多个元素
numbers.AddRange(new int[] { 30, 40, 50 });

插入元素 

List<int> numbers = new List<int> { 10, 20, 30 };

// 在索引位置 1 插入一个元素
numbers.Insert(1, 15); // 结果:10, 15, 20, 30

// 在索引位置 2 插入多个元素
numbers.InsertRange(2, new int[] { 25, 35 }); // 结果:10, 15, 25, 35, 20, 30

 删除元素

List<int> numbers = new List<int> { 10, 20, 30, 40, 50 };

// 删除指定元素
numbers.Remove(30); // 结果:10, 20, 40, 50

// 删除指定索引的元素
numbers.RemoveAt(1); // 结果:10, 40, 50

// 删除满足条件的元素
numbers.RemoveAll(n => n > 20); // 结果:10

//清空列表 
List<int> numbers = new List<int> { 10, 20, 30 };
numbers.Clear(); // 清空所有元素

查找元素

List<int> numbers = new List<int> { 10, 20, 30, 40 };

// 查找元素是否存在
bool contains = numbers.Contains(20); // true

// 查找元素的索引
int index = numbers.IndexOf(30); // 2

// 查找符合条件的元素
int found = numbers.Find(n => n > 25); // 30 (第一个大于 25 的元素)

// 查找所有符合条件的元素
List<int> foundAll = numbers.FindAll(n => n > 15); // 结果:20, 30, 40

访问元素

List<int> numbers = new List<int> { 10, 20, 30, 40 };

// 使用索引访问元素
int first = numbers[0]; // 10
int second = numbers[1]; // 20

// 使用索引修改元素
numbers[2] = 35; // 结果:10, 20, 35, 40

 排序和反转

List<int> numbers = new List<int> { 40, 10, 30, 20 };

// 排序
numbers.Sort(); // 结果:10, 20, 30, 40

// 反转顺序
numbers.Reverse(); // 结果:40, 30, 20, 10

 容量与计数

List<int> numbers = new List<int> { 10, 20, 30 };

// 获取元素数量
int count = numbers.Count; // 3

// 获取当前容量
int capacity = numbers.Capacity; // 4 (默认容量,可能因动态扩展而改变)

遍历 

List<int> numbers = new List<int> { 10, 20, 30 };

// 打印每个元素
numbers.ForEach(n => Console.WriteLine(n));

其它函数

List<int> numbers = new List<int> { 10, 20, 30 };

// 判断是否存在大于 15 的元素
bool exists = numbers.Exists(n => n > 15); // true



List<int> numbers = new List<int> { 10, 20, 30 };

// 查找大于 15 的元素的索引
int index = numbers.FindIndex(n => n > 15); // 1 (20的索引)

2.Dictionary

Dictionary<TKey, TValue> 是 C# 中的一种常用集合类,它属于每个元素都由一个键(key)和一个值(value)组成,键是唯一的,值则可以是任意类型。Dictionary 是一种哈希表(Hash Table)实现,它通过键快速查找对应的值,因此查询性能非常高(大部分情况下是 O(1) 时间复杂度)

字典的使用

using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 1. 创建字典并初始化
        Dictionary<int, string> fruits = new Dictionary<int, string>()
        {
            { 1, "Apple" },
            { 2, "Banana" },
            { 3, "Cherry" }
        };

        // 2. 添加新元素
        fruits.Add(4, "Date");
        Console.WriteLine("Added new item (4, Date)");

        // 3. 修改已有元素
        fruits[1] = "Green Apple";
        Console.WriteLine("Updated item (1, Green Apple)");

        // 4. 查找元素
        if (fruits.TryGetValue(2, out string value))
        {
            Console.WriteLine("Found item with key 2: " + value);
        }
        else
        {
            Console.WriteLine("Key not found.");
        }
        fruits.TryGetValue(2, out string value):

        /*2 是你想查找的键。
        out string value 是一个输出参数,用于存储查找结果。
        out 关键字的作用:

        out 表示 value 参数是一个输出参数,它将在方法内部被赋值。即使 TryGetValue 返回 false                
      (表示未找到该键),value 也会被赋予默认值。在 string 类型中,默认值是 null。
        TryGetValue 的返回值:

        如果字典中包含指定的键(此处是 2),TryGetValue 会返回 true,并将对应的值(例如     
        "Apple")赋给 value。
        如果字典中不包含该键,它会返回 false,并将 value 设置为 string 的默认值(即 null)。*/

        // 5. 删除元素
        if (fruits.Remove(3))
        {
            Console.WriteLine("Removed item with key 3");
        }
        else
        {
            Console.WriteLine("Key 3 not found.");
        }

        // 6. 遍历字典中的所有键值对
        Console.WriteLine("\nAll items in the dictionary:");
        foreach (var pair in fruits)
        {
            Console.WriteLine($"Key: {pair.Key}, Value: {pair.Value}");
        }
        //$ 是 字符串插值 (String Interpolation) 的一个标记,
        //它用于在字符串中嵌入变量的值或者表达式的结果。
        //具体来说,$ 后面的字符串允许你直接在字符串中嵌入变量、属性或任何有效的表达式,
        //而不需要使用 + 来连接字符串。

        // 7. 检查字典中是否包含某个键
        if (fruits.ContainsKey(1))
        {
            Console.WriteLine("Key 1 exists in the dictionary.");
        }

        // 8. 清空字典
        fruits.Clear();
        Console.WriteLine("\nThe dictionary has been cleared.");

        // 9. 输出字典的数量
        Console.WriteLine("Number of items in dictionary: " + fruits.Count);
    }
}
  • 查找性能Dictionary<TKey, TValue> 基于哈希表实现,查找键的时间复杂度是 O(1)(在大多数情况下)。因此,字典在需要频繁查找元素时比列表(List)更高效。
  • 插入性能:插入键值对的时间复杂度也是 O(1)(在大多数情况下)。
  • 删除性能:删除元素的时间复杂度是 O(1)(在大多数情况下)。
  • 键的唯一性Dictionary 中的每个键必须是唯一的,如果尝试插入一个已存在的键,会抛出 ArgumentException
  • 键的哈希码Dictionary 使用键的哈希码来确定元素的存储位置。因此,键类型必须能够提供一个合理的哈希码(通过 GetHashCode 方法)。对于不可变类型(如 stringint 等),哈希码是稳定的,而对于可变类型,如果在字典中存储对象后修改了对象的键值,会导致字典变得不一致,从而引发潜在的问题。

 3.HashSet<T>

  1. 不允许重复元素HashSet<T> 只会存储唯一的元素。如果你尝试添加一个已存在的元素,它会被忽略。
  2. 无序存储:元素在集合中的顺序是不确定的,元素的存储顺序可能与添加顺序不同。
  3. 查找性能优秀:因为使用了哈希算法,所以它的查找、插入和删除操作平均时间复杂度为 O(1),非常高效。
using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 创建一个 HashSet,存储学生的名字
        HashSet<string> students = new HashSet<string>();

        // 添加学生名字
        students.Add("Alice");
        students.Add("Bob");
        students.Add("Charlie");
        students.Add("Diana");

        // 尝试添加重复的名字
        bool added = students.Add("Alice"); // 这个将返回 false,因为 "Alice" 已经在集合中
        Console.WriteLine("Was Alice added again? " + added);  // 输出: Was Alice added again? False

        // 打印集合中的所有学生名字
        Console.WriteLine("Students in the HashSet:");
        foreach (var student in students)
        {
            Console.WriteLine(student);
        }

        // 查找是否存在某个学生
        if (students.Contains("Bob"))
        {
            Console.WriteLine("Bob is in the list.");
        }
        
        // 删除一个学生
        students.Remove("Diana");
        Console.WriteLine("After removing Diana, the students are:");
        foreach (var student in students)
        {
            Console.WriteLine(student);
        }

        // 输出 HashSet 的元素个数
        Console.WriteLine("Number of students in the HashSet: " + students.Count);
    }
}

 4.Stack

Stack栈表示后进先出(LIFO, Last In First Out)的数据结构。它允许我们将元素添加到栈顶,并从栈顶移除元素。常见的使用场景包括:浏览器历史记录、表达式求值、递归问题的迭代实现等。

  • Push():将元素压入栈中。
  • Pop():移除栈顶元素,并返回它。
  • Peek():返回栈顶元素,但不移除它。
using System;
using System.Collections.Generic;

class BrowserHistory
{
    // 使用两个栈来模拟浏览器历史
    private Stack<string> backStack = new Stack<string>();  // 后退栈
    private Stack<string> forwardStack = new Stack<string>();  // 前进栈

    // 当前页面
    private string currentPage;

    public BrowserHistory(string homepage)
    {
        currentPage = homepage;
        Console.WriteLine($"访问: {currentPage}");
    }

    // 访问新页面
    public void Visit(string url)
    {
        // 当前页面被压入后退栈
        backStack.Push(currentPage);
        // 清空前进栈
        forwardStack.Clear();
        // 更新当前页面
        currentPage = url;
        Console.WriteLine($"访问: {currentPage}");
    }

    // 后退
    public bool Back()
    {
        if (backStack.Count > 0)
        {
            // 将当前页面压入前进栈
            forwardStack.Push(currentPage);
            // 从后退栈中弹出页面并更新当前页面
            currentPage = backStack.Pop();
            Console.WriteLine($"后退到: {currentPage}");
            return true;
        }
        else
        {
            Console.WriteLine("没有历史记录可以后退!");
            return false;
        }
    }

    // 前进
    public bool Forward()
    {
        if (forwardStack.Count > 0)
        {
            // 将当前页面压入后退栈
            backStack.Push(currentPage);
            // 从前进栈中弹出页面并更新当前页面
            currentPage = forwardStack.Pop();
            Console.WriteLine($"前进到: {currentPage}");
            return true;
        }
        else
        {
            Console.WriteLine("没有前进记录!");
            return false;
        }
    }
}

class Program
{
    static void Main()
    {
        // 初始化浏览器历史,假设主页是 "www.homepage.com"
        BrowserHistory browserHistory = new BrowserHistory("www.homepage.com");

        // 访问几个网页
        browserHistory.Visit("www.page1.com");
        browserHistory.Visit("www.page2.com");
        browserHistory.Visit("www.page3.com");

        // 后退操作
        browserHistory.Back(); // 后退到 www.page2.com
        browserHistory.Back(); // 后退到 www.page1.com

        // 前进操作
        browserHistory.Forward(); // 前进到 www.page2.com
        browserHistory.Forward(); // 前进到 www.page3.com

        // 再次后退
        browserHistory.Back(); // 后退到 www.page2.com
    }
}

5.Queue

Queue是一种 先进先出(FIFO, First In First Out)数据结构,它的基本特性是 先进入队列的元素,先被移除。在程序设计中,Queue 常用于需要按顺序处理任务或数据的场景,比如任务调度、消息队列、打印队列等。 

Queue<int> queue = new Queue<int>();
queue.Enqueue(1);  // 将元素1添加到队列
queue.Enqueue(2);  // 将元素2添加到队列


int firstItem = queue.Dequeue();  // 返回并移除队首元素1
Console.WriteLine(firstItem);  // 输出 1

int nextItem = queue.Peek();  // 查看队首元素2,但不移除它
Console.WriteLine(nextItem);  // 输出 2

Console.WriteLine(queue.Count);  // 输出 1(因为队列中只剩下元素2)

queue.Clear();  // 清空队列

委托 Delegate

 Delegate 是一个类型安全的引用,它指向一个或多个方法。通过 Delegate,我们可以将方法作为参数传递,甚至在运行时决定调用哪个方法。    

Delegate 的声明和定义

// 声明一个委托类型
delegate void MyDelegate(string message);  // 一个没有返回值的委托,参数为字符串

// 使用 delegate 定义一个方法与委托匹配
class Program
{
    static void PrintMessage(string message)
    {
        Console.WriteLine(message);
    }

    static void Main()
    {
        // 使用委托类型创建委托实例
        MyDelegate del = new MyDelegate(PrintMessage);
        MyDelegate del2 = PrintMessage;  // 自动推断方法签名,简写形式

        // 调用委托
        del("Hello, delegate!");  // 输出: Hello, delegate!
    }
}

 Delegate 作为参数传递

// 定义一个方法,它接受一个委托作为参数
static void ExecuteAction(MyDelegate del)
{
    del("Executing action...");
}

static void Main()
{
    MyDelegate del = PrintMessage;
    ExecuteAction(del);  // 将委托作为参数传递
}

多播委托

delegate void MyDelegate(string message);

static void MethodA(string msg)
{
    Console.WriteLine("Method A: " + msg);
}

static void MethodB(string msg)
{
    Console.WriteLine("Method B: " + msg);
}

static void Main()
{
    MyDelegate del = MethodA;
    del += MethodB;  // 添加方法到委托链

    del("Hello!");  // 输出 "Method A: Hello!" 和 "Method B: Hello!"
}
  • 使用 += 运算符可以将多个方法绑定到一个委托实例。
  • 当调用委托时,绑定的所有方法会按顺序依次执行。
  • 使用 -= 运算符可以移除方法。

Action

Action 是一个没有返回值的委托类型。它通常用于表示那些执行某种操作但不返回结果的方法。

Action 委托最多可以有 16 个输入参数(从 Action<T1> 到 Action<T1, T2, ..., T16>),但没有返回值。用于需要执行某个动作(如打印信息、更新状态等),但不关心执行结果的场景。

// 定义一个参数且没有返回值的 Action
Action<string> greetAction = name => Console.WriteLine($"Hello, {name}!");

greetAction("Alice");  // 输出: Hello, Alice!

// 定义一个有多个参数的 Action
Action<int, int> sumAction = (a, b) => Console.WriteLine($"Sum: {a + b}");

sumAction(5, 10);  // 输出: Sum: 15

Func

Func 是一个有返回值的委托类型。它表示一个方法,这个方法接受一些输入参数并返回一个值。Func 委托最多支持 16 个输入参数。

// 定义一个没有输入参数,返回一个整数的 Func
Func<int> getRandomNumber = () => new Random().Next(1, 100);

Console.WriteLine(getRandomNumber());  // 输出: 随机生成的一个整数

// 定义一个有两个参数,返回其和的 Func
Func<int, int, int> add = (a, b) => a + b;

Console.WriteLine(add(5, 10));  // 输出: 15

// 定义一个有多个参数并返回一个复杂类型的 Func
Func<string, int, string> createMessage = (name, age) => $"Name: {name}, Age: {age}";

Console.WriteLine(createMessage("Alice", 25));  // 输出: Name: Alice, Age: 25
//结合使用
class Program
{
    static void Main()
    {
        // 使用 Func 来计算总和
        Func<int, int, int> add = (a, b) => a + b;
        
        // 使用 Action 来打印结果
        Action<int> printResult = result => Console.WriteLine($"The result is: {result}");

        // 执行操作
        int sum = add(5, 10);
        printResult(sum);  // 输出: The result is: 15
    }
}

Delegate 与事件

delegate 是实现事件处理的基础。事件通常由委托定义,事件处理程序则是与这些委托关联的方法。委托使得 C# 中的事件和回调机制变得非常灵活和强大。

泛型委托

异步委托

反射

反射(Reflection) 是一种强大的机制,允许程序在运行时检查其自身结构(如类型、方法、属性、字段等),并动态地访问和修改这些结构。它是通过 System.Reflection 命名空间提供的类实现的。

反射的用途包括:

1.可以遍历成员,并且调用成员

宏定义

事件

同步异步异常

c#的内存分布

1. 栈(Stack)

栈是一种 后进先出(LIFO) 数据结构,用于存储方法调用时的局部变量、参数和返回地址等信息。栈的内存分配和释放非常高效,因为它是通过指针来管理的。每当方法被调用时,栈会为该方法的局部变量分配空间,当方法返回时,栈会自动释放相关空间。

在 C# 中,栈通常用于存储 值类型 的数据(如 int, double, char, struct 等)。这些类型的数据在栈中分配时,其内存地址是固定的,而且它们的生命周期由作用域决定。当超出作用域时,栈上的数据就会被自动销毁。

栈的特点:

  • 快速分配和释放:栈内存管理非常高效,分配和释放操作是通过移动栈指针来实现的。
  • 生命周期短:栈内存中的对象仅在方法调用期间存在,当方法返回时,它们会被销毁。
  • 大小有限:栈的大小是有限制的,通常由操作系统设定。如果栈空间不足(比如递归过深),会导致栈溢出(StackOverflow)。

2. 堆(Heap)

堆是用于动态分配内存的区域,内存的分配和释放由 垃圾回收器(GC) 管理。堆中的内存分配更为灵活,但分配和释放相对较慢。与栈不同,堆中的数据在内存中是分散的,不会像栈那样按顺序排列。

在 C# 中,堆通常用于存储 引用类型 的数据(如 object, string, class delegate 等)。这些数据的分配是动态的,并且其生命周期由垃圾回收器决定。

堆的特点:

  • 灵活的内存分配:堆内存分配可以存储任意大小和复杂结构的数据。
  • 垃圾回收:堆内存由垃圾回收器负责管理,系统会定期扫描堆,回收不再使用的对象。
  • 较慢的分配和释放:与栈相比,堆内存的分配和释放速度较慢,因为需要额外的管理开销。
  • 内存碎片:由于内存分配和释放不按顺序进行,堆中可能会发生内存碎片化。

3. 静态区(Static)

静态区存储的是类的静态成员和静态数据。静态数据在程序整个运行过程中只会有一份实例,并且由类本身而非对象来访问。静态成员和静态数据在应用程序启动时分配内存,并且直到程序终止时才会被销毁。

静态区的特点:

  • 存储类的静态成员和数据。
  • 只会被分配一次内存,在程序整个生命周期内都存在。
  • 静态成员可以被类的所有实例共享。

4. 常量池(Interned Strings Pool)

C# 中的字符串是引用类型,但它们有一个特殊的管理方式。字符串常量池用于存储已经创建过的字符串字面量,如果多个地方使用相同的字符串字面量,系统会将这些字符串指向同一内存位置,从而节省内存。

常量池的特点:

  • 只存储唯一的字符串字面量。
  • 如果多个字符串具有相同的值,它们会共享内存,避免重复分配。

5. 托管堆 vs 非托管堆

  • 托管堆:C# 程序中的大多数对象(如类实例、数组等)都分配在托管堆上,由垃圾回收器负责管理。
  • 非托管堆:一些特定的资源(如通过 IntPtr 或指针来操作的内存块、Windows API 的资源等)并不受垃圾回收器管理,称为非托管内存。C# 提供了 unsafe 代码和 Marshal 类来操作非托管内存。

6. 内存管理和垃圾回收(GC)

在 C# 中,内存管理主要通过 垃圾回收器(GC) 来实现。垃圾回收器的任务是自动管理内存,回收那些不再被引用的对象,从而避免内存泄漏。

垃圾回收器的工作原理大致如下:

  1. 标记阶段:垃圾回收器会标记所有仍在使用的对象。
  2. 清理阶段:它会释放不再被引用的对象。
  3. 压缩阶段:有时,垃圾回收器还会对堆中的对象进行压缩,合并堆中的空闲空间,以减少内存碎片。

GC 的运行是自动的,但可以通过 GC.Collect() 方法手动触发垃圾回收,尽管这种操作通常不推荐,除非有特殊需求。

7. 其他内存区域

除了上述主要的内存区域,C# 程序在运行时还可能涉及一些其他内存区域,如:

  • 代码区(Code Area):存储程序的可执行代码。
  • 数据区(Data Area):存储全局变量、常量和静态变量等数据。

lambda 表达式

Lambda expressions - Lambda expressions and anonymous functions - C# reference | Microsoft Learn

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐