1.什么是虚方法?它和抽象方法有什么不同?

共同点:都支持多态、在子类中都可以进行重写、必须在类的内部(不能是static的)

不同点:

对比项virtual 虚方法abstract 抽象方法
是否有默认实现✅ 有默认实现。❌ 没有实现,派生类必须实现
是否必须被重写❌ 可以不重写,派生类也能继承使用。必须重写,否则编译报错。
所在类是否可以实例化✅ 所在类可以实例化(非抽象类)。❌ 所在类必须是抽象类,不能实例化。
用途场景提供一个可被覆盖但非必须覆盖的方法。强制派生类提供特定行为的实现。
public abstract class Animal
{
    public abstract void Speak();
}

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

抽象方法 Speak() 没有实现,所有非抽象派生类必须重写它。

2.接口可以作为参数传递给函数吗

是的,接口在C#中完全可以作为参数传递给函数。这是接口最常见和最强大的用途之一:它允许你传入任何实现了该接口的对象,实现 松耦合多态性

public interface ILogger {
    void Log(string message);
}

public class ConsoleLogger : ILogger {
    public void Log(string message) {
        Console.WriteLine("Console: " + message);
    }
}

public class FileLogger : ILogger {
    public void Log(string message) {
        // 假设写入文件
        Console.WriteLine("File: " + message);
    }
}

public class Processor {
    public void Process(ILogger logger) {
        logger.Log("Processing started...");
    }
}

class Program {
    static void Main() {
        Processor p = new Processor();

        // 传入 ConsoleLogger 实例
        p.Process(new ConsoleLogger());  // 输出:Console: Processing started...

        // 传入 FileLogger 实例
        p.Process(new FileLogger());     // 输出:File: Processing started...
    }
}

注:接口的用法:通过实现接口的类来使用接口方法

必须创建一个实现了该接口的类的实例,然后可以通过该类对象(或接口引用)来调用接口中定义的方法。

3.关键字区别:abstract和virtual

抽象类中:

你想要的行为用法
让子类必须实现这个方法abstract
提供一个默认实现,让子类可以选择是否重写virtual
不想让子类改这个方法普通方法(不加任何关键字)

4.Lambda表达式

4.1 语法:(参数) => { 语句块 }

例1:x => x * x 等效于 (int x) => { return x * x; }

例2:两个参数的lambda表达式 (x, y) => x + y


4.2 委托和lambda表达式的配合:

4.2.1 委托的定义:委托可以理解为 函数的引用类型。它可以存储对方法的引用,就像变量可以存储数值一样。

// 定义一个接收int,返回int的委托类型
public delegate int MyDelegate(int x);
// 目标方法
public static int Square(int x) => x * x;

public static void Main()
{
    // 创建委托实例,指向 Square 方法
    MyDelegate del = Square;

    int result = del(5); // 调用委托
    Console.WriteLine(result); // 输出 25
}

上面的int Square(int x) => x * x是表达式体成员(Expression-bodied member),等价于:

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

4.2.2 委托+lambda表达式例子

MyDelegate del = x => x * x;
Console.WriteLine(del(6)); // 输出 36

4.3 常用的内置委托类型

委托类型用法说明
Action无返回值
Func<T>有返回值(最后一个类型是返回类型)
Predicate<T>返回 bool 的函数

Func<> 是 C# 提供的 内置委托类型,用于表示:有返回值的方法或 lambda 表达式。

语法: Func<参数类型1, 参数类型2, …, 返回值类型> 变量名 = lambda表达式或方法;

例:两个参数,一个返回值

Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(2, 3)); // 输出 5

5.匿名方法和lambda表达式的关系

在 C# 中,匿名方法(Anonymous Methods) 是一种没有名称的方法,通常用于快速定义委托的内容,特别是在需要临时、内联逻辑的场景中。匿名方法是在 C# 2.0 中引入的,后来被 Lambda 表达式(C# 3.0) 所取代或简化。

也就是说,lambda是一种简洁地表示匿名方法的语法。

// 定义一个委托类型
delegate int MyDelegate(int x, int y);

class Program
{
    static void Main()
    {
        // 使用匿名方法创建委托实例
        MyDelegate add = delegate(int x, int y)
        {
            return x + y;
        };

        Console.WriteLine(add(3, 4)); // 输出 7
    }
}
Func<int, int> anon = delegate(int i) 
{ 
 i = i+1; 
 return i; 
}; 
//输出2 
Console.WriteLine(anon(1)); 

6. 什么是闭包

闭包是指 Lambda 表达式或匿名方法中引用了外部作用域中的变量,这些变量会随着 Lambda 一起“打包”进委托中,延长其生命周期。

public static Func<int, int> CreateAdder(int y)
{
    return x => x + y; // y 是外部变量,被闭包捕获
}

var add5 = CreateAdder(5);
Console.WriteLine(add5(10)); // 输出 15

这里的 y 虽然定义在 CreateAdder 方法中,但在 Lambda 中被使用,所以会被“捕获”,即便方法返回之后,y 仍然存在。


例子:

List<Func<int>> actions = new List<Func<int>>();

for (int i = 0; i < 3; i++)
{
    actions.Add(() => i);
}

foreach (var act in actions)
    Console.WriteLine(act()); // 输出什么?

解析:lambda表达式 () => i 捕获了变量 i,而不是 i 的值。由于 i 是 for 循环的迭代变量,它在循环中是同一个变量(每次迭代会更新值),因此所有lambda表达式都捕获了同一个 i

它最后值变为 3,所以每个 lambda 都输出 3。

7.回顾:重载和重写的区别

1. 作用域

  • 重载(Overload)
    发生在同一作用域内(如同一个类中或全局作用域)。
  • 重写(Override)
    发生在继承关系中,派生类对基类的虚函数进行重新定义。

2. 函数签名

  • 重载(Overload)
    • 函数名相同,但参数列表必须不同(参数类型、个数或顺序不同)。
    • 返回类型可以不同,但仅返回类型不同不足以构成重载。
void print(int a);          // 合法重载
void print(double a);       // 合法重载
int print(int a);           // 错误:仅返回类型不同,无法重载
  • 重写(Override)
    • 函数名、参数列表、返回类型必须与基类虚函数严格一致(协变返回类型除外)。
    • 基类函数必须声明为 virtual,派生类函数可以用 override 显式标记。
class Base {
public:
    virtual void show() { cout << "Base"; }
    virtual Base* clone() { return new Base(); }
};
class Derived : public Base {
public:
    void show() override { cout << "Derived"; }  // 合法重写
    Derived* clone() override { return new Derived(); } // 协变返回类型
};

3. 多态性

  • 重载(Overload)
    属于静态多态(编译时多态),编译器根据调用时的参数决定具体函数。
obj.print(5);     // 调用 print(int)
obj.print(3.14);  // 调用 print(double)
  • 重写(Override)
    属于动态多态(运行时多态),通过虚函数表和基类指针/引用实现动态绑定。
Base* ptr = new Derived();
ptr->show();  // 输出 "Derived"

4. 其他关键点

  • 虚函数要求
    • 重写必须基于基类的虚函数(virtual)。
    • 重载与虚函数无关,可以是普通函数或虚函数。
  • const 限定符
    • 成员函数的 const 版本和非 const 版本可构成重载。
    • 重写时,const 限定符必须一致。
void func() const;    // 重载:const 版本
void func();          // 非 const 版本
  • 隐藏(Hiding)
    若派生类定义了与基类同名但参数不同的函数,会隐藏基类函数(需用 using 或作用域运算符访问)。
class Base { public: void func(int); };
class Derived : public Base { public: void func(double); };
Derived d;
d.func(5);          // 调用 Derived::func(double),Base::func(int) 被隐藏
d.Base::func(5);    // 显式调用基类版本
特性重载(Overload)重写(Override)
作用域同一作用域基类与派生类之间
函数签名参数列表不同函数名、参数、返回类型一致
多态类型静态多态(编译时)动态多态(运行时)
虚函数要求无关必须基于基类虚函数
关键字virtual(基类)、override(派生类)
典型用途扩展同名函数功能实现多态性,定制派生类行为

此作者没有提供个人介绍。
最后更新于 2025-05-24